summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorEllie Huxtable <ellie@elliehuxtable.com>2024-04-23 14:02:30 +0100
committerEllie Huxtable <ellie@elliehuxtable.com>2024-04-23 14:02:30 +0100
commitf3dc8d7c38264b97928799c235d7f159cf43a1c2 (patch)
tree9ac9dd5c18140195acf75ea91a3fb42b6e315586
parentc084fad4e77f5ed29f9eeed8ab8c21610ce4cccb (diff)
-rw-r--r--Cargo.lock62
-rw-r--r--Cargo.toml2
-rw-r--r--crates/atuin-dotfiles/src/shell.rs100
-rw-r--r--crates/atuin-run/Cargo.toml5
-rw-r--r--crates/atuin-run/src/markdown.rs2
-rw-r--r--crates/atuin-run/src/pty.rs220
-rw-r--r--crates/atuin/Cargo.toml4
-rw-r--r--crates/atuin/src/command/client.rs6
-rw-r--r--crates/atuin/src/command/client/run.rs41
-rw-r--r--crates/atuin/src/command/client/search/interactive.rs2
10 files changed, 331 insertions, 113 deletions
diff --git a/Cargo.lock b/Cargo.lock
index cef6f4f7..269376d3 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -317,9 +317,14 @@ dependencies = [
name = "atuin-run"
version = "0.1.0"
dependencies = [
+ "bytes",
"comrak",
+ "crossterm",
"eyre",
"portable-pty",
+ "ratatui",
+ "tokio",
+ "tui-term",
"vt100",
]
@@ -611,6 +616,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53"
[[package]]
+name = "castaway"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a17ed5635fc8536268e5d4de1e22e81ac34419e5f052d4d51f4e01dcc263fcc"
+dependencies = [
+ "rustversion",
+]
+
+[[package]]
name = "cc"
version = "1.0.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -785,6 +799,19 @@ dependencies = [
]
[[package]]
+name = "compact_str"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f86b9c4c00838774a6d902ef931eff7470720c51d90c2e32cfe15dc304737b3f"
+dependencies = [
+ "castaway",
+ "cfg-if",
+ "itoa",
+ "ryu",
+ "static_assertions",
+]
+
+[[package]]
name = "comrak"
version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2897,12 +2924,13 @@ dependencies = [
[[package]]
name = "ratatui"
-version = "0.25.0"
+version = "0.26.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a5659e52e4ba6e07b2dad9f1158f578ef84a73762625ddb51536019f34d180eb"
+checksum = "a564a852040e82671dc50a37d88f3aa83bbc690dfc6844cfe7a2591620206a80"
dependencies = [
"bitflags 2.5.0",
"cassowary",
+ "compact_str",
"crossterm",
"indoc",
"itertools",
@@ -3884,15 +3912,21 @@ dependencies = [
[[package]]
name = "stability"
-version = "0.1.1"
+version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ebd1b177894da2a2d9120208c3386066af06a488255caabc5de8ddca22dbc3ce"
+checksum = "2ff9eaf853dec4c8802325d8b6d3dffa86cc707fd7a1a4cdbf416e13b061787a"
dependencies = [
"quote",
- "syn 1.0.109",
+ "syn 2.0.58",
]
[[package]]
+name = "static_assertions"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
+
+[[package]]
name = "str-buf"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3923,18 +3957,18 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "strum"
-version = "0.25.0"
+version = "0.26.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125"
+checksum = "5d8cec3501a5194c432b2b7976db6b7d10ec95c253208b45f83f7136aa985e29"
dependencies = [
"strum_macros",
]
[[package]]
name = "strum_macros"
-version = "0.25.3"
+version = "0.26.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "23dc1fa9ac9c169a78ba62f0b841814b7abae11bdd047b9c58f893439e309ea0"
+checksum = "c6cf59daf282c0a494ba14fd21610a0325f9f90ec9d1231dea26bcb1d696c946"
dependencies = [
"heck 0.4.1",
"proc-macro2",
@@ -4382,6 +4416,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
[[package]]
+name = "tui-term"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3e2ba3e0ff5cf7ff043b20f3e390c6efd16b65a7bef0840a093821b8c71c4c72"
+dependencies = [
+ "ratatui",
+ "vt100",
+]
+
+[[package]]
name = "typed-arena"
version = "2.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/Cargo.toml b/Cargo.toml
index 38991ee0..e0ae07f5 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -42,6 +42,8 @@ typed-builder = "0.18.0"
pretty_assertions = "1.3.0"
thiserror = "1.0"
rustix = { version = "0.38.30", features = ["process", "fs"] }
+ratatui = "0.26"
+crossterm = { version = "0.27", features = ["use-dev-tty"] }
[workspace.dependencies.reqwest]
version = "0.11"
diff --git a/crates/atuin-dotfiles/src/shell.rs b/crates/atuin-dotfiles/src/shell.rs
index 7912bc34..a5cb0b7a 100644
--- a/crates/atuin-dotfiles/src/shell.rs
+++ b/crates/atuin-dotfiles/src/shell.rs
@@ -16,16 +16,41 @@ pub struct Alias {
pub value: String,
}
-pub fn parse_alias(line: &str) -> Alias {
- let mut parts = line.split('=');
+pub fn parse_alias(line: &str) -> Option<Alias> {
+ // consider the fact we might be importing a fish alias
+ // 'alias' output
+ // fish: alias foo bar
+ // posix: foo=bar
+
+ let is_fish = line.split(' ').next().unwrap_or("") == "alias";
+
+ let parts: Vec<&str> = if is_fish {
+ line.split(' ')
+ .enumerate()
+ .filter_map(|(n, i)| if n == 0 { None } else { Some(i) })
+ .collect()
+ } else {
+ line.split('=').collect()
+ };
+
+ if parts.len() <= 1 {
+ return None;
+ }
+
+ let mut parts = parts.iter().map(|s| s.to_string());
let name = parts.next().unwrap().to_string();
- let remaining = parts.collect::<Vec<&str>>().join("=").to_string();
- Alias {
+ let remaining = if is_fish {
+ parts.collect::<Vec<String>>().join(" ").to_string()
+ } else {
+ parts.collect::<Vec<String>>().join("=").to_string()
+ };
+
+ Some(Alias {
name,
- value: remaining,
- }
+ value: remaining.trim().to_string(),
+ })
}
pub fn existing_aliases(shell: Option<Shell>) -> Result<Vec<Alias>, ShellError> {
@@ -43,7 +68,8 @@ pub fn existing_aliases(shell: Option<Shell>) -> Result<Vec<Alias>, ShellError>
// This will return a list of aliases, each on its own line
// They will be in the form foo=bar
let aliases = shell.run_interactive(["alias"])?;
- let aliases: Vec<Alias> = aliases.lines().map(parse_alias).collect();
+
+ let aliases: Vec<Alias> = aliases.lines().filter_map(parse_alias).collect();
Ok(aliases)
}
@@ -73,28 +99,80 @@ pub async fn import_aliases(store: AliasStore) -> Result<Vec<Alias>> {
#[cfg(test)]
mod tests {
+ use crate::shell::{parse_alias, Alias};
+
#[test]
fn test_parse_simple_alias() {
- let alias = super::parse_alias("foo=bar");
+ let alias = super::parse_alias("foo=bar").expect("failed to parse alias");
assert_eq!(alias.name, "foo");
assert_eq!(alias.value, "bar");
}
#[test]
fn test_parse_quoted_alias() {
- let alias = super::parse_alias("emacs='TERM=xterm-24bits emacs -nw'");
+ let alias = super::parse_alias("emacs='TERM=xterm-24bits emacs -nw'")
+ .expect("failed to parse alias");
+
assert_eq!(alias.name, "emacs");
assert_eq!(alias.value, "'TERM=xterm-24bits emacs -nw'");
- let git_alias = super::parse_alias("gwip='git add -A; git rm $(git ls-files --deleted) 2> /dev/null; git commit --no-verify --no-gpg-sign --message \"--wip-- [skip ci]\"'");
+ let git_alias = super::parse_alias("gwip='git add -A; git rm $(git ls-files --deleted) 2> /dev/null; git commit --no-verify --no-gpg-sign --message \"--wip-- [skip ci]\"'").expect("failed to parse alias");
assert_eq!(git_alias.name, "gwip");
assert_eq!(git_alias.value, "'git add -A; git rm $(git ls-files --deleted) 2> /dev/null; git commit --no-verify --no-gpg-sign --message \"--wip-- [skip ci]\"'");
}
#[test]
fn test_parse_quoted_alias_equals() {
- let alias = super::parse_alias("emacs='TERM=xterm-24bits emacs -nw --foo=bar'");
+ let alias = super::parse_alias("emacs='TERM=xterm-24bits emacs -nw --foo=bar'")
+ .expect("failed to parse alias");
assert_eq!(alias.name, "emacs");
assert_eq!(alias.value, "'TERM=xterm-24bits emacs -nw --foo=bar'");
}
+
+ #[test]
+ fn test_parse_fish() {
+ let alias = super::parse_alias("alias foo bar").expect("failed to parse alias");
+ assert_eq!(alias.name, "foo");
+ assert_eq!(alias.value, "bar");
+
+ let alias =
+ super::parse_alias("alias x 'exa --icons --git --classify --group-directories-first'")
+ .expect("failed to parse alias");
+
+ assert_eq!(alias.name, "x");
+ assert_eq!(
+ alias.value,
+ "'exa --icons --git --classify --group-directories-first'"
+ );
+ }
+
+ #[test]
+ fn test_parse_with_fortune() {
+ // Because we run the alias command in an interactive subshell
+ // there may be other output.
+ // Ensure that the parser can handle it
+ // Annoyingly not all aliases are picked up all the time if we use
+ // a non-interactive subshell. Boo.
+ let shell = "
+/ In a consumer society there are \\
+| inevitably two kinds of slaves: the |
+| prisoners of addiction and the |
+\\ prisoners of envy. /
+ -------------------------------------
+ \\ ^__^
+ \\ (oo)\\_______
+ (__)\\ )\\/\\
+ ||----w |
+ || ||
+emacs='TERM=xterm-24bits emacs -nw --foo=bar'
+k=kubectl
+";
+
+ let aliases: Vec<Alias> = shell.lines().filter_map(parse_alias).collect();
+ assert_eq!(aliases[0].name, "emacs");
+ assert_eq!(aliases[0].value, "'TERM=xterm-24bits emacs -nw --foo=bar'");
+
+ assert_eq!(aliases[1].name, "k");
+ assert_eq!(aliases[1].value, "kubectl");
+ }
}
diff --git a/crates/atuin-run/Cargo.toml b/crates/atuin-run/Cargo.toml
index 41207780..07171656 100644
--- a/crates/atuin-run/Cargo.toml
+++ b/crates/atuin-run/Cargo.toml
@@ -13,6 +13,11 @@ readme.workspace = true
[dependencies]
eyre.workspace = true
+ratatui.workspace = true
+tokio.workspace = true
+crossterm.workspace = true
comrak = "0.22"
portable-pty = "0.8.1"
vt100 = "0.15.2"
+tui-term = "0.1.10"
+bytes = "1.6.0"
diff --git a/crates/atuin-run/src/markdown.rs b/crates/atuin-run/src/markdown.rs
index f3e3c0c2..4485e540 100644
--- a/crates/atuin-run/src/markdown.rs
+++ b/crates/atuin-run/src/markdown.rs
@@ -4,7 +4,7 @@ use comrak::{
parse_document, Arena, ComrakOptions,
};
-#[derive(Debug)]
+#[derive(Debug, Clone)]
pub struct Block {
pub info: String,
pub code: String,
diff --git a/crates/atuin-run/src/pty.rs b/crates/atuin-run/src/pty.rs
index 998dfa34..49476c06 100644
--- a/crates/atuin-run/src/pty.rs
+++ b/crates/atuin-run/src/pty.rs
@@ -1,96 +1,178 @@
/// Create and manage pseudoterminals
-use eyre::Result;
+use std::{
+ io::{self, BufWriter, Read, Write},
+ sync::{Arc, RwLock},
+ time::Duration,
+};
+
+use bytes::Bytes;
+use crossterm::{
+ event::{self, Event, KeyCode, KeyEventKind},
+ execute,
+ style::ResetColor,
+ terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
+};
use portable_pty::{CommandBuilder, NativePtySystem, PtySize, PtySystem};
-use std::sync::mpsc::channel;
+use ratatui::{
+ backend::{Backend, CrosstermBackend},
+ layout::Alignment,
+ style::{Modifier, Style},
+ widgets::{Block, Borders, Paragraph},
+ Frame, Terminal,
+};
+use tokio::{
+ sync::mpsc::{channel, Sender},
+ task,
+};
+use tui_term::widget::PseudoTerminal;
use vt100::Screen;
-/// Run a command in a pty, return output. Pty is closed once the command has completed.
-/// If a child process would work, prefer that approach - this is a bit slower and heavier.
-pub fn run_pty() -> Result<String> {
+#[derive(Debug)]
+struct Size {
+ cols: u16,
+ rows: u16,
+}
+
+pub async fn run_pty(blocks: Vec<crate::markdown::Block>) -> io::Result<()> {
+ let mut stdout = io::stdout();
+ execute!(stdout, ResetColor)?;
+ enable_raw_mode()?;
+ let mut stdout = io::stdout();
+ execute!(stdout, EnterAlternateScreen)?;
+ let backend = CrosstermBackend::new(stdout);
+ let mut terminal = Terminal::new(backend)?;
+
let pty_system = NativePtySystem::default();
+ let cwd = std::env::current_dir().unwrap();
+ let mut cmd = CommandBuilder::new_default_prog();
+ cmd.cwd(cwd);
+
+ let size = Size {
+ rows: terminal.size()?.height,
+ cols: terminal.size()?.width,
+ };
let pair = pty_system
.openpty(PtySize {
- rows: 24,
- cols: 80,
+ rows: size.rows,
+ cols: size.cols,
pixel_width: 0,
pixel_height: 0,
})
.unwrap();
+ // Wait for the child to complete
+ task::spawn_blocking(move || {
+ let mut child = pair.slave.spawn_command(cmd).unwrap();
+ let _child_exit_status = child.wait().unwrap();
+ drop(pair.slave);
+ });
- let cmd = CommandBuilder::new("bash");
- let mut child = pair.slave.spawn_command(cmd).unwrap();
+ let mut reader = pair.master.try_clone_reader().unwrap();
+ let parser = Arc::new(RwLock::new(vt100::Parser::new(size.rows, size.cols, 0)));
- // Release any handles owned by the slave: we don't need it now
- // that we've spawned the child.
- drop(pair.slave);
+ {
+ let parser = parser.clone();
+ task::spawn_blocking(move || {
+ // Consume the output from the child
+ // Can't read the full buffer, since that would wait for EOF
+ let mut buf = [0u8; 8192];
+ let mut processed_buf = Vec::new();
+ loop {
+ let size = reader.read(&mut buf).unwrap();
+ if size == 0 {
+ break;
+ }
+ if size > 0 {
+ processed_buf.extend_from_slice(&buf[..size]);
+ let mut parser = parser.write().unwrap();
+ parser.process(&processed_buf);
- // Read the output in another thread.
- // This is important because it is easy to encounter a situation
- // where read/write buffers fill and block either your process
- // or the spawned process.
- let (tx, rx) = channel();
- let mut reader = pair.master.try_clone_reader().unwrap();
+ // Clear the processed portion of the buffer
+ processed_buf.clear();
+ }
+ }
+ });
+ }
- std::thread::spawn(move || {
- // Consume the output from the child
- let mut s = String::new();
- reader.read_to_string(&mut s).unwrap();
- tx.send(s).unwrap();
- });
+ let (tx, mut rx) = channel::<Bytes>(32);
- {
- // Obtain the writer.
- // When the writer is dropped, EOF will be sent to
- // the program that was spawned.
- // It is important to take the writer even if you don't
- // send anything to its stdin so that EOF can be
- // generated, otherwise you risk deadlocking yourself.
- let mut writer = pair.master.take_writer().unwrap();
-
- if cfg!(target_os = "macos") {
- // macOS quirk: the child and reader must be started and
- // allowed a brief grace period to run before we allow
- // the writer to drop. Otherwise, the data we send to
- // the kernel to trigger EOF is interleaved with the
- // data read by the reader! WTF!?
- // This appears to be a race condition for very short
- // lived processes on macOS.
- // I'd love to find a more deterministic solution to
- // this than sleeping.
- std::thread::sleep(std::time::Duration::from_millis(20));
+ let mut writer = BufWriter::new(pair.master.take_writer().unwrap());
+
+ // Drop writer on purpose
+ tokio::spawn(async move {
+ while let Some(bytes) = rx.recv().await {
+ writer.write_all(&bytes).unwrap();
+ writer.flush().unwrap();
}
+ drop(pair.master);
+ });
+
+ println!("{blocks:?}");
+ run(&mut terminal, parser, tx, blocks).await?;
+
+ tokio::time::sleep(Duration::from_secs(2)).await;
+
+ // restore terminal
+ disable_raw_mode()?;
+ execute!(terminal.backend_mut(), LeaveAlternateScreen,)?;
+ terminal.show_cursor()?;
+
+ Ok(())
+}
+
+async fn run<B: Backend>(
+ terminal: &mut Terminal<B>,
+ parser: Arc<RwLock<vt100::Parser>>,
+ sender: Sender<Bytes>,
+ blocks: Vec<crate::markdown::Block>,
+) -> io::Result<()> {
+ tokio::time::sleep(Duration::from_millis(500)).await;
- // This example doesn't need to write anything, but if you
- // want to send data to the child, you'd set `to_write` to
- // that data and do it like this:
- let to_write = "echo 'omg the pty DID SOMETHING'\r\nexit\r\n";
- if !to_write.is_empty() {
- // To avoid deadlock, wrt. reading and waiting, we send
- // data to the stdin of the child in a different thread.
- std::thread::spawn(move || {
- writer.write_all(to_write.as_bytes()).unwrap();
- });
+ for i in blocks {
+ terminal.draw(|f| ui(f, parser.read().unwrap().screen()))?;
+
+ tokio::time::sleep(Duration::from_millis(500)).await;
+
+ for line in i.code.lines() {
+ let b = Bytes::from(line.trim_end().to_string().into_bytes());
+
+ sender.send(b).await;
+ sender.send(Bytes::from(vec![b'\n'])).await;
+
+ terminal.draw(|f| ui(f, parser.read().unwrap().screen()))?;
+ tokio::time::sleep(Duration::from_millis(500)).await;
}
}
- // Wait for the child to complete
- println!("child status: {:?}", child.wait().unwrap());
+ terminal.draw(|f| ui(f, parser.read().unwrap().screen()))?;
+
+ tokio::time::sleep(Duration::from_millis(1500)).await;
- // Take care to drop the master after our processes are
- // done, as some platforms get unhappy if it is dropped
- // sooner than that.
- drop(pair.master);
+ Ok(())
+}
- // Now wait for the output to be read by our reader thread
- let output = rx.recv().unwrap();
+fn ui(f: &mut Frame, screen: &Screen) {
+ let chunks = ratatui::layout::Layout::default()
+ .direction(ratatui::layout::Direction::Vertical)
+ .margin(1)
+ .constraints(
+ [
+ ratatui::layout::Constraint::Percentage(100),
+ ratatui::layout::Constraint::Min(1),
+ ]
+ .as_ref(),
+ )
+ .split(f.size());
+ let block = Block::default()
+ .borders(Borders::ALL)
+ .style(Style::default().add_modifier(Modifier::BOLD));
- // We print with escapes escaped because the windows conpty
- // implementation synthesizes title change escape sequences
- // in the output stream and it can be confusing to see those
- // printed out raw in another terminal.
- let out = output.to_string();
- println!("{out}");
+ let pseudo_term = PseudoTerminal::new(screen).block(block);
+ f.render_widget(pseudo_term, chunks[0]);
- Ok("".to_string())
+ let explanation = "Press q to exit".to_string();
+ let explanation = Paragraph::new(explanation)
+ .style(Style::default().add_modifier(Modifier::BOLD | Modifier::REVERSED))
+ .alignment(Alignment::Center);
+ f.render_widget(explanation, chunks[1]);
}
diff --git a/crates/atuin/Cargo.toml b/crates/atuin/Cargo.toml
index 2c7d09d2..14e48eff 100644
--- a/crates/atuin/Cargo.toml
+++ b/crates/atuin/Cargo.toml
@@ -56,7 +56,7 @@ directories = { workspace = true }
indicatif = "0.17.5"
serde = { workspace = true }
serde_json = { workspace = true }
-crossterm = { version = "0.27", features = ["use-dev-tty"] }
+crossterm.workspace = true
unicode-width = "0.1"
itertools = { workspace = true }
tokio = { workspace = true }
@@ -76,7 +76,7 @@ tiny-bip39 = "1"
futures-util = "0.3"
fuzzy-matcher = "0.3.7"
colored = "2.0.4"
-ratatui = "0.25"
+ratatui.workspace = true
tracing = "0.1"
uuid = { workspace = true }
unicode-segmentation = "1.11.0"
diff --git a/crates/atuin/src/command/client.rs b/crates/atuin/src/command/client.rs
index 45e039c6..afceea34 100644
--- a/crates/atuin/src/command/client.rs
+++ b/crates/atuin/src/command/client.rs
@@ -75,8 +75,8 @@ pub enum Cmd {
Doctor,
/// Execute a runbook or workflow
- #[command()]
- Run,
+ #[command(subcommand)]
+ Run(run::Cmd),
/// Print example configuration
#[command()]
@@ -140,7 +140,7 @@ impl Cmd {
Self::Doctor => doctor::run(&settings),
- Self::Run => run::run(),
+ Self::Run(r) => r.run().await,
Self::DefaultConfig => {
default_config::run();
diff --git a/crates/atuin/src/command/client/run.rs b/crates/atuin/src/command/client/run.rs
index 4dcdf18b..5d197fdb 100644
--- a/crates/atuin/src/command/client/run.rs
+++ b/crates/atuin/src/command/client/run.rs
@@ -1,25 +1,32 @@
-use eyre::Result;
+use std::path::PathBuf;
+
+use clap::Subcommand;
+use eyre::{eyre, Result};
use atuin_run::{markdown::parse, pty::run_pty};
+use rustix::path::Arg;
-pub fn run() -> Result<()> {
- let blocks = parse(
- "
-1. do a thing
-```sh
-echo 'foo'
-```
+#[derive(Debug, Subcommand)]
+pub enum Cmd {
+ Markdown { path: String },
+}
-2. do another thing
-```sh
-echo 'bar'
-```
-",
- );
+impl Cmd {
+ pub async fn run(&self) -> Result<()> {
+ match self {
+ Cmd::Markdown { path } => {
+ let file = PathBuf::from(path);
- println!("{:?}", blocks);
+ if !file.exists() {
+ return Err(eyre!("File does not exist at {path}"));
+ }
- run_pty();
+ let md = tokio::fs::read_to_string(file).await?;
+ let blocks = parse(md.as_str());
+ run_pty(blocks).await?;
+ }
+ }
- Ok(())
+ Ok(())
+ }
}
diff --git a/crates/atuin/src/command/client/search/interactive.rs b/crates/atuin/src/command/client/search/interactive.rs
index 7a3a834b..87506c5e 100644
--- a/crates/atuin/src/command/client/search/interactive.rs
+++ b/crates/atuin/src/command/client/search/interactive.rs
@@ -643,7 +643,7 @@ impl State {
// TODO: this should be split so that we have one interactive search container that is
// EITHER a search box or an inspector. But I'm not doing that now, way too much atm.
// also allocate less 🙈
- let titles = TAB_TITLES.iter().copied().map(Line::from).collect();
+ let titles: Vec<Line> = TAB_TITLES.iter().copied().map(Line::from).collect();
let tabs = Tabs::new(titles)
.block(Block::default().borders(Borders::NONE))