From cdc9e7e266779592828cc1955869b1697ad9b503 Mon Sep 17 00:00:00 2001 From: pagedmov Date: Thu, 5 Mar 2026 13:34:34 -0500 Subject: [PATCH] fixed compound commands not working in pipelines improved pipe assignment logic to prevent potential resource leaks --- docs/my_prompt.md | 96 ------------------- examples/cool_prompt.sh | 200 ++++++++++++++++++++++++++++++++++++++++ nix/hm-module.nix | 2 +- src/main.rs | 2 +- src/parse/execute.rs | 125 +++++++++++++++---------- src/parse/mod.rs | 42 ++++----- src/procio.rs | 60 ++++++++++-- src/readline/mod.rs | 4 + 8 files changed, 353 insertions(+), 178 deletions(-) delete mode 100644 docs/my_prompt.md create mode 100644 examples/cool_prompt.sh diff --git a/docs/my_prompt.md b/docs/my_prompt.md deleted file mode 100644 index b646dce..0000000 --- a/docs/my_prompt.md +++ /dev/null @@ -1,96 +0,0 @@ -## Prompt example - -This is the `shed` code for the prompt that I currently use. Note that the scripting language for `shed` is essentially identical to bash. This prompt code uses the `\!` escape sequence which lets you use the output of a function as your prompt. - -Also note that in `shed`, the `echo` builtin has a new `-p` flag which expands prompt escape sequences. This allows you to access these escape sequences in any context. - -The end result is the prompt that appears in the README: - -shed - -```bash -prompt_topline() { - local user_and_host="\e[0m\e[1m$USER\e[1;36m@\e[1;31m$HOST\e[0m" - echo -n "\e[1;34m┏━ $user_and_host\n" -} - -prompt_stat_line() { - local last_exit_code="$?" - local last_cmd_status - local last_cmd_runtime - if [ "$last_exit_code" -eq "0" ]; then - last_cmd_status="\e[1;32m\e[0m" - else - last_cmd_status="\e[1;31m\e[0m" - fi - local last_runtime_raw="$(echo -p "\t")" - if [ -z "$last_runtime_raw" ]; then - return 0 - else - last_cmd_runtime="\e[1;38;2;249;226;175m󰔛 $(echo -p "\T")\e[0m" - fi - - echo -n "\e[1;34m┃ $last_cmd_runtime ($last_cmd_status)\n" -} - -prompt_git_line() { - git rev-parse --is-inside-work-tree > /dev/null 2>&1 || return - - local gitsigns - local status="$(git status --porcelain 2>/dev/null)" - local branch="$(git branch --show-current 2>/dev/null)" - - [ -n "$status" ] && echo "$status" | command grep -q '^ [MADR]' && gitsigns="$gitsigns!" - [ -n "$status" ] && echo "$status" | command grep -q '^??' && gitsigns="$gitsigns?" - [ -n "$status" ] && echo "$status" | command grep -q '^[MADR]' && gitsigns="$gitsigns+" - - local ahead="$(git rev-list --count @{upstream}..HEAD 2>/dev/null)" - local behind="$(git rev-list --count HEAD..@{upstream} 2>/dev/null)" - [ $ahead -gt 0 ] && gitsigns="$gitsigns↑" - [ $behind -gt 0 ] && gitsigns="$gitsigns↓" - - if [ -n "$gitsigns" ] || [ -n "$branch" ]; then - if [ -n "$gitsigns" ]; then - gitsigns="\e[1;31m[$gitsigns]" - fi - echo -n "\e[1;34m┃ \e[1;35m ${branch}$gitsigns\e[0m\n" - fi -} - -prompt_jobs_line() { - local job_count="$(echo -p '\j')" - if [ "$job_count" -gt 0 ]; then - echo -n "\e[1;34m┃ \e[1;33m󰒓 $job_count job(s) running\e[0m\n" - fi -} - -prompt_ssh_line() { - local ssh_server="$(echo $SSH_CONNECTION | cut -f3 -d' ')" - [ -n "$ssh_server" ] && echo -n "\e[1;34m┃ \e[1;39m🌐 $ssh_server\e[0m\n" -} - -prompt_pwd_line() { - echo -p "\e[1;34m┣━━ \e[1;36m\W\e[1;32m/" -} - -prompt_dollar_line() { - local dollar="$(echo -p "\$ ")" - local dollar="$(echo -e "\e[1;32m$dollar\e[0m")" - echo -n "\e[1;34m┗━ $dollar " -} - -prompt() { - local statline="$(prompt_stat_line)" - local topline="$(prompt_topline)" - local gitline="$(prompt_git_line)" - local jobsline="$(prompt_jobs_line)" - local sshline="$(prompt_ssh_line)" - local pwdline="$(prompt_pwd_line)" - local dollarline="$(prompt_dollar_line)" - local prompt="$topline$statline$gitline$jobsline$sshline$pwdline\n$dollarline" - - echo -en "$prompt" -} - -export PS1="\!prompt " -``` diff --git a/examples/cool_prompt.sh b/examples/cool_prompt.sh new file mode 100644 index 0000000..b12d260 --- /dev/null +++ b/examples/cool_prompt.sh @@ -0,0 +1,200 @@ +# This is the code for the prompt I currently use +# It makes use of the '\@funcname' function expansion escape sequence +# and the '-p' flag for echo which expands prompt escape sequences +# +# The final product looks like this: +# ┏━ user@hostname INSERT +# ┃ 󰔛 1ms +# ┃  main[!?] ~1 +1 -1 +# ┃ 󰒓 1 job(s) running +# ┣━━ ~/path/to/pwd/ +# ┗━ $ $shed 0.5.0 (x86_64 linux) +# (The vi mode indicator is styled to match the color of the separators) + +prompt() { + local statline="$(prompt_stat_line)" + local topline="$(prompt_topline)" + local jobsline="$(prompt_jobs_line)" + local sshline="$(prompt_ssh_line)" + local pwdline="$(prompt_pwd_line)" + local dollarline="$(prompt_dollar_line)" + local prompt="$topline$statline$PROMPT_GIT_LINE$jobsline$sshline$pwdline\n$dollarline" + + echo -en "$prompt" + +} +prompt_dollar_line() { + local dollar="$(echo -p "\$ ")" + local dollar="$(echo -e "\e[1;32m$dollar\e[0m")" + echo -n "\e[1;34m┗━ $dollar " + +} +prompt_git_line() { + # git is really expensive so we've gotta make these calls count + + # get the status + local status="$(git status --porcelain -b 2>/dev/null)" || return + + local branch="" gitsigns="" ahead=0 behind=0 + # split at the first linebreak + local header="${status%%$'\n'*}" + + # cut the '## ' prefix + branch="${header#\#\# }" + # cut the '..' suffix + branch="${branch%%...*}" + + # parse ahead/behind counts + case "$header" in + *ahead*) ahead="${header#*ahead }"; ahead="${ahead%%[],]*}"; gitsigns="${gitsigns}↑" ;; + esac + case "$header" in + *behind*) behind="${header#*behind }"; behind="${behind%%[],]*}"; gitsigns="${gitsigns}↓" ;; + esac + + # grab gitsigns + case "$status" in + # unstaged changes + *$'\n'" "[MAR]*) gitsigns="${gitsigns}!" ;; + esac + case "$status" in + # untracked files + *$'\n'"??"*) gitsigns="${gitsigns}?" ;; + esac + case "$status" in + # deleted files + *$'\n'" "[D]*) gitsigns="${gitsigns}" ;; + esac + case "$status" in + # staged changes + *$'\n'[MADR]*) gitsigns="${gitsigns}+" ;; + esac + + # unfortunately we need one more git fork + local diff="$(git diff --shortstat 2>/dev/null)" + + local changed="" add="" del="" + if [ -n "$diff" ]; then + changed="${diff%% file*}"; changed="${changed##* }" + case "$diff" in + *insertion*) add="${diff#*, }"; add="${add%% *}" ;; + esac + case "$diff" in + *deletion*) del="${diff% deletion*}"; del="${del##* }" ;; + esac + fi + + if [ -n "$gitsigns" ] || [ -n "$branch" ]; then + # style gitsigns if not empty + [ -n "$gitsigns" ] && gitsigns="\e[1;31m[$gitsigns]" + # style changed/deleted/added text + [ -n "$changed" ] && [ "$changed" -gt 0 ] && changed="\e[1;34m~$changed \e[0m" + [ -n "$add" ] && [ "$add" -gt 0 ] && add="\e[1;32m+$add \e[0m" + [ -n "$del" ] && [ "$del" -gt 0 ] && del="\e[1;31m-$del\e[0m" + + # echo the final product + echo -n "\e[1;34m┃ \e[1;35m $branch$gitsigns\e[0m $changed$add$del\n" + fi + +} +prompt_jobs_line() { + local job_count="$(echo -p '\j')" + if [ "$job_count" -gt 0 ]; then + echo -n "\e[1;34m┃ \e[1;33m󰒓 $job_count job(s) running\e[0m\n" + fi + +} +prompt_mode() { + local mode="" + local normal_fg='\e[0m\e[30m\e[1;43m' + local normal_bg='\e[0m\e[33m' + local insert_fg='\e[0m\e[30m\e[1;46m' + local insert_bg='\e[0m\e[36m' + local command_fg='\e[0m\e[30m\e[1;42m' + local command_bg='\e[0m\e[32m' + local visual_fg='\e[0m\e[30m\e[1;45m' + local visual_bg='\e[0m\e[35m' + local replace_fg='\e[0m\e[30m\e[1;41m' + local replace_bg='\e[0m\e[31m' + local search_fg='\e[0m\e[30m\e[1;47m' + local search_bg='\e[0m\e[39m' + local complete_fg='\e[0m\e[30m\e[1;47m' + local complete_bg='\e[0m\e[39m' + + # shed exposes it's current vi mode as a variable + case "$SHED_VI_MODE" in + "NORMAL") + mode="$normal_bg${normal_fg}NORMAL$normal_bg\e[0m" + ;; + "INSERT") + mode="$insert_bg${insert_fg}INSERT$insert_bg\e[0m" + ;; + "COMMAND") + mode="$command_bg${command_fg}COMMAND$command_bg\e[0m" + ;; + "VISUAL") + mode="$visual_bg${visual_fg}VISUAL$visual_bg\e[0m" + ;; + "REPLACE") + mode="$replace_bg${replace_fg}REPLACE$replace_bg\e[0m" + ;; + "VERBATIM") + mode="$replace_bg${replace_fg}VERBATIM$replace_bg\e[0m" + ;; + "COMPLETE") + mode="$complete_bg${complete_fg}COMPLETE$complete_bg\e[0m" + ;; + "SEARCH") + mode="$search_bg${search_fg}SEARCH$search_bg\e[0m" + ;; + *) + mode="" + ;; + esac + + echo -en "$mode\n" + +} +prompt_pwd_line() { + # the -p flag exposes prompt escape sequences like '\W' + echo -p "\e[1;34m┣━━ \e[1;36m\W\e[1;32m/" + +} +prompt_ssh_line() { + local ssh_server="$(echo $SSH_CONNECTION | cut -f3 -d' ')" + [ -n "$ssh_server" ] && echo -n "\e[1;34m┃ \e[1;39m🌐 $ssh_server\e[0m\n" + +} +prompt_stat_line() { + local last_exit_code="$?" + local last_cmd_status + local last_cmd_runtime + if [ "$last_exit_code" -eq "0" ]; then + last_cmd_status="\e[1;32m" + else + last_cmd_status="\e[1;31m" + fi + local last_runtime_raw="$(echo -p "\t")" + if [ -z "$last_runtime_raw" ]; then + return 0 + else + last_cmd_runtime="\e[1;38;2;249;226;175m󰔛 ${last_cmd_status}$(echo -p "\T")\e[0m" + fi + + echo -n "\e[1;34m┃ $last_cmd_runtime\e[0m\n" + +} +prompt_topline() { + local user_and_host="\e[0m\e[1m$USER\e[1;36m@\e[1;31m$HOST\e[0m" + local mode_text="$(prompt_mode)" + echo -n "\e[1;34m┏━ $user_and_host $mode_text\n" + +} +shed_ver() { + shed --version + +} + +export PS1="\@prompt " +# PSR is the text that expands on the right side of the prompt +export PSR='\e[36;1m$\@shed_ver\e[0m' diff --git a/nix/hm-module.nix b/nix/hm-module.nix index 4ccfcb0..be96042 100644 --- a/nix/hm-module.nix +++ b/nix/hm-module.nix @@ -126,7 +126,7 @@ in }; }; }); - default = {}; + default = []; description = "Custom keymaps to set when shed starts"; }; diff --git a/src/main.rs b/src/main.rs index 22dfd4e..0cc32fa 100644 --- a/src/main.rs +++ b/src/main.rs @@ -216,7 +216,7 @@ fn shed_interactive(args: ShedArgs) -> ShResult<()> { if let Err(e) = check_signals() { match e.kind() { ShErrKind::ClearReadline => { - // Ctrl+C - clear current input and redraw + // We got Ctrl+C - clear current input and redraw readline.reset_active_widget(false)?; } ShErrKind::CleanExit(code) => { diff --git a/src/parse/execute.rs b/src/parse/execute.rs index 8a3bec6..37dd8e8 100644 --- a/src/parse/execute.rs +++ b/src/parse/execute.rs @@ -39,7 +39,7 @@ use crate::{ utils::RedirVecUtils, }, prelude::*, - procio::{IoMode, IoStack}, + procio::{IoMode, IoStack, PipeGenerator}, state::{ self, ShFunc, VarFlags, VarKind, read_logic, read_shopts, write_jobs, write_logic, write_vars, }, @@ -319,22 +319,17 @@ impl Dispatcher { }; let name = self.source_name.clone(); + self.io_stack.append_to_frame(subsh.redirs); + let _guard = self.io_stack.pop_frame().redirect()?; + self.run_fork("anonymous_subshell", |s| { if let Err(e) = s.set_assignments(assignments, AssignBehavior::Export) { e.print_error(); return; }; - s.io_stack.append_to_frame(subsh.redirs); - let mut argv = match prepare_argv(argv) { - Ok(argv) => argv, - Err(e) => { - e.try_blame(blame).print_error(); - return; - } - }; - let subsh = argv.remove(0); - let subsh_body = subsh.0.to_string(); + let subsh_raw = argv[0].span.as_str(); + let subsh_body = subsh_raw[1..subsh_raw.len() - 1].to_string(); // Remove surrounding parentheses if let Err(e) = exec_input(subsh_body, None, s.interactive, Some(name)) { e.print_error(); @@ -385,7 +380,7 @@ impl Dispatcher { func_body.body_mut().propagate_context(func_ctx); func_body.body_mut().flags = func.flags; - if let Err(e) = self.exec_brc_grp(func_body.body().clone()) { + if let Err(e) = self.exec_pipeline(func_body.body().clone()) { match e.kind() { ShErrKind::FuncReturn(code) => { state::set_status(*code); @@ -409,11 +404,15 @@ impl Dispatcher { } fn exec_brc_grp(&mut self, brc_grp: Node) -> ShResult<()> { let NdRule::BraceGrp { body } = brc_grp.class else { - unreachable!() + unreachable!("expected BraceGrp node, got {:?}", brc_grp.class) }; + if self.interactive { + log::debug!("Executing brace group, body: {:?}", body); + } let fork_builtins = brc_grp.flags.contains(NdFlags::FORK_BUILTINS); self.io_stack.append_to_frame(brc_grp.redirs); + if self.interactive {} let guard = self.io_stack.pop_frame().redirect()?; let brc_grp_logic = |s: &mut Self| -> ShResult<()> { for node in body { @@ -705,40 +704,14 @@ impl Dispatcher { let NdRule::Pipeline { cmds } = pipeline.class else { unreachable!() }; - self.job_stack.new_job(); - let fork_builtin = cmds.len() > 1; // If there's more than one command, we need to fork builtins - let (mut in_redirs, mut out_redirs) = self.io_stack.pop_frame().redirs.split_by_channel(); - - // Zip the commands and their respective pipes into an iterator - let pipes_and_cmds = get_pipe_stack(cmds.len()).into_iter().zip(cmds); - + if self.interactive { + log::debug!("Executing pipeline, cmds: {:#?}", cmds); + } let is_bg = pipeline.flags.contains(NdFlags::BACKGROUND); - self.fg_job = !is_bg && self.interactive; - let mut tty_attached = false; - - for ((rpipe, wpipe), mut cmd) in pipes_and_cmds { - if let Some(pipe) = rpipe { - self.io_stack.push_to_frame(pipe); - } else { - for redir in std::mem::take(&mut in_redirs) { - self.io_stack.push_to_frame(redir); - } - } - if let Some(pipe) = wpipe { - self.io_stack.push_to_frame(pipe); - if cmd.flags.contains(NdFlags::PIPE_ERR) { - let err_redir = Redir::new(IoMode::Fd { tgt_fd: STDERR_FILENO, src_fd: STDOUT_FILENO }, RedirType::Output); - self.io_stack.push_to_frame(err_redir); - } - } else { - for redir in std::mem::take(&mut out_redirs) { - self.io_stack.push_to_frame(redir); - } - } - - if fork_builtin { - cmd.flags |= NdFlags::FORK_BUILTINS; - } + self.job_stack.new_job(); + if cmds.len() == 1 { + self.fg_job = !is_bg && self.interactive; + let cmd = cmds.into_iter().next().unwrap(); self.dispatch_node(cmd)?; // Give the pipeline terminal control as soon as the first child @@ -746,13 +719,57 @@ impl Dispatcher { // SIGTTOU when they try to modify terminal attributes. // Only for interactive (top-level) pipelines — command substitution // and other non-interactive contexts must not steal the terminal. - if !tty_attached - && !is_bg + if !is_bg && self.interactive && let Some(pgid) = self.job_stack.curr_job_mut().unwrap().pgid() { attach_tty(pgid).ok(); - tty_attached = true; + } + } else { + let (mut in_redirs, mut out_redirs) = self.io_stack.pop_frame().redirs.split_by_channel(); + + let mut pipes = PipeGenerator::new(cmds.len()).as_io_frames(); + + self.fg_job = !is_bg && self.interactive; + let mut tty_attached = false; + + let last_cmd = cmds.len() - 1; + for (i, mut cmd) in cmds.into_iter().enumerate() { + let mut frame = pipes.next().ok_or_else(|| { + ShErr::at( + ShErrKind::InternalErr, + cmd.get_span(), + "failed to set up pipeline redirections".to_string(), + ) + })?; + if i == 0 { + for redir in std::mem::take(&mut in_redirs) { + frame.push(redir); + } + } else if i == last_cmd { + for redir in std::mem::take(&mut out_redirs) { + frame.push(redir); + } + } + + let _guard = frame.redirect()?; + + cmd.flags |= NdFlags::FORK_BUILTINS; // multiple cmds means builtins must fork + self.dispatch_node(cmd)?; + + // Give the pipeline terminal control as soon as the first child + // establishes the PGID, so later children (e.g. nvim) don't get + // SIGTTOU when they try to modify terminal attributes. + // Only for interactive (top-level) pipelines — command substitution + // and other non-interactive contexts must not steal the terminal. + if !tty_attached + && !is_bg + && self.interactive + && let Some(pgid) = self.job_stack.curr_job_mut().unwrap().pgid() + { + attach_tty(pgid).ok(); + tty_attached = true; + } } } let job = self.job_stack.finalize_job().unwrap(); @@ -818,7 +835,15 @@ impl Dispatcher { // Set up redirections here so we can attach the guard to propagated errors. self.io_stack.append_to_frame(mem::take(&mut cmd.redirs)); - let redir_guard = self.io_stack.pop_frame().redirect()?; + let frame = self.io_stack.pop_frame(); + if self.interactive { + log::debug!( + "popped frame for builtin '{}', frame: {:#?}", + cmd_raw, + frame + ); + } + let redir_guard = frame.redirect()?; // Register ChildProc in current job let job = self.job_stack.curr_job_mut().unwrap(); diff --git a/src/parse/mod.rs b/src/parse/mod.rs index 7f3056d..accbcd1 100644 --- a/src/parse/mod.rs +++ b/src/parse/mod.rs @@ -87,7 +87,6 @@ impl ParsedSrc { Err(error) => return Err(vec![error]), } } - log::debug!("Tokens: {:#?}", tokens); let mut errors = vec![]; let mut nodes = vec![]; @@ -214,9 +213,7 @@ impl Node { assign_node.walk_tree(f); } } - NdRule::Pipeline { - ref mut cmds, - } => { + NdRule::Pipeline { ref mut cmds } => { for cmd_node in cmds { cmd_node.walk_tree(f); } @@ -271,7 +268,7 @@ bitflags! { const FORK_BUILTINS = 0b000010; const NO_FORK = 0b000100; const ARR_ASSIGN = 0b001000; - const PIPE_ERR = 0b010000; + const PIPE_ERR = 0b010000; } } @@ -642,7 +639,7 @@ pub enum NdRule { argv: Vec, }, Pipeline { - cmds: Vec + cmds: Vec, }, Conjunction { elements: Vec, @@ -778,16 +775,16 @@ impl ParseStream { /// appearing at the bottom The check_pipelines parameter is used to prevent /// left-recursion issues in self.parse_pipeln() fn parse_block(&mut self, check_pipelines: bool) -> ShResult> { - try_match!(self.parse_func_def()?); - try_match!(self.parse_brc_grp(false /* from_func_def */)?); - try_match!(self.parse_case()?); - try_match!(self.parse_loop()?); - try_match!(self.parse_for()?); - try_match!(self.parse_if()?); - try_match!(self.parse_test()?); if check_pipelines { try_match!(self.parse_pipeln()?); } else { + try_match!(self.parse_func_def()?); + try_match!(self.parse_brc_grp(false /* from_func_def */)?); + try_match!(self.parse_case()?); + try_match!(self.parse_loop()?); + try_match!(self.parse_for()?); + try_match!(self.parse_if()?); + try_match!(self.parse_test()?); try_match!(self.parse_cmd()?); } Ok(None) @@ -1448,16 +1445,17 @@ impl ParseStream { while let Some(mut cmd) = self.parse_block(false)? { let is_punctuated = node_is_punctuated(&cmd.tokens); node_tks.append(&mut cmd.tokens.clone()); - if *self.next_tk_class() == TkRule::ErrPipe { - cmd.flags |= NdFlags::PIPE_ERR; - } + if *self.next_tk_class() == TkRule::ErrPipe { + cmd.flags |= NdFlags::PIPE_ERR; + } cmds.push(cmd); if *self.next_tk_class() == TkRule::Bg { let tk = self.next_tk().unwrap(); node_tks.push(tk.clone()); flags |= NdFlags::BACKGROUND; break; - } else if (!matches!(*self.next_tk_class(),TkRule::Pipe | TkRule::ErrPipe)) || is_punctuated { + } else if (!matches!(*self.next_tk_class(), TkRule::Pipe | TkRule::ErrPipe)) || is_punctuated + { break; } else if let Some(pipe) = self.next_tk() { node_tks.push(pipe) @@ -1470,9 +1468,7 @@ impl ParseStream { } else { Ok(Some(Node { // TODO: implement pipe_err support - class: NdRule::Pipeline { - cmds, - }, + class: NdRule::Pipeline { cmds }, flags, redirs: vec![], context: self.context.clone(), @@ -1555,7 +1551,7 @@ impl ParseStream { match tk.class { TkRule::EOI | TkRule::Pipe - | TkRule::ErrPipe + | TkRule::ErrPipe | TkRule::And | TkRule::BraceGrpEnd | TkRule::Or @@ -1867,9 +1863,7 @@ where check_node(assign_node, filter, operation); } } - NdRule::Pipeline { - ref mut cmds, - } => { + NdRule::Pipeline { ref mut cmds } => { for cmd_node in cmds { check_node(cmd_node, filter, operation); } diff --git a/src/procio.rs b/src/procio.rs index 6915bf4..f9bf592 100644 --- a/src/procio.rs +++ b/src/procio.rs @@ -1,5 +1,6 @@ use std::{ fmt::Debug, + iter::Map, ops::{Deref, DerefMut}, }; @@ -220,12 +221,6 @@ impl<'e> IoFrame { let tgt_fd = io_mode.tgt_fd(); let src_fd = io_mode.src_fd(); dup2(src_fd, tgt_fd)?; - // Close the original pipe fd after dup2 — it's been duplicated to - // tgt_fd and keeping it open prevents SIGPIPE delivery in pipelines. - // We replace the IoMode to drop the Arc, which closes the fd. - if matches!(io_mode, IoMode::Pipe { .. }) { - *io_mode = IoMode::Close { tgt_fd }; - } } Ok(RedirGuard::new(self)) } @@ -337,3 +332,56 @@ impl From> for IoStack { pub fn borrow_fd<'f>(fd: i32) -> BorrowedFd<'f> { unsafe { BorrowedFd::borrow_raw(fd) } } + +pub struct PipeGenerator { + num_cmds: usize, + cursor: usize, + last_rpipe: Option, +} + +impl PipeGenerator { + pub fn new(num_cmds: usize) -> Self { + Self { + num_cmds, + cursor: 0, + last_rpipe: None, + } + } + pub fn as_io_frames(self) -> Map, Option)) -> IoFrame> { + self.map(|(r, w)| { + let mut frame = IoFrame::new(); + if let Some(r) = r { + frame.push(r); + } + if let Some(w) = w { + frame.push(w); + } + frame + }) + } +} + +impl Iterator for PipeGenerator { + type Item = (Option, Option); + fn next(&mut self) -> Option { + if self.cursor == self.num_cmds { + return None; + } + if self.cursor + 1 == self.num_cmds { + if self.num_cmds == 1 { + return None; + } else { + self.cursor += 1; + return Some((self.last_rpipe.take(), None)); + } + } + let (r, w) = IoMode::get_pipes(); + let mut rpipe = Some(Redir::new(r, RedirType::Input)); + std::mem::swap(&mut self.last_rpipe, &mut rpipe); + + let wpipe = Redir::new(w, RedirType::Output); + + self.cursor += 1; + Some((rpipe, Some(wpipe))) + } +} diff --git a/src/readline/mod.rs b/src/readline/mod.rs index 77c1768..ddfe350 100644 --- a/src/readline/mod.rs +++ b/src/readline/mod.rs @@ -639,6 +639,10 @@ impl ShedVi { .update_pending_cmd((self.editor.as_str(), self.editor.cursor.get())); let hint = self.history.get_hint(); self.editor.set_hint(hint); + + // If we are here, we hit a case where pressing tab returned a single candidate + // So we can just go ahead and reset the completer after this + self.completer.reset(); } Ok(None) => { let post_cmds = read_logic(|l| l.get_autocmds(AutoCmdKind::OnCompletionStart));