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.
|
next ID.
|
||||||
* __MoveFocus: <Direction\>__ - moves focus in the specified direction (Left,
|
* __MoveFocus: <Direction\>__ - moves focus in the specified direction (Left,
|
||||||
Right, Up, Down).
|
Right, Up, Down).
|
||||||
|
* __DumpScreen: <File\>__ - dumps the screen in the specified file.
|
||||||
* __ScrollUp__ - scrolls up 1 line in the focused pane.
|
* __ScrollUp__ - scrolls up 1 line in the focused pane.
|
||||||
* __ScrollDown__ - scrolls down 1 line in the focused pane.
|
* __ScrollDown__ - scrolls down 1 line in the focused pane.
|
||||||
* __PageScrollUp__ - scrolls up 1 page in the focused pane.
|
* __PageScrollUp__ - scrolls up 1 page in the focused pane.
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use std::{fs::File, io::Write};
|
||||||
|
|
||||||
use crate::panes::PaneId;
|
use crate::panes::PaneId;
|
||||||
|
use zellij_utils::tempfile::tempfile;
|
||||||
|
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::os::unix::io::RawFd;
|
use std::os::unix::io::RawFd;
|
||||||
|
|
@ -268,6 +270,8 @@ pub trait ServerOsApi: Send + Sync {
|
||||||
fn load_palette(&self) -> Palette;
|
fn load_palette(&self) -> Palette;
|
||||||
/// Returns the current working directory for a given pid
|
/// Returns the current working directory for a given pid
|
||||||
fn get_cwd(&self, pid: Pid) -> Option<PathBuf>;
|
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 {
|
impl ServerOsApi for ServerOsInputOutput {
|
||||||
|
|
@ -345,6 +349,16 @@ impl ServerOsApi for ServerOsInputOutput {
|
||||||
}
|
}
|
||||||
None
|
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> {
|
impl Clone for Box<dyn ServerOsApi> {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use unicode_width::UnicodeWidthChar;
|
use unicode_width::UnicodeWidthChar;
|
||||||
|
use zellij_utils::regex::Regex;
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
cmp::Ordering,
|
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)]
|
#[derive(Clone)]
|
||||||
pub struct Grid {
|
pub struct Grid {
|
||||||
lines_above: VecDeque<Row>,
|
lines_above: VecDeque<Row>,
|
||||||
|
|
@ -813,6 +834,16 @@ impl Grid {
|
||||||
Some((self.cursor.x, self.cursor.y))
|
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) {
|
pub fn move_viewport_up(&mut self, count: usize) {
|
||||||
for _ in 0..count {
|
for _ in 0..count {
|
||||||
self.scroll_up_one_line();
|
self.scroll_up_one_line();
|
||||||
|
|
|
||||||
|
|
@ -386,6 +386,9 @@ impl Pane for TerminalPane {
|
||||||
self.geom.y -= count;
|
self.geom.y -= count;
|
||||||
self.reflow_lines();
|
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) {
|
fn scroll_up(&mut self, count: usize, _client_id: ClientId) {
|
||||||
self.grid.move_viewport_up(count);
|
self.grid.move_viewport_up(count);
|
||||||
self.set_should_render(true);
|
self.set_should_render(true);
|
||||||
|
|
|
||||||
|
|
@ -146,6 +146,12 @@ fn route_action(
|
||||||
};
|
};
|
||||||
session.senders.send_to_screen(screen_instr).unwrap();
|
session.senders.send_to_screen(screen_instr).unwrap();
|
||||||
}
|
}
|
||||||
|
Action::DumpScreen(val) => {
|
||||||
|
session
|
||||||
|
.senders
|
||||||
|
.send_to_screen(ScreenInstruction::DumpScreen(val, client_id))
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
Action::ScrollUp => {
|
Action::ScrollUp => {
|
||||||
session
|
session
|
||||||
.senders
|
.senders
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,7 @@ pub enum ScreenInstruction {
|
||||||
MovePaneRight(ClientId),
|
MovePaneRight(ClientId),
|
||||||
MovePaneLeft(ClientId),
|
MovePaneLeft(ClientId),
|
||||||
Exit,
|
Exit,
|
||||||
|
DumpScreen(String, ClientId),
|
||||||
ScrollUp(ClientId),
|
ScrollUp(ClientId),
|
||||||
ScrollUpAt(Position, ClientId),
|
ScrollUpAt(Position, ClientId),
|
||||||
ScrollDown(ClientId),
|
ScrollDown(ClientId),
|
||||||
|
|
@ -146,6 +147,7 @@ impl From<&ScreenInstruction> for ScreenContext {
|
||||||
ScreenInstruction::MovePaneRight(..) => ScreenContext::MovePaneRight,
|
ScreenInstruction::MovePaneRight(..) => ScreenContext::MovePaneRight,
|
||||||
ScreenInstruction::MovePaneLeft(..) => ScreenContext::MovePaneLeft,
|
ScreenInstruction::MovePaneLeft(..) => ScreenContext::MovePaneLeft,
|
||||||
ScreenInstruction::Exit => ScreenContext::Exit,
|
ScreenInstruction::Exit => ScreenContext::Exit,
|
||||||
|
ScreenInstruction::DumpScreen(..) => ScreenContext::DumpScreen,
|
||||||
ScreenInstruction::ScrollUp(..) => ScreenContext::ScrollUp,
|
ScreenInstruction::ScrollUp(..) => ScreenContext::ScrollUp,
|
||||||
ScreenInstruction::ScrollDown(..) => ScreenContext::ScrollDown,
|
ScreenInstruction::ScrollDown(..) => ScreenContext::ScrollDown,
|
||||||
ScreenInstruction::ScrollToBottom(..) => ScreenContext::ScrollToBottom,
|
ScreenInstruction::ScrollToBottom(..) => ScreenContext::ScrollToBottom,
|
||||||
|
|
@ -1068,6 +1070,15 @@ pub(crate) fn screen_thread_main(
|
||||||
|
|
||||||
screen.render();
|
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) => {
|
ScreenInstruction::ScrollUp(client_id) => {
|
||||||
if let Some(active_tab) = screen.get_active_tab_mut(client_id) {
|
if let Some(active_tab) = screen.get_active_tab_mut(client_id) {
|
||||||
active_tab.scroll_active_terminal_up(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 push_right(&mut self, count: usize);
|
||||||
fn pull_left(&mut self, count: usize);
|
fn pull_left(&mut self, count: usize);
|
||||||
fn pull_up(&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_up(&mut self, count: usize, client_id: ClientId);
|
||||||
fn scroll_down(&mut self, count: usize, client_id: ClientId);
|
fn scroll_down(&mut self, count: usize, client_id: ClientId);
|
||||||
fn clear_scroll(&mut self);
|
fn clear_scroll(&mut self);
|
||||||
|
|
@ -1379,6 +1382,12 @@ impl Tab {
|
||||||
.unwrap();
|
.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) {
|
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) {
|
if let Some(active_pane) = self.get_active_pane_or_floating_pane_mut(client_id) {
|
||||||
active_pane.scroll_up(1, client_id);
|
active_pane.scroll_up(1, client_id);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
use super::{Output, Tab};
|
use super::{Output, Tab};
|
||||||
use crate::screen::CopyOptions;
|
use crate::screen::CopyOptions;
|
||||||
use crate::zellij_tile::data::{ModeInfo, Palette};
|
use crate::zellij_tile::data::{ModeInfo, Palette};
|
||||||
|
use crate::Arc;
|
||||||
|
use crate::Mutex;
|
||||||
use crate::{
|
use crate::{
|
||||||
os_input_output::{AsyncReader, Pid, ServerOsApi},
|
os_input_output::{AsyncReader, Pid, ServerOsApi},
|
||||||
panes::PaneId,
|
panes::PaneId,
|
||||||
|
|
@ -17,6 +19,7 @@ use zellij_utils::pane_size::Size;
|
||||||
use zellij_utils::position::Position;
|
use zellij_utils::position::Position;
|
||||||
|
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
|
use std::collections::HashMap;
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use std::os::unix::io::RawFd;
|
use std::os::unix::io::RawFd;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
@ -30,7 +33,9 @@ use zellij_utils::{
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
struct FakeInputOutput {}
|
struct FakeInputOutput {
|
||||||
|
file_dumps: Arc<Mutex<HashMap<String, String>>>,
|
||||||
|
}
|
||||||
|
|
||||||
impl ServerOsApi for FakeInputOutput {
|
impl ServerOsApi for FakeInputOutput {
|
||||||
fn set_terminal_size_using_fd(&self, _fd: RawFd, _cols: u16, _rows: u16) {
|
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> {
|
fn get_cwd(&self, _pid: Pid) -> Option<PathBuf> {
|
||||||
unimplemented!()
|
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
|
// TODO: move to shared thingy with other test file
|
||||||
|
|
@ -91,7 +104,9 @@ fn create_new_tab(size: Size) -> Tab {
|
||||||
let index = 0;
|
let index = 0;
|
||||||
let position = 0;
|
let position = 0;
|
||||||
let name = String::new();
|
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 senders = ThreadSenders::default().silently_fail_on_send();
|
||||||
let max_panes = None;
|
let max_panes = None;
|
||||||
let mode_info = ModeInfo::default();
|
let mode_info = ModeInfo::default();
|
||||||
|
|
@ -183,6 +198,30 @@ fn take_snapshot_and_cursor_position(
|
||||||
(format!("{:?}", grid), grid.cursor_coordinates())
|
(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]
|
#[test]
|
||||||
fn new_floating_pane() {
|
fn new_floating_pane() {
|
||||||
let size = Size {
|
let size = Size {
|
||||||
|
|
|
||||||
|
|
@ -81,6 +81,10 @@ impl ServerOsApi for FakeInputOutput {
|
||||||
fn get_cwd(&self, _pid: Pid) -> Option<PathBuf> {
|
fn get_cwd(&self, _pid: Pid) -> Option<PathBuf> {
|
||||||
unimplemented!()
|
unimplemented!()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn write_to_file(&mut self, _buf: String, _name: Option<String>) {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_new_tab(size: Size) -> Tab {
|
fn create_new_tab(size: Size) -> Tab {
|
||||||
|
|
|
||||||
|
|
@ -77,6 +77,9 @@ impl ServerOsApi for FakeInputOutput {
|
||||||
fn get_cwd(&self, _pid: Pid) -> Option<PathBuf> {
|
fn get_cwd(&self, _pid: Pid) -> Option<PathBuf> {
|
||||||
unimplemented!()
|
unimplemented!()
|
||||||
}
|
}
|
||||||
|
fn write_to_file(&mut self, _: String, _: Option<String>) {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_new_screen(size: Size) -> Screen {
|
fn create_new_screen(size: Size) -> Screen {
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,7 @@ unicode-width = "0.1.8"
|
||||||
miette = { version = "3.3.0", features = ["fancy"] }
|
miette = { version = "3.3.0", features = ["fancy"] }
|
||||||
regex = "1.5.5"
|
regex = "1.5.5"
|
||||||
termwiz = "0.16.0"
|
termwiz = "0.16.0"
|
||||||
|
tempfile = "3.2.0"
|
||||||
|
|
||||||
|
|
||||||
[dependencies.async-std]
|
[dependencies.async-std]
|
||||||
|
|
@ -46,7 +47,6 @@ version = "1.3.0"
|
||||||
features = ["unstable"]
|
features = ["unstable"]
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tempfile = "3.2.0"
|
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
disable_automatic_asset_installation = []
|
disable_automatic_asset_installation = []
|
||||||
|
|
|
||||||
|
|
@ -242,6 +242,7 @@ pub enum ScreenContext {
|
||||||
MovePaneRight,
|
MovePaneRight,
|
||||||
MovePaneLeft,
|
MovePaneLeft,
|
||||||
Exit,
|
Exit,
|
||||||
|
DumpScreen,
|
||||||
ScrollUp,
|
ScrollUp,
|
||||||
ScrollUpAt,
|
ScrollUpAt,
|
||||||
ScrollDown,
|
ScrollDown,
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,8 @@ pub enum Action {
|
||||||
/// If there is no pane in the direction, move to previous/next Tab.
|
/// If there is no pane in the direction, move to previous/next Tab.
|
||||||
MoveFocusOrTab(Direction),
|
MoveFocusOrTab(Direction),
|
||||||
MovePane(Option<Direction>),
|
MovePane(Option<Direction>),
|
||||||
|
/// Dumps the screen to a file
|
||||||
|
DumpScreen(String),
|
||||||
/// Scroll up in focus pane.
|
/// Scroll up in focus pane.
|
||||||
ScrollUp,
|
ScrollUp,
|
||||||
/// Scroll up at point
|
/// Scroll up at point
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ pub use regex;
|
||||||
pub use serde;
|
pub use serde;
|
||||||
pub use serde_yaml;
|
pub use serde_yaml;
|
||||||
pub use signal_hook;
|
pub use signal_hook;
|
||||||
|
pub use tempfile;
|
||||||
pub use termwiz;
|
pub use termwiz;
|
||||||
pub use vte;
|
pub use vte;
|
||||||
pub use zellij_tile;
|
pub use zellij_tile;
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue