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:
Cosmin Popescu 2022-05-20 11:22:40 +02:00 committed by GitHub
parent e663ef2db7
commit 76d871294d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 128 additions and 3 deletions

View file

@ -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.

View file

@ -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> {

View file

@ -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();

View file

@ -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);

View file

@ -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

View file

@ -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);

View file

@ -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);

View file

@ -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 {

View file

@ -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 {

View file

@ -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 {

View file

@ -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 = []

View file

@ -242,6 +242,7 @@ pub enum ScreenContext {
MovePaneRight,
MovePaneLeft,
Exit,
DumpScreen,
ScrollUp,
ScrollUpAt,
ScrollDown,

View file

@ -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

View file

@ -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;