Bump to 0.6.1: add Ctrl-D/U half-screen scrolling, Ctrl-G position info, and status messages

This commit is contained in:
2026-03-20 18:44:43 -04:00
parent 939888e579
commit 4b07990fc5
12 changed files with 204 additions and 34 deletions

2
Cargo.lock generated
View File

@@ -573,7 +573,7 @@ dependencies = [
[[package]] [[package]]
name = "shed" name = "shed"
version = "0.6.0" version = "0.6.1"
dependencies = [ dependencies = [
"ariadne", "ariadne",
"bitflags", "bitflags",

View File

@@ -2,7 +2,7 @@
name = "shed" name = "shed"
description = "A linux shell written in rust" description = "A linux shell written in rust"
publish = false publish = false
version = "0.6.0" version = "0.6.1"
edition = "2024" edition = "2024"

View File

@@ -14,7 +14,7 @@
{ {
packages.default = pkgs.rustPlatform.buildRustPackage { packages.default = pkgs.rustPlatform.buildRustPackage {
pname = "shed"; pname = "shed";
version = "0.6.0"; version = "0.6.1";
src = self; src = self;

View File

@@ -734,7 +734,7 @@ pub trait Completer {
let (s, e) = self.token_span(); let (s, e) = self.token_span();
orig.get(s..e).unwrap_or(orig) orig.get(s..e).unwrap_or(orig)
} }
fn draw(&mut self, writer: &mut TermWriter) -> ShResult<()>; fn draw(&mut self, writer: &mut TermWriter) -> ShResult<usize>;
fn clear(&mut self, _writer: &mut TermWriter) -> ShResult<()> { fn clear(&mut self, _writer: &mut TermWriter) -> ShResult<()> {
Ok(()) Ok(())
} }
@@ -1122,9 +1122,9 @@ impl FuzzySelector {
} }
} }
pub fn draw(&mut self, writer: &mut TermWriter) -> ShResult<()> { pub fn draw(&mut self, writer: &mut TermWriter) -> ShResult<usize> {
if !self.active { if !self.active {
return Ok(()); return Ok(0);
} }
let (cols, _) = get_win_size(*TTY_FILENO); let (cols, _) = get_win_size(*TTY_FILENO);
@@ -1272,7 +1272,7 @@ impl FuzzySelector {
writer.flush_write(&buf)?; writer.flush_write(&buf)?;
self.old_layout = Some(new_layout); self.old_layout = Some(new_layout);
Ok(()) Ok(rows as usize)
} }
pub fn clear(&mut self, writer: &mut TermWriter) -> ShResult<()> { pub fn clear(&mut self, writer: &mut TermWriter) -> ShResult<()> {
@@ -1421,7 +1421,7 @@ impl Completer for FuzzyCompleter {
fn clear(&mut self, writer: &mut TermWriter) -> ShResult<()> { fn clear(&mut self, writer: &mut TermWriter) -> ShResult<()> {
self.selector.clear(writer) self.selector.clear(writer)
} }
fn draw(&mut self, writer: &mut TermWriter) -> ShResult<()> { fn draw(&mut self, writer: &mut TermWriter) -> ShResult<usize> {
self.selector.draw(writer) self.selector.draw(writer)
} }
fn reset(&mut self) { fn reset(&mut self) {
@@ -1497,8 +1497,8 @@ impl Completer for SimpleCompleter {
self.token_span self.token_span
} }
fn draw(&mut self, _writer: &mut TermWriter) -> ShResult<()> { fn draw(&mut self, _writer: &mut TermWriter) -> ShResult<usize> {
Ok(()) Ok(0)
} }
fn original_input(&self) -> &str { fn original_input(&self) -> &str {

View File

@@ -138,6 +138,13 @@ pub fn split_lines_at(lines: &mut Vec<Line>, pos: Pos) -> Vec<Line> {
rest rest
} }
pub fn split_lines(mut lines: Vec<Line>, pos: Pos) -> (Vec<Line>,Vec<Line>) {
let tail = lines[pos.row].split_off(pos.col);
let mut rest: Vec<Line> = lines.drain(pos.row + 1..).collect();
rest.insert(0, tail);
(lines, rest)
}
pub fn attach_lines(lines: &mut Vec<Line>, other: &mut Vec<Line>) { pub fn attach_lines(lines: &mut Vec<Line>, other: &mut Vec<Line>) {
if other.is_empty() { if other.is_empty() {
return; return;
@@ -1860,6 +1867,16 @@ impl LineBuf {
let (s, e) = ordered(*s, *e); let (s, e) = ordered(*s, *e);
Some(MotionKind::Block { start: s, end: e }) Some(MotionKind::Block { start: s, end: e })
} }
dir @ (Motion::HalfScreenUp | Motion::HalfScreenDown) => {
let off = match dir {
Motion::HalfScreenUp => -(self.get_viewport_height() as isize / 2),
Motion::HalfScreenDown => self.get_viewport_height() as isize / 2,
_ => unreachable!(),
};
let row = self.row();
let target_row = self.offset_row(off);
Some(MotionKind::Line { start: target_row, end: row, inclusive: false })
}
Motion::RepeatMotion | Motion::RepeatMotion |
Motion::RepeatMotionRev => unreachable!("Repeat motions should have been resolved in readline/mod.rs"), Motion::RepeatMotionRev => unreachable!("Repeat motions should have been resolved in readline/mod.rs"),
Motion::Global(val) | Motion::Global(val) |
@@ -1907,7 +1924,6 @@ impl LineBuf {
Ok(()) Ok(())
} }
fn extract_range(&mut self, motion: &MotionKind) -> Vec<Line> { fn extract_range(&mut self, motion: &MotionKind) -> Vec<Line> {
log::debug!("Extracting range for motion: {:?}", motion);
let extracted = match motion { let extracted = match motion {
MotionKind::Char { MotionKind::Char {
start, start,
@@ -2484,9 +2500,31 @@ impl LineBuf {
} }
} }
Verb::EndOfFile => {
self.lines.clear();
}
Verb::PrintPosition => {
let num_lines = self.lines.len();
let row = self.row() + 1;
let col = self.col() + 1;
let total_graphemes = self.count_graphemes();
let (left,_) = split_lines(self.lines.clone(), self.cursor.pos);
let total_in_left = left.iter().map(|l| l.len()).sum::<usize>();
let percentage = if total_graphemes > 0 {
(total_in_left as f64 / total_graphemes as f64) * 100.0
} else {
100.0
}.round() as usize;
let msg = format!("line: {row}/{num_lines}, col: {col} --{percentage}%--");
write_meta(|m| {
m.post_status_message(msg);
})
}
Verb::Complete Verb::Complete
| Verb::ExMode | Verb::ExMode
| Verb::EndOfFile
| Verb::InsertMode | Verb::InsertMode
| Verb::NormalMode | Verb::NormalMode
| Verb::VisualMode | Verb::VisualMode
@@ -2600,7 +2638,13 @@ impl LineBuf {
} }
pub fn fix_cursor(&mut self) { pub fn fix_cursor(&mut self) {
// we are now going to enforce some invariants and do some bookkeeping
if self.lines.is_empty() {
// self.lines must always have at least one line
self.lines.push(Line::default());
}
if self.cursor.pos.row >= self.lines.len() { if self.cursor.pos.row >= self.lines.len() {
// clamp this now so self.cur_line() cannot panic
self.cursor.pos.row = self.lines.len().saturating_sub(1); self.cursor.pos.row = self.lines.len().saturating_sub(1);
} }
if self.cursor.exclusive { if self.cursor.exclusive {
@@ -2616,6 +2660,8 @@ impl LineBuf {
self.cursor.pos.col = line.len(); self.cursor.pos.col = line.len();
} }
} }
// update viewport scroll offset
self.update_scroll_offset(); self.update_scroll_offset();
} }

View File

@@ -1,6 +1,7 @@
use history::History; use history::History;
use keys::{KeyCode, KeyEvent, ModKeys}; use keys::{KeyCode, KeyEvent, ModKeys};
use linebuf::{LineBuf, SelectMode}; use linebuf::{LineBuf, SelectMode};
use std::collections::VecDeque;
use std::fmt::Write; use std::fmt::Write;
use term::{KeyReader, Layout, LineWriter, PollReader, TermWriter, get_win_size}; use term::{KeyReader, Layout, LineWriter, PollReader, TermWriter, get_win_size};
use unicode_width::UnicodeWidthStr; use unicode_width::UnicodeWidthStr;
@@ -16,8 +17,7 @@ use crate::readline::complete::{FuzzyCompleter, SelectorResponse};
use crate::readline::term::{Pos, TermReader, calc_str_width}; use crate::readline::term::{Pos, TermReader, calc_str_width};
use crate::readline::vimode::{ViEx, ViVerbatim}; use crate::readline::vimode::{ViEx, ViVerbatim};
use crate::state::{ use crate::state::{
AutoCmdKind, ShellParam, Var, VarFlags, VarKind, read_logic, read_shopts, with_vars, write_meta, AutoCmdKind, ShellParam, Var, VarFlags, VarKind, read_logic, read_meta, read_shopts, with_vars, write_meta, write_vars
write_vars,
}; };
use crate::{ use crate::{
libsh::error::ShResult, libsh::error::ShResult,
@@ -272,6 +272,8 @@ pub struct ShedVi {
pub ex_history: History, pub ex_history: History,
pub needs_redraw: bool, pub needs_redraw: bool,
pub ctrl_d_warning_counter: usize,
pub status_msgs: VecDeque<(String, Instant)>
} }
impl ShedVi { impl ShedVi {
@@ -293,6 +295,8 @@ impl ShedVi {
history: History::new()?, history: History::new()?,
ex_history: History::empty(), ex_history: History::empty(),
needs_redraw: true, needs_redraw: true,
ctrl_d_warning_counter: 0,
status_msgs: VecDeque::new()
}; };
write_vars(|v| { write_vars(|v| {
v.set_var( v.set_var(
@@ -325,6 +329,8 @@ impl ShedVi {
history: History::empty(), history: History::empty(),
ex_history: History::empty(), ex_history: History::empty(),
needs_redraw: true, needs_redraw: true,
ctrl_d_warning_counter: 0,
status_msgs: VecDeque::new()
}; };
write_vars(|v| { write_vars(|v| {
v.set_var( v.set_var(
@@ -883,16 +889,9 @@ impl ShedVi {
return Ok(Some(ReadlineEvent::Line(buf))); return Ok(Some(ReadlineEvent::Line(buf)));
} }
if cmd.verb().is_some_and(|v| v.1 == Verb::EndOfFile) { if (cmd.verb().is_some_and(|v| v.1 == Verb::EndOfFile)
if self.focused_editor().joined().is_empty() { && self.focused_editor().joined().is_empty())
return Ok(Some(ReadlineEvent::Eof)); || cmd.verb().is_some_and(|v| v.1 == Verb::Quit) {
} else {
*self.focused_editor() = LineBuf::new();
self.mode = Box::new(ViInsert::new());
self.needs_redraw = true;
return Ok(None);
}
} else if cmd.verb().is_some_and(|v| v.1 == Verb::Quit) {
return Ok(Some(ReadlineEvent::Eof)); return Ok(Some(ReadlineEvent::Eof));
} }
@@ -902,6 +901,8 @@ impl ShedVi {
// this is only used for ringing the bell // this is only used for ringing the bell
let has_edit_verb = cmd.verb().is_some_and(|v| v.1.is_edit() && v.1 != Verb::Change); let has_edit_verb = cmd.verb().is_some_and(|v| v.1.is_edit() && v.1 != Verb::Change);
let is_ctrl_d_motion = cmd.motion().is_some_and(|m| m.1 == Motion::HalfScreenDown);
let is_shell_cmd = cmd.verb().is_some_and(|v| matches!(v.1, Verb::ShellCmd(_))); let is_shell_cmd = cmd.verb().is_some_and(|v| matches!(v.1, Verb::ShellCmd(_)));
let is_ex_cmd = cmd.flags.contains(CmdFlags::IS_EX_CMD); let is_ex_cmd = cmd.flags.contains(CmdFlags::IS_EX_CMD);
if is_shell_cmd { if is_shell_cmd {
@@ -913,6 +914,7 @@ impl ShedVi {
} }
let before = self.editor.joined(); let before = self.editor.joined();
let before_cursor = self.editor.cursor;
self.exec_cmd(cmd, false)?; self.exec_cmd(cmd, false)?;
@@ -922,6 +924,7 @@ impl ShedVi {
} }
} }
let after = self.editor.joined(); let after = self.editor.joined();
let after_cursor = self.editor.cursor;
if before != after { if before != after {
self self
@@ -929,6 +932,17 @@ impl ShedVi {
.update_pending_cmd((&self.editor.joined(), self.editor.cursor_to_flat())); .update_pending_cmd((&self.editor.joined(), self.editor.cursor_to_flat()));
} else if before == after && has_edit_verb { } else if before == after && has_edit_verb {
self.writer.send_bell().ok(); self.writer.send_bell().ok();
} else if before_cursor == after_cursor && is_ctrl_d_motion {
if self.ctrl_d_warning_counter == 3 || self.editor.is_empty() {
// our silly user is spamming ctrl+d for some reason
// maybe they want to exit the shell?
write_meta(|m| {
m.post_status_message("Ctrl+D only quits in insert mode. try ':q' or entering insert mode with 'i'".into())
});
self.ctrl_d_warning_counter = 0;
} else {
self.ctrl_d_warning_counter += 1;
}
} }
let hint = self.history.get_hint(); let hint = self.history.get_hint();
@@ -1126,7 +1140,9 @@ impl ShedVi {
self.writer.flush_write(&buf)?; self.writer.flush_write(&buf)?;
// Move to end of layout for overlay draws (completer, history search) // Move to end of layout for overlay draws (completer, history search)
let has_overlays = self.completer.is_active() || self.focused_history().fuzzy_finder.is_active(); let has_overlays = self.completer.is_active()
|| self.focused_history().fuzzy_finder.is_active();
let down = new_layout.end.row.saturating_sub(new_layout.cursor.row); let down = new_layout.end.row.saturating_sub(new_layout.cursor.row);
if has_overlays && down > 0 { if has_overlays && down > 0 {
self.writer.flush_write(&format!("\x1b[{down}B"))?; self.writer.flush_write(&format!("\x1b[{down}B"))?;
@@ -1141,9 +1157,11 @@ impl ShedVi {
// Without PSR, use the content width on the cursor's row // Without PSR, use the content width on the cursor's row
(new_layout.end.col + 1).max(new_layout.cursor.col + 1) (new_layout.end.col + 1).max(new_layout.cursor.col + 1)
}; };
let mut fuzzy_window_rows = 0;
self.completer self.completer
.set_prompt_line_context(preceding_width, new_layout.end.col); .set_prompt_line_context(preceding_width, new_layout.end.col);
self.completer.draw(&mut self.writer)?; fuzzy_window_rows += self.completer.draw(&mut self.writer)?;
{ {
self.focused_history() self.focused_history()
@@ -1151,10 +1169,37 @@ impl ShedVi {
.set_prompt_line_context(preceding_width, new_layout.end.col); .set_prompt_line_context(preceding_width, new_layout.end.col);
let mut writer = std::mem::take(&mut self.writer); let mut writer = std::mem::take(&mut self.writer);
self.focused_history().fuzzy_finder.draw(&mut writer)?; fuzzy_window_rows += self.focused_history().fuzzy_finder.draw(&mut writer)?;
self.writer = writer; self.writer = writer;
} }
while let Some(msg) = write_meta(|m| m.pop_status_message()) {
let now = Instant::now();
self.status_msgs.push_back((msg,now));
}
while let Some((msg,time)) = self.status_msgs.front() {
if time.elapsed().as_secs() < 5 {
log::debug!("drawing status message: {msg}");
let down = new_layout.end.row - new_layout.cursor.row;
log::debug!("status message down: {down}");
let fuzzy_rows = fuzzy_window_rows.saturating_sub(1); // the cursor is one row below the top
let total = down.saturating_add(fuzzy_rows as u16);
let move_down = if total > 0 {
format!("\x1b[{total}B")
} else {
String::new()
};
let move_up = total + 2;
let col = new_layout.cursor.col + 1;
self.writer.flush_write(&format!("{move_down}\n\n\x1b7\x1b[2K{msg}\x1b8\x1b[{move_up}A\x1b[{col}G"))?;
new_layout.end.row += (2 + msg.chars().filter(|c| *c == '\n').count()) as u16;
break
} else {
self.status_msgs.pop_front();
}
}
self.old_layout = Some(new_layout); self.old_layout = Some(new_layout);
self.needs_redraw = false; self.needs_redraw = false;

View File

@@ -243,6 +243,7 @@ pub enum Verb {
Equalize, Equalize,
AcceptLineOrNewline, AcceptLineOrNewline,
EndOfFile, EndOfFile,
PrintPosition,
// Ex-mode verbs // Ex-mode verbs
ExMode, ExMode,
ShellCmd(String), ShellCmd(String),
@@ -335,6 +336,8 @@ pub enum Motion {
EndOfBuffer, EndOfBuffer,
ToColumn, ToColumn,
ToDelimMatch, ToDelimMatch,
HalfScreenDown,
HalfScreenUp,
ToBrace(Direction), ToBrace(Direction),
ToBracket(Direction), ToBracket(Direction),
ToParen(Direction), ToParen(Direction),

View File

@@ -120,7 +120,7 @@ impl ViMode for ViEx {
Ok(cmd) => Ok(cmd), Ok(cmd) => Ok(cmd),
Err(e) => { Err(e) => {
let msg = e.unwrap_or(format!("Not an editor command: {}", &input)); let msg = e.unwrap_or(format!("Not an editor command: {}", &input));
write_meta(|m| m.post_system_message(msg.clone())); write_meta(|m| m.post_status_message(msg.clone()));
Err(ShErr::simple(ShErrKind::ParseErr, msg)) Err(ShErr::simple(ShErrKind::ParseErr, msg))
} }
} }

View File

@@ -116,6 +116,7 @@ pub fn common_cmds(key: E) -> Option<ViCmd> {
E(K::Right, M::NONE) => pending_cmd.set_motion(MotionCmd(1, Motion::ForwardChar)), E(K::Right, M::NONE) => pending_cmd.set_motion(MotionCmd(1, Motion::ForwardChar)),
E(K::Up, M::NONE) => pending_cmd.set_motion(MotionCmd(1, Motion::LineUp)), E(K::Up, M::NONE) => pending_cmd.set_motion(MotionCmd(1, Motion::LineUp)),
E(K::Down, M::NONE) => pending_cmd.set_motion(MotionCmd(1, Motion::LineDown)), E(K::Down, M::NONE) => pending_cmd.set_motion(MotionCmd(1, Motion::LineDown)),
E(K::Enter, M::SHIFT) => pending_cmd.set_verb(VerbCmd(1, Verb::InsertChar('\n'))),
E(K::Enter, M::NONE) => pending_cmd.set_verb(VerbCmd(1, Verb::AcceptLineOrNewline)), E(K::Enter, M::NONE) => pending_cmd.set_verb(VerbCmd(1, Verb::AcceptLineOrNewline)),
E(K::Char('D'), M::CTRL) => pending_cmd.set_verb(VerbCmd(1, Verb::EndOfFile)), E(K::Char('D'), M::CTRL) => pending_cmd.set_verb(VerbCmd(1, Verb::EndOfFile)),
E(K::Delete, M::NONE) => { E(K::Delete, M::NONE) => {

View File

@@ -776,6 +776,36 @@ impl ViMode for ViNormal {
flags: self.flags(), flags: self.flags(),
}) })
} }
E(K::Char('G'), M::CTRL) => {
self.pending_seq.clear();
Some(ViCmd {
register: Default::default(),
verb: Some(VerbCmd(1, Verb::PrintPosition)),
motion: None,
raw_seq: "".into(),
flags: self.flags(),
})
}
E(K::Char('D'), M::CTRL) => {
self.pending_seq.clear();
Some(ViCmd {
register: Default::default(),
verb: None,
motion: Some(MotionCmd(1, Motion::HalfScreenDown)),
raw_seq: "".into(),
flags: self.flags(),
})
}
E(K::Char('U'), M::CTRL) => {
self.pending_seq.clear();
Some(ViCmd {
register: Default::default(),
verb: None,
motion: Some(MotionCmd(1, Motion::HalfScreenUp)),
raw_seq: "".into(),
flags: self.flags(),
})
}
E(K::Char(ch), M::NONE) => self.try_parse(ch), E(K::Char(ch), M::NONE) => self.try_parse(ch),
E(K::Backspace, M::NONE) => Some(ViCmd { E(K::Backspace, M::NONE) => Some(ViCmd {

View File

@@ -663,6 +663,36 @@ impl ViMode for ViVisual {
flags: CmdFlags::empty(), flags: CmdFlags::empty(),
}) })
} }
E(K::Char('G'), M::CTRL) => {
self.pending_seq.clear();
Some(ViCmd {
register: Default::default(),
verb: Some(VerbCmd(1, Verb::PrintPosition)),
motion: None,
raw_seq: "".into(),
flags: CmdFlags::empty()
})
}
E(K::Char('D'), M::CTRL) => {
self.pending_seq.clear();
Some(ViCmd {
register: Default::default(),
verb: None,
motion: Some(MotionCmd(1, Motion::HalfScreenDown)),
raw_seq: "".into(),
flags: CmdFlags::empty(),
})
}
E(K::Char('U'), M::CTRL) => {
self.pending_seq.clear();
Some(ViCmd {
register: Default::default(),
verb: None,
motion: Some(MotionCmd(1, Motion::HalfScreenUp)),
raw_seq: "".into(),
flags: CmdFlags::empty(),
})
}
E(K::Char('R'), M::CTRL) => { E(K::Char('R'), M::CTRL) => {
let mut chars = self.pending_seq.chars().peekable(); let mut chars = self.pending_seq.chars().peekable();
let count = self.parse_count(&mut chars).unwrap_or(1); let count = self.parse_count(&mut chars).unwrap_or(1);

View File

@@ -1369,7 +1369,12 @@ pub struct MetaTab {
runtime_stop: Option<Instant>, runtime_stop: Option<Instant>,
// pending system messages // pending system messages
system_msg: Vec<String>, // are drawn above the prompt and survive redraws
system_msg: VecDeque<String>,
// same as system messages,
// but they appear under the prompt and are erased on redraw
status_msg: VecDeque<String>,
// pushd/popd stack // pushd/popd stack
dir_stack: VecDeque<PathBuf>, dir_stack: VecDeque<PathBuf>,
@@ -1394,7 +1399,8 @@ impl Default for MetaTab {
shell_time: Instant::now(), shell_time: Instant::now(),
runtime_start: None, runtime_start: None,
runtime_stop: None, runtime_stop: None,
system_msg: vec![], system_msg: VecDeque::new(),
status_msg: VecDeque::new(),
dir_stack: VecDeque::new(), dir_stack: VecDeque::new(),
getopts_offset: 0, getopts_offset: 0,
old_path: None, old_path: None,
@@ -1614,14 +1620,23 @@ impl MetaTab {
} }
} }
pub fn post_system_message(&mut self, message: String) { pub fn post_system_message(&mut self, message: String) {
self.system_msg.push(message); self.system_msg.push_back(message);
} }
pub fn pop_system_message(&mut self) -> Option<String> { pub fn pop_system_message(&mut self) -> Option<String> {
self.system_msg.pop() self.system_msg.pop_front()
} }
pub fn system_msg_pending(&self) -> bool { pub fn system_msg_pending(&self) -> bool {
!self.system_msg.is_empty() !self.system_msg.is_empty()
} }
pub fn post_status_message(&mut self, message: String) {
self.status_msg.push_back(message);
}
pub fn pop_status_message(&mut self) -> Option<String> {
self.status_msg.pop_front()
}
pub fn status_msg_pending(&self) -> bool {
!self.status_msg.is_empty()
}
pub fn dir_stack_top(&self) -> Option<&PathBuf> { pub fn dir_stack_top(&self) -> Option<&PathBuf> {
self.dir_stack.front() self.dir_stack.front()
} }