use textmode::Textmode as _; use unicode_width::{UnicodeWidthChar as _, UnicodeWidthStr as _}; pub struct Readline { size: (u16, u16), prompt: String, input_line: String, pos: usize, } impl Readline { pub fn new() -> Self { Self { size: (24, 80), prompt: if users::get_current_uid() == 0 { "# " } else { "$ " } .into(), input_line: "".into(), pos: 0, } } pub async fn handle_key( &mut self, key: textmode::Key, ) -> Option { match key { textmode::Key::String(s) => self.add_input(&s), textmode::Key::Char(c) => { self.add_input(&c.to_string()); } textmode::Key::Ctrl(b'c') => self.clear_input(), textmode::Key::Ctrl(b'd') => { return Some(crate::action::Action::Quit); } textmode::Key::Ctrl(b'l') => { return Some(crate::action::Action::ForceRedraw); } textmode::Key::Ctrl(b'm') => { let cmd = self.input(); self.clear_input(); return Some(crate::action::Action::Run(cmd)); } textmode::Key::Ctrl(b'u') => self.clear_backwards(), textmode::Key::Backspace => self.backspace(), textmode::Key::Left => self.cursor_left(), textmode::Key::Right => self.cursor_right(), _ => {} } Some(crate::action::Action::Render) } pub async fn render( &self, out: &mut textmode::Output, focus: bool, ) -> anyhow::Result<()> { let mut pwd = std::env::current_dir()?.display().to_string(); let home = std::env::var("HOME")?; if pwd.starts_with(&home) { pwd.replace_range(..home.len(), "~"); } let user = users::get_current_username() .unwrap() .to_string_lossy() .into_owned(); let mut hostname = hostname::get().unwrap().to_string_lossy().into_owned(); if let Some(idx) = hostname.find('.') { hostname.truncate(idx); } let id = format!("{}@{}", user, hostname); let idlen: u16 = id.len().try_into().unwrap(); let time = chrono::Local::now().format("%H:%M:%S").to_string(); let timelen: u16 = time.len().try_into().unwrap(); out.move_to(self.size.0 - 2, 0); out.set_bgcolor(textmode::Color::Rgb(32, 32, 64)); out.write(b"\x1b[K"); out.write(b" ("); out.write_str(&pwd); out.write(b")"); out.move_to(self.size.0 - 2, self.size.1 - 4 - idlen - timelen); out.write_str(&id); out.write_str(" ["); out.write_str(&time); out.write_str("]"); out.move_to(self.size.0 - 1, 0); if focus { out.set_fgcolor(textmode::color::BLACK); out.set_bgcolor(textmode::color::CYAN); } else { out.set_bgcolor(textmode::Color::Rgb(32, 32, 32)); } out.write_str(&self.prompt); out.reset_attributes(); out.set_bgcolor(textmode::Color::Rgb(32, 32, 32)); out.write(b"\x1b[K"); out.write_str(&self.input_line); out.reset_attributes(); out.move_to(self.size.0 - 1, self.prompt_width() + self.pos_width()); if focus { out.write(b"\x1b[?25h"); } Ok(()) } pub async fn resize(&mut self, size: (u16, u16)) { self.size = size; } pub fn lines(&self) -> usize { 2 // XXX handle wrapping } fn input(&self) -> String { self.input_line.clone() } fn add_input(&mut self, s: &str) { self.input_line.insert_str(self.byte_pos(), s); self.pos += s.chars().count(); } fn backspace(&mut self) { while self.pos > 0 { self.pos -= 1; let width = self.input_line.remove(self.byte_pos()).width().unwrap_or(0); if width > 0 { break; } } } fn clear_input(&mut self) { self.input_line.clear(); self.pos = 0; } fn clear_backwards(&mut self) { self.input_line = self.input_line.chars().skip(self.pos).collect(); self.pos = 0; } fn cursor_left(&mut self) { if self.pos == 0 { return; } self.pos -= 1; while let Some(c) = self.input_line.chars().nth(self.pos) { if c.width().unwrap_or(0) == 0 { self.pos -= 1; } else { break; } } } fn cursor_right(&mut self) { if self.pos == self.input_line.chars().count() { return; } self.pos += 1; while let Some(c) = self.input_line.chars().nth(self.pos) { if c.width().unwrap_or(0) == 0 { self.pos += 1; } else { break; } } } fn prompt_width(&self) -> u16 { self.prompt.width().try_into().unwrap() } fn pos_width(&self) -> u16 { self.input_line .chars() .take(self.pos) .collect::() .width() .try_into() .unwrap() } fn byte_pos(&self) -> usize { self.input_line .char_indices() .nth(self.pos) .map_or(self.input_line.len(), |(i, _)| i) } }