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

                             

                     
        
 
                    
                     
                                             
                      


              
                          
              
                           
                            
                          
         

     

                        
                                          

                             
                        
                                
                             
                                        
                              
                               

                                                                  
                                                                    

                                                                       
                        
                                                                            

                  



                                   
                          



                          




                                                   

             
                                           
                                      
                                  



              

                                   
                                          





                                                           



                                                                    


                                                      
                                                      


         

                     
                      
                  
                                                   
                                
                                                                 
                                                                   
 
                                                 
                                



                        
           
                     
                                
                                        
                        


                     

          
                                 


                                  
                                                                         
                                          

     


                                        
























































                                                                       

                                                                       
                                                      








                                 
                                                                             








                                                       
                                                                      





                                                                 
                                                    









                                                        

 
                
                    
                                      
                 

                                                       
                                               
   
                                       
                                      
                                          


                     
                            



                                                       
                              

                                                                      
                  



                                                                        



                       
                    

                                                                           






                                                               
                                      







                                                                            
                 
              
                               
 
                                                          
                          


       
                        
                  
                   
                  
                                               
                                                     
                                                                      
                               
                        
                                                                             
                                                    

                              
                                          


                  
                                
                                
 


























                                                                          

                  
                                                        


                                                            

                                                                          






                                              
                                   
                                                 






                                                                 
                                                       

                                                                          
                 
                                                        



                                     
                                  

                                                                         
                                         

                                         
                     

                                     
                 

                                      
                                         

                                  
                                 

             
                                                              
                                        

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

mod entry;
pub use entry::Entry;
mod pty;

pub struct History {
    size: (u16, u16),
    entries: Vec<crate::mutex::Mutex<Entry>>,
    scroll_pos: usize,
}

impl History {
    pub fn new() -> Self {
        Self {
            size: (24, 80),
            entries: vec![],
            scroll_pos: 0,
        }
    }

    pub async fn render(
        &self,
        out: &mut impl textmode::Textmode,
        repl_lines: usize,
        focus: Option<usize>,
        scrolling: bool,
        offset: time::UtcOffset,
    ) -> anyhow::Result<()> {
        let mut used_lines = repl_lines;
        let mut cursor = None;
        for (idx, mut entry) in
            self.visible(repl_lines, focus, scrolling).await.rev()
        {
            let focused = focus.map_or(false, |focus| idx == focus);
            used_lines +=
                entry.lines(self.entry_count(), focused && !scrolling);
            out.move_to(
                (usize::from(self.size.0) - used_lines).try_into().unwrap(),
                0,
            );
            entry.render(
                out,
                idx,
                self.entry_count(),
                self.size,
                focused,
                scrolling,
                offset,
            );
            if focused && !scrolling {
                cursor = Some((
                    out.screen().cursor_position(),
                    out.screen().hide_cursor(),
                ));
            }
        }
        if let Some((pos, hide)) = cursor {
            out.move_to(pos.0, pos.1);
            out.hide_cursor(hide);
        }
        Ok(())
    }

    pub async fn render_fullscreen(
        &self,
        out: &mut impl textmode::Textmode,
        idx: usize,
    ) {
        let mut entry = self.entries[idx].lock_arc().await;
        entry.render_fullscreen(out);
    }

    pub async fn send_input(&mut self, idx: usize, input: Vec<u8>) {
        self.entry(idx).await.send_input(input).await;
    }

    pub async fn resize(&mut self, size: (u16, u16)) {
        self.size = size;
        for entry in &self.entries {
            entry.lock_arc().await.resize(size).await;
        }
    }

    pub async fn run(
        &mut self,
        cmdline: &str,
        env: &Env,
        event_w: async_std::channel::Sender<Event>,
    ) -> anyhow::Result<usize> {
        let (input_w, input_r) = async_std::channel::unbounded();
        let (resize_w, resize_r) = async_std::channel::unbounded();

        let entry = crate::mutex::new(Entry::new(
            cmdline.to_string(),
            env.clone(),
            self.size,
            input_w,
            resize_w,
        ));
        run_commands(
            cmdline.to_string(),
            crate::mutex::clone(&entry),
            env.clone(),
            input_r,
            resize_r,
            event_w,
        );

        self.entries.push(entry);
        Ok(self.entries.len() - 1)
    }

    pub async fn entry(&self, idx: usize) -> crate::mutex::Guard<Entry> {
        self.entries[idx].lock_arc().await
    }

    pub fn entry_count(&self) -> usize {
        self.entries.len()
    }

    pub async fn make_focus_visible(
        &mut self,
        repl_lines: usize,
        focus: Option<usize>,
        scrolling: bool,
    ) {
        if self.entries.is_empty() || focus.is_none() {
            return;
        }
        let focus = focus.unwrap();

        let mut done = false;
        while focus
            < self
                .visible(repl_lines, Some(focus), scrolling)
                .await
                .map(|(idx, _)| idx)
                .next()
                .unwrap()
        {
            self.scroll_pos += 1;
            done = true;
        }
        if done {
            return;
        }

        while focus
            > self
                .visible(repl_lines, Some(focus), scrolling)
                .await
                .map(|(idx, _)| idx)
                .last()
                .unwrap()
        {
            self.scroll_pos -= 1;
        }
    }

    async fn visible(
        &self,
        repl_lines: usize,
        focus: Option<usize>,
        scrolling: bool,
    ) -> VisibleEntries {
        let mut iter = VisibleEntries::new();
        if self.entries.is_empty() {
            return iter;
        }

        let mut used_lines = repl_lines;
        for (idx, entry) in
            self.entries.iter().enumerate().rev().skip(self.scroll_pos)
        {
            let entry = entry.lock_arc().await;
            let focused = focus.map_or(false, |focus| idx == focus);
            used_lines +=
                entry.lines(self.entry_count(), focused && !scrolling);
            if used_lines > usize::from(self.size.0) {
                break;
            }
            iter.add(idx, entry);
        }
        iter
    }
}

struct VisibleEntries {
    entries: std::collections::VecDeque<(usize, crate::mutex::Guard<Entry>)>,
}

impl VisibleEntries {
    fn new() -> Self {
        Self {
            entries: std::collections::VecDeque::new(),
        }
    }

    fn add(&mut self, idx: usize, entry: crate::mutex::Guard<Entry>) {
        // push_front because we are adding them in reverse order
        self.entries.push_front((idx, entry));
    }
}

impl std::iter::Iterator for VisibleEntries {
    type Item = (usize, crate::mutex::Guard<Entry>);

    fn next(&mut self) -> Option<Self::Item> {
        self.entries.pop_front()
    }
}

impl std::iter::DoubleEndedIterator for VisibleEntries {
    fn next_back(&mut self) -> Option<Self::Item> {
        self.entries.pop_back()
    }
}

fn run_commands(
    cmdline: String,
    entry: crate::mutex::Mutex<Entry>,
    mut env: Env,
    input_r: async_std::channel::Receiver<Vec<u8>>,
    resize_r: async_std::channel::Receiver<(u16, u16)>,
    event_w: async_std::channel::Sender<Event>,
) {
    async_std::task::spawn(async move {
        let pty = match pty::Pty::new(
            entry.lock_arc().await.size(),
            &entry,
            input_r,
            resize_r,
            event_w.clone(),
        ) {
            Ok(pty) => pty,
            Err(e) => {
                let mut entry = entry.lock_arc().await;
                entry.process(
                    format!("nbsh: failed to allocate pty: {}\r\n", e)
                        .as_bytes(),
                );
                env.set_status(async_std::process::ExitStatus::from_raw(
                    1 << 8,
                ));
                entry.finish(env, event_w).await;
                return;
            }
        };

        let status =
            match spawn_commands(&cmdline, &pty, &mut env, event_w.clone())
                .await
            {
                Ok(status) => status,
                Err(e) => {
                    let mut entry = entry.lock_arc().await;
                    entry.process(
                        format!(
                            "nbsh: failed to spawn {}: {}\r\n",
                            cmdline, e
                        )
                        .as_bytes(),
                    );
                    env.set_status(async_std::process::ExitStatus::from_raw(
                        1 << 8,
                    ));
                    entry.finish(env, event_w).await;
                    return;
                }
            };
        env.set_status(status);

        entry.lock_arc().await.finish(env, event_w).await;
        pty.close().await;
    });
}

async fn spawn_commands(
    cmdline: &str,
    pty: &pty::Pty,
    env: &mut Env,
    event_w: async_std::channel::Sender<Event>,
) -> anyhow::Result<async_std::process::ExitStatus> {
    let mut cmd = pty_process::Command::new(std::env::current_exe()?);
    cmd.args(&["-c", cmdline]);
    env.apply(&mut cmd);
    let (from_r, from_w) = nix::unistd::pipe2(nix::fcntl::OFlag::O_CLOEXEC)?;
    // Safety: dup2 is an async-signal-safe function
    unsafe {
        cmd.pre_exec(move || {
            nix::unistd::dup2(from_w, 3)?;
            Ok(())
        });
    }
    let child = pty.spawn(cmd)?;
    nix::unistd::close(from_w)?;

    let (read_w, read_r) = async_std::channel::unbounded();
    let new_read = move || {
        let read_w = read_w.clone();
        async_std::task::spawn(async move {
            let event = blocking::unblock(move || {
                // Safety: from_r was just opened above and is only
                // referenced in this closure, which takes ownership of it
                // at the start and returns ownership of it at the end
                let fh = unsafe { std::fs::File::from_raw_fd(from_r) };
                let event = bincode::deserialize_from(&fh);
                let _ = fh.into_raw_fd();
                event
            })
            .await;
            if read_w.is_closed() {
                // we should never drop read_r while there are still valid
                // things to read
                assert!(event.is_err());
            } else {
                read_w.send(event).await.unwrap();
            }
        });
    };

    new_read();
    let mut read_done = false;
    let mut exit_done = None;
    loop {
        enum Res {
            Read(bincode::Result<crate::runner::Event>),
            Exit(std::io::Result<std::process::ExitStatus>),
        }

        let read_r = read_r.clone();
        let read = async move { Res::Read(read_r.recv().await.unwrap()) };
        let exit = async {
            Res::Exit(if exit_done.is_none() {
                child.status_no_drop().await
            } else {
                std::future::pending().await
            })
        };
        match read.or(exit).await {
            Res::Read(Ok(event)) => match event {
                crate::runner::Event::RunPipeline(idx, span) => {
                    event_w
                        .send(Event::ChildRunPipeline(idx, span))
                        .await
                        .unwrap();
                    new_read();
                }
                crate::runner::Event::Suspend(idx) => {
                    event_w.send(Event::ChildSuspend(idx)).await.unwrap();
                    new_read();
                }
                crate::runner::Event::Exit(new_env) => {
                    *env = new_env;
                    read_done = true;
                }
            },
            Res::Read(Err(e)) => {
                if let bincode::ErrorKind::Io(io_e) = &*e {
                    if io_e.kind() == std::io::ErrorKind::UnexpectedEof {
                        read_done = true;
                    } else {
                        anyhow::bail!(e);
                    }
                } else {
                    anyhow::bail!(e);
                }
            }
            Res::Exit(Ok(status)) => {
                exit_done = Some(status);
            }
            Res::Exit(Err(e)) => {
                anyhow::bail!(e);
            }
        }
        if let (true, Some(status)) = (read_done, exit_done) {
            nix::unistd::close(from_r)?;
            // nix::sys::signal::Signal is repr(i32)
            #[allow(clippy::as_conversions)]
            return Ok(status);
        }
    }
}