summaryrefslogtreecommitdiffstats
path: root/src/shell/readline.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/shell/readline.rs')
-rw-r--r--src/shell/readline.rs223
1 files changed, 223 insertions, 0 deletions
diff --git a/src/shell/readline.rs b/src/shell/readline.rs
new file mode 100644
index 0000000..654d264
--- /dev/null
+++ b/src/shell/readline.rs
@@ -0,0 +1,223 @@
+use crate::shell::prelude::*;
+
+use unicode_width::{UnicodeWidthChar as _, UnicodeWidthStr as _};
+
+pub struct Readline {
+ size: (u16, u16),
+ input_line: String,
+ scroll: usize,
+ pos: usize,
+}
+
+impl Readline {
+ pub fn new() -> Self {
+ Self {
+ size: (24, 80),
+ input_line: "".into(),
+ scroll: 0,
+ pos: 0,
+ }
+ }
+
+ pub fn render(
+ &self,
+ out: &mut impl textmode::Textmode,
+ env: &Env,
+ git: Option<&super::inputs::GitInfo>,
+ focus: bool,
+ offset: time::UtcOffset,
+ ) -> Result<()> {
+ let pwd = env.pwd();
+ let user = crate::info::user()?;
+ let hostname = crate::info::hostname()?;
+ let time = crate::info::time(offset)?;
+ let prompt_char = crate::info::prompt_char()?;
+
+ let id = format!("{}@{}", user, hostname);
+ let idlen: u16 = id.len().try_into().unwrap();
+ let timelen: u16 = time.len().try_into().unwrap();
+
+ out.move_to(self.size.0 - 2, 0);
+ if focus {
+ out.set_bgcolor(textmode::Color::Rgb(0x56, 0x1b, 0x8b));
+ } else if env.idx() % 2 == 0 {
+ out.set_bgcolor(textmode::Color::Rgb(0x24, 0x21, 0x00));
+ } else {
+ out.set_bgcolor(textmode::Color::Rgb(0x20, 0x20, 0x20));
+ }
+ out.write(b"\x1b[K");
+ out.set_fgcolor(textmode::color::YELLOW);
+ out.write_str(&format!("{}", env.idx() + 1));
+ out.reset_attributes();
+ if focus {
+ out.set_bgcolor(textmode::Color::Rgb(0x56, 0x1b, 0x8b));
+ } else if env.idx() % 2 == 0 {
+ out.set_bgcolor(textmode::Color::Rgb(0x24, 0x21, 0x00));
+ } else {
+ out.set_bgcolor(textmode::Color::Rgb(0x20, 0x20, 0x20));
+ }
+ out.write_str(" (");
+ out.write_str(&crate::format::path(pwd));
+ if let Some(info) = git {
+ out.write_str(&format!("|{}", info));
+ }
+ out.write_str(")");
+ 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);
+ out.reset_attributes();
+ out.write_str(&prompt_char);
+ out.write_str(" ");
+ out.reset_attributes();
+ out.write(b"\x1b[K");
+ out.write_str(self.visible_input());
+ out.reset_attributes();
+ out.move_to(self.size.0 - 1, 2 + self.pos_width());
+ if focus {
+ out.hide_cursor(false);
+ }
+ Ok(())
+ }
+
+ pub fn resize(&mut self, size: (u16, u16)) {
+ self.size = size;
+ }
+
+ // self will be used eventually
+ #[allow(clippy::unused_self)]
+ pub fn lines(&self) -> usize {
+ 2 // XXX handle wrapping
+ }
+
+ pub fn input(&self) -> &str {
+ &self.input_line
+ }
+
+ pub fn add_input(&mut self, s: &str) {
+ self.input_line.insert_str(self.byte_pos(), s);
+ self.inc_pos(s.chars().count());
+ }
+
+ pub fn set_input(&mut self, s: String) {
+ self.set_pos(s.chars().count());
+ self.input_line = s;
+ }
+
+ pub fn backspace(&mut self) {
+ while self.pos > 0 {
+ self.dec_pos(1);
+ let width =
+ self.input_line.remove(self.byte_pos()).width().unwrap_or(0);
+ if width > 0 {
+ break;
+ }
+ }
+ }
+
+ pub fn clear_input(&mut self) {
+ self.input_line.clear();
+ self.set_pos(0);
+ }
+
+ pub fn clear_backwards(&mut self) {
+ self.input_line = self.input_line.chars().skip(self.pos).collect();
+ self.set_pos(0);
+ }
+
+ pub fn cursor_left(&mut self) {
+ if self.pos == 0 {
+ return;
+ }
+ self.dec_pos(1);
+ while let Some(c) = self.input_line.chars().nth(self.pos) {
+ if c.width().unwrap_or(0) == 0 {
+ self.dec_pos(1);
+ } else {
+ break;
+ }
+ }
+ }
+
+ pub fn cursor_right(&mut self) {
+ if self.pos == self.input_line.chars().count() {
+ return;
+ }
+ self.inc_pos(1);
+ while let Some(c) = self.input_line.chars().nth(self.pos) {
+ if c.width().unwrap_or(0) == 0 {
+ self.inc_pos(1);
+ } else {
+ break;
+ }
+ }
+ }
+
+ fn set_pos(&mut self, pos: usize) {
+ self.pos = pos;
+ if self.pos < self.scroll || self.pos_width() > self.size.1 - 2 {
+ self.scroll = self.pos;
+ let mut extra_scroll = usize::from(self.size.1) / 2;
+ while extra_scroll > 0 && self.scroll > 0 {
+ self.scroll -= 1;
+ extra_scroll -= self
+ .input_line
+ .chars()
+ .nth(self.scroll)
+ .unwrap()
+ .width()
+ .unwrap_or(1);
+ }
+ }
+ }
+
+ fn inc_pos(&mut self, inc: usize) {
+ self.set_pos(self.pos + inc);
+ }
+
+ fn dec_pos(&mut self, dec: usize) {
+ self.set_pos(self.pos - dec);
+ }
+
+ fn pos_width(&self) -> u16 {
+ let start = self
+ .input_line
+ .char_indices()
+ .nth(self.scroll)
+ .map_or(self.input_line.len(), |(i, _)| i);
+ let end = self
+ .input_line
+ .char_indices()
+ .nth(self.pos)
+ .map_or(self.input_line.len(), |(i, _)| i);
+ self.input_line[start..end].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)
+ }
+
+ fn visible_input(&self) -> &str {
+ let start = self
+ .input_line
+ .char_indices()
+ .nth(self.scroll)
+ .map_or(self.input_line.len(), |(i, _)| i);
+ let mut end = self.input_line.len();
+ let mut width = 0;
+ for (i, c) in self.input_line.char_indices().skip(self.scroll) {
+ if width >= usize::from(self.size.1) - 2 {
+ end = i;
+ break;
+ }
+ width += c.width().unwrap_or(1);
+ }
+ &self.input_line[start..end]
+ }
+}