Added -j flag to 'complete' for completing job names/pids

This commit is contained in:
2026-02-27 11:03:56 -05:00
parent e141e39c7e
commit c508180228
44 changed files with 3259 additions and 2853 deletions

View File

@@ -36,12 +36,12 @@ pub mod vimode;
pub mod markers {
use super::Marker;
/*
* These are invisible Unicode characters used to annotate
* strings with various contextual metadata.
*/
/*
* These are invisible Unicode characters used to annotate
* strings with various contextual metadata.
*/
/* Highlight Markers */
/* Highlight Markers */
// token-level (derived from token class)
pub const COMMAND: Marker = '\u{e100}';
@@ -71,36 +71,36 @@ pub mod markers {
pub const ESCAPE: Marker = '\u{e116}';
pub const GLOB: Marker = '\u{e117}';
// other
pub const VISUAL_MODE_START: Marker = '\u{e118}';
pub const VISUAL_MODE_END: Marker = '\u{e119}';
// other
pub const VISUAL_MODE_START: Marker = '\u{e118}';
pub const VISUAL_MODE_END: Marker = '\u{e119}';
pub const RESET: Marker = '\u{e11a}';
pub const NULL: Marker = '\u{e11b}';
/* Expansion Markers */
/// Double quote '"' marker
pub const DUB_QUOTE: Marker = '\u{e001}';
/// Single quote '\\'' marker
pub const SNG_QUOTE: Marker = '\u{e002}';
/// Tilde sub marker
pub const TILDE_SUB: Marker = '\u{e003}';
/// Input process sub marker
pub const PROC_SUB_IN: Marker = '\u{e005}';
/// Output process sub marker
pub const PROC_SUB_OUT: Marker = '\u{e006}';
/// Marker for null expansion
/// This is used for when "$@" or "$*" are used in quotes and there are no
/// arguments Without this marker, it would be handled like an empty string,
/// which breaks some commands
pub const NULL_EXPAND: Marker = '\u{e007}';
/// Explicit marker for argument separation
/// This is used to join the arguments given by "$@", and preserves exact formatting
/// of the original arguments, including quoting
pub const ARG_SEP: Marker = '\u{e008}';
/* Expansion Markers */
/// Double quote '"' marker
pub const DUB_QUOTE: Marker = '\u{e001}';
/// Single quote '\\'' marker
pub const SNG_QUOTE: Marker = '\u{e002}';
/// Tilde sub marker
pub const TILDE_SUB: Marker = '\u{e003}';
/// Input process sub marker
pub const PROC_SUB_IN: Marker = '\u{e005}';
/// Output process sub marker
pub const PROC_SUB_OUT: Marker = '\u{e006}';
/// Marker for null expansion
/// This is used for when "$@" or "$*" are used in quotes and there are no
/// arguments Without this marker, it would be handled like an empty string,
/// which breaks some commands
pub const NULL_EXPAND: Marker = '\u{e007}';
/// Explicit marker for argument separation
/// This is used to join the arguments given by "$@", and preserves exact
/// formatting of the original arguments, including quoting
pub const ARG_SEP: Marker = '\u{e008}';
pub const VI_SEQ_EXP: Marker = '\u{e009}';
pub const VI_SEQ_EXP: Marker = '\u{e009}';
pub const END_MARKERS: [Marker; 7] = [
VAR_SUB_END,
@@ -116,10 +116,10 @@ pub mod markers {
];
pub const SUB_TOKEN: [Marker; 6] = [VAR_SUB, CMD_SUB, PROC_SUB, STRING_DQ, STRING_SQ, GLOB];
pub const MISC: [Marker; 3] = [ESCAPE, VISUAL_MODE_START, VISUAL_MODE_END];
pub const MISC: [Marker; 3] = [ESCAPE, VISUAL_MODE_START, VISUAL_MODE_END];
pub fn is_marker(c: Marker) -> bool {
('\u{e000}'..'\u{efff}').contains(&c)
('\u{e000}'..'\u{efff}').contains(&c)
}
}
type Marker = char;
@@ -135,66 +135,73 @@ pub enum ReadlineEvent {
}
pub struct Prompt {
ps1_expanded: String,
ps1_raw: String,
psr_expanded: Option<String>,
psr_raw: Option<String>,
ps1_expanded: String,
ps1_raw: String,
psr_expanded: Option<String>,
psr_raw: Option<String>,
}
impl Prompt {
const DEFAULT_PS1: &str = "\\e[0m\\n\\e[1;0m\\u\\e[1;36m@\\e[1;31m\\h\\n\\e[1;36m\\W\\e[1;32m/\\n\\e[1;32m\\$\\e[0m ";
pub fn new() -> Self {
let Ok(ps1_raw) = env::var("PS1") else {
return Self::default();
};
let Ok(ps1_expanded) = expand_prompt(&ps1_raw) else {
return Self::default();
};
let psr_raw = env::var("PSR").ok();
let psr_expanded = psr_raw.clone().map(|r| expand_prompt(&r)).transpose().ok().flatten();
Self {
ps1_expanded,
ps1_raw,
psr_expanded,
psr_raw,
}
}
const DEFAULT_PS1: &str =
"\\e[0m\\n\\e[1;0m\\u\\e[1;36m@\\e[1;31m\\h\\n\\e[1;36m\\W\\e[1;32m/\\n\\e[1;32m\\$\\e[0m ";
pub fn new() -> Self {
let Ok(ps1_raw) = env::var("PS1") else {
return Self::default();
};
let Ok(ps1_expanded) = expand_prompt(&ps1_raw) else {
return Self::default();
};
let psr_raw = env::var("PSR").ok();
let psr_expanded = psr_raw
.clone()
.map(|r| expand_prompt(&r))
.transpose()
.ok()
.flatten();
Self {
ps1_expanded,
ps1_raw,
psr_expanded,
psr_raw,
}
}
pub fn get_ps1(&self) -> &str {
&self.ps1_expanded
}
pub fn set_ps1(&mut self, ps1_raw: String) -> ShResult<()> {
self.ps1_expanded = expand_prompt(&ps1_raw)?;
self.ps1_raw = ps1_raw;
Ok(())
}
pub fn set_psr(&mut self, psr_raw: String) -> ShResult<()> {
self.psr_expanded = Some(expand_prompt(&psr_raw)?);
self.psr_raw = Some(psr_raw);
Ok(())
}
pub fn get_psr(&self) -> Option<&str> {
self.psr_expanded.as_deref()
}
pub fn get_ps1(&self) -> &str {
&self.ps1_expanded
}
pub fn set_ps1(&mut self, ps1_raw: String) -> ShResult<()> {
self.ps1_expanded = expand_prompt(&ps1_raw)?;
self.ps1_raw = ps1_raw;
Ok(())
}
pub fn set_psr(&mut self, psr_raw: String) -> ShResult<()> {
self.psr_expanded = Some(expand_prompt(&psr_raw)?);
self.psr_raw = Some(psr_raw);
Ok(())
}
pub fn get_psr(&self) -> Option<&str> {
self.psr_expanded.as_deref()
}
pub fn refresh(&mut self) -> ShResult<()> {
self.ps1_expanded = expand_prompt(&self.ps1_raw)?;
if let Some(psr_raw) = &self.psr_raw {
self.psr_expanded = Some(expand_prompt(psr_raw)?);
}
Ok(())
}
pub fn refresh(&mut self) -> ShResult<()> {
self.ps1_expanded = expand_prompt(&self.ps1_raw)?;
if let Some(psr_raw) = &self.psr_raw {
self.psr_expanded = Some(expand_prompt(psr_raw)?);
}
Ok(())
}
}
impl Default for Prompt {
fn default() -> Self {
Self {
ps1_expanded: expand_prompt(Self::DEFAULT_PS1).unwrap_or_else(|_| Self::DEFAULT_PS1.to_string()),
ps1_raw: Self::DEFAULT_PS1.to_string(),
psr_expanded: None,
psr_raw: None,
}
}
fn default() -> Self {
Self {
ps1_expanded: expand_prompt(Self::DEFAULT_PS1)
.unwrap_or_else(|_| Self::DEFAULT_PS1.to_string()),
ps1_raw: Self::DEFAULT_PS1.to_string(),
psr_expanded: None,
psr_raw: None,
}
}
}
pub struct ShedVi {
@@ -232,7 +239,7 @@ impl ShedVi {
history: History::new()?,
needs_redraw: true,
};
new.writer.flush_write("\n")?; // ensure we start on a new line, in case the previous command didn't end with a newline
new.writer.flush_write("\n")?; // ensure we start on a new line, in case the previous command didn't end with a newline
new.print_line(false)?;
Ok(new)
}
@@ -255,58 +262,55 @@ impl ShedVi {
self.needs_redraw = true;
}
/// Reset readline state for a new prompt
pub fn reset(&mut self, full_redraw: bool) -> ShResult<()> {
// Clear old display before resetting state — old_layout must survive
// so print_line can call clear_rows with the full multi-line layout
self.prompt = Prompt::new();
// Clear old display before resetting state — old_layout must survive
// so print_line can call clear_rows with the full multi-line layout
self.prompt = Prompt::new();
self.editor = Default::default();
self.mode = Box::new(ViInsert::new());
self.needs_redraw = true;
if full_redraw {
self.old_layout = None;
}
if full_redraw {
self.old_layout = None;
}
self.history.pending = None;
self.history.reset();
self.print_line(false)
self.print_line(false)
}
pub fn prompt(&self) -> &Prompt {
&self.prompt
}
pub fn prompt(&self) -> &Prompt {
&self.prompt
}
pub fn prompt_mut(&mut self) -> &mut Prompt {
&mut self.prompt
}
pub fn prompt_mut(&mut self) -> &mut Prompt {
&mut self.prompt
}
fn should_submit(&mut self) -> ShResult<bool> {
if self.mode.report_mode() == ModeReport::Normal {
return Ok(true);
}
let input = Arc::new(self.editor.buffer.clone());
self.editor.calc_indent_level();
let lex_result1 = LexStream::new(Arc::clone(&input), LexFlags::LEX_UNFINISHED).collect::<ShResult<Vec<_>>>();
let lex_result2 = LexStream::new(Arc::clone(&input), LexFlags::empty()).collect::<ShResult<Vec<_>>>();
let is_top_level = self.editor.auto_indent_level == 0;
fn should_submit(&mut self) -> ShResult<bool> {
if self.mode.report_mode() == ModeReport::Normal {
return Ok(true);
}
let input = Arc::new(self.editor.buffer.clone());
self.editor.calc_indent_level();
let lex_result1 =
LexStream::new(Arc::clone(&input), LexFlags::LEX_UNFINISHED).collect::<ShResult<Vec<_>>>();
let lex_result2 =
LexStream::new(Arc::clone(&input), LexFlags::empty()).collect::<ShResult<Vec<_>>>();
let is_top_level = self.editor.auto_indent_level == 0;
let is_complete = match (lex_result1.is_err(), lex_result2.is_err()) {
(true, true) => {
return Err(lex_result2.unwrap_err());
}
(true, false) => {
return Err(lex_result1.unwrap_err());
}
(false, true) => {
false
}
(false, false) => {
true
}
};
let is_complete = match (lex_result1.is_err(), lex_result2.is_err()) {
(true, true) => {
return Err(lex_result2.unwrap_err());
}
(true, false) => {
return Err(lex_result1.unwrap_err());
}
(false, true) => false,
(false, false) => true,
};
Ok(is_complete && is_top_level)
}
Ok(is_complete && is_top_level)
}
/// Process any available input and return readline event
/// This is non-blocking - returns Pending if no complete line yet
@@ -362,8 +366,8 @@ impl ShedVi {
self.editor.set_hint(hint);
}
None => {
self.writer.send_bell().ok();
},
self.writer.send_bell().ok();
}
}
self.needs_redraw = true;
@@ -385,9 +389,11 @@ impl ShedVi {
continue;
}
if cmd.is_submit_action() && (self.should_submit()? || !read_shopts(|o| o.prompt.linebreak_on_incomplete)) {
if cmd.is_submit_action()
&& (self.should_submit()? || !read_shopts(|o| o.prompt.linebreak_on_incomplete))
{
self.editor.set_hint(None);
self.editor.cursor.set(self.editor.cursor_max()); // Move the cursor to the very end
self.editor.cursor.set(self.editor.cursor_max()); // Move the cursor to the very end
self.print_line(true)?; // Redraw
self.writer.flush_write("\n")?;
let buf = self.editor.take_buf();
@@ -407,13 +413,13 @@ impl ShedVi {
return Ok(ReadlineEvent::Eof);
} else {
self.editor = LineBuf::new();
self.mode = Box::new(ViInsert::new());
self.mode = Box::new(ViInsert::new());
self.needs_redraw = true;
continue;
}
}
let has_edit_verb = cmd.verb().is_some_and(|v| v.1.is_edit());
let has_edit_verb = cmd.verb().is_some_and(|v| v.1.is_edit());
let before = self.editor.buffer.clone();
self.exec_cmd(cmd)?;
@@ -424,8 +430,8 @@ impl ShedVi {
.history
.update_pending_cmd((self.editor.as_str(), self.editor.cursor.get()));
} else if before == after && has_edit_verb {
self.writer.send_bell().ok(); // bell on no-op commands with a verb (e.g., 'x' on empty line)
}
self.writer.send_bell().ok(); // bell on no-op commands with a verb (e.g., 'x' on empty line)
}
let hint = self.history.get_hint();
self.editor.set_hint(hint);
@@ -462,21 +468,21 @@ impl ShedVi {
};
let entry = self.history.scroll(count);
if let Some(entry) = entry {
let editor = std::mem::take(&mut self.editor);
let editor = std::mem::take(&mut self.editor);
self.editor.set_buffer(entry.command().to_string());
if self.history.pending.is_none() {
self.history.pending = Some(editor);
}
self.editor.set_hint(None);
self.editor.move_cursor_to_end();
self.editor.move_cursor_to_end();
} else if let Some(pending) = self.history.pending.take() {
self.editor = pending;
} else {
// If we are here it should mean we are on our pending command
// And the user tried to scroll history down
// Since there is no "future" history, we should just bell and do nothing
self.writer.send_bell().ok();
}
// If we are here it should mean we are on our pending command
// And the user tried to scroll history down
// Since there is no "future" history, we should just bell and do nothing
self.writer.send_bell().ok();
}
}
pub fn should_accept_hint(&self, event: &KeyEvent) -> bool {
if self.editor.cursor_at_max() && self.editor.has_hint() {
@@ -512,7 +518,9 @@ impl ShedVi {
let line = self.editor.to_string();
let hint = self.editor.get_hint_text();
if crate::state::read_shopts(|s| s.prompt.highlight) {
self.highlighter.load_input(&line,self.editor.cursor_byte_pos());
self
.highlighter
.load_input(&line, self.editor.cursor_byte_pos());
self.highlighter.highlight();
let highlighted = self.highlighter.take();
format!("{highlighted}{hint}")
@@ -524,53 +532,84 @@ impl ShedVi {
pub fn print_line(&mut self, final_draw: bool) -> ShResult<()> {
let line = self.line_text();
let new_layout = self.get_layout(&line);
let pending_seq = self.mode.pending_seq();
let mut prompt_string_right = self.prompt.psr_expanded.clone();
let pending_seq = self.mode.pending_seq();
let mut prompt_string_right = self.prompt.psr_expanded.clone();
if prompt_string_right.as_ref().is_some_and(|psr| psr.lines().count() > 1) {
log::warn!("PSR has multiple lines, truncating to one line");
prompt_string_right = prompt_string_right.map(|psr| psr.lines().next().unwrap_or_default().to_string());
}
let row0_used = self.prompt
.get_ps1()
.lines()
.next()
.map(|l| Layout::calc_pos(self.writer.t_cols, l, Pos { col: 0, row: 0 }, 0))
.map(|p| p.col)
.unwrap_or_default() as usize;
let one_line = new_layout.end.row == 0;
if prompt_string_right
.as_ref()
.is_some_and(|psr| psr.lines().count() > 1)
{
log::warn!("PSR has multiple lines, truncating to one line");
prompt_string_right =
prompt_string_right.map(|psr| psr.lines().next().unwrap_or_default().to_string());
}
let row0_used = self
.prompt
.get_ps1()
.lines()
.next()
.map(|l| Layout::calc_pos(self.writer.t_cols, l, Pos { col: 0, row: 0 }, 0))
.map(|p| p.col)
.unwrap_or_default() as usize;
let one_line = new_layout.end.row == 0;
if let Some(layout) = self.old_layout.as_ref() {
self.writer.clear_rows(layout)?;
}
self.writer.redraw(self.prompt.get_ps1(), &line, &new_layout)?;
self
.writer
.redraw(self.prompt.get_ps1(), &line, &new_layout)?;
let seq_fits = pending_seq.as_ref().is_some_and(|seq| row0_used + 1 < self.writer.t_cols as usize - seq.width());
let psr_fits = prompt_string_right.as_ref().is_some_and(|psr| new_layout.end.col as usize + 1 < (self.writer.t_cols as usize).saturating_sub(psr.width()));
let seq_fits = pending_seq
.as_ref()
.is_some_and(|seq| row0_used + 1 < self.writer.t_cols as usize - seq.width());
let psr_fits = prompt_string_right.as_ref().is_some_and(|psr| {
new_layout.end.col as usize + 1 < (self.writer.t_cols as usize).saturating_sub(psr.width())
});
if !final_draw && let Some(seq) = pending_seq && !seq.is_empty() && !(prompt_string_right.is_some() && one_line) && seq_fits {
let to_col = self.writer.t_cols - calc_str_width(&seq);
let up = new_layout.cursor.row; // rows to move up from cursor to top line of prompt
if !final_draw
&& let Some(seq) = pending_seq
&& !seq.is_empty()
&& !(prompt_string_right.is_some() && one_line)
&& seq_fits
{
let to_col = self.writer.t_cols - calc_str_width(&seq);
let up = new_layout.cursor.row; // rows to move up from cursor to top line of prompt
let move_up = if up > 0 { format!("\x1b[{up}A") } else { String::new() };
let move_up = if up > 0 {
format!("\x1b[{up}A")
} else {
String::new()
};
// Save cursor, move up to top row, move right to column, write sequence, restore cursor
self.writer.flush_write(&format!("\x1b7{move_up}\x1b[{to_col}G{seq}\x1b8"))?;
} else if !final_draw && let Some(psr) = prompt_string_right && psr_fits {
let to_col = self.writer.t_cols - calc_str_width(&psr);
let down = new_layout.end.row - new_layout.cursor.row;
let move_down = if down > 0 { format!("\x1b[{down}B") } else { String::new() };
// Save cursor, move up to top row, move right to column, write sequence,
// restore cursor
self
.writer
.flush_write(&format!("\x1b7{move_up}\x1b[{to_col}G{seq}\x1b8"))?;
} else if !final_draw
&& let Some(psr) = prompt_string_right
&& psr_fits
{
let to_col = self.writer.t_cols - calc_str_width(&psr);
let down = new_layout.end.row - new_layout.cursor.row;
let move_down = if down > 0 {
format!("\x1b[{down}B")
} else {
String::new()
};
self.writer.flush_write(&format!("\x1b7{move_down}\x1b[{to_col}G{psr}\x1b8"))?;
}
self
.writer
.flush_write(&format!("\x1b7{move_down}\x1b[{to_col}G{psr}\x1b8"))?;
}
self.writer.flush_write(&self.mode.cursor_style())?;
self.old_layout = Some(new_layout);
self.needs_redraw = false;
self.needs_redraw = false;
Ok(())
}
@@ -853,19 +892,22 @@ pub fn get_insertions(input: &str) -> Vec<(usize, Marker)> {
/// - Unimplemented features (comments, brace groups)
pub fn marker_for(class: &TkRule) -> Option<Marker> {
match class {
TkRule::Pipe |
TkRule::ErrPipe |
TkRule::And |
TkRule::Or |
TkRule::Bg |
TkRule::BraceGrpStart |
TkRule::BraceGrpEnd => {
Some(markers::OPERATOR)
}
TkRule::Pipe
| TkRule::ErrPipe
| TkRule::And
| TkRule::Or
| TkRule::Bg
| TkRule::BraceGrpStart
| TkRule::BraceGrpEnd => Some(markers::OPERATOR),
TkRule::Sep => Some(markers::CMD_SEP),
TkRule::Redir => Some(markers::REDIRECT),
TkRule::Comment => Some(markers::COMMENT),
TkRule::Expanded { exp: _ } | TkRule::EOI | TkRule::SOI | TkRule::Null | TkRule::Str | TkRule::CasePattern => None,
TkRule::Expanded { exp: _ }
| TkRule::EOI
| TkRule::SOI
| TkRule::Null
| TkRule::Str
| TkRule::CasePattern => None,
}
}
@@ -880,9 +922,7 @@ pub fn annotate_token(token: Tk) -> Vec<(usize, Marker)> {
std::cmp::Ordering::Equal => {
let priority = |m: Marker| -> u8 {
match m {
markers::VISUAL_MODE_END
| markers::VISUAL_MODE_START
| markers::RESET => 0,
markers::VISUAL_MODE_END | markers::VISUAL_MODE_START | markers::RESET => 0,
markers::VAR_SUB
| markers::VAR_SUB_END
| markers::CMD_SUB
@@ -911,9 +951,7 @@ pub fn annotate_token(token: Tk) -> Vec<(usize, Marker)> {
std::cmp::Ordering::Equal => {
let priority = |m: Marker| -> u8 {
match m {
markers::VISUAL_MODE_END
| markers::VISUAL_MODE_START
| markers::RESET => 0,
markers::VISUAL_MODE_END | markers::VISUAL_MODE_START | markers::RESET => 0,
markers::VAR_SUB
| markers::VAR_SUB_END
| markers::CMD_SUB
@@ -926,7 +964,7 @@ pub fn annotate_token(token: Tk) -> Vec<(usize, Marker)> {
| markers::STRING_SQ_END
| markers::SUBSH_END => 2,
| markers::ARG => 3, // Lowest priority - processed first, overridden by sub-tokens
markers::ARG => 3, // Lowest priority - processed first, overridden by sub-tokens
_ => 1,
}
};
@@ -960,11 +998,11 @@ pub fn annotate_token(token: Tk) -> Vec<(usize, Marker)> {
insertions.push((token.span.start, markers::SUBSH));
return insertions;
} else if token.class == TkRule::CasePattern {
insertions.push((token.span.end, markers::RESET));
insertions.push((token.span.end - 1, markers::CASE_PAT));
insertions.push((token.span.start, markers::OPERATOR));
return insertions;
}
insertions.push((token.span.end, markers::RESET));
insertions.push((token.span.end - 1, markers::CASE_PAT));
insertions.push((token.span.start, markers::OPERATOR));
return insertions;
}
let token_raw = token.span.as_str();
let mut token_chars = token_raw.char_indices().peekable();
@@ -1144,17 +1182,17 @@ pub fn annotate_token(token: Tk) -> Vec<(usize, Marker)> {
}
}
'*' | '?' if (!in_dub_qt && !in_sng_qt) => {
let glob_ch = *ch;
let glob_ch = *ch;
token_chars.next(); // consume the first glob char
if !in_context(markers::COMMAND, &insertions) {
let offset = if glob_ch == '*' && token_chars.peek().is_some_and(|(_, c)| *c == '*') {
// it's one of these probably: ./dir/**/*.txt
token_chars.next(); // consume the second *
2
} else {
// just a regular glob char
1
};
let offset = if glob_ch == '*' && token_chars.peek().is_some_and(|(_, c)| *c == '*') {
// it's one of these probably: ./dir/**/*.txt
token_chars.next(); // consume the second *
2
} else {
// just a regular glob char
1
};
insertions.push((span_start + index + offset, markers::RESET));
insertions.push((span_start + index, markers::GLOB));
}