diff --git a/Cargo.lock b/Cargo.lock index b7edee50..9f734a1e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2726,6 +2726,16 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "936e4b492acfd135421d8dca4b1aa80a7bfc26e702ef3af710e0752684df5372" +[[package]] +name = "uuid" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" +dependencies = [ + "getrandom 0.2.6", + "serde", +] + [[package]] name = "value-bag" version = "1.0.0-alpha.9" @@ -3262,6 +3272,7 @@ dependencies = [ "typetag", "unicode-width", "url", + "uuid", "wasmer", "wasmer-wasi", "zellij-tile", diff --git a/docs/MANPAGE.md b/docs/MANPAGE.md index 8f24bbc5..877141b9 100644 --- a/docs/MANPAGE.md +++ b/docs/MANPAGE.md @@ -152,6 +152,7 @@ ACTIONS * __MoveFocus: __ - moves focus in the specified direction (Left, Right, Up, Down). * __DumpScreen: __ - dumps the screen in the specified file. +* __EditScrollback__ - replaces the current pane with the scrollback buffer. * __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/src/tests/e2e/cases.rs b/src/tests/e2e/cases.rs index 92438525..00cf2715 100644 --- a/src/tests/e2e/cases.rs +++ b/src/tests/e2e/cases.rs @@ -38,6 +38,7 @@ pub const SCROLL_UP_IN_SCROLL_MODE: [u8; 1] = [107]; // k pub const SCROLL_DOWN_IN_SCROLL_MODE: [u8; 1] = [106]; // j pub const SCROLL_PAGE_UP_IN_SCROLL_MODE: [u8; 1] = [2]; // ctrl-b pub const SCROLL_PAGE_DOWN_IN_SCROLL_MODE: [u8; 1] = [6]; // ctrl-f +pub const EDIT_SCROLLBACK: [u8; 1] = [101]; // e pub const RESIZE_MODE: [u8; 1] = [14]; // ctrl-n pub const RESIZE_DOWN_IN_RESIZE_MODE: [u8; 1] = [106]; // j @@ -1804,3 +1805,51 @@ pub fn tmux_mode() { }; assert_snapshot!(last_snapshot); } + +#[test] +#[ignore] +pub fn edit_scrollback() { + let fake_win_size = Size { + cols: 120, + rows: 24, + }; + + let mut test_attempts = 10; + let last_snapshot = loop { + RemoteRunner::kill_running_sessions(fake_win_size); + let mut runner = RemoteRunner::new(fake_win_size).add_step(Step { + name: "Split pane to the right", + instruction: |mut remote_terminal: RemoteTerminal| -> bool { + let mut step_is_complete = false; + if remote_terminal.status_bar_appears() && remote_terminal.cursor_position_is(3, 2) + { + remote_terminal.send_key(&SCROLL_MODE); + remote_terminal.send_key(&EDIT_SCROLLBACK); + step_is_complete = true; + } + step_is_complete + }, + }); + runner.run_all_steps(); + let last_snapshot = runner.take_snapshot_after(Step { + name: "Wait for editor to appear", + instruction: |remote_terminal: RemoteTerminal| -> bool { + let mut step_is_complete = false; + if remote_terminal.snapshot_contains(".dump") { + // the .dump is an indication we get on the bottom line of vi when editing a + // file + // the temp file name is randomly generated, so we don't assert the whole snapshot + step_is_complete = true; + } + step_is_complete + }, + }); + if runner.test_timed_out && test_attempts > 0 { + test_attempts -= 1; + continue; + } else { + break last_snapshot; + } + }; + assert!(last_snapshot.contains(".dump")); +} diff --git a/src/tests/e2e/remote_runner.rs b/src/tests/e2e/remote_runner.rs index 466b1805..564714bb 100644 --- a/src/tests/e2e/remote_runner.rs +++ b/src/tests/e2e/remote_runner.rs @@ -17,6 +17,7 @@ use std::cell::RefCell; use std::rc::Rc; const ZELLIJ_EXECUTABLE_LOCATION: &str = "/usr/src/zellij/x86_64-unknown-linux-musl/release/zellij"; +const SET_ENV_VARIABLES: &str = "EDITOR=/usr/bin/vi"; const ZELLIJ_LAYOUT_PATH: &str = "/usr/src/zellij/fixtures/layouts"; const ZELLIJ_DATA_DIR: &str = "/usr/src/zellij/e2e-data"; const CONNECTION_STRING: &str = "127.0.0.1:2222"; @@ -64,8 +65,8 @@ fn start_zellij(channel: &mut ssh2::Channel) { channel .write_all( format!( - "{} --session {} --data-dir {}\n", - ZELLIJ_EXECUTABLE_LOCATION, SESSION_NAME, ZELLIJ_DATA_DIR + "{} {} --session {} --data-dir {}\n", + SET_ENV_VARIABLES, ZELLIJ_EXECUTABLE_LOCATION, SESSION_NAME, ZELLIJ_DATA_DIR ) .as_bytes(), ) @@ -78,8 +79,8 @@ fn start_zellij_mirrored_session(channel: &mut ssh2::Channel) { channel .write_all( format!( - "{} --session {} --data-dir {} options --mirror-session true\n", - ZELLIJ_EXECUTABLE_LOCATION, SESSION_NAME, ZELLIJ_DATA_DIR + "{} {} --session {} --data-dir {} options --mirror-session true\n", + SET_ENV_VARIABLES, ZELLIJ_EXECUTABLE_LOCATION, SESSION_NAME, ZELLIJ_DATA_DIR ) .as_bytes(), ) @@ -92,8 +93,12 @@ fn start_zellij_in_session(channel: &mut ssh2::Channel, session_name: &str, mirr channel .write_all( format!( - "{} --session {} --data-dir {} options --mirror-session {}\n", - ZELLIJ_EXECUTABLE_LOCATION, session_name, ZELLIJ_DATA_DIR, mirrored + "{} {} --session {} --data-dir {} options --mirror-session {}\n", + SET_ENV_VARIABLES, + ZELLIJ_EXECUTABLE_LOCATION, + session_name, + ZELLIJ_DATA_DIR, + mirrored ) .as_bytes(), ) @@ -103,7 +108,13 @@ fn start_zellij_in_session(channel: &mut ssh2::Channel, session_name: &str, mirr fn attach_to_existing_session(channel: &mut ssh2::Channel, session_name: &str) { channel - .write_all(format!("{} attach {}\n", ZELLIJ_EXECUTABLE_LOCATION, session_name).as_bytes()) + .write_all( + format!( + "{} {} attach {}\n", + SET_ENV_VARIABLES, ZELLIJ_EXECUTABLE_LOCATION, session_name + ) + .as_bytes(), + ) .unwrap(); channel.flush().unwrap(); } @@ -113,8 +124,8 @@ fn start_zellij_without_frames(channel: &mut ssh2::Channel) { channel .write_all( format!( - "{} --session {} --data-dir {} options --no-pane-frames\n", - ZELLIJ_EXECUTABLE_LOCATION, SESSION_NAME, ZELLIJ_DATA_DIR + "{} {} --session {} --data-dir {} options --no-pane-frames\n", + SET_ENV_VARIABLES, ZELLIJ_EXECUTABLE_LOCATION, SESSION_NAME, ZELLIJ_DATA_DIR ) .as_bytes(), ) @@ -127,8 +138,12 @@ fn start_zellij_with_layout(channel: &mut ssh2::Channel, layout_path: &str) { channel .write_all( format!( - "{} --layout {} --session {} --data-dir {}\n", - ZELLIJ_EXECUTABLE_LOCATION, layout_path, SESSION_NAME, ZELLIJ_DATA_DIR + "{} {} --layout {} --session {} --data-dir {}\n", + SET_ENV_VARIABLES, + ZELLIJ_EXECUTABLE_LOCATION, + layout_path, + SESSION_NAME, + ZELLIJ_DATA_DIR ) .as_bytes(), ) diff --git a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__scrolling_inside_a_pane.snap b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__scrolling_inside_a_pane.snap index 664e881b..422a5616 100644 --- a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__scrolling_inside_a_pane.snap +++ b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__scrolling_inside_a_pane.snap @@ -1,7 +1,7 @@ --- source: src/tests/e2e/cases.rs +assertion_line: 295 expression: last_snapshot - --- Zellij (e2e-test)  Tab #1  ┌ Pane #1 ─────────────────────────────────────────────────┐┌ Pane #2 ─────────────────────────────────── SCROLL: 1/1 ┐ @@ -26,4 +26,4 @@ expression: last_snapshot │ ││line19 00000000000000000000000000000000000000000000000000█│ └──────────────────────────────────────────────────────────┘└──────────────────────────────────────────────────────────┘ Ctrl + LOCK 

PANE  TAB  RESIZE  MOVE  SCROLL  SESSION  QUIT  - <↓↑> Scroll / Scroll Page / Scroll Half Page / Select pane + <↓↑> Scroll / Scroll / Scroll / Edit / Select pane diff --git a/zellij-server/Cargo.toml b/zellij-server/Cargo.toml index d1cf9eb2..74767439 100644 --- a/zellij-server/Cargo.toml +++ b/zellij-server/Cargo.toml @@ -28,6 +28,7 @@ typetag = "0.1.7" chrono = "0.4.19" close_fds = "0.3.2" sysinfo = "0.22.5" +uuid = { version = "0.8.2", features = ["serde", "v4"] } [dev-dependencies] insta = "1.6.0" diff --git a/zellij-server/src/lib.rs b/zellij-server/src/lib.rs index 81b66801..0a587d7c 100644 --- a/zellij-server/src/lib.rs +++ b/zellij-server/src/lib.rs @@ -620,6 +620,7 @@ fn init_session( Some(os_input.clone()), ), opts.debug, + config_options.scrollback_editor.clone(), ); move || pty_thread_main(pty, layout) diff --git a/zellij-server/src/os_input_output.rs b/zellij-server/src/os_input_output.rs index 1e43e47f..b757ad46 100644 --- a/zellij-server/src/os_input_output.rs +++ b/zellij-server/src/os_input_output.rs @@ -145,6 +145,7 @@ fn handle_openpty( /// fn handle_terminal( cmd: RunCommand, + failover_cmd: Option, orig_termios: termios::Termios, quit_cb: Box, ) -> (RawFd, RawFd) { @@ -152,9 +153,12 @@ fn handle_terminal( // parent. match openpty(None, Some(&orig_termios)) { Ok(open_pty_res) => handle_openpty(open_pty_res, cmd, quit_cb), - Err(e) => { - panic!("failed to start pty{:?}", e); - } + Err(e) => match failover_cmd { + Some(failover_cmd) => handle_terminal(failover_cmd, None, orig_termios, quit_cb), + None => { + panic!("failed to start pty{:?}", e); + } + }, } } @@ -174,19 +178,40 @@ pub fn spawn_terminal( terminal_action: TerminalAction, orig_termios: termios::Termios, quit_cb: Box, -) -> (RawFd, RawFd) { + default_editor: Option, +) -> Result<(RawFd, RawFd), &'static str> { + let mut failover_cmd_args = None; let cmd = match terminal_action { - TerminalAction::OpenFile(file_to_open) => { - if env::var("EDITOR").is_err() && env::var("VISUAL").is_err() { - panic!("Can't edit files if an editor is not defined. To fix: define the EDITOR or VISUAL environment variables with the path to your editor (eg. /usr/bin/vim)"); + TerminalAction::OpenFile(file_to_open, line_number) => { + if default_editor.is_none() + && env::var("EDITOR").is_err() + && env::var("VISUAL").is_err() + { + return Err( + "No Editor found, consider setting a path to one in $EDITOR or $VISUAL", + ); } - let command = - PathBuf::from(env::var("EDITOR").unwrap_or_else(|_| env::var("VISUAL").unwrap())); + let command = default_editor.unwrap_or_else(|| { + PathBuf::from(env::var("EDITOR").unwrap_or_else(|_| env::var("VISUAL").unwrap())) + }); - let args = vec![file_to_open + let mut args = vec![]; + let file_to_open = file_to_open .into_os_string() .into_string() - .expect("Not valid Utf8 Encoding")]; + .expect("Not valid Utf8 Encoding"); + if let Some(line_number) = line_number { + if command.ends_with("vim") + || command.ends_with("nvim") + || command.ends_with("emacs") + || command.ends_with("nano") + || command.ends_with("kak") + { + failover_cmd_args = Some(vec![file_to_open.clone()]); + args.push(format!("+{}", line_number)); + } + } + args.push(file_to_open); RunCommand { command, args, @@ -195,8 +220,15 @@ pub fn spawn_terminal( } TerminalAction::RunCommand(command) => command, }; + let failover_cmd = if let Some(failover_cmd_args) = failover_cmd_args { + let mut cmd = cmd.clone(); + cmd.args = failover_cmd_args; + Some(cmd) + } else { + None + }; - handle_terminal(cmd, orig_termios, quit_cb) + Ok(handle_terminal(cmd, failover_cmd, orig_termios, quit_cb)) } #[derive(Clone)] @@ -245,7 +277,8 @@ pub trait ServerOsApi: Send + Sync { &self, terminal_action: TerminalAction, quit_cb: Box, - ) -> (RawFd, RawFd); + default_editor: Option, + ) -> Result<(RawFd, RawFd), &'static str>; /// Read bytes from the standard output of the virtual terminal referred to by `fd`. fn read_from_tty_stdout(&self, fd: RawFd, buf: &mut [u8]) -> Result; /// Creates an `AsyncReader` that can be used to read from `fd` in an async context @@ -284,9 +317,15 @@ impl ServerOsApi for ServerOsInputOutput { &self, terminal_action: TerminalAction, quit_cb: Box, - ) -> (RawFd, RawFd) { + default_editor: Option, + ) -> Result<(RawFd, RawFd), &'static str> { let orig_termios = self.orig_termios.lock().unwrap(); - spawn_terminal(terminal_action, orig_termios.clone(), quit_cb) + spawn_terminal( + terminal_action, + orig_termios.clone(), + quit_cb, + default_editor, + ) } fn read_from_tty_stdout(&self, fd: RawFd, buf: &mut [u8]) -> Result { unistd::read(fd, buf) diff --git a/zellij-server/src/panes/floating_panes/mod.rs b/zellij-server/src/panes/floating_panes/mod.rs index e4925c36..f8bd46bc 100644 --- a/zellij-server/src/panes/floating_panes/mod.rs +++ b/zellij-server/src/panes/floating_panes/mod.rs @@ -101,6 +101,49 @@ impl FloatingPanes { self.panes.insert(pane_id, pane); self.z_indices.push(pane_id); } + pub fn replace_active_pane( + &mut self, + pane: Box, + client_id: ClientId, + ) -> Option> { + self.active_panes + .get(&client_id) + .copied() + .and_then(|active_pane_id| self.replace_pane(active_pane_id, pane)) + } + pub fn replace_pane( + &mut self, + pane_id: PaneId, + mut with_pane: Box, + ) -> Option> { + let with_pane_id = with_pane.pid(); + with_pane.set_content_offset(Offset::frame(1)); + let removed_pane = self.panes.remove(&pane_id).map(|removed_pane| { + let removed_pane_id = removed_pane.pid(); + let with_pane_id = with_pane.pid(); + let removed_pane_geom = removed_pane.current_geom(); + with_pane.set_geom(removed_pane_geom); + self.panes.insert(with_pane_id, with_pane); + let z_index = self + .z_indices + .iter() + .position(|pane_id| pane_id == &removed_pane_id) + .unwrap(); + self.z_indices.remove(z_index); + self.z_indices.insert(z_index, with_pane_id); + removed_pane + }); + + // update the desired_pane_positions to relate to the new pane + if let Some(desired_pane_position) = self.desired_pane_positions.remove(&pane_id) { + self.desired_pane_positions + .insert(with_pane_id, desired_pane_position); + } + + // move clients from the previously active pane to the new pane we just inserted + self.move_clients_between_panes(pane_id, with_pane_id); + removed_pane + } pub fn remove_pane(&mut self, pane_id: PaneId) -> Option> { self.z_indices.retain(|p_id| *p_id != pane_id); self.desired_pane_positions.remove(&pane_id); @@ -241,6 +284,11 @@ impl FloatingPanes { floating_pane_grid.resize(new_screen_size); self.set_force_render(); } + pub fn resize_pty_all_panes(&mut self, os_api: &mut Box) { + for pane in self.panes.values_mut() { + resize_pty!(pane, os_api); + } + } pub fn resize_active_pane_left( &mut self, client_id: ClientId, @@ -875,4 +923,16 @@ impl FloatingPanes { pub fn get_panes(&self) -> impl Iterator)> { self.panes.iter() } + fn move_clients_between_panes(&mut self, from_pane_id: PaneId, to_pane_id: PaneId) { + let clients_in_pane: Vec = self + .active_panes + .iter() + .filter(|(_cid, pid)| **pid == from_pane_id) + .map(|(cid, _pid)| *cid) + .collect(); + for client_id in clients_in_pane { + self.active_panes.remove(&client_id); + self.active_panes.insert(client_id, to_pane_id); + } + } } diff --git a/zellij-server/src/panes/grid.rs b/zellij-server/src/panes/grid.rs index e01c1ad7..55b9edab 100644 --- a/zellij-server/src/panes/grid.rs +++ b/zellij-server/src/panes/grid.rs @@ -1460,6 +1460,9 @@ impl Grid { Some(selection.join("\n")) } } + pub fn absolute_position_in_scrollback(&self) -> usize { + self.lines_above.len() + self.cursor.y + } fn update_selected_lines(&mut self, old_selection: &Selection, new_selection: &Selection) { for l in old_selection.diff(new_selection, self.height) { diff --git a/zellij-server/src/panes/terminal_pane.rs b/zellij-server/src/panes/terminal_pane.rs index 7ada5f02..dd91149b 100644 --- a/zellij-server/src/panes/terminal_pane.rs +++ b/zellij-server/src/panes/terminal_pane.rs @@ -481,6 +481,10 @@ impl Pane for TerminalPane { fn mouse_mode(&self) -> bool { self.grid.mouse_mode } + fn get_line_number(&self) -> Option { + // + 1 because the absolute position in the scrollback is 0 indexed and this should be 1 indexed + Some(self.grid.absolute_position_in_scrollback() + 1) + } } impl TerminalPane { diff --git a/zellij-server/src/panes/tiled_panes/mod.rs b/zellij-server/src/panes/tiled_panes/mod.rs index 468303b7..1f958297 100644 --- a/zellij-server/src/panes/tiled_panes/mod.rs +++ b/zellij-server/src/panes/tiled_panes/mod.rs @@ -105,13 +105,58 @@ impl TiledPanes { os_api, } } - pub fn add_pane_with_existing_geom(&mut self, pane_id: PaneId, pane: Box) { + pub fn add_pane_with_existing_geom(&mut self, pane_id: PaneId, mut pane: Box) { + if self.draw_pane_frames { + pane.set_content_offset(Offset::frame(1)); + } self.panes.insert(pane_id, pane); } + pub fn replace_active_pane( + &mut self, + pane: Box, + client_id: ClientId, + ) -> Option> { + let pane_id = pane.pid(); + // remove the currently active pane + let previously_active_pane = self + .active_panes + .get(&client_id) + .copied() + .and_then(|active_pane_id| self.replace_pane(active_pane_id, pane)); + + // move clients from the previously active pane to the new pane we just inserted + if let Some(previously_active_pane) = previously_active_pane.as_ref() { + let previously_active_pane_id = previously_active_pane.pid(); + self.move_clients_between_panes(previously_active_pane_id, pane_id); + } + previously_active_pane + } + pub fn replace_pane( + &mut self, + pane_id: PaneId, + mut with_pane: Box, + ) -> Option> { + let with_pane_id = with_pane.pid(); + if self.draw_pane_frames { + with_pane.set_content_offset(Offset::frame(1)); + } + let removed_pane = self.panes.remove(&pane_id).map(|removed_pane| { + let with_pane_id = with_pane.pid(); + let removed_pane_geom = removed_pane.current_geom(); + with_pane.set_geom(removed_pane_geom); + self.panes.insert(with_pane_id, with_pane); + removed_pane + }); + + // move clients from the previously active pane to the new pane we just inserted + self.move_clients_between_panes(pane_id, with_pane_id); + removed_pane + } pub fn insert_pane(&mut self, pane_id: PaneId, mut pane: Box) { let cursor_height_width_ratio = self.cursor_height_width_ratio(); let pane_grid = TiledPaneGrid::new( &mut self.panes, + &self.panes_to_hide, *self.display_area.borrow(), *self.viewport.borrow(), ); @@ -133,6 +178,7 @@ impl TiledPanes { let cursor_height_width_ratio = self.cursor_height_width_ratio(); let pane_grid = TiledPaneGrid::new( &mut self.panes, + &self.panes_to_hide, *self.display_area.borrow(), *self.viewport.borrow(), ); @@ -166,6 +212,7 @@ impl TiledPanes { pub fn relayout(&mut self, direction: Direction) { let mut pane_grid = TiledPaneGrid::new( &mut self.panes, + &self.panes_to_hide, *self.display_area.borrow(), *self.viewport.borrow(), ); @@ -399,12 +446,13 @@ impl TiledPanes { { let mut display_area = self.display_area.borrow_mut(); let mut viewport = self.viewport.borrow_mut(); - let panes = self - .panes - .iter_mut() - .filter(|(pid, _)| !self.panes_to_hide.contains(pid)); let Size { rows, cols } = new_screen_size; - let mut pane_grid = TiledPaneGrid::new(panes, *display_area, *viewport); + let mut pane_grid = TiledPaneGrid::new( + &mut self.panes, + &self.panes_to_hide, + *display_area, + *viewport, + ); if pane_grid.layout(Direction::Horizontal, cols).is_ok() { let column_difference = cols as isize - display_area.cols as isize; // FIXME: Should the viewport be an Offset? @@ -427,6 +475,7 @@ impl TiledPanes { if let Some(active_pane_id) = self.get_active_pane_id(client_id) { let mut pane_grid = TiledPaneGrid::new( &mut self.panes, + &self.panes_to_hide, *self.display_area.borrow(), *self.viewport.borrow(), ); @@ -440,6 +489,7 @@ impl TiledPanes { if let Some(active_pane_id) = self.get_active_pane_id(client_id) { let mut pane_grid = TiledPaneGrid::new( &mut self.panes, + &self.panes_to_hide, *self.display_area.borrow(), *self.viewport.borrow(), ); @@ -453,6 +503,7 @@ impl TiledPanes { if let Some(active_pane_id) = self.get_active_pane_id(client_id) { let mut pane_grid = TiledPaneGrid::new( &mut self.panes, + &self.panes_to_hide, *self.display_area.borrow(), *self.viewport.borrow(), ); @@ -466,6 +517,7 @@ impl TiledPanes { if let Some(active_pane_id) = self.get_active_pane_id(client_id) { let mut pane_grid = TiledPaneGrid::new( &mut self.panes, + &self.panes_to_hide, *self.display_area.borrow(), *self.viewport.borrow(), ); @@ -479,6 +531,7 @@ impl TiledPanes { if let Some(active_pane_id) = self.get_active_pane_id(client_id) { let mut pane_grid = TiledPaneGrid::new( &mut self.panes, + &self.panes_to_hide, *self.display_area.borrow(), *self.viewport.borrow(), ); @@ -492,6 +545,7 @@ impl TiledPanes { if let Some(active_pane_id) = self.get_active_pane_id(client_id) { let mut pane_grid = TiledPaneGrid::new( &mut self.panes, + &self.panes_to_hide, *self.display_area.borrow(), *self.viewport.borrow(), ); @@ -507,6 +561,7 @@ impl TiledPanes { let active_pane_id = self.get_active_pane_id(client_id).unwrap(); let pane_grid = TiledPaneGrid::new( &mut self.panes, + &self.panes_to_hide, *self.display_area.borrow(), *self.viewport.borrow(), ); @@ -522,6 +577,7 @@ impl TiledPanes { let active_pane_id = self.get_active_pane_id(client_id).unwrap(); let pane_grid = TiledPaneGrid::new( &mut self.panes, + &self.panes_to_hide, *self.display_area.borrow(), *self.viewport.borrow(), ); @@ -547,6 +603,7 @@ impl TiledPanes { Some(active_pane_id) => { let pane_grid = TiledPaneGrid::new( &mut self.panes, + &self.panes_to_hide, *self.display_area.borrow(), *self.viewport.borrow(), ); @@ -587,6 +644,7 @@ impl TiledPanes { Some(active_pane_id) => { let pane_grid = TiledPaneGrid::new( &mut self.panes, + &self.panes_to_hide, *self.display_area.borrow(), *self.viewport.borrow(), ); @@ -627,6 +685,7 @@ impl TiledPanes { Some(active_pane_id) => { let pane_grid = TiledPaneGrid::new( &mut self.panes, + &self.panes_to_hide, *self.display_area.borrow(), *self.viewport.borrow(), ); @@ -667,6 +726,7 @@ impl TiledPanes { Some(active_pane_id) => { let pane_grid = TiledPaneGrid::new( &mut self.panes, + &self.panes_to_hide, *self.display_area.borrow(), *self.viewport.borrow(), ); @@ -706,6 +766,7 @@ impl TiledPanes { let active_pane_id = self.get_active_pane_id(client_id).unwrap(); let pane_grid = TiledPaneGrid::new( &mut self.panes, + &self.panes_to_hide, *self.display_area.borrow(), *self.viewport.borrow(), ); @@ -736,6 +797,7 @@ impl TiledPanes { if let Some(active_pane_id) = self.get_active_pane_id(client_id) { let pane_grid = TiledPaneGrid::new( &mut self.panes, + &self.panes_to_hide, *self.display_area.borrow(), *self.viewport.borrow(), ); @@ -770,6 +832,7 @@ impl TiledPanes { if let Some(active_pane_id) = self.get_active_pane_id(client_id) { let pane_grid = TiledPaneGrid::new( &mut self.panes, + &self.panes_to_hide, *self.display_area.borrow(), *self.viewport.borrow(), ); @@ -804,6 +867,7 @@ impl TiledPanes { if let Some(active_pane_id) = self.get_active_pane_id(client_id) { let pane_grid = TiledPaneGrid::new( &mut self.panes, + &self.panes_to_hide, *self.display_area.borrow(), *self.viewport.borrow(), ); @@ -838,6 +902,7 @@ impl TiledPanes { if let Some(active_pane_id) = self.get_active_pane_id(client_id) { let pane_grid = TiledPaneGrid::new( &mut self.panes, + &self.panes_to_hide, *self.display_area.borrow(), *self.viewport.borrow(), ); @@ -877,6 +942,7 @@ impl TiledPanes { match self .panes .iter() + .filter(|(p_id, _)| !self.panes_to_hide.contains(p_id)) .find(|(p_id, p)| **p_id != pane_id && p.selectable()) .map(|(p_id, _p)| p_id) { @@ -890,9 +956,13 @@ impl TiledPanes { None => self.active_panes.clear(), } } + pub fn extract_pane(&mut self, pane_id: PaneId) -> Option> { + self.panes.remove(&pane_id) + } pub fn remove_pane(&mut self, pane_id: PaneId) -> Option> { let mut pane_grid = TiledPaneGrid::new( &mut self.panes, + &self.panes_to_hide, *self.display_area.borrow(), *self.viewport.borrow(), ); @@ -1015,6 +1085,24 @@ impl TiledPanes { pub fn panes_to_hide_count(&self) -> usize { self.panes_to_hide.len() } + pub fn add_to_hidden_panels(&mut self, pid: PaneId) { + self.panes_to_hide.insert(pid); + } + pub fn remove_from_hidden_panels(&mut self, pid: PaneId) { + self.panes_to_hide.remove(&pid); + } + fn move_clients_between_panes(&mut self, from_pane_id: PaneId, to_pane_id: PaneId) { + let clients_in_pane: Vec = self + .active_panes + .iter() + .filter(|(_cid, pid)| **pid == from_pane_id) + .map(|(cid, _pid)| *cid) + .collect(); + for client_id in clients_in_pane { + self.active_panes.remove(&client_id); + self.active_panes.insert(client_id, to_pane_id); + } + } } #[allow(clippy::borrowed_box)] diff --git a/zellij-server/src/panes/tiled_panes/tiled_pane_grid.rs b/zellij-server/src/panes/tiled_panes/tiled_pane_grid.rs index 0eeaeee7..53a48f9e 100644 --- a/zellij-server/src/panes/tiled_panes/tiled_pane_grid.rs +++ b/zellij-server/src/panes/tiled_panes/tiled_pane_grid.rs @@ -26,10 +26,15 @@ pub struct TiledPaneGrid<'a> { impl<'a> TiledPaneGrid<'a> { pub fn new( panes: impl IntoIterator)>, + panes_to_hide: &HashSet, display_area: Size, viewport: Viewport, ) -> Self { - let panes: HashMap<_, _> = panes.into_iter().map(|(p_id, p)| (*p_id, p)).collect(); + let panes: HashMap<_, _> = panes + .into_iter() + .filter(|(p_id, _)| !panes_to_hide.contains(p_id)) + .map(|(p_id, p)| (*p_id, p)) + .collect(); TiledPaneGrid { panes: Rc::new(RefCell::new(panes)), display_area, diff --git a/zellij-server/src/pty.rs b/zellij-server/src/pty.rs index 327347cb..6abcd667 100644 --- a/zellij-server/src/pty.rs +++ b/zellij-server/src/pty.rs @@ -41,6 +41,7 @@ pub enum ClientOrTabIndex { #[derive(Clone, Debug)] pub(crate) enum PtyInstruction { SpawnTerminal(Option, ClientOrTabIndex), + OpenInPlaceEditor(PathBuf, Option, ClientId), // Option is the optional line number SpawnTerminalVertically(Option, ClientId), SpawnTerminalHorizontally(Option, ClientId), UpdateActivePane(Option, ClientId), @@ -55,6 +56,7 @@ impl From<&PtyInstruction> for PtyContext { fn from(pty_instruction: &PtyInstruction) -> Self { match *pty_instruction { PtyInstruction::SpawnTerminal(..) => PtyContext::SpawnTerminal, + PtyInstruction::OpenInPlaceEditor(..) => PtyContext::OpenInPlaceEditor, PtyInstruction::SpawnTerminalVertically(..) => PtyContext::SpawnTerminalVertically, PtyInstruction::SpawnTerminalHorizontally(..) => PtyContext::SpawnTerminalHorizontally, PtyInstruction::UpdateActivePane(..) => PtyContext::UpdateActivePane, @@ -73,6 +75,7 @@ pub(crate) struct Pty { pub id_to_child_pid: HashMap, // pty_primary => child raw fd debug_to_file: bool, task_handles: HashMap>, + default_editor: Option, } use std::convert::TryFrom; @@ -83,7 +86,9 @@ pub(crate) fn pty_thread_main(mut pty: Pty, layout: Box) { err_ctx.add_call(ContextType::Pty((&event).into())); match event { PtyInstruction::SpawnTerminal(terminal_action, client_or_tab_index) => { - let pid = pty.spawn_terminal(terminal_action, client_or_tab_index); + let pid = pty + .spawn_terminal(terminal_action, client_or_tab_index) + .unwrap(); // TODO: handle error here pty.bus .senders .send_to_screen(ScreenInstruction::NewPane( @@ -92,9 +97,29 @@ pub(crate) fn pty_thread_main(mut pty: Pty, layout: Box) { )) .unwrap(); } + PtyInstruction::OpenInPlaceEditor(temp_file, line_number, client_id) => { + match pty.spawn_terminal( + Some(TerminalAction::OpenFile(temp_file, line_number)), + ClientOrTabIndex::ClientId(client_id), + ) { + Ok(pid) => { + pty.bus + .senders + .send_to_screen(ScreenInstruction::OpenInPlaceEditor( + PaneId::Terminal(pid), + client_id, + )) + .unwrap(); + } + Err(e) => { + log::error!("Failed to open editor: {}", e); + } + } + } PtyInstruction::SpawnTerminalVertically(terminal_action, client_id) => { - let pid = - pty.spawn_terminal(terminal_action, ClientOrTabIndex::ClientId(client_id)); + let pid = pty + .spawn_terminal(terminal_action, ClientOrTabIndex::ClientId(client_id)) + .unwrap(); // TODO: handle error here pty.bus .senders .send_to_screen(ScreenInstruction::VerticalSplit( @@ -104,8 +129,9 @@ pub(crate) fn pty_thread_main(mut pty: Pty, layout: Box) { .unwrap(); } PtyInstruction::SpawnTerminalHorizontally(terminal_action, client_id) => { - let pid = - pty.spawn_terminal(terminal_action, ClientOrTabIndex::ClientId(client_id)); + let pid = pty + .spawn_terminal(terminal_action, ClientOrTabIndex::ClientId(client_id)) + .unwrap(); // TODO: handle error here pty.bus .senders .send_to_screen(ScreenInstruction::HorizontalSplit( @@ -267,13 +293,18 @@ fn stream_terminal_bytes( } impl Pty { - pub fn new(bus: Bus, debug_to_file: bool) -> Self { + pub fn new( + bus: Bus, + debug_to_file: bool, + default_editor: Option, + ) -> Self { Pty { active_panes: HashMap::new(), bus, id_to_child_pid: HashMap::new(), debug_to_file, task_handles: HashMap::new(), + default_editor, } } pub fn get_default_terminal(&self) -> TerminalAction { @@ -306,7 +337,7 @@ impl Pty { &mut self, terminal_action: Option, client_or_tab_index: ClientOrTabIndex, - ) -> RawFd { + ) -> Result { let terminal_action = match client_or_tab_index { ClientOrTabIndex::ClientId(client_id) => { let mut terminal_action = @@ -329,7 +360,7 @@ impl Pty { .os_input .as_mut() .unwrap() - .spawn_terminal(terminal_action, quit_cb); + .spawn_terminal(terminal_action, quit_cb, self.default_editor.clone())?; let task_handle = stream_terminal_bytes( pid_primary, self.bus.senders.clone(), @@ -338,7 +369,7 @@ impl Pty { ); self.task_handles.insert(pid_primary, task_handle); self.id_to_child_pid.insert(pid_primary, child_fd); - pid_primary + Ok(pid_primary) } pub fn spawn_terminals_for_layout( &mut self, @@ -365,7 +396,8 @@ impl Pty { .os_input .as_mut() .unwrap() - .spawn_terminal(cmd, quit_cb); + .spawn_terminal(cmd, quit_cb, self.default_editor.clone()) + .unwrap(); // TODO: handle error here self.id_to_child_pid.insert(pid_primary, child_fd); new_pane_pids.push(pid_primary); } @@ -375,7 +407,8 @@ impl Pty { .os_input .as_mut() .unwrap() - .spawn_terminal(default_shell.clone(), quit_cb); + .spawn_terminal(default_shell.clone(), quit_cb, self.default_editor.clone()) + .unwrap(); // TODO: handle error here self.id_to_child_pid.insert(pid_primary, child_fd); new_pane_pids.push(pid_primary); } diff --git a/zellij-server/src/route.rs b/zellij-server/src/route.rs index e8620368..f8388323 100644 --- a/zellij-server/src/route.rs +++ b/zellij-server/src/route.rs @@ -152,6 +152,12 @@ fn route_action( .send_to_screen(ScreenInstruction::DumpScreen(val, client_id)) .unwrap(); } + Action::EditScrollback => { + session + .senders + .send_to_screen(ScreenInstruction::EditScrollback(client_id)) + .unwrap(); + } Action::ScrollUp => { session .senders diff --git a/zellij-server/src/screen.rs b/zellij-server/src/screen.rs index f83f9da0..1723528d 100644 --- a/zellij-server/src/screen.rs +++ b/zellij-server/src/screen.rs @@ -40,6 +40,7 @@ pub enum ScreenInstruction { PtyBytes(RawFd, VteBytes), Render, NewPane(PaneId, ClientOrTabIndex), + OpenInPlaceEditor(PaneId, ClientId), TogglePaneEmbedOrFloating(ClientId), ToggleFloatingPanes(ClientId, Option), HorizontalSplit(PaneId, ClientId), @@ -67,6 +68,7 @@ pub enum ScreenInstruction { MovePaneLeft(ClientId), Exit, DumpScreen(String, ClientId), + EditScrollback(ClientId), ScrollUp(ClientId), ScrollUpAt(Position, ClientId), ScrollDown(ClientId), @@ -115,6 +117,7 @@ impl From<&ScreenInstruction> for ScreenContext { ScreenInstruction::PtyBytes(..) => ScreenContext::HandlePtyBytes, ScreenInstruction::Render => ScreenContext::Render, ScreenInstruction::NewPane(..) => ScreenContext::NewPane, + ScreenInstruction::OpenInPlaceEditor(..) => ScreenContext::OpenInPlaceEditor, ScreenInstruction::TogglePaneEmbedOrFloating(..) => { ScreenContext::TogglePaneEmbedOrFloating } @@ -148,6 +151,7 @@ impl From<&ScreenInstruction> for ScreenContext { ScreenInstruction::MovePaneLeft(..) => ScreenContext::MovePaneLeft, ScreenInstruction::Exit => ScreenContext::Exit, ScreenInstruction::DumpScreen(..) => ScreenContext::DumpScreen, + ScreenInstruction::EditScrollback(..) => ScreenContext::EditScrollback, ScreenInstruction::ScrollUp(..) => ScreenContext::ScrollUp, ScreenInstruction::ScrollDown(..) => ScreenContext::ScrollDown, ScreenInstruction::ScrollToBottom(..) => ScreenContext::ScrollToBottom, @@ -798,6 +802,7 @@ pub(crate) fn screen_thread_main( client_attributes: ClientAttributes, config_options: Box, ) { + // let mut scrollbacks: HashMap = HashMap::new(); let capabilities = config_options.simplified_ui; let draw_pane_frames = config_options.pane_frames.unwrap_or(true); let session_is_mirrored = config_options.mirror_session.unwrap_or(false); @@ -867,6 +872,22 @@ pub(crate) fn screen_thread_main( screen.render(); } + ScreenInstruction::OpenInPlaceEditor(pid, client_id) => { + if let Some(active_tab) = screen.get_active_tab_mut(client_id) { + active_tab.suppress_active_pane(pid, client_id); + } else { + log::error!("Active tab not found for client id: {:?}", client_id); + return; + } + screen + .bus + .senders + .send_to_server(ServerInstruction::UnblockInputThread) + .unwrap(); + screen.update_tabs(); + + screen.render(); + } ScreenInstruction::TogglePaneEmbedOrFloating(client_id) => { if let Some(active_tab) = screen.get_active_tab_mut(client_id) { active_tab.toggle_pane_embed_or_floating(client_id); @@ -1079,6 +1100,15 @@ pub(crate) fn screen_thread_main( screen.render(); } + ScreenInstruction::EditScrollback(client_id) => { + if let Some(active_tab) = screen.get_active_tab_mut(client_id) { + active_tab.edit_scrollback(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); @@ -1241,7 +1271,7 @@ pub(crate) fn screen_thread_main( Some(client_id) => { screen .get_active_tab_mut(client_id) - .and_then(|active_tab| active_tab.close_pane(id)) + .and_then(|active_tab| active_tab.close_pane(id, false)) .or_else(|| { log::error!("Active tab not found for client id: {:?}", client_id); None @@ -1250,7 +1280,7 @@ pub(crate) fn screen_thread_main( None => { for tab in screen.tabs.values_mut() { if tab.get_all_pane_ids().contains(&id) { - tab.close_pane(id); + tab.close_pane(id, false); break; } } diff --git a/zellij-server/src/tab/mod.rs b/zellij-server/src/tab/mod.rs index cdd968c3..5c3d0434 100644 --- a/zellij-server/src/tab/mod.rs +++ b/zellij-server/src/tab/mod.rs @@ -5,6 +5,8 @@ mod clipboard; mod copy_command; use copy_command::CopyCommand; +use std::env::temp_dir; +use uuid::Uuid; use zellij_tile::prelude::Style; use zellij_utils::position::{Column, Line}; use zellij_utils::{position::Position, serde, zellij_tile}; @@ -70,6 +72,7 @@ pub(crate) struct Tab { pub name: String, tiled_panes: TiledPanes, floating_panes: FloatingPanes, + suppressed_panes: HashMap>, max_panes: Option, viewport: Rc>, // includes all non-UI panes display_area: Rc>, // includes all panes (including eg. the status bar and tab bar in the default layout) @@ -268,6 +271,9 @@ pub trait Pane { fn borderless(&self) -> bool; fn handle_right_click(&mut self, _to: &Position, _client_id: ClientId) {} fn mouse_mode(&self) -> bool; + fn get_line_number(&self) -> Option { + None + } } impl Tab { @@ -339,6 +345,7 @@ impl Tab { position, tiled_panes, floating_panes, + suppressed_panes: HashMap::new(), name, max_panes, viewport, @@ -577,7 +584,8 @@ impl Tab { if let Some(focused_floating_pane_id) = self.floating_panes.active_pane_id(client_id) { if self.tiled_panes.has_room_for_new_pane() { // this unwrap is safe because floating panes should not be visible if there are no floating panes - let floating_pane_to_embed = self.close_pane(focused_floating_pane_id).unwrap(); + let floating_pane_to_embed = + self.close_pane(focused_floating_pane_id, true).unwrap(); self.tiled_panes .insert_pane(focused_floating_pane_id, floating_pane_to_embed); self.should_clear_display_before_rendering = true; @@ -592,7 +600,7 @@ impl Tab { // don't close the only pane on screen... return; } - if let Some(mut embedded_pane_to_float) = self.close_pane(focused_pane_id) { + if let Some(mut embedded_pane_to_float) = self.close_pane(focused_pane_id, true) { embedded_pane_to_float.set_geom(new_pane_geom); resize_pty!(embedded_pane_to_float, self.os_api); embedded_pane_to_float.set_active_at(Instant::now()); @@ -691,6 +699,47 @@ impl Tab { } } } + pub fn suppress_active_pane(&mut self, pid: PaneId, client_id: ClientId) { + // this method creates a new pane from pid and replaces it with the active pane + // the active pane is then suppressed (hidden and not rendered) until the current + // created pane is closed, in which case it will be replaced back by it + match pid { + PaneId::Terminal(pid) => { + let next_terminal_position = self.get_next_terminal_position(); // TODO: this is not accurate in this case + let new_pane = TerminalPane::new( + pid, + PaneGeom::default(), // the initial size will be set later + self.style, + next_terminal_position, + String::new(), + self.link_handler.clone(), + self.character_cell_size.clone(), + self.terminal_emulator_colors.clone(), + ); + let replaced_pane = if self.floating_panes.panes_are_visible() { + self.floating_panes + .replace_active_pane(Box::new(new_pane), client_id) + } else { + self.tiled_panes + .replace_active_pane(Box::new(new_pane), client_id) + }; + match replaced_pane { + Some(replaced_pane) => { + self.suppressed_panes + .insert(PaneId::Terminal(pid), replaced_pane); + let current_active_pane = self.get_active_pane(client_id).unwrap(); // this will be the newly replaced pane we just created + resize_pty!(current_active_pane, self.os_api); + } + None => { + log::error!("Could not find editor pane to replace - is no pane focused?") + } + } + } + PaneId::Plugin(_pid) => { + // TBD, currently unsupported + } + } + } pub fn horizontal_split(&mut self, pid: PaneId, client_id: ClientId) { if self.floating_panes.panes_are_visible() { return; @@ -792,12 +841,22 @@ impl Tab { pub fn has_terminal_pid(&self, pid: RawFd) -> bool { self.tiled_panes.panes_contain(&PaneId::Terminal(pid)) || self.floating_panes.panes_contain(&PaneId::Terminal(pid)) + || self + .suppressed_panes + .values() + .find(|s_p| s_p.pid() == PaneId::Terminal(pid)) + .is_some() } pub fn handle_pty_bytes(&mut self, pid: RawFd, bytes: VteBytes) { if let Some(terminal_output) = self .tiled_panes .get_pane_mut(PaneId::Terminal(pid)) .or_else(|| self.floating_panes.get_pane_mut(PaneId::Terminal(pid))) + .or_else(|| { + self.suppressed_panes + .values_mut() + .find(|s_p| s_p.pid() == PaneId::Terminal(pid)) + }) { // If the pane is scrolled buffer the vte events if terminal_output.is_scrolled() { @@ -827,6 +886,11 @@ impl Tab { .tiled_panes .get_pane_mut(PaneId::Terminal(pid)) .or_else(|| self.floating_panes.get_pane_mut(PaneId::Terminal(pid))) + .or_else(|| { + self.suppressed_panes + .values_mut() + .find(|s_p| s_p.pid() == PaneId::Terminal(pid)) + }) { terminal_output.handle_pty_bytes(bytes); let messages_to_pty = terminal_output.drain_messages_to_pty(); @@ -1070,6 +1134,7 @@ impl Tab { } pub fn resize_whole_tab(&mut self, new_screen_size: Size) { self.floating_panes.resize(new_screen_size); + self.floating_panes.resize_pty_all_panes(&mut self.os_api); // we need to do this explicitly because floating_panes.resize does not do this self.tiled_panes.resize(new_screen_size); self.should_clear_display_before_rendering = true; } @@ -1308,7 +1373,7 @@ impl Tab { self.senders .send_to_pty(PtyInstruction::ClosePane(pid)) .unwrap(); - self.close_pane(pid); + self.close_pane(pid, false); } } } @@ -1345,7 +1410,20 @@ impl Tab { } } } - pub fn close_pane(&mut self, id: PaneId) -> Option> { + pub fn close_pane( + &mut self, + id: PaneId, + ignore_suppressed_panes: bool, + ) -> Option> { + // we need to ignore suppressed panes when we toggle a pane to be floating/embedded(tiled) + // this is because in that case, while we do use this logic, we're not actually closing the + // pane, we're moving it + // + // TODO: separate the "close_pane" logic and the "move_pane_somewhere_else" logic, they're + // overloaded here and that's not great + if !ignore_suppressed_panes && self.suppressed_panes.contains_key(&id) { + return self.replace_pane_with_suppressed_pane(id); + } if self.floating_panes.panes_contain(&id) { let closed_pane = self.floating_panes.remove_pane(id); self.floating_panes.move_clients_out_of_pane(id); @@ -1365,10 +1443,38 @@ impl Tab { closed_pane } } + pub fn replace_pane_with_suppressed_pane(&mut self, pane_id: PaneId) -> Option> { + self.suppressed_panes + .remove(&pane_id) + .and_then(|suppressed_pane| { + let suppressed_pane_id = suppressed_pane.pid(); + let replaced_pane = if self.are_floating_panes_visible() { + self.floating_panes.replace_pane(pane_id, suppressed_pane) + } else { + self.tiled_panes.replace_pane(pane_id, suppressed_pane) + }; + if let Some(suppressed_pane) = self + .floating_panes + .get_pane(suppressed_pane_id) + .or_else(|| self.tiled_panes.get_pane(suppressed_pane_id)) + { + // You may be thinking: why aren't we using the original "suppressed_pane" here, + // isn't it the same one? + // + // Yes, you are right! However, we moved it into its correct environment above + // (either floating_panes or tiled_panes) where it received a new geometry based on + // the pane there we replaced. Now, we need to update its pty about its new size. + // We couldn't do that before, and we can't use the original moved item now - so we + // need to refetch it + resize_pty!(suppressed_pane, self.os_api); + } + replaced_pane + }) + } pub fn close_focused_pane(&mut self, client_id: ClientId) { if self.floating_panes.panes_are_visible() { if let Some(active_floating_pane_id) = self.floating_panes.active_pane_id(client_id) { - self.close_pane(active_floating_pane_id); + self.close_pane(active_floating_pane_id, false); self.senders .send_to_pty(PtyInstruction::ClosePane(active_floating_pane_id)) .unwrap(); @@ -1376,7 +1482,7 @@ impl Tab { } } if let Some(active_pane_id) = self.tiled_panes.get_active_pane_id(client_id) { - self.close_pane(active_pane_id); + self.close_pane(active_pane_id, false); self.senders .send_to_pty(PtyInstruction::ClosePane(active_pane_id)) .unwrap(); @@ -1388,6 +1494,22 @@ impl Tab { self.os_api.write_to_file(dump, file); } } + pub fn edit_scrollback(&mut self, client_id: ClientId) { + let mut file = temp_dir(); + file.push(format!("{}.dump", Uuid::new_v4())); + self.dump_active_terminal_screen(Some(String::from(file.to_string_lossy())), client_id); + // let line_number = self.get_active_pane(client_id).map(|a_t| a_t.get_line_number()); + let line_number = self + .get_active_pane(client_id) + .and_then(|a_t| a_t.get_line_number()); + self.senders + .send_to_pty(PtyInstruction::OpenInPlaceEditor( + file, + line_number, + client_id, + )) + .unwrap(); + } 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/snapshots/zellij_server__tab__tab_integration_tests__close_suppressing_floating_pane.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__close_suppressing_floating_pane.snap new file mode 100644 index 00000000..91910115 --- /dev/null +++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__close_suppressing_floating_pane.snap @@ -0,0 +1,26 @@ +--- +source: zellij-server/src/tab/./unit/tab_integration_tests.rs +assertion_line: 1352 +expression: snapshot +--- +00 (C): ┌ Pane #1 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +01 (C): │ │ +02 (C): │ │ +03 (C): │ │ +04 (C): │ │ +05 (C): │ ┌ Pane #2 ─────────────────────────────────────────────────┐ │ +06 (C): │ │ │ │ +07 (C): │ │ │ │ +08 (C): │ │ │ │ +09 (C): │ │I am the original pane │ │ +10 (C): │ │ │ │ +11 (C): │ │ │ │ +12 (C): │ │ │ │ +13 (C): │ │ │ │ +14 (C): │ └──────────────────────────────────────────────────────────┘ │ +15 (C): │ │ +16 (C): │ │ +17 (C): │ │ +18 (C): │ │ +19 (C): └───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ + diff --git a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__close_suppressing_tiled_pane.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__close_suppressing_tiled_pane.snap new file mode 100644 index 00000000..f2f19516 --- /dev/null +++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__close_suppressing_tiled_pane.snap @@ -0,0 +1,26 @@ +--- +source: zellij-server/src/tab/./unit/tab_integration_tests.rs +assertion_line: 1318 +expression: snapshot +--- +00 (C): ┌ Pane #1 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +01 (C): │ │ +02 (C): │ │ +03 (C): │ │ +04 (C): │I am the original pane │ +05 (C): │ │ +06 (C): │ │ +07 (C): │ │ +08 (C): │ │ +09 (C): │ │ +10 (C): │ │ +11 (C): │ │ +12 (C): │ │ +13 (C): │ │ +14 (C): │ │ +15 (C): │ │ +16 (C): │ │ +17 (C): │ │ +18 (C): │ │ +19 (C): └───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ + diff --git a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__resize_whole_tab_while_floting_pane_is_suppressed.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__resize_whole_tab_while_floting_pane_is_suppressed.snap new file mode 100644 index 00000000..a339044f --- /dev/null +++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__resize_whole_tab_while_floting_pane_is_suppressed.snap @@ -0,0 +1,26 @@ +--- +source: zellij-server/src/tab/./unit/tab_integration_tests.rs +assertion_line: 1474 +expression: snapshot +--- +00 (C): ┌ Pane #1 ────────────────────┌ Pane #3 ─────────────────────────────────────────────────┐─────────┐ +01 (C): │ │ │ │ +02 (C): │ │ │ │ +03 (C): │ │ │ │ +04 (C): │ │I am an editor pane │ │ +05 (C): │ │ │ │ +06 (C): │ │ │ │ +07 (C): │ │ │ │ +08 (C): │ │ │ │ +09 (C): └─────────────────────────────└──────────────────────────────────────────────────────────┘─────────┘ +10 (C): +11 (C): +12 (C): +13 (C): +14 (C): +15 (C): +16 (C): +17 (C): +18 (C): +19 (C): + diff --git a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__resize_whole_tab_while_tiled_pane_is_suppressed.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__resize_whole_tab_while_tiled_pane_is_suppressed.snap new file mode 100644 index 00000000..8d17504f --- /dev/null +++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__resize_whole_tab_while_tiled_pane_is_suppressed.snap @@ -0,0 +1,26 @@ +--- +source: zellij-server/src/tab/./unit/tab_integration_tests.rs +assertion_line: 1444 +expression: snapshot +--- +00 (C): ┌ Pane #2 ─────────────────────────────────────────────────────────────────────────────────────────┐ +01 (C): │ │ +02 (C): │ │ +03 (C): │ │ +04 (C): │I am an editor pane │ +05 (C): │ │ +06 (C): │ │ +07 (C): │ │ +08 (C): │ │ +09 (C): └──────────────────────────────────────────────────────────────────────────────────────────────────┘ +10 (C): +11 (C): +12 (C): +13 (C): +14 (C): +15 (C): +16 (C): +17 (C): +18 (C): +19 (C): + diff --git a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__suppress_floating_pane.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__suppress_floating_pane.snap new file mode 100644 index 00000000..2b7d6a21 --- /dev/null +++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__suppress_floating_pane.snap @@ -0,0 +1,26 @@ +--- +source: zellij-server/src/tab/./unit/tab_integration_tests.rs +assertion_line: 1288 +expression: snapshot +--- +00 (C): ┌ Pane #1 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +01 (C): │ │ +02 (C): │ │ +03 (C): │ │ +04 (C): │ │ +05 (C): │ ┌ Pane #3 ─────────────────────────────────────────────────┐ │ +06 (C): │ │ │ │ +07 (C): │ │ │ │ +08 (C): │ │ │ │ +09 (C): │ │I am an editor pane │ │ +10 (C): │ │ │ │ +11 (C): │ │ │ │ +12 (C): │ │ │ │ +13 (C): │ │ │ │ +14 (C): │ └──────────────────────────────────────────────────────────┘ │ +15 (C): │ │ +16 (C): │ │ +17 (C): │ │ +18 (C): │ │ +19 (C): └───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ + diff --git a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__suppress_floating_pane_embed_it_and_close_it.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__suppress_floating_pane_embed_it_and_close_it.snap new file mode 100644 index 00000000..84545289 --- /dev/null +++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__suppress_floating_pane_embed_it_and_close_it.snap @@ -0,0 +1,26 @@ +--- +source: zellij-server/src/tab/./unit/tab_integration_tests.rs +assertion_line: 1418 +expression: snapshot +--- +00 (C): ┌ Pane #1 ──────────────────────────────────────────────────┐┌ Pane #2 ─────────────────────────────────────────────────┐ +01 (C): │ ││ │ +02 (C): │ ││ │ +03 (C): │ ││ │ +04 (C): │ ││I am the original pane │ +05 (C): │ ││ │ +06 (C): │ ││ │ +07 (C): │ ││ │ +08 (C): │ ││ │ +09 (C): │ ││ │ +10 (C): │ ││ │ +11 (C): │ ││ │ +12 (C): │ ││ │ +13 (C): │ ││ │ +14 (C): │ ││ │ +15 (C): │ ││ │ +16 (C): │ ││ │ +17 (C): │ ││ │ +18 (C): │ ││ │ +19 (C): └───────────────────────────────────────────────────────────┘└──────────────────────────────────────────────────────────┘ + diff --git a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__suppress_tiled_pane.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__suppress_tiled_pane.snap new file mode 100644 index 00000000..9e91a126 --- /dev/null +++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__suppress_tiled_pane.snap @@ -0,0 +1,26 @@ +--- +source: zellij-server/src/tab/./unit/tab_integration_tests.rs +assertion_line: 1259 +expression: snapshot +--- +00 (C): ┌ Pane #2 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +01 (C): │ │ +02 (C): │ │ +03 (C): │ │ +04 (C): │I am an editor pane │ +05 (C): │ │ +06 (C): │ │ +07 (C): │ │ +08 (C): │ │ +09 (C): │ │ +10 (C): │ │ +11 (C): │ │ +12 (C): │ │ +13 (C): │ │ +14 (C): │ │ +15 (C): │ │ +16 (C): │ │ +17 (C): │ │ +18 (C): │ │ +19 (C): └───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ + diff --git a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__suppress_tiled_pane_float_it_and_close.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__suppress_tiled_pane_float_it_and_close.snap new file mode 100644 index 00000000..1f480ebf --- /dev/null +++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__suppress_tiled_pane_float_it_and_close.snap @@ -0,0 +1,26 @@ +--- +source: zellij-server/src/tab/./unit/tab_integration_tests.rs +assertion_line: 1383 +expression: snapshot +--- +00 (C): ┌ Pane #1 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +01 (C): │ │ +02 (C): │ │ +03 (C): │ │ +04 (C): │I am the original pane │ +05 (C): │ │ +06 (C): │ │ +07 (C): │ │ +08 (C): │ │ +09 (C): │ │ +10 (C): │ │ +11 (C): │ │ +12 (C): │ │ +13 (C): │ │ +14 (C): │ │ +15 (C): │ │ +16 (C): │ │ +17 (C): │ │ +18 (C): │ │ +19 (C): └───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ + diff --git a/zellij-server/src/tab/unit/tab_integration_tests.rs b/zellij-server/src/tab/unit/tab_integration_tests.rs index dc162762..949affa3 100644 --- a/zellij-server/src/tab/unit/tab_integration_tests.rs +++ b/zellij-server/src/tab/unit/tab_integration_tests.rs @@ -45,7 +45,8 @@ impl ServerOsApi for FakeInputOutput { &self, _file_to_open: TerminalAction, _quit_cb: Box, - ) -> (RawFd, RawFd) { + _default_editor: Option, + ) -> Result<(RawFd, RawFd), &'static str> { unimplemented!() } fn read_from_tty_stdout(&self, _fd: RawFd, _buf: &mut [u8]) -> Result { @@ -1233,3 +1234,213 @@ fn save_cursor_position_across_resizes() { ); assert_snapshot!(snapshot); } + +#[test] +fn suppress_tiled_pane() { + let size = Size { + cols: 121, + rows: 20, + }; + let client_id = 1; + let mut tab = create_new_tab(size); + let new_pane_id = PaneId::Terminal(2); + let mut output = Output::default(); + tab.suppress_active_pane(new_pane_id, client_id); + tab.handle_pty_bytes(2, Vec::from("\n\n\nI am an editor pane".as_bytes())); + tab.render(&mut output, None); + let snapshot = take_snapshot( + output.serialize().get(&client_id).unwrap(), + size.rows, + size.cols, + Palette::default(), + ); + assert_snapshot!(snapshot); +} + +#[test] +fn suppress_floating_pane() { + let size = Size { + cols: 121, + rows: 20, + }; + let client_id = 1; + let mut tab = create_new_tab(size); + let new_pane_id = PaneId::Terminal(2); + let editor_pane_id = PaneId::Terminal(3); + let mut output = Output::default(); + + tab.toggle_floating_panes(client_id, None); + tab.new_pane(new_pane_id, Some(client_id)); + tab.suppress_active_pane(editor_pane_id, client_id); + tab.handle_pty_bytes(3, Vec::from("\n\n\nI am an editor pane".as_bytes())); + tab.render(&mut output, None); + let snapshot = take_snapshot( + output.serialize().get(&client_id).unwrap(), + size.rows, + size.cols, + Palette::default(), + ); + assert_snapshot!(snapshot); +} + +#[test] +fn close_suppressing_tiled_pane() { + let size = Size { + cols: 121, + rows: 20, + }; + let client_id = 1; + let mut tab = create_new_tab(size); + let new_pane_id = PaneId::Terminal(2); + let mut output = Output::default(); + tab.suppress_active_pane(new_pane_id, client_id); + tab.handle_pty_bytes(2, Vec::from("\n\n\nI am an editor pane".as_bytes())); + tab.handle_pty_bytes(1, Vec::from("\n\n\nI am the original pane".as_bytes())); + tab.close_pane(new_pane_id, false); + tab.render(&mut output, None); + let snapshot = take_snapshot( + output.serialize().get(&client_id).unwrap(), + size.rows, + size.cols, + Palette::default(), + ); + assert_snapshot!(snapshot); +} + +#[test] +fn close_suppressing_floating_pane() { + let size = Size { + cols: 121, + rows: 20, + }; + let client_id = 1; + let mut tab = create_new_tab(size); + let new_pane_id = PaneId::Terminal(2); + let editor_pane_id = PaneId::Terminal(3); + let mut output = Output::default(); + + tab.toggle_floating_panes(client_id, None); + tab.new_pane(new_pane_id, Some(client_id)); + tab.suppress_active_pane(editor_pane_id, client_id); + tab.handle_pty_bytes(3, Vec::from("\n\n\nI am an editor pane".as_bytes())); + tab.handle_pty_bytes(2, Vec::from("\n\n\nI am the original pane".as_bytes())); + tab.close_pane(editor_pane_id, false); + tab.render(&mut output, None); + let snapshot = take_snapshot( + output.serialize().get(&client_id).unwrap(), + size.rows, + size.cols, + Palette::default(), + ); + assert_snapshot!(snapshot); +} + +#[test] +fn suppress_tiled_pane_float_it_and_close() { + let size = Size { + cols: 121, + rows: 20, + }; + let client_id = 1; + let mut tab = create_new_tab(size); + let new_pane_id = PaneId::Terminal(2); + let mut output = Output::default(); + tab.suppress_active_pane(new_pane_id, client_id); + tab.handle_pty_bytes(2, Vec::from("\n\n\nI am an editor pane".as_bytes())); + tab.handle_pty_bytes(1, Vec::from("\n\n\nI am the original pane".as_bytes())); + tab.toggle_pane_embed_or_floating(client_id); + tab.close_pane(new_pane_id, false); + tab.render(&mut output, None); + let snapshot = take_snapshot( + output.serialize().get(&client_id).unwrap(), + size.rows, + size.cols, + Palette::default(), + ); + assert_snapshot!(snapshot); +} + +#[test] +fn suppress_floating_pane_embed_it_and_close_it() { + let size = Size { + cols: 121, + rows: 20, + }; + let client_id = 1; + let mut tab = create_new_tab(size); + let new_pane_id = PaneId::Terminal(2); + let editor_pane_id = PaneId::Terminal(3); + let mut output = Output::default(); + + tab.toggle_floating_panes(client_id, None); + tab.new_pane(new_pane_id, Some(client_id)); + tab.suppress_active_pane(editor_pane_id, client_id); + tab.handle_pty_bytes(3, Vec::from("\n\n\nI am an editor pane".as_bytes())); + tab.handle_pty_bytes(2, Vec::from("\n\n\nI am the original pane".as_bytes())); + tab.toggle_pane_embed_or_floating(client_id); + tab.close_pane(editor_pane_id, false); + tab.render(&mut output, None); + let snapshot = take_snapshot( + output.serialize().get(&client_id).unwrap(), + size.rows, + size.cols, + Palette::default(), + ); + assert_snapshot!(snapshot); +} + +#[test] +fn resize_whole_tab_while_tiled_pane_is_suppressed() { + let size = Size { + cols: 121, + rows: 20, + }; + let client_id = 1; + let mut tab = create_new_tab(size); + let new_pane_id = PaneId::Terminal(2); + let mut output = Output::default(); + tab.suppress_active_pane(new_pane_id, client_id); + tab.handle_pty_bytes(2, Vec::from("\n\n\nI am an editor pane".as_bytes())); + tab.resize_whole_tab(Size { + cols: 100, + rows: 10, + }); + tab.render(&mut output, None); + let snapshot = take_snapshot( + output.serialize().get(&client_id).unwrap(), + size.rows, + size.cols, + Palette::default(), + ); + assert_snapshot!(snapshot); +} + +#[test] +fn resize_whole_tab_while_floting_pane_is_suppressed() { + let size = Size { + cols: 121, + rows: 20, + }; + let client_id = 1; + let mut tab = create_new_tab(size); + let new_pane_id = PaneId::Terminal(2); + let editor_pane_id = PaneId::Terminal(3); + let mut output = Output::default(); + + tab.toggle_floating_panes(client_id, None); + tab.new_pane(new_pane_id, Some(client_id)); + tab.suppress_active_pane(editor_pane_id, client_id); + tab.handle_pty_bytes(3, Vec::from("\n\n\nI am an editor pane".as_bytes())); + tab.resize_whole_tab(Size { + cols: 100, + rows: 10, + }); + tab.render(&mut output, None); + let snapshot = take_snapshot( + output.serialize().get(&client_id).unwrap(), + size.rows, + size.cols, + Palette::default(), + ); + assert_snapshot!(snapshot); +} diff --git a/zellij-server/src/tab/unit/tab_tests.rs b/zellij-server/src/tab/unit/tab_tests.rs index 7b2baa18..cec8fa2b 100644 --- a/zellij-server/src/tab/unit/tab_tests.rs +++ b/zellij-server/src/tab/unit/tab_tests.rs @@ -38,7 +38,8 @@ impl ServerOsApi for FakeInputOutput { &self, _file_to_open: TerminalAction, _quit_cb: Box, - ) -> (RawFd, RawFd) { + _default_editor: Option, + ) -> Result<(RawFd, RawFd), &'static str> { unimplemented!() } fn read_from_tty_stdout(&self, _fd: RawFd, _buf: &mut [u8]) -> Result { diff --git a/zellij-server/src/unit/screen_tests.rs b/zellij-server/src/unit/screen_tests.rs index ebd33096..489b507d 100644 --- a/zellij-server/src/unit/screen_tests.rs +++ b/zellij-server/src/unit/screen_tests.rs @@ -34,7 +34,8 @@ impl ServerOsApi for FakeInputOutput { &self, _file_to_open: TerminalAction, _quit_db: Box, - ) -> (RawFd, RawFd) { + _default_editor: Option, + ) -> Result<(RawFd, RawFd), &'static str> { unimplemented!() } fn read_from_tty_stdout(&self, _fd: RawFd, _buf: &mut [u8]) -> Result { diff --git a/zellij-server/src/wasm_vm.rs b/zellij-server/src/wasm_vm.rs index cfd4f760..4ae8bd9c 100644 --- a/zellij-server/src/wasm_vm.rs +++ b/zellij-server/src/wasm_vm.rs @@ -393,7 +393,7 @@ fn host_open_file(plugin_env: &PluginEnv) { plugin_env .senders .send_to_pty(PtyInstruction::SpawnTerminal( - Some(TerminalAction::OpenFile(path)), + Some(TerminalAction::OpenFile(path, None)), ClientOrTabIndex::TabIndex(plugin_env.tab_index), )) .unwrap(); diff --git a/zellij-utils/assets/config/default.yaml b/zellij-utils/assets/config/default.yaml index b053fa4a..961d37fb 100644 --- a/zellij-utils/assets/config/default.yaml +++ b/zellij-utils/assets/config/default.yaml @@ -271,6 +271,8 @@ keybinds: - action: [Resize: Decrease,] key: [ Alt: '-'] scroll: + - action: [EditScrollback, SwitchToMode: Normal] + key: [Char: 'e'] - action: [SwitchToMode: Normal,] key: [Ctrl: 's', Char: ' ', Char: "\n", Esc] - action: [SwitchToMode: Tab,] @@ -548,3 +550,6 @@ plugins: # Enable or disable automatic copy (and clear) of selection when releasing mouse #copy_on_select: true + +# Path to the default editor to use to edit pane scrollbuffer +# scrollback_editor: /usr/bin/nano diff --git a/zellij-utils/src/errors.rs b/zellij-utils/src/errors.rs index 677716a1..10c7330b 100644 --- a/zellij-utils/src/errors.rs +++ b/zellij-utils/src/errors.rs @@ -215,6 +215,7 @@ pub enum ScreenContext { HandlePtyBytes, Render, NewPane, + OpenInPlaceEditor, ToggleFloatingPanes, TogglePaneEmbedOrFloating, HorizontalSplit, @@ -243,6 +244,7 @@ pub enum ScreenContext { MovePaneLeft, Exit, DumpScreen, + EditScrollback, ScrollUp, ScrollUpAt, ScrollDown, @@ -292,6 +294,7 @@ pub enum ScreenContext { #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] pub enum PtyContext { SpawnTerminal, + OpenInPlaceEditor, SpawnTerminalVertically, SpawnTerminalHorizontally, UpdateActivePane, diff --git a/zellij-utils/src/input/actions.rs b/zellij-utils/src/input/actions.rs index e2a7ad38..99852b48 100644 --- a/zellij-utils/src/input/actions.rs +++ b/zellij-utils/src/input/actions.rs @@ -57,6 +57,7 @@ pub enum Action { /// Dumps the screen to a file DumpScreen(String), /// Scroll up in focus pane. + EditScrollback, ScrollUp, /// Scroll up at point ScrollUpAt(Position), diff --git a/zellij-utils/src/input/command.rs b/zellij-utils/src/input/command.rs index debd5bc5..6f093a4a 100644 --- a/zellij-utils/src/input/command.rs +++ b/zellij-utils/src/input/command.rs @@ -5,7 +5,7 @@ use std::path::PathBuf; #[derive(Debug, Clone)] pub enum TerminalAction { - OpenFile(PathBuf), + OpenFile(PathBuf, Option), // path to file and optional line_number RunCommand(RunCommand), } diff --git a/zellij-utils/src/input/mod.rs b/zellij-utils/src/input/mod.rs index 2fb7deaf..0d51f4f5 100644 --- a/zellij-utils/src/input/mod.rs +++ b/zellij-utils/src/input/mod.rs @@ -55,6 +55,10 @@ pub fn get_mode_info(mode: InputMode, style: Style, capabilities: PluginCapabili ("↓↑".to_string(), "Scroll".to_string()), ("PgUp/PgDn".to_string(), "Scroll Page".to_string()), ("u/d".to_string(), "Scroll Half Page".to_string()), + ( + "e".to_string(), + "Edit Scrollback in Default Editor".to_string(), + ), ], InputMode::RenameTab => vec![("Enter".to_string(), "when done".to_string())], InputMode::RenamePane => vec![("Enter".to_string(), "when done".to_string())], diff --git a/zellij-utils/src/input/options.rs b/zellij-utils/src/input/options.rs index 861e4877..bc23cf3c 100644 --- a/zellij-utils/src/input/options.rs +++ b/zellij-utils/src/input/options.rs @@ -89,6 +89,10 @@ pub struct Options { #[clap(long)] #[serde(default)] pub copy_on_select: Option, + + /// Explicit full path to open the scrollback editor (default is $EDITOR or $VISUAL) + #[clap(long, parse(from_os_str))] + pub scrollback_editor: Option, } #[derive(ArgEnum, Deserialize, Serialize, Debug, Clone, Copy, PartialEq)] @@ -131,6 +135,9 @@ impl Options { let copy_command = other.copy_command.or_else(|| self.copy_command.clone()); let copy_clipboard = other.copy_clipboard.or(self.copy_clipboard); let copy_on_select = other.copy_on_select.or(self.copy_on_select); + let scrollback_editor = other + .scrollback_editor + .or_else(|| self.scrollback_editor.clone()); Options { simplified_ui, @@ -146,6 +153,7 @@ impl Options { copy_command, copy_clipboard, copy_on_select, + scrollback_editor, } } @@ -178,6 +186,9 @@ impl Options { let copy_command = other.copy_command.or_else(|| self.copy_command.clone()); let copy_clipboard = other.copy_clipboard.or(self.copy_clipboard); let copy_on_select = other.copy_on_select.or(self.copy_on_select); + let scrollback_editor = other + .scrollback_editor + .or_else(|| self.scrollback_editor.clone()); Options { simplified_ui, @@ -193,6 +204,7 @@ impl Options { copy_command, copy_clipboard, copy_on_select, + scrollback_editor, } } @@ -244,6 +256,7 @@ impl From for Options { copy_command: opts.copy_command, copy_clipboard: opts.copy_clipboard, copy_on_select: opts.copy_on_select, + scrollback_editor: opts.scrollback_editor, } } } diff --git a/zellij-utils/src/setup.rs b/zellij-utils/src/setup.rs index 2df138ed..b117766d 100644 --- a/zellij-utils/src/setup.rs +++ b/zellij-utils/src/setup.rs @@ -403,6 +403,10 @@ impl Setup { message.push_str(" Can be temporarily disabled through pressing the [SHIFT] key.\n"); message.push_str(" If that doesn't fix any issues consider to disable the mouse handling of zellij: 'zellij options --disable-mouse-mode'\n"); + let default_editor = std::env::var("EDITOR") + .or_else(|_| std::env::var("VISUAL")) + .unwrap_or_else(|_| String::from("Not set, checked $EDITOR and $VISUAL")); + writeln!(&mut message, "[DEFAULT EDITOR]: {}", default_editor).unwrap(); writeln!(&mut message, "[FEATURES]: {:?}", FEATURES).unwrap(); let mut hyperlink = String::new(); hyperlink.push_str(hyperlink_start);