diff --git a/Cargo.lock b/Cargo.lock index aa93df0..d8da452 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -257,12 +257,44 @@ version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.59.0", +] + [[package]] name = "dotenvy" version = "0.15.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", +] + [[package]] name = "getrandom" version = "0.3.3" @@ -345,6 +377,16 @@ version = "0.2.174" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" +[[package]] +name = "libredox" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "391290121bad3d37fbddad76d8f5d1c1c314cfc646d143d7e07a3086ddff0ce3" +dependencies = [ + "bitflags", + "libc", +] + [[package]] name = "lock_api" version = "0.4.13" @@ -399,6 +441,7 @@ dependencies = [ "ignore", "pretty_assertions", "rand", + "shellexpand", "tokio", "tracing", "tracing-subscriber", @@ -436,6 +479,12 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + [[package]] name = "overload" version = "0.1.1" @@ -540,7 +589,7 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" dependencies = [ - "getrandom", + "getrandom 0.3.3", ] [[package]] @@ -552,6 +601,17 @@ dependencies = [ "bitflags", ] +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.16", + "libredox", + "thiserror", +] + [[package]] name = "regex-automata" version = "0.4.9" @@ -619,6 +679,15 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "shellexpand" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b1fdf65dd6331831494dd616b30351c38e96e45921a27745cf98490458b90bb" +dependencies = [ + "dirs", +] + [[package]] name = "shlex" version = "1.3.0" @@ -673,6 +742,26 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "thiserror" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "thread_local" version = "1.1.9" diff --git a/crates/noil/Cargo.toml b/crates/noil/Cargo.toml index 0aa5c97..346c246 100644 --- a/crates/noil/Cargo.toml +++ b/crates/noil/Cargo.toml @@ -21,6 +21,7 @@ ignore = "0.4.23" blake3 = "1.8.2" rand = "0.9.2" ansi_term = "0.12.1" +shellexpand = "3.1.1" [dev-dependencies] pretty_assertions = "1.4.1" diff --git a/crates/noil/src/cli/apply.rs b/crates/noil/src/cli/apply.rs index 261355f..e4a0871 100644 --- a/crates/noil/src/cli/apply.rs +++ b/crates/noil/src/cli/apply.rs @@ -34,6 +34,7 @@ impl ApplyCommand { &original, ApplyOptions { chooser_file: self.chooser_file.clone(), + ..Default::default() }, ) .await @@ -49,6 +50,7 @@ impl ApplyCommand { &input, ApplyOptions { chooser_file: self.chooser_file.clone(), + ..Default::default() }, ) .await diff --git a/crates/noil/src/cli/edit.rs b/crates/noil/src/cli/edit.rs index 1b2adb4..18864be 100644 --- a/crates/noil/src/cli/edit.rs +++ b/crates/noil/src/cli/edit.rs @@ -1,13 +1,17 @@ use std::{ env::temp_dir, - io::Write, + io::{IsTerminal, Write}, path::{Path, PathBuf}, + process::Stdio, }; use ansi_term::Color; use anyhow::{Context, bail}; use clap::Parser; -use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; +use tokio::{ + fs::OpenOptions, + io::{AsyncBufReadExt, AsyncWriteExt, BufReader}, +}; use crate::{ commit::{Action, print_changes}, @@ -26,6 +30,12 @@ pub struct EditCommand { #[arg(long = "chooser-file", env = "NOIL_CHOOSER_FILE")] chooser_file: Option, + + #[arg(long = "commit")] + commit: bool, + + #[arg(long = "quiet")] + quiet: bool, } impl EditCommand { @@ -52,9 +62,13 @@ impl EditCommand { .await .context("create temp file for noil")?; - let output = get_outputs(&self.path, true).await?; - file.write_all(output.as_bytes()).await?; - file.flush().await?; + let output = get_outputs(&self.get_path().await.context("get path")?, true) + .await + .context("get output")?; + file.write_all(output.as_bytes()) + .await + .context("write contents for edit")?; + file.flush().await.context("flush contents for edit")?; let editor = std::env::var("EDITOR").context("EDITOR not found in env")?; @@ -62,7 +76,22 @@ impl EditCommand { let mut cmd = tokio::process::Command::new(editor.trim()); cmd.arg(&file_path); - let mut process = cmd.spawn()?; + if !std::io::stdout().is_terminal() { + let tty = OpenOptions::new() + .read(true) + .write(true) + .open("/dev/tty") + .await + .context("open tty")?; + let tty_in = tty.try_clone().await.context("clone ttyin")?; + let tty_out = tty.try_clone().await.context("clone ttyout")?; + + cmd.stdin(Stdio::from(tty_in.into_std().await)) + .stdout(Stdio::from(tty_out.into_std().await)) + .stderr(Stdio::from(tty.into_std().await)); + } + + let mut process = cmd.spawn().context("command not found")?; let status = process.wait().await.context("editor closed prematurely")?; if !status.success() { let code = status.code().unwrap_or(-1); @@ -73,7 +102,13 @@ impl EditCommand { .await .context("read noil file")?; - let res = print_changes(&noil_content, PREVIEW).await; + let res = if !self.commit { + print_changes(&noil_content, PREVIEW).await + } else { + Ok(Action::Apply { + original: noil_content, + }) + }; let action = match res { Ok(a) => a, @@ -83,7 +118,7 @@ impl EditCommand { Color::Red.normal().paint(format!("{e:?}")) ); - wait_user().await?; + wait_user().await.context("user finished prematurely")?; continue; } @@ -96,6 +131,7 @@ impl EditCommand { &original, ApplyOptions { chooser_file: self.chooser_file.clone(), + quiet: self.quiet, }, ) .await; @@ -104,6 +140,25 @@ impl EditCommand { } } } + + async fn get_path(&self) -> anyhow::Result { + let path_str = self.path.display().to_string(); + let expanded_path = shellexpand::full(&path_str)?; + let path = PathBuf::from(expanded_path.to_string()); + + if !path.exists() { + anyhow::bail!("path: {} does not exist", self.path.display()); + } + + if path.is_file() { + return path + .parent() + .map(|p| p.to_path_buf()) + .ok_or(anyhow::anyhow!("parent doesn't exist for file")); + } + + Ok(path.clone()) + } } async fn wait_user() -> Result<(), anyhow::Error> { @@ -112,13 +167,17 @@ async fn wait_user() -> Result<(), anyhow::Error> { let stdin = tokio::io::stdin(); let mut reader = BufReader::new(stdin); let mut input_buf = String::new(); - reader.read_line(&mut input_buf).await?; + reader + .read_line(&mut input_buf) + .await + .context("failed to read stdin")?; Ok(()) } #[derive(Default, Clone, Debug)] pub struct ApplyOptions { pub chooser_file: Option, + pub quiet: bool, } /// the philosphy behind apply is that we try unlike normal file system operations to be idempotent. @@ -129,7 +188,9 @@ pub struct ApplyOptions { /// /// All in all apply is mostly idempotent, and won't override files, it tries to be as non destructive as possible. For example move will only throw a warning if the source file doesn't exists, but the destination does pub async fn apply(input: &str, options: ApplyOptions) -> anyhow::Result<()> { - eprintln!("applying changes"); + if !options.quiet { + eprintln!("applying changes"); + } let noil_index = parse::parse_input(input).context("parse input")?;