diff --git a/docs/MANPAGE.md b/docs/MANPAGE.md index 2f78b831..3a063ba7 100644 --- a/docs/MANPAGE.md +++ b/docs/MANPAGE.md @@ -151,6 +151,7 @@ ACTIONS next ID. * __MoveFocus: __ - moves focus in the specified direction (Left, Right, Up, Down). +* __DumpScreen: __ - dumps the screen in the specified file. * __ScrollUp__ - scrolls up 1 line in the focused pane. * __ScrollDown__ - scrolls down 1 line in the focused pane. * __PageScrollUp__ - scrolls up 1 page in the focused pane. diff --git a/zellij-server/src/os_input_output.rs b/zellij-server/src/os_input_output.rs index 7905a6e3..1e43e47f 100644 --- a/zellij-server/src/os_input_output.rs +++ b/zellij-server/src/os_input_output.rs @@ -1,6 +1,8 @@ use std::collections::HashMap; +use std::{fs::File, io::Write}; use crate::panes::PaneId; +use zellij_utils::tempfile::tempfile; use std::env; use std::os::unix::io::RawFd; @@ -268,6 +270,8 @@ pub trait ServerOsApi: Send + Sync { fn load_palette(&self) -> Palette; /// Returns the current working directory for a given pid fn get_cwd(&self, pid: Pid) -> Option; + /// Writes the given buffer to a string + fn write_to_file(&mut self, buf: String, file: Option); } impl ServerOsApi for ServerOsInputOutput { @@ -345,6 +349,16 @@ impl ServerOsApi for ServerOsInputOutput { } None } + fn write_to_file(&mut self, buf: String, name: Option) { + let mut f: File; + match name { + Some(x) => f = File::create(x).unwrap(), + None => f = tempfile().unwrap(), + } + if let Err(e) = write!(f, "{}", buf) { + log::error!("could not write to file: {}", e); + } + } } impl Clone for Box { diff --git a/zellij-server/src/panes/grid.rs b/zellij-server/src/panes/grid.rs index ec2d7e9b..e01c1ad7 100644 --- a/zellij-server/src/panes/grid.rs +++ b/zellij-server/src/panes/grid.rs @@ -1,6 +1,7 @@ use std::cell::RefCell; use std::rc::Rc; use unicode_width::UnicodeWidthChar; +use zellij_utils::regex::Regex; use std::{ cmp::Ordering, @@ -271,6 +272,26 @@ fn subtract_isize_from_usize(u: usize, i: isize) -> usize { } } +macro_rules! dump_screen { + ($lines:expr) => {{ + let mut is_first = true; + let mut buf = "".to_owned(); + + for line in &$lines { + if line.is_canonical && !is_first { + buf.push_str("\n"); + } + let s: String = (&line.columns).into_iter().map(|x| x.character).collect(); + // Replace the spaces at the end of the line. Sometimes, the lines are + // collected with spaces until the end of the panel. + let re = Regex::new("([^ ])[ ]*$").unwrap(); + buf.push_str(&(re.replace(&s, "${1}"))); + is_first = false; + } + buf + }}; +} + #[derive(Clone)] pub struct Grid { lines_above: VecDeque, @@ -813,6 +834,16 @@ impl Grid { Some((self.cursor.x, self.cursor.y)) } } + + pub fn dump_screen(&mut self) -> String { + let mut scrollback: String = dump_screen!(self.lines_above); + let viewport: String = dump_screen!(self.viewport); + if !scrollback.is_empty() { + scrollback.push_str("\n"); + } + scrollback.push_str(&viewport); + return scrollback; + } pub fn move_viewport_up(&mut self, count: usize) { for _ in 0..count { self.scroll_up_one_line(); diff --git a/zellij-server/src/panes/terminal_pane.rs b/zellij-server/src/panes/terminal_pane.rs index 2b68ea33..7ada5f02 100644 --- a/zellij-server/src/panes/terminal_pane.rs +++ b/zellij-server/src/panes/terminal_pane.rs @@ -386,6 +386,9 @@ impl Pane for TerminalPane { self.geom.y -= count; self.reflow_lines(); } + fn dump_screen(&mut self, _client_id: ClientId) -> String { + return self.grid.dump_screen(); + } fn scroll_up(&mut self, count: usize, _client_id: ClientId) { self.grid.move_viewport_up(count); self.set_should_render(true); diff --git a/zellij-server/src/route.rs b/zellij-server/src/route.rs index a6315447..e8620368 100644 --- a/zellij-server/src/route.rs +++ b/zellij-server/src/route.rs @@ -146,6 +146,12 @@ fn route_action( }; session.senders.send_to_screen(screen_instr).unwrap(); } + Action::DumpScreen(val) => { + session + .senders + .send_to_screen(ScreenInstruction::DumpScreen(val, client_id)) + .unwrap(); + } Action::ScrollUp => { session .senders diff --git a/zellij-server/src/screen.rs b/zellij-server/src/screen.rs index 52b4af87..f83f9da0 100644 --- a/zellij-server/src/screen.rs +++ b/zellij-server/src/screen.rs @@ -66,6 +66,7 @@ pub enum ScreenInstruction { MovePaneRight(ClientId), MovePaneLeft(ClientId), Exit, + DumpScreen(String, ClientId), ScrollUp(ClientId), ScrollUpAt(Position, ClientId), ScrollDown(ClientId), @@ -146,6 +147,7 @@ impl From<&ScreenInstruction> for ScreenContext { ScreenInstruction::MovePaneRight(..) => ScreenContext::MovePaneRight, ScreenInstruction::MovePaneLeft(..) => ScreenContext::MovePaneLeft, ScreenInstruction::Exit => ScreenContext::Exit, + ScreenInstruction::DumpScreen(..) => ScreenContext::DumpScreen, ScreenInstruction::ScrollUp(..) => ScreenContext::ScrollUp, ScreenInstruction::ScrollDown(..) => ScreenContext::ScrollDown, ScreenInstruction::ScrollToBottom(..) => ScreenContext::ScrollToBottom, @@ -1068,6 +1070,15 @@ pub(crate) fn screen_thread_main( screen.render(); } + ScreenInstruction::DumpScreen(file, client_id) => { + if let Some(active_tab) = screen.get_active_tab_mut(client_id) { + active_tab.dump_active_terminal_screen(Some(file.to_string()), client_id); + } else { + log::error!("Active tab not found for client id: {:?}", client_id); + } + + screen.render(); + } ScreenInstruction::ScrollUp(client_id) => { if let Some(active_tab) = screen.get_active_tab_mut(client_id) { active_tab.scroll_active_terminal_up(client_id); diff --git a/zellij-server/src/tab/mod.rs b/zellij-server/src/tab/mod.rs index 56e06c92..cdd968c3 100644 --- a/zellij-server/src/tab/mod.rs +++ b/zellij-server/src/tab/mod.rs @@ -155,6 +155,9 @@ pub trait Pane { fn push_right(&mut self, count: usize); fn pull_left(&mut self, count: usize); fn pull_up(&mut self, count: usize); + fn dump_screen(&mut self, _client_id: ClientId) -> String { + return "".to_owned(); + } fn scroll_up(&mut self, count: usize, client_id: ClientId); fn scroll_down(&mut self, count: usize, client_id: ClientId); fn clear_scroll(&mut self); @@ -1379,6 +1382,12 @@ impl Tab { .unwrap(); } } + pub fn dump_active_terminal_screen(&mut self, file: Option, client_id: ClientId) { + if let Some(active_pane) = self.get_active_pane_or_floating_pane_mut(client_id) { + let dump = active_pane.dump_screen(client_id); + self.os_api.write_to_file(dump, file); + } + } pub fn scroll_active_terminal_up(&mut self, client_id: ClientId) { if let Some(active_pane) = self.get_active_pane_or_floating_pane_mut(client_id) { active_pane.scroll_up(1, client_id); diff --git a/zellij-server/src/tab/unit/tab_integration_tests.rs b/zellij-server/src/tab/unit/tab_integration_tests.rs index fc35b444..dc162762 100644 --- a/zellij-server/src/tab/unit/tab_integration_tests.rs +++ b/zellij-server/src/tab/unit/tab_integration_tests.rs @@ -1,6 +1,8 @@ use super::{Output, Tab}; use crate::screen::CopyOptions; use crate::zellij_tile::data::{ModeInfo, Palette}; +use crate::Arc; +use crate::Mutex; use crate::{ os_input_output::{AsyncReader, Pid, ServerOsApi}, panes::PaneId, @@ -17,6 +19,7 @@ use zellij_utils::pane_size::Size; use zellij_utils::position::Position; use std::cell::RefCell; +use std::collections::HashMap; use std::collections::HashSet; use std::os::unix::io::RawFd; use std::rc::Rc; @@ -30,7 +33,9 @@ use zellij_utils::{ }; #[derive(Clone)] -struct FakeInputOutput {} +struct FakeInputOutput { + file_dumps: Arc>>, +} impl ServerOsApi for FakeInputOutput { fn set_terminal_size_using_fd(&self, _fd: RawFd, _cols: u16, _rows: u16) { @@ -83,6 +88,14 @@ impl ServerOsApi for FakeInputOutput { fn get_cwd(&self, _pid: Pid) -> Option { unimplemented!() } + fn write_to_file(&mut self, buf: String, name: Option) { + let f: String; + match name { + Some(x) => f = x, + None => f = "tmp-name".to_owned(), + } + self.file_dumps.lock().unwrap().insert(f, buf); + } } // TODO: move to shared thingy with other test file @@ -91,7 +104,9 @@ fn create_new_tab(size: Size) -> Tab { let index = 0; let position = 0; let name = String::new(); - let os_api = Box::new(FakeInputOutput {}); + let os_api = Box::new(FakeInputOutput { + file_dumps: Arc::new(Mutex::new(HashMap::new())), + }); let senders = ThreadSenders::default().silently_fail_on_send(); let max_panes = None; let mode_info = ModeInfo::default(); @@ -183,6 +198,30 @@ fn take_snapshot_and_cursor_position( (format!("{:?}", grid), grid.cursor_coordinates()) } +#[test] +fn dump_screen() { + let size = Size { + cols: 121, + rows: 20, + }; + let client_id = 1; + let mut tab = create_new_tab(size); + let map = Arc::new(Mutex::new(HashMap::new())); + tab.os_api = Box::new(FakeInputOutput { + file_dumps: map.clone(), + }); + let new_pane_id = PaneId::Terminal(2); + tab.new_pane(new_pane_id, Some(client_id)); + tab.handle_pty_bytes(2, Vec::from("scratch".as_bytes())); + let file = "/tmp/log.sh"; + tab.dump_active_terminal_screen(Some(file.to_string()), client_id); + assert_eq!( + map.lock().unwrap().get(file).unwrap(), + "scratch", + "screen was dumped properly" + ); +} + #[test] fn new_floating_pane() { let size = Size { diff --git a/zellij-server/src/tab/unit/tab_tests.rs b/zellij-server/src/tab/unit/tab_tests.rs index c68d77bf..7b2baa18 100644 --- a/zellij-server/src/tab/unit/tab_tests.rs +++ b/zellij-server/src/tab/unit/tab_tests.rs @@ -81,6 +81,10 @@ impl ServerOsApi for FakeInputOutput { fn get_cwd(&self, _pid: Pid) -> Option { unimplemented!() } + + fn write_to_file(&mut self, _buf: String, _name: Option) { + unimplemented!() + } } fn create_new_tab(size: Size) -> Tab { diff --git a/zellij-server/src/unit/screen_tests.rs b/zellij-server/src/unit/screen_tests.rs index e52a88ae..ebd33096 100644 --- a/zellij-server/src/unit/screen_tests.rs +++ b/zellij-server/src/unit/screen_tests.rs @@ -77,6 +77,9 @@ impl ServerOsApi for FakeInputOutput { fn get_cwd(&self, _pid: Pid) -> Option { unimplemented!() } + fn write_to_file(&mut self, _: String, _: Option) { + unimplemented!() + } } fn create_new_screen(size: Size) -> Screen { diff --git a/zellij-utils/Cargo.toml b/zellij-utils/Cargo.toml index bde1d07a..75bcba06 100644 --- a/zellij-utils/Cargo.toml +++ b/zellij-utils/Cargo.toml @@ -39,6 +39,7 @@ unicode-width = "0.1.8" miette = { version = "3.3.0", features = ["fancy"] } regex = "1.5.5" termwiz = "0.16.0" +tempfile = "3.2.0" [dependencies.async-std] @@ -46,7 +47,6 @@ version = "1.3.0" features = ["unstable"] [dev-dependencies] -tempfile = "3.2.0" [features] disable_automatic_asset_installation = [] diff --git a/zellij-utils/src/errors.rs b/zellij-utils/src/errors.rs index 718cb35f..677716a1 100644 --- a/zellij-utils/src/errors.rs +++ b/zellij-utils/src/errors.rs @@ -242,6 +242,7 @@ pub enum ScreenContext { MovePaneRight, MovePaneLeft, Exit, + DumpScreen, ScrollUp, ScrollUpAt, ScrollDown, diff --git a/zellij-utils/src/input/actions.rs b/zellij-utils/src/input/actions.rs index acc86e3b..e2a7ad38 100644 --- a/zellij-utils/src/input/actions.rs +++ b/zellij-utils/src/input/actions.rs @@ -54,6 +54,8 @@ pub enum Action { /// If there is no pane in the direction, move to previous/next Tab. MoveFocusOrTab(Direction), MovePane(Option), + /// Dumps the screen to a file + DumpScreen(String), /// Scroll up in focus pane. ScrollUp, /// Scroll up at point diff --git a/zellij-utils/src/lib.rs b/zellij-utils/src/lib.rs index 2a635867..b7c2cc95 100644 --- a/zellij-utils/src/lib.rs +++ b/zellij-utils/src/lib.rs @@ -22,6 +22,7 @@ pub use regex; pub use serde; pub use serde_yaml; pub use signal_hook; +pub use tempfile; pub use termwiz; pub use vte; pub use zellij_tile;