summaryrefslogblamecommitdiffstats
path: root/src/readline.rs
blob: 8b3c847fe6ff7858f2d5f7dfe1e2a5bc14789a3d (plain) (tree)
1
2
3
4
5
6
7
8
9
10
                            
                                                                 
 
                     
                     

                       
               

 
               
                          
              
                           





                                                      
                                  
                   


         



                                        






                                                            
                                                         
             
                                          
                                                                
             
                                          
                                       
                                   
                                                             
             
                                                                
                                                         

                                                        

                   
                                           

     


                                   
                    
                             



















                                                                       
                                                          









                                                                        
                                        


                                                    

                                                              
         
                                    
                               
                                                          
                             
                                        
                               
                                                                             


                                    

              
 



                                                      



                                  




                                      

                                                       


                             






                                                                             
         



                                
                     
     

                                   


















































                                                                           
     
 
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<crate::action::Action> {
        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::<String>()
            .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)
    }
}