summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authori love my dog <50598611+ms-jpq@users.noreply.github.com>2021-08-14 09:18:22 -0700
committerGitHub <noreply@github.com>2021-08-14 09:18:22 -0700
commit6a35c6599c5546a03720774919e9dd2569541657 (patch)
tree10466290f325dd669e6c15508a2b3e6cdcfe6b27
parentf1afb5e46642d52eaaaf2eaf6df5ebad6fa9ca62 (diff)
parentb8131287b6b15ae80cd10ce483049132cd9be841 (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.yml4
-rw-r--r--Cargo.lock2
-rw-r--r--Cargo.toml8
-rw-r--r--ci/linux/Dockerfile6
-rw-r--r--release_notes.md6
-rw-r--r--src/argparse.rs144
-rw-r--r--src/displace.rs76
-rw-r--r--src/errors.rs121
-rw-r--r--src/fs_pipe.rs74
-rw-r--r--src/fzf.rs255
-rw-r--r--src/input.rs229
-rw-r--r--src/main.rs134
-rw-r--r--src/output.rs79
-rw-r--r--src/subprocess.rs123
-rw-r--r--src/types.rs80
-rw-r--r--src/udiff.rs150
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: |-
diff --git a/Cargo.lock b/Cargo.lock
index 617936a..3ba9b14 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -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",
diff --git a/Cargo.toml b/Cargo.toml
index 0740055..7bdb905 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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(())
}
diff --git a/src/fzf.rs b/src/fzf.rs
index a1fa27d..c2c0249 100644
--- a/src/fzf.rs
+++ b/src/fzf.rs
@@ -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