diff options
author | i love my dog <50598611+ms-jpq@users.noreply.github.com> | 2021-08-14 09:18:22 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-08-14 09:18:22 -0700 |
commit | 6a35c6599c5546a03720774919e9dd2569541657 (patch) | |
tree | 10466290f325dd669e6c15508a2b3e6cdcfe6b27 | |
parent | f1afb5e46642d52eaaaf2eaf6df5ebad6fa9ca62 (diff) | |
parent | b8131287b6b15ae80cd10ce483049132cd9be841 (diff) |
Merge pull request #89 from ms-jpq/devv0.4.13ci_0.4.13_2021-08-14_16-25
rewrote
-rw-r--r-- | .github/workflows/release.yml | 4 | ||||
-rw-r--r-- | Cargo.lock | 2 | ||||
-rw-r--r-- | Cargo.toml | 8 | ||||
-rw-r--r-- | ci/linux/Dockerfile | 6 | ||||
-rw-r--r-- | release_notes.md | 6 | ||||
-rw-r--r-- | src/argparse.rs | 144 | ||||
-rw-r--r-- | src/displace.rs | 76 | ||||
-rw-r--r-- | src/errors.rs | 121 | ||||
-rw-r--r-- | src/fs_pipe.rs | 74 | ||||
-rw-r--r-- | src/fzf.rs | 255 | ||||
-rw-r--r-- | src/input.rs | 229 | ||||
-rw-r--r-- | src/main.rs | 134 | ||||
-rw-r--r-- | src/output.rs | 79 | ||||
-rw-r--r-- | src/subprocess.rs | 123 | ||||
-rw-r--r-- | src/types.rs | 80 | ||||
-rw-r--r-- | src/udiff.rs | 150 |
16 files changed, 741 insertions, 750 deletions
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ad8fae8..8b90732 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -17,9 +17,9 @@ jobs: - name: Test run: cargo test - - name: Build - run: cargo build --release --target=x86_64-apple-darwin + + run: cargo build --release --locked --target=x86_64-apple-darwin - name: Package Artifacts run: |- @@ -463,7 +463,7 @@ checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" [[package]] name = "sad" -version = "0.4.12" +version = "0.4.13" dependencies = [ "aho-corasick", "ansi_term 0.12.1", @@ -1,6 +1,6 @@ [package] name = "sad" -version = "0.4.12" +version = "0.4.13" description = "Space Age seD | https://github.com/ms-jpq/sad" authors = ["git@bigly.dog"] edition = "2018" @@ -8,14 +8,14 @@ edition = "2018" [dependencies] aho-corasick = "0.7" ansi_term = "0.12" -async-channel = { version = "1.6" } +async-channel = "1.6" atty = "0.2" difflib = "0.4" futures = "0.3" num_cpus = "1" -pathdiff = "0.2.0" +pathdiff = "0.2" regex = "1" structopt = "0.3" -tokio = { version = "1.9", features = ["full"] } +tokio = { version = "1", features = ["full"] } uuid = { version = "0.8", features = ["v4"] } which = "4" diff --git a/ci/linux/Dockerfile b/ci/linux/Dockerfile index 7a0a760..4b46f0e 100644 --- a/ci/linux/Dockerfile +++ b/ci/linux/Dockerfile @@ -3,9 +3,9 @@ FROM rust:latest # SET UP WORKDIR /WORK -RUN mkdir -p artifacts && \ +RUN mkdir --parents -- artifacts && \ apt update && \ - apt install -y zip + apt install --yes -- zip # COPY @@ -14,5 +14,5 @@ COPY . . # BUILD RUN cargo test -RUN cargo build --release --target=x86_64-unknown-linux-gnu +RUN cargo build --release --locked --target=x86_64-unknown-linux-gnu RUN zip -j artifacts/sad.zip target/x86_64-unknown-linux-gnu/release/sad diff --git a/release_notes.md b/release_notes.md index bea7a5e..649b2af 100644 --- a/release_notes.md +++ b/release_notes.md @@ -1,11 +1,11 @@ ## Good news -v0.4.9 +v0.4.13 - imperceptibly faster -- fzf now shows relative instead of absolute path +- rewrote error paths -- fix bugs +- cut down on runtime **Released by CI** diff --git a/src/argparse.rs b/src/argparse.rs index f239520..dc7ce85 100644 --- a/src/argparse.rs +++ b/src/argparse.rs @@ -1,5 +1,5 @@ -use super::errors::{Failure, SadResult, SadnessFrom}; use super::subprocess::SubprocessCommand; +use super::types::Fail; use aho_corasick::{AhoCorasick, AhoCorasickBuilder}; use regex::{Regex, RegexBuilder}; use std::{collections::HashMap, env, path::PathBuf}; @@ -66,21 +66,19 @@ pub struct Arguments { pub internal_patch: Option<PathBuf>, } -impl Arguments { - pub fn new() -> SadResult<Arguments> { - let args = env::args().collect::<Vec<_>>(); - match (args.get(1), args.get(2)) { - (Some(lhs), Some(rhs)) if lhs == "-c" => { - if rhs.contains('\x04') { - Ok(Arguments::from_iter(rhs.split('\x04'))) - } else { - Err(Failure::Simple( - "`-c` is a reserved flag, use --k, or --commit".to_owned(), - )) - } +pub fn parse_args() -> Result<Arguments, Fail> { + let args = env::args().collect::<Vec<_>>(); + match (args.get(1), args.get(2)) { + (Some(lhs), Some(rhs)) if lhs == "-c" => { + if rhs.contains('\x04') { + Ok(Arguments::from_iter(rhs.split('\x04'))) + } else { + Err(Fail::ArgumentError( + "`-c` is a reserved flag, use --k, or --commit".to_owned(), + )) } - _ => Ok(Arguments::from_args()), } + _ => Ok(Arguments::from_args()), } } @@ -112,72 +110,29 @@ pub struct Options { pub unified: usize, } -impl Options { - pub fn new(args: Arguments) -> SadResult<Options> { - let mut flagset = p_auto_flags(&args.pattern); - flagset.extend( - args - .flags - .unwrap_or_default() - .split_terminator("") - .skip(1) - .map(String::from), - ); - - let engine = { - let replace = args.replace.unwrap_or_default(); - if args.exact { - Engine::AhoCorasick(p_aho_corasick(&args.pattern, &flagset)?, replace) - } else { - Engine::Regex(p_regex(&args.pattern, &flagset)?, replace) - } - }; - - let action = if args.commit || args.internal_patch != None { - Action::Commit - } else { - match (args.internal_preview, p_fzf(args.fzf)) { - (Some(_), _) => Action::Preview, - (_, None) => Action::Preview, - (_, Some((bin, args))) => Action::Fzf(bin, args), - } - }; - - let printer = match p_pager(&args.pager) { - Some(cmd) => Printer::Pager(cmd), - None => Printer::Stdout, - }; - - Ok(Options { - cwd: env::current_dir().ok(), - action, - engine, - printer, - unified: args.unified.unwrap_or(3), - }) +fn p_auto_flags(exact: bool, pattern: &str) -> Vec<String> { + let mut flags = vec!["i".to_owned()]; + if !exact { + flags.push("m".to_owned()) } -} - -fn p_auto_flags(pattern: &str) -> Vec<String> { - let mut flags = vec!["m".into(), "i".into()]; for c in pattern.chars() { if c.is_uppercase() { - flags.push("I".into()); + flags.push("I".to_owned()); break; } } flags } -fn p_aho_corasick(pattern: &str, flags: &[String]) -> SadResult<AhoCorasick> { +fn p_aho_corasick(pattern: &str, flags: Vec<String>) -> Result<AhoCorasick, Fail> { let mut ac = AhoCorasickBuilder::new(); for flag in flags { match flag.as_str() { "i" => ac.ascii_case_insensitive(true), "I" => ac.ascii_case_insensitive(false), _ => { - return Err(Failure::Simple(format!( - "Invaild regex flag for exact matches -{}", + return Err(Fail::ArgumentError(format!( + "Invaild regex flag, see `--help` :: {}", flag ))) } @@ -186,7 +141,7 @@ fn p_aho_corasick(pattern: &str, flags: &[String]) -> SadResult<AhoCorasick> { Ok(ac.build(&[pattern])) } -fn p_regex(pattern: &str, flags: &[String]) -> SadResult<Regex> { +fn p_regex(pattern: &str, flags: Vec<String>) -> Result<Regex, Fail> { let mut re = RegexBuilder::new(pattern); for flag in flags { match flag.as_str() { @@ -200,10 +155,15 @@ fn p_regex(pattern: &str, flags: &[String]) -> SadResult<Regex> { "U" => re.swap_greed(false), "x" => re.ignore_whitespace(true), "X" => re.ignore_whitespace(false), - _ => return Err(Failure::Simple(format!("Invaild regex flag -{}", flag))), + _ => { + return Err(Fail::ArgumentError(format!( + "Invaild regex flag, see `--help` :: {}", + flag + ))) + } }; } - re.build().into_sadness() + Ok(re.build()?) } fn p_fzf(fzf: Option<String>) -> Option<(PathBuf, Vec<String>)> { @@ -237,8 +197,52 @@ fn p_pager(pager: &Option<String>) -> Option<SubprocessCommand> { }; prog.map(|program| SubprocessCommand { - arguments, - program, + args: arguments, + prog: program, env: HashMap::new(), }) } + +pub fn parse_opts(args: Arguments) -> Result<Options, Fail> { + let mut flagset = p_auto_flags(args.exact, &args.pattern); + flagset.extend( + args + .flags + .unwrap_or_default() + .split_terminator("") + .skip(1) + .map(String::from), + ); + + let engine = { + let replace = args.replace.unwrap_or_default(); + if args.exact { + Engine::AhoCorasick(p_aho_corasick(&args.pattern, flagset)?, replace) + } else { + Engine::Regex(p_regex(&args.pattern, flagset)?, replace) + } + }; + + let action = if args.commit || args.internal_patch != None { + Action::Commit + } else { + match (args.internal_preview, p_fzf(args.fzf)) { + (Some(_), _) => Action::Preview, + (_, None) => Action::Preview, + (_, Some((bin, args))) => Action::Fzf(bin, args), + } + }; + + let printer = match p_pager(&args.pager) { + Some(cmd) => Printer::Pager(cmd), + None => Printer::Stdout, + }; + + Ok(Options { + cwd: env::current_dir().ok(), + action, + engine, + printer, + unified: args.unified.unwrap_or(3), + }) +} diff --git a/src/displace.rs b/src/displace.rs index b2f1aec..7390ef8 100644 --- a/src/displace.rs +++ b/src/displace.rs @@ -1,17 +1,18 @@ use super::argparse::{Action, Engine, Options}; -use super::errors::{Failure, SadResult}; use super::fs_pipe::{slurp, spit}; use super::input::Payload; -use super::udiff::{udiff, DiffRanges, Diffs, Patchable, Picker}; +use super::types::Fail; +use super::udiff::{apply_patches, patches, pure_diffs, udiff}; use ansi_term::Colour; use pathdiff::diff_paths; -use std::path::PathBuf; +use std::{path::PathBuf, sync::Arc}; +use tokio::task::spawn_blocking; impl Engine { fn replace(&self, before: &str) -> String { match self { - Engine::AhoCorasick(ac, replace) => ac.replace_all(&before, &[replace.as_str()]), - Engine::Regex(re, replace) => re.replace_all(&before, replace.as_str()).into(), + Engine::AhoCorasick(ac, replace) => ac.replace_all(before, &[replace.as_str()]), + Engine::Regex(re, replace) => re.replace_all(before, replace.as_str()).into(), } } } @@ -25,56 +26,61 @@ impl Payload { } } -async fn displace_impl(opts: &Options, payload: &Payload) -> SadResult<String> { +pub async fn displace(opts: &Arc<Options>, payload: Payload) -> Result<String, Fail> { let path = payload.path().clone(); - let slurped = slurp(&path).await?; let rel_path = opts .cwd .as_ref() .and_then(|cwd| diff_paths(&path, cwd)) - .map(|p| p) - .unwrap_or(path); + .unwrap_or_else(|| path.clone()); + let name = format!("{}", rel_path.display()); + + let slurped = slurp(&path).await?; + let before = Arc::new(slurped.content); - let name = rel_path.display(); - let (canonical, meta, before) = (slurped.path, slurped.meta, slurped.content); - let after = opts.engine.replace(&before); + let o = opts.clone(); + let o2 = opts.clone(); + let b = before.clone(); + let after = spawn_blocking(move || o.engine.replace(&b)).await?; - if before == after { + if *before == after { Ok(String::new()) } else { - let print = match (&opts.action, &payload) { - (Action::Preview, Payload::Entire(_)) => udiff(None, opts.unified, &name, &before, &after), + let print = match (&opts.action, payload) { + (Action::Preview, Payload::Entire(_)) => { + spawn_blocking(move || udiff(None, o2.unified, &name, &before, &after)).await? + } (Action::Preview, Payload::Piecewise(_, ranges)) => { - udiff(Some(ranges), opts.unified, &name, &before, &after) + spawn_blocking(move || udiff(Some(&ranges), o2.unified, &name, &before, &after)).await? } (Action::Commit, Payload::Entire(_)) => { - spit(&canonical, &meta, &after).await?; + spit(&path, &slurped.meta, &after).await?; format!("{}\n", name) } (Action::Commit, Payload::Piecewise(_, ranges)) => { - let diffs: Diffs = Patchable::new(opts.unified, &before, &after); - let after = diffs.patch(&ranges, &before); - spit(&canonical, &meta, &after).await?; + let after = spawn_blocking(move || { + let patches = patches(o2.unified, &before, &after); + apply_patches(patches, &ranges, &before) + }) + .await?; + + spit(&path, &slurped.meta, &after).await?; format!("{}\n", name) } (Action::Fzf(_, _), _) => { - let ranges: DiffRanges = Picker::new(opts.unified, &before, &after); - let mut fzf_lines = String::new(); - for range in ranges { - let repr = Colour::Red.paint(format!("{}", range)); - let line = format!("{}\n\n\n\n{}\0", &name, repr); - fzf_lines.push_str(&line); - } - fzf_lines + spawn_blocking(move || { + let ranges = pure_diffs(o2.unified, &before, &after); + let mut fzf_lines = String::new(); + for range in ranges { + let repr = Colour::Red.paint(format!("{}", range)); + let line = format!("{}\n\n\n\n{}\0", name, repr); + fzf_lines.push_str(&line); + } + fzf_lines + }) + .await? } }; Ok(print) } } - -pub async fn displace(opts: &Options, payload: Payload) -> SadResult<String> { - match displace_impl(opts, &payload).await { - Ok(ret) => Ok(ret), - Err(err) => Err(Failure::Displace(format!("{:#?}", payload), Box::new(err))), - } -} diff --git a/src/errors.rs b/src/errors.rs deleted file mode 100644 index 4f63847..0000000 --- a/src/errors.rs +++ /dev/null @@ -1,121 +0,0 @@ -use async_channel::{RecvError, SendError}; -use std::{ - env::VarError, - error::Error, - fmt::{self, Display, Formatter}, - io, num, string, -}; -use tokio::task::JoinError; - -/* - * Consolidate Error Handling - * ========================== - */ - -#[derive(Debug)] -pub enum Failure { - Channel, - Compound(Box<Failure>, Box<Failure>), - Displace(String, Box<Failure>), - Fzf(String), - Interrupt, - IO(io::Error), - JoinError, - NilStdin, - Parse(String), - Regex(regex::Error), - Simple(String), - Str(string::FromUtf8Error), - VarErr, -} - -impl Failure { - pub fn exit_message(&self) -> Option<String> { - match self { - Failure::Interrupt => None, - _ => Some(format!("{}", self)), - } - } - - pub fn exit_code(&self) -> i32 { - match self { - Failure::Interrupt => 130, - _ => 1, - } - } -} - -impl Display for Failure { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - write!(f, "Error:\n{:#?}", self) - } -} - -impl Error for Failure {} - -pub type SadResult<T> = Result<T, Failure>; - -pub trait SadnessFrom<T> { - fn into_sadness(self) -> SadResult<T>; -} - -impl<T, E: Into<Failure>> SadnessFrom<T> for Result<T, E> { - fn into_sadness(self) -> SadResult<T> { - match self { - Ok(val) => Ok(val), - Err(e) => Err(e.into()), - } - } -} - -/* ========================== - * Consolidate Error Handling - */ - -impl<T> From<SendError<T>> for Failure { - fn from(_: SendError<T>) -> Self { - Failure::Channel - } -} - -impl From<RecvError> for Failure { - fn from(_: RecvError) -> Self { - Failure::Channel - } -} - -impl From<io::Error> for Failure { - fn from(err: io::Error) -> Self { - Failure::IO(err) - } -} - -impl From<string::FromUtf8Error> for Failure { - fn from(err: string::FromUtf8Error) -> Self { - Failure::Str(err) - } -} - -impl From<num::ParseIntError> for Failure { - fn from(err: num::ParseIntError) -> Self { - Failure::Parse(format!("{:#?}", err)) - } -} - -impl From<regex::Error> for Failure { - fn from(err: regex::Error) -> Self { - Failure::Regex(err) - } -} - -impl From<VarError> for Failure { - fn from(_: VarError) -> Self { - Failure::VarErr - } -} - -impl From<JoinError> for Failure { - fn from(_: JoinError) -> Self { - Failure::JoinError - } -} diff --git a/src/fs_pipe.rs b/src/fs_pipe.rs index 1217496..fdf395d 100644 --- a/src/fs_pipe.rs +++ b/src/fs_pipe.rs @@ -1,50 +1,66 @@ -use super::errors::{Failure, SadResult, SadnessFrom}; -use std::{fs::Metadata, io::ErrorKind, path::PathBuf}; -use tokio::fs::{metadata, read_to_string, remove_file, rename, set_permissions, write}; +use super::types::Fail; +use std::{ffi::OsString, fs::Metadata, io::ErrorKind, path::Path}; +use tokio::{ + fs::{rename, File, OpenOptions}, + io::{AsyncReadExt, AsyncWriteExt}, +}; use uuid::Uuid; pub struct Slurpee { - pub path: PathBuf, pub meta: Metadata, pub content: String, } -pub async fn slurp(path: &PathBuf) -> SadResult<Slurpee> { - let meta = metadata(&path).await.into_sadness()?; +pub async fn slurp(path: &Path) -> Result<Slurpee, Fail> { + let mut fd = File::open(path) + .await + .map_err(|e| Fail::IO(path.to_owned(), e.kind()))?; + + let meta = fd + .metadata() + .await + .map_err(|e| Fail::IO(path.to_owned(), e.kind()))?; + let content = if meta.is_file() { - match read_to_string(&path).await { - Ok(text) => text, - Err(err) if err.kind() == ErrorKind::InvalidData => String::new(), - Err(err) => Err(err).into_sadness()?, + let mut s = String::new(); + match fd.read_to_string(&mut s).await { + Ok(_) => s, + Err(err) if err.kind() == ErrorKind::InvalidData => s, + Err(err) => return Err(Fail::IO(path.to_owned(), err.kind())), } } else { String::new() }; - let slurpee = Slurpee { - path: path.clone(), - meta, - content, - }; - Ok(slurpee) + + Ok(Slurpee { meta, content }) } -pub async fn spit(canonical: &PathBuf, meta: &Metadata, text: &str) -> SadResult<()> { +pub async fn spit(canonical: &Path, meta: &Metadata, text: &str) -> Result<(), Fail> { let uuid = Uuid::new_v4().to_simple().to_string(); let mut file_name = canonical .file_name() - .and_then(|s| s.to_str()) - .map(String::from) - .ok_or_else(|| Failure::Simple(format!("Bad file name - {}", canonical.display())))?; - file_name.push_str("___"); - file_name.push_str(&uuid); - - let backup = canonical.with_file_name(file_name); - rename(&canonical, &backup).await.into_sadness()?; - write(&canonical, text).await.into_sadness()?; - set_permissions(&canonical, meta.permissions()) + .map(|n| n.to_owned()) + .unwrap_or_else(|| OsString::from("")); + file_name.push("___"); + file_name.push(uuid); + let tmp = canonical.with_file_name(file_name); + + let mut fd = OpenOptions::new() + .create_new(true) + .write(true) + .open(&tmp) + .await + .map_err(|e| Fail::IO(tmp.clone(), e.kind()))?; + fd.set_permissions(meta.permissions()) + .await + .map_err(|e| Fail::IO(tmp.clone(), e.kind()))?; + fd.write_all(text.as_bytes()) + .await + .map_err(|e| Fail::IO(tmp.clone(), e.kind()))?; + + rename(&tmp, &canonical) .await - .into_sadness()?; - remove_file(&backup).await.into_sadness()?; + .map_err(|e| Fail::IO(canonical.to_owned(), e.kind()))?; Ok(()) } @@ -1,25 +1,130 @@ -use super::errors::{Failure, SadResult, SadnessFrom}; +use super::subprocess::stream_into; use super::subprocess::SubprocessCommand; -use super::types::Task; -use async_channel::{bounded, Receiver, Sender}; +use super::types::{Abort, Fail}; use futures::future::try_join; -use std::{collections::HashMap, env, path::PathBuf, process::Stdio}; +use std::{collections::HashMap, env, path::PathBuf, process::Stdio, sync::Arc}; use tokio::{ - io::{self, AsyncWriteExt, BufWriter}, + io::{AsyncWriteExt, BufWriter, ErrorKind}, process::Command, - select, task, + select, + sync::mpsc::Receiver, + task::{spawn, JoinHandle}, }; use which::which; -pub fn run_fzf( +async fn reset_term() -> Result<(), Fail> { + if let Ok(path) = which("tput") { + let status = Command::new(&path) + .kill_on_drop(true) + .stdin(Stdio::null()) + .arg("reset") + .status() + .await + .map_err(|e| Fail::IO(path, e.kind()))?; + + if status.success() { + return Ok(()); + } + } + if let Ok(path) = which("reset") { + let status = Command::new(&path) + .kill_on_drop(true) + .stdin(Stdio::null()) + .status() + .await + .map_err(|e| Fail::IO(path, e.kind()))?; + if status.success() { + return Ok(()); + } + } + Err(Fail::IO(PathBuf::from("reset"), ErrorKind::NotFound)) +} + +fn run_fzf(abort: &Arc<Abort>, cmd: SubprocessCommand, stream: Receiver<String>) -> JoinHandle<()> { + let abort = abort.clone(); + + spawn(async move { + let subprocess = Command::new(&cmd.prog) + .kill_on_drop(true) + .args(&cmd.args) + .envs(&cmd.env) + .stdin(Stdio::piped()) + .spawn(); + + match subprocess { + Err(err) => { + abort.send(Fail::IO(cmd.prog, err.kind())).await; + } + Ok(mut child) => { + let mut stdin = child.stdin.take().map(BufWriter::new).expect("nil stdin"); + + let abort_1 = abort.clone(); + let p1 = cmd.prog.clone(); + let handle_in = spawn(async move { + stream_into(&abort_1, p1.clone(), &mut stdin, stream).await; + if let Err(err) = stdin.shutdown().await { + abort_1.send(Fail::IO(p1, err.kind())).await; + } + }); + + let abort_2 = abort.clone(); + let p2 = cmd.prog.clone(); + let handle_child = spawn(async move { + select! { + _ = abort_2.notified() => { + match child.kill().await { + Err(err) => { + abort_2.send(Fail::IO(p2, err.kind())).await; + }, + _ => { + if let Err(err) = reset_term().await { + abort_2.send(err).await; + } + } + } + }, + rhs = child.wait() => { + match rhs { + Ok(status) => { + match status.code() { + Some(0) | Some(1) | None => (), + Some(130) => { + abort_2.send(Fail::Interrupt).await; + } + Some(c) => { + abort_2.send(Fail::BadExit(p2, c)).await; + if let Err(err) = reset_term().await { + abort_2.send(err).await; + } + } + } + } + Err(err) => { + abort_2.send(Fail::IO(p2, err.kind())).await; + } + } + } + } + }); + + if let Err(err) = try_join(handle_child, handle_in).await { + abort.send(err.into()).await; + } + } + } + }) +} + +pub fn stream_fzf( + abort: &Arc<Abort>, bin: PathBuf, args: Vec<String>, - stream: Receiver<SadResult<String>>, -) -> (Task, Receiver<SadResult<String>>) { + stream: Receiver<String>, +) -> JoinHandle<()> { let sad = env::current_exe() .or_else(|_| which("sad".to_owned())) .map(|p| format!("{}", p.display())) - .unwrap_or("sad".to_owned()); + .unwrap_or_else(|_| "sad".to_owned()); let preview_args = env::args().skip(1).collect::<Vec<_>>().join("\x04"); let execute = format!( @@ -42,132 +147,12 @@ pub fn run_fzf( arguments.extend(args); let mut env = HashMap::new(); env.insert("SHELL".to_owned(), sad); + // WARN -- versions of FZF only work with C locale. + env.insert("LC_ALL".to_owned(), "C".to_owned()); let cmd = SubprocessCommand { - program: bin, - arguments, + prog: bin, + args: arguments, env, }; - stream_fzf(&cmd, stream) -} - -fn stream_fzf( - cmd: &SubprocessCommand, - stream: Receiver<SadResult<String>>, -) -> (Task, Receiver<SadResult<String>>) { - let (tx, rx) = bounded::<SadResult<String>>(1); - let (tix, rix) = bounded::<Failure>(1); - let ta = Sender::clone(&tx); - - let subprocess = Command::new(&cmd.program) - .args(&cmd.arguments) - .envs(&cmd.env) - .kill_on_drop(true) - .stdin(Stdio::piped()) - .spawn(); - - let mut child = match subprocess.into_sadness() { - Ok(child) => child, - Err(err) => { - let handle = task::spawn(async move { tx.send(Err(err)).await.expect("<CHANNEL>") }); - return (handle, rx); - } - }; - - let mut stdin = child.stdin.take().map(BufWriter::new).expect("nil stdin"); - - let handle_in = task::spawn(async move { - while let Ok(print) = stream.recv().await { - match print { - Ok(val) => { - if let Err(err) = stdin.write(val.as_bytes()).await.into_sadness() { - tix.send(err).await.expect("<CHAN>") - } - } - Err(err) => { - tix.send(err).await.expect("<CHANNEL>"); - break; - } - } - } - if let Err(err) = stdin.shutdown().await { - tix.send(err.into()).await.expect("<CHANNEL>") - } - }); - - let handle_kill = task::spawn(async move { - match rix.recv().await { - Ok(err) => Some(err), - Err(_) => None, - } - }); - - let handle_child = task::spawn(async move { - select! { - lhs = child.wait() => { - match lhs { - Ok(status) => process_status_code(status.code(), tx).await, - Err(err) => tx.send(Err(err.into())).await.expect("<CHANNEL>") - } - }, - rhs = handle_kill => { - match rhs { - Ok(Some(err)) => { - let err = combine_err(err, child.kill().await.into_sadness()); - let err = combine_err(err, child.wait().await.into_sadness()); - let err = combine_err(err, reset_term().await); - tx.send(Err(err)).await.expect("<CHAN>") - }, - Ok(None) => match child.wait().await.into_sadness() { - Err(err) => tx.send(Err(err)).await.expect("<CHANNEL>"), - Ok(status) => process_status_code(status.code(), tx).await, - } - Err(err) => tx.send(Err(err.into())).await.expect("<CHANNEL>") - } - } - } - }); - - let handle = task::spawn(async move { - if let Err(err) = try_join(handle_child, handle_in).await { - ta.send(Err(err.into())).await.expect("<CHAN>") - } - }); - - (handle, rx) -} - -fn combine_err<T>(err: Failure, res: SadResult<T>) -> Failure { - match res { - Ok(_) => err, - Err(e) => Failure::Compound(Box::new(err), Box::new(e)), - } -} - -async fn process_status_code(code: Option<i32>, tx: Sender<SadResult<String>>) { - match code { - Some(0) | Some(1) | None => {} - Some(130) => tx.send(Err(Failure::Interrupt)).await.expect("<CHANNEL>"), - Some(c) => tx - .send(Err(Failure::Fzf(format!("Error exit - {}", c)))) - .await - .expect("<CHANNEL>"), - } -} - -async fn reset_term() -> SadResult<()> { - io::stdout().flush().await.into_sadness()?; - io::stderr().flush().await.into_sadness()?; - if which("tput").is_ok() { - Command::new("tput") - .arg("reset") - .status() - .await - .into_sadness()?; - Ok(()) - } else if which("reset").is_ok() { - Command::new("reset").status().await.into_sadness()?; - Ok(()) - } else { - Err(Failure::Fzf("Unable to clear screen".to_owned())) - } + run_fzf(abort, cmd, stream) } diff --git a/src/input.rs b/src/input.rs index a3ed148..0c64119 100644 --- a/src/input.rs +++ b/src/input.rs @@ -1,20 +1,24 @@ use super::argparse::Arguments; -use super::errors::{Failure, SadResult, SadnessFrom}; -use super::types::Task; +use super::types::{Abort, Fail}; use super::udiff::DiffRange; use async_channel::{bounded, Receiver}; +use futures::{ + future::{select, Either}, + pin_mut, +}; use regex::Regex; -use std::os::unix::ffi::OsStringExt; use std::{ collections::{HashMap, HashSet}, - convert::TryFrom, ffi::OsString, - path::PathBuf, + io::ErrorKind, + os::unix::ffi::OsStringExt, + path::{Path, PathBuf}, + sync::Arc, }; use tokio::{ fs::{canonicalize, File}, io::{self, AsyncBufReadExt, BufReader}, - task, + task::{spawn, JoinHandle}, }; #[derive(Debug)] @@ -23,82 +27,68 @@ pub enum Payload { Piecewise(PathBuf, HashSet<DiffRange>), } -impl Arguments { - pub fn stream(&self) -> (Task, Receiver<SadResult<Payload>>) { - if let Some(preview) = &self.internal_preview { - stream_patch(preview.clone()) - } else if let Some(patch) = &self.internal_patch { - stream_patch(patch.clone()) - } else { - stream_stdin(self.nul_delim) - } - } -} +struct DiffLine(PathBuf, DiffRange); -fn p_path(name: Vec<u8>) -> PathBuf { - PathBuf::from(OsString::from_vec(name)) -} +fn p_line(line: String) -> Result<DiffLine, Fail> { + let f = Fail::ArgumentError(String::new()); + let preg = "\n\n\n\n@@ -(\\d+),(\\d+) \\+(\\d+),(\\d+) @@$"; + let re = Regex::new(preg).map_err(Fail::RegexError)?; + let captures = re.captures(&line).ok_or_else(|| f.clone())?; -struct DiffLine(PathBuf, DiffRange); + let before_start = captures + .get(1) + .ok_or_else(|| f.clone())? + .as_str() + .parse::<usize>() + .map_err(|_| f.clone())?; + let before_inc = captures + .get(2) + .ok_or_e |