summaryrefslogblamecommitdiffstats
path: root/src/shell/history/entry.rs
blob: a45d99d2c433576f0153ddabc3c1f0ffa0b865ef (plain) (tree)
1
2
3
4
5
6
7
8
9

                             




                            

                    
             
                 


                              


                            




                                                   




                        
                 



                                                       
                                      


                    
                                        


                                                      


                                     




                                                        



                  
                  


                                          
                         



                                
                                                
















                                                                           





                                      









                                                                      
                                              












                                                                    




                                                              
                            
                                       
                                                                  
                                                       
                                  

















                                                                      


                               














                                                                      




                                                     
                                                                          









                                                                                              
                                   




                                                  


                                                                    



                                                       
                               






                                                                   
                                          

                               
                                                         

























                                                                            



                                                       





















                                                      














                                                                 





                               
                               















                                                                         
                                               





                                     












                                                                     

     
                                                        





                                      
                                                                     

















                                                                   
                                                      


                                                    

     

                        
                 
                                                   
       
                                                                       
                       
                                                     
     






                                                        











                                                           
























                                                                            
use crate::shell::prelude::*;

enum State {
    Running((usize, usize)),
    Exited(ExitInfo),
}

pub struct Entry {
    cmdline: String,
    env: Env,
    state: State,
    vt: vt100::Parser,
    audible_bell_state: usize,
    visual_bell_state: usize,
    audible_bell: bool,
    visual_bell: bool,
    real_bell_pending: bool,
    fullscreen: Option<bool>,
    input: async_std::channel::Sender<Vec<u8>>,
    resize: async_std::channel::Sender<(u16, u16)>,
    start_time: time::OffsetDateTime,
    start_instant: std::time::Instant,
}

impl Entry {
    pub fn new(
        cmdline: String,
        env: Env,
        size: (u16, u16),
        input: async_std::channel::Sender<Vec<u8>>,
        resize: async_std::channel::Sender<(u16, u16)>,
    ) -> Self {
        let span = (0, cmdline.len());
        Self {
            cmdline,
            env,
            state: State::Running(span),
            vt: vt100::Parser::new(size.0, size.1, 0),
            audible_bell_state: 0,
            visual_bell_state: 0,
            audible_bell: false,
            visual_bell: false,
            real_bell_pending: false,
            input,
            resize,
            fullscreen: None,
            start_time: time::OffsetDateTime::now_utc(),
            start_instant: std::time::Instant::now(),
        }
    }

    pub fn render(
        &mut self,
        out: &mut impl textmode::Textmode,
        idx: usize,
        entry_count: usize,
        size: (u16, u16),
        focused: bool,
        scrolling: bool,
        offset: time::UtcOffset,
    ) {
        let time = self.exit_info().map_or_else(
            || {
                format!(
                    "[{}]",
                    crate::format::time(self.start_time.to_offset(offset))
                )
            },
            |info| {
                format!(
                    "({}) [{}]",
                    crate::format::duration(
                        info.instant - self.start_instant
                    ),
                    crate::format::time(self.start_time.to_offset(offset)),
                )
            },
        );

        self.bell(out);
        if focused {
            self.audible_bell = false;
            self.visual_bell = false;
        }

        set_bgcolor(out, idx, focused);
        out.set_fgcolor(textmode::color::YELLOW);
        let entry_count_width = format!("{}", entry_count + 1).len();
        let idx_str = format!("{}", idx + 1);
        out.write_str(&" ".repeat(entry_count_width - idx_str.len()));
        out.write_str(&idx_str);
        out.write_str(" ");
        out.reset_attributes();

        set_bgcolor(out, idx, focused);
        if let Some(info) = self.exit_info() {
            if info.status.signal().is_some() {
                out.set_fgcolor(textmode::color::MAGENTA);
            } else if info.status.success() {
                out.set_fgcolor(textmode::color::DARKGREY);
            } else {
                out.set_fgcolor(textmode::color::RED);
            }
            out.write_str(&crate::format::exit_status(info.status));
        } else {
            out.write_str("     ");
        }
        out.reset_attributes();

        if self.audible_bell || self.visual_bell {
            out.set_bgcolor(textmode::Color::Rgb(64, 16, 16));
        } else {
            set_bgcolor(out, idx, focused);
        }
        out.write_str("$ ");
        set_bgcolor(out, idx, focused);
        let start = usize::from(out.screen().cursor_position().1);
        let end = usize::from(size.1) - time.len() - 2;
        let max_len = end - start;
        let cmd = if self.cmd().len() > max_len {
            &self.cmd()[..(max_len - 4)]
        } else {
            self.cmd()
        };
        if let State::Running(span) = self.state {
            let span = (span.0.min(cmd.len()), span.1.min(cmd.len()));
            if !cmd[..span.0].is_empty() {
                out.write_str(&cmd[..span.0]);
            }
            if !cmd[span.0..span.1].is_empty() {
                out.set_bgcolor(textmode::Color::Rgb(16, 64, 16));
                out.write_str(&cmd[span.0..span.1]);
                set_bgcolor(out, idx, focused);
            }
            if !cmd[span.1..].is_empty() {
                out.write_str(&cmd[span.1..]);
            }
        } else {
            out.write_str(cmd);
        }
        if self.cmd().len() > max_len {
            if let State::Running(span) = self.state {
                if span.0 < cmd.len() && span.1 > cmd.len() {
                    out.set_bgcolor(textmode::Color::Rgb(16, 64, 16));
                }
            }
            out.write_str(" ");
            if let State::Running(span) = self.state {
                if span.1 > cmd.len() {
                    out.set_bgcolor(textmode::Color::Rgb(16, 64, 16));
                }
            }
            out.set_fgcolor(textmode::color::BLUE);
            out.write_str("...");
        }
        out.reset_attributes();

        set_bgcolor(out, idx, focused);
        let cur_pos = out.screen().cursor_position();
        out.write_str(&" ".repeat(
            usize::from(size.1) - time.len() - 1 - usize::from(cur_pos.1),
        ));
        out.write_str(&time);
        out.write_str(" ");
        out.reset_attributes();

        if self.binary() {
            let msg = "This appears to be binary data. Fullscreen this entry to view anyway.";
            let len: u16 = msg.len().try_into().unwrap();
            out.move_to(
                out.screen().cursor_position().0 + 1,
                (size.1 - len) / 2,
            );
            out.set_fgcolor(textmode::color::RED);
            out.write_str(msg);
            out.hide_cursor(true);
        } else {
            let last_row = self.output_lines(focused && !scrolling);
            let mut max_lines = self.max_lines(entry_count);
            if last_row > max_lines {
                out.write(b"\r\n");
                out.set_fgcolor(textmode::color::BLUE);
                out.write_str("...");
                out.reset_attributes();
                max_lines -= 1;
            }
            let mut out_row = out.screen().cursor_position().0 + 1;
            let screen = self.vt.screen();
            let pos = screen.cursor_position();
            let mut wrapped = false;
            let mut cursor_found = None;
            for (idx, row) in screen
                .rows_formatted(0, size.1)
                .enumerate()
                .take(last_row)
                .skip(last_row.saturating_sub(max_lines))
            {
                let idx: u16 = idx.try_into().unwrap();
                out.reset_attributes();
                if !wrapped {
                    out.move_to(out_row, 0);
                }
                out.write(&row);
                wrapped = screen.row_wrapped(idx);
                if pos.0 == idx {
                    cursor_found = Some(out_row);
                }
                out_row += 1;
            }
            if focused && !scrolling {
                if let Some(row) = cursor_found {
                    out.hide_cursor(screen.hide_cursor());
                    out.move_to(row, pos.1);
                } else {
                    out.hide_cursor(true);
                }
            }
        }
        out.reset_attributes();
    }

    pub fn render_fullscreen(&mut self, out: &mut impl textmode::Textmode) {
        out.write(&self.vt.screen().state_formatted());
        self.bell(out);
        self.audible_bell = false;
        self.visual_bell = false;
        out.reset_attributes();
    }

    pub async fn send_input(&self, bytes: Vec<u8>) {
        if self.running() {
            self.input.send(bytes).await.unwrap();
        }
    }

    pub async fn resize(&mut self, size: (u16, u16)) {
        if self.running() {
            self.resize.send(size).await.unwrap();
            self.vt.set_size(size.0, size.1);
        }
    }

    pub fn size(&self) -> (u16, u16) {
        self.vt.screen().size()
    }

    pub fn process(&mut self, input: &[u8]) {
        self.vt.process(input);
        let screen = self.vt.screen();

        let new_audible_bell_state = screen.audible_bell_count();
        if new_audible_bell_state != self.audible_bell_state {
            self.audible_bell = true;
            self.real_bell_pending = true;
            self.audible_bell_state = new_audible_bell_state;
        }

        let new_visual_bell_state = screen.visual_bell_count();
        if new_visual_bell_state != self.visual_bell_state {
            self.visual_bell = true;
            self.real_bell_pending = true;
            self.visual_bell_state = new_visual_bell_state;
        }
    }

    pub fn cmd(&self) -> &str {
        &self.cmdline
    }

    pub fn env(&self) -> &Env {
        &self.env
    }

    pub fn toggle_fullscreen(&mut self) {
        if let Some(fullscreen) = self.fullscreen {
            self.fullscreen = Some(!fullscreen);
        } else {
            self.fullscreen = Some(!self.vt.screen().alternate_screen());
        }
    }

    pub fn set_fullscreen(&mut self, fullscreen: bool) {
        self.fullscreen = Some(fullscreen);
    }

    pub fn running(&self) -> bool {
        matches!(self.state, State::Running(_))
    }

    pub fn binary(&self) -> bool {
        self.vt.screen().errors() > 5
    }

    pub fn lines(&self, entry_count: usize, focused: bool) -> usize {
        1 + std::cmp::min(
            self.output_lines(focused),
            self.max_lines(entry_count),
        )
    }

    fn max_lines(&self, entry_count: usize) -> usize {
        if self.env.idx() == entry_count - 1 {
            usize::from(self.size().0) * 2 / 3
        } else {
            5
        }
    }

    pub fn output_lines(&self, focused: bool) -> usize {
        if self.binary() {
            return 1;
        }

        let screen = self.vt.screen();
        let mut last_row = 0;
        for (idx, row) in screen.rows(0, self.size().1).enumerate() {
            if !row.is_empty() {
                last_row = idx + 1;
            }
        }
        if focused && self.running() {
            last_row = std::cmp::max(
                last_row,
                usize::from(screen.cursor_position().0) + 1,
            );
        }
        last_row
    }

    pub fn should_fullscreen(&self) -> bool {
        self.fullscreen
            .unwrap_or_else(|| self.vt.screen().alternate_screen())
    }

    pub fn set_span(&mut self, span: (usize, usize)) {
        if matches!(self.state, State::Running(_)) {
            self.state = State::Running(span);
        }
    }

    pub async fn finish(
        &mut self,
        env: Env,
        event_w: async_std::channel::Sender<Event>,
    ) {
        self.state = State::Exited(ExitInfo::new(env.latest_status()));
        self.env = env;
        event_w.send(Event::PtyClose).await.unwrap();
    }

    fn exit_info(&self) -> Option<&ExitInfo> {
        match &self.state {
            State::Running(..) => None,
            State::Exited(exit_info) => Some(exit_info),
        }
    }

    fn bell(&mut self, out: &mut impl textmode::Textmode) {
        if self.real_bell_pending {
            if self.audible_bell {
                out.write(b"\x07");
            }
            if self.visual_bell {
                out.write(b"\x1bg");
            }
            self.real_bell_pending = false;
        }
    }
}

struct ExitInfo {
    status: async_std::process::ExitStatus,
    instant: std::time::Instant,
}

impl ExitInfo {
    fn new(status: async_std::process::ExitStatus) -> Self {
        Self {
            status,
            instant: std::time::Instant::now(),
        }
    }
}

fn set_bgcolor(out: &mut impl textmode::Textmode, idx: usize, focus: bool) {
    if focus {
        out.set_bgcolor(textmode::Color::Rgb(0x56, 0x1b, 0x8b));
    } else if idx % 2 == 0 {
        out.set_bgcolor(textmode::Color::Rgb(0x24, 0x21, 0x00));
    } else {
        out.set_bgcolor(textmode::Color::Rgb(0x20, 0x20, 0x20));
    }
}