feat(actions): dump the terminal screen into a file (#1375)
* Initial commit for fixing #1353 * adding a new line between the lines_above and the viewport * changes following code review * implementing a test case for the dump screen * implemented test case for dump_screen * better regexp replace * fixes following code review * style(api): remove extraneous method in plugin pane * style(fmt): rustfmt * style(tests): fix method name Co-authored-by: Aram Drevekenin <aram@poor.dev>
This commit is contained in:
parent
e663ef2db7
commit
76d871294d
14 changed files with 128 additions and 3 deletions
|
|
@ -151,6 +151,7 @@ ACTIONS
|
|||
next ID.
|
||||
* __MoveFocus: <Direction\>__ - moves focus in the specified direction (Left,
|
||||
Right, Up, Down).
|
||||
* __DumpScreen: <File\>__ - 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.
|
||||
|
|
|
|||
|
|
@ -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<PathBuf>;
|
||||
/// Writes the given buffer to a string
|
||||
fn write_to_file(&mut self, buf: String, file: Option<String>);
|
||||
}
|
||||
|
||||
impl ServerOsApi for ServerOsInputOutput {
|
||||
|
|
@ -345,6 +349,16 @@ impl ServerOsApi for ServerOsInputOutput {
|
|||
}
|
||||
None
|
||||
}
|
||||
fn write_to_file(&mut self, buf: String, name: Option<String>) {
|
||||
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<dyn ServerOsApi> {
|
||||
|
|
|
|||
|
|
@ -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<Row>,
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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<String>, 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);
|
||||
|
|
|
|||
|
|
@ -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<Mutex<HashMap<String, String>>>,
|
||||
}
|
||||
|
||||
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<PathBuf> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn write_to_file(&mut self, buf: String, name: Option<String>) {
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -81,6 +81,10 @@ impl ServerOsApi for FakeInputOutput {
|
|||
fn get_cwd(&self, _pid: Pid) -> Option<PathBuf> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn write_to_file(&mut self, _buf: String, _name: Option<String>) {
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
|
||||
fn create_new_tab(size: Size) -> Tab {
|
||||
|
|
|
|||
|
|
@ -77,6 +77,9 @@ impl ServerOsApi for FakeInputOutput {
|
|||
fn get_cwd(&self, _pid: Pid) -> Option<PathBuf> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn write_to_file(&mut self, _: String, _: Option<String>) {
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
|
||||
fn create_new_screen(size: Size) -> Screen {
|
||||
|
|
|
|||
|
|
@ -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 = []
|
||||
|
|
|
|||
|
|
@ -242,6 +242,7 @@ pub enum ScreenContext {
|
|||
MovePaneRight,
|
||||
MovePaneLeft,
|
||||
Exit,
|
||||
DumpScreen,
|
||||
ScrollUp,
|
||||
ScrollUpAt,
|
||||
ScrollDown,
|
||||
|
|
|
|||
|
|
@ -54,6 +54,8 @@ pub enum Action {
|
|||
/// If there is no pane in the direction, move to previous/next Tab.
|
||||
MoveFocusOrTab(Direction),
|
||||
MovePane(Option<Direction>),
|
||||
/// Dumps the screen to a file
|
||||
DumpScreen(String),
|
||||
/// Scroll up in focus pane.
|
||||
ScrollUp,
|
||||
/// Scroll up at point
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue