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:
-
-
-
-```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