diff --git a/CHANGELOG.md b/CHANGELOG.md index 316de36177..4481475cf5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) * Feature: Option to create a new session if attach fails (`zellij attach --create`) (https://github.com/zellij-org/zellij/pull/731) * Feature: Added the new `Visible` event, allowing plugins to detect if they are visible in the current tab (https://github.com/zellij-org/zellij/pull/717) * Feature: Plugins now have access to a data directory at `/data` – the working directory is now mounted at `/host` instead of `.` (https://github.com/zellij-org/zellij/pull/723) +* Feature: Add ability to solely specify the tab name in the `tabs` section (https://github.com/zellij-org/zellij/pull/722) +* Feature: Plugins can be configured and the groundwork for "Headless" plugins has been laid (https://github.com/zellij-org/zellij/pull/660) +* Automatically update `example/default.yaml` on release (https://github.com/zellij-org/zellij/pull/736) +* Feature: allow mirroring sessions in multiple terminal windows (https://github.com/zellij-org/zellij/pull/740) ## [0.17.0] - 2021-09-15 * New panes/tabs now open in CWD of focused pane (https://github.com/zellij-org/zellij/pull/691) diff --git a/Cargo.lock b/Cargo.lock index 66a0b3cbce..27d8446517 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -748,6 +748,16 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "form_urlencoded" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" +dependencies = [ + "matches", + "percent-encoding", +] + [[package]] name = "fuchsia-cprng" version = "0.1.1" @@ -970,6 +980,17 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" +[[package]] +name = "idna" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" +dependencies = [ + "matches", + "unicode-bidi", + "unicode-normalization", +] + [[package]] name = "indexmap" version = "1.7.0" @@ -1206,6 +1227,12 @@ dependencies = [ "libc", ] +[[package]] +name = "matches" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" + [[package]] name = "memchr" version = "2.3.4" @@ -1432,6 +1459,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "percent-encoding" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" + [[package]] name = "pin-project-lite" version = "0.2.7" @@ -2107,6 +2140,21 @@ dependencies = [ "winapi", ] +[[package]] +name = "tinyvec" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "848a1e1181b9f6753b5e96a092749e29b11d19ede67dfbbd6c7dc7e0f49b5338" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" + [[package]] name = "tracing" version = "0.1.26" @@ -2179,6 +2227,21 @@ dependencies = [ "syn", ] +[[package]] +name = "unicode-bidi" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "246f4c42e67e7a4e3c6106ff716a5d067d4132a642840b242e357e468a2a0085" + +[[package]] +name = "unicode-normalization" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9" +dependencies = [ + "tinyvec", +] + [[package]] name = "unicode-segmentation" version = "1.8.0" @@ -2206,6 +2269,19 @@ dependencies = [ "traitobject", ] +[[package]] +name = "url" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c" +dependencies = [ + "form_urlencoded", + "idna", + "matches", + "percent-encoding", + "serde", +] + [[package]] name = "utf8parse" version = "0.1.1" @@ -2660,6 +2736,7 @@ dependencies = [ "log", "mio", "termbg", + "zellij-tile", "zellij-utils", ] @@ -2680,6 +2757,7 @@ dependencies = [ "serde_json", "typetag", "unicode-width", + "url", "wasmer", "wasmer-wasi", "zellij-utils", @@ -2720,6 +2798,7 @@ dependencies = [ "nix", "once_cell", "serde", + "serde_json", "serde_yaml", "signal-hook 0.3.9", "strip-ansi-escapes", @@ -2728,6 +2807,7 @@ dependencies = [ "tempfile", "termion", "unicode-width", + "url", "vte 0.10.1", "zellij-tile", ] diff --git a/Makefile.toml b/Makefile.toml index 7438ceeeef..79392a6479 100644 --- a/Makefile.toml +++ b/Makefile.toml @@ -3,6 +3,8 @@ CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true CARGO_TARGET_DIR = "${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/target" SKIP_TEST = false +ZELLIJ_EXAMPLE_DIR = "${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/example" +ZELLIJ_ASSETS_DIR = "${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/zellij-utils/assets" # Add clippy to the default flow [tasks.dev-test-flow] @@ -118,6 +120,16 @@ dependencies = ["install-mandown"] command = "cargo" args = ["install", "mandown"] + +# copy the example default config from assets directory to a more user facing one +[tasks.update-default-config] +workspace = false +dependencies = [] +script_runner = "@duckscript" +script = ''' +cp ${ZELLIJ_ASSETS_DIR}/config/default.yaml ${ZELLIJ_EXAMPLE_DIR}/default.yaml +''' + # CI Releasing Zellij [tasks.ci-build-release] workspace = false @@ -130,7 +142,7 @@ args = ["build", "--verbose", "--release", "--target", "${CARGO_MAKE_TASK_ARGS}" workspace = false dependencies = ["build-plugins", "build-dev-data-dir"] command = "cargo" -args = ["build", "--verbose", "--target", "x86_64-unknown-linux-musl"] +args = ["build", "--verbose", "--release", "--target", "x86_64-unknown-linux-musl"] # Run e2e tests - we mark the e2e tests as "ignored" so they will not be run with the normal ones [tasks.e2e-test] @@ -193,6 +205,6 @@ cwd = "zellij-tile-utils" script = "cargo publish && sleep 15" [tasks.publish-zellij] -dependencies = ["publish-zellij-client", "publish-zellij-server", "publish-zellij-utils"] +dependencies = ["publish-zellij-client", "publish-zellij-server", "publish-zellij-utils", "update-default-config"] command = "cargo" args = ["publish"] diff --git a/default-plugins/status-bar/src/main.rs b/default-plugins/status-bar/src/main.rs index 9f8e9eaab4..f1b2197e49 100644 --- a/default-plugins/status-bar/src/main.rs +++ b/default-plugins/status-bar/src/main.rs @@ -8,7 +8,9 @@ use zellij_tile::prelude::*; use zellij_tile_utils::style; use first_line::{ctrl_keys, superkey}; -use second_line::{keybinds, text_copied_hint}; +use second_line::{ + fullscreen_panes_to_hide, keybinds, locked_fullscreen_panes_to_hide, text_copied_hint, +}; // for more of these, copy paste from: https://en.wikipedia.org/wiki/Box-drawing_character static ARROW_SEPARATOR: &str = ""; @@ -16,6 +18,7 @@ static MORE_MSG: &str = " ... "; #[derive(Default)] struct State { + tabs: Vec, mode_info: ModeInfo, diplay_text_copied_hint: bool, } @@ -137,6 +140,7 @@ impl ZellijPlugin for State { set_selectable(false); subscribe(&[ EventType::ModeUpdate, + EventType::TabUpdate, EventType::CopyToClipboard, EventType::InputReceived, ]); @@ -147,6 +151,9 @@ impl ZellijPlugin for State { Event::ModeUpdate(mode_info) => { self.mode_info = mode_info; } + Event::TabUpdate(tabs) => { + self.tabs = tabs; + } Event::CopyToClipboard => { self.diplay_text_copied_hint = true; } @@ -173,11 +180,54 @@ impl ZellijPlugin for State { ); let first_line = format!("{}{}", superkey, ctrl_keys); - let second_line = if self.diplay_text_copied_hint { - text_copied_hint(&self.mode_info.palette) - } else { - keybinds(&self.mode_info, cols) - }; + + let mut second_line = LinePart::default(); + for t in self.tabs.iter_mut() { + if t.active { + match self.mode_info.mode { + InputMode::Normal => { + if t.is_fullscreen_active { + second_line = if self.diplay_text_copied_hint { + text_copied_hint(&self.mode_info.palette) + } else { + fullscreen_panes_to_hide(&self.mode_info.palette, t.panes_to_hide) + } + } else { + second_line = if self.diplay_text_copied_hint { + text_copied_hint(&self.mode_info.palette) + } else { + keybinds(&self.mode_info, cols) + } + } + } + InputMode::Locked => { + if t.is_fullscreen_active { + second_line = if self.diplay_text_copied_hint { + text_copied_hint(&self.mode_info.palette) + } else { + locked_fullscreen_panes_to_hide( + &self.mode_info.palette, + t.panes_to_hide, + ) + } + } else { + second_line = if self.diplay_text_copied_hint { + text_copied_hint(&self.mode_info.palette) + } else { + keybinds(&self.mode_info, cols) + } + } + } + _ => { + second_line = if self.diplay_text_copied_hint { + text_copied_hint(&self.mode_info.palette) + } else { + keybinds(&self.mode_info, cols) + } + } + } + } + } // [48;5;238m is gray background, [0K is so that it fills the rest of the line // [m is background reset, [0K is so that it clears the rest of the line diff --git a/default-plugins/status-bar/src/second_line.rs b/default-plugins/status-bar/src/second_line.rs index d73cb28745..0e59fcb8e0 100644 --- a/default-plugins/status-bar/src/second_line.rs +++ b/default-plugins/status-bar/src/second_line.rs @@ -387,3 +387,82 @@ pub fn text_copied_hint(palette: &Palette) -> LinePart { len: hint.len(), } } + +pub fn fullscreen_panes_to_hide(palette: &Palette, panes_to_hide: usize) -> LinePart { + let white_color = match palette.white { + PaletteColor::Rgb((r, g, b)) => RGB(r, g, b), + PaletteColor::EightBit(color) => Fixed(color), + }; + let green_color = match palette.green { + PaletteColor::Rgb((r, g, b)) => RGB(r, g, b), + PaletteColor::EightBit(color) => Fixed(color), + }; + let orange_color = match palette.orange { + PaletteColor::Rgb((r, g, b)) => RGB(r, g, b), + PaletteColor::EightBit(color) => Fixed(color), + }; + let shortcut_left_separator = Style::new().fg(white_color).bold().paint(" ("); + let shortcut_right_separator = Style::new().fg(white_color).bold().paint("): "); + let fullscreen = "FULLSCREEN"; + let puls = "+ "; + let panes = panes_to_hide.to_string(); + let hide = " hidden panes"; + let len = fullscreen.chars().count() + + puls.chars().count() + + panes.chars().count() + + hide.chars().count() + + 5; // 3 for ():'s around shortcut, 2 for the space + LinePart { + part: format!( + "{}{}{}{}{}{}", + shortcut_left_separator, + Style::new().fg(orange_color).bold().paint(fullscreen), + shortcut_right_separator, + Style::new().fg(white_color).bold().paint(puls), + Style::new().fg(green_color).bold().paint(panes), + Style::new().fg(white_color).bold().paint(hide) + ), + len, + } +} + +pub fn locked_fullscreen_panes_to_hide(palette: &Palette, panes_to_hide: usize) -> LinePart { + let white_color = match palette.white { + PaletteColor::Rgb((r, g, b)) => RGB(r, g, b), + PaletteColor::EightBit(color) => Fixed(color), + }; + let green_color = match palette.green { + PaletteColor::Rgb((r, g, b)) => RGB(r, g, b), + PaletteColor::EightBit(color) => Fixed(color), + }; + let orange_color = match palette.orange { + PaletteColor::Rgb((r, g, b)) => RGB(r, g, b), + PaletteColor::EightBit(color) => Fixed(color), + }; + let locked_text = " -- INTERFACE LOCKED -- "; + let shortcut_left_separator = Style::new().fg(white_color).bold().paint(" ("); + let shortcut_right_separator = Style::new().fg(white_color).bold().paint("): "); + let fullscreen = "FULLSCREEN"; + let puls = "+ "; + let panes = panes_to_hide.to_string(); + let hide = " hidden panes"; + let len = locked_text.chars().count() + + fullscreen.chars().count() + + puls.chars().count() + + panes.chars().count() + + hide.chars().count() + + 5; // 3 for ():'s around shortcut, 2 for the space + LinePart { + part: format!( + "{}{}{}{}{}{}{}", + Style::new().fg(white_color).bold().paint(locked_text), + shortcut_left_separator, + Style::new().fg(orange_color).bold().paint(fullscreen), + shortcut_right_separator, + Style::new().fg(white_color).bold().paint(puls), + Style::new().fg(green_color).bold().paint(panes), + Style::new().fg(white_color).bold().paint(hide) + ), + len, + } +} diff --git a/default-plugins/strider/src/main.rs b/default-plugins/strider/src/main.rs index 8edc1e197c..701e02bdec 100644 --- a/default-plugins/strider/src/main.rs +++ b/default-plugins/strider/src/main.rs @@ -2,9 +2,11 @@ mod state; use colored::*; use state::{FsEntry, State}; -use std::{cmp::min, fs::read_dir}; +use std::{cmp::min, fs::read_dir, path::Path}; use zellij_tile::prelude::*; +const ROOT: &str = "/host"; + register_plugin!(State); impl ZellijPlugin for State { @@ -29,7 +31,7 @@ impl ZellijPlugin for State { self.path = p; refresh_directory(self); } - FsEntry::File(p, _) => open_file(&p), + FsEntry::File(p, _) => open_file(p.strip_prefix(ROOT).unwrap()), } } Key::Left | Key::Char('h') => { @@ -76,7 +78,7 @@ impl ZellijPlugin for State { } fn refresh_directory(state: &mut State) { - state.files = read_dir(&state.path) + state.files = read_dir(Path::new(ROOT).join(&state.path)) .unwrap() .filter_map(|res| { res.and_then(|d| { diff --git a/example/default.yaml b/example/default.yaml index 1a72a6343c..60b4069ebf 100644 --- a/example/default.yaml +++ b/example/default.yaml @@ -1,5 +1,4 @@ --- -simplified_ui: true keybinds: unbind: true normal: @@ -8,11 +7,13 @@ keybinds: - action: [SwitchToMode: Pane,] key: [Ctrl: 'p',] - action: [SwitchToMode: Resize,] - key: [Ctrl: 'r',] + key: [Ctrl: 'n',] - action: [SwitchToMode: Tab,] key: [Ctrl: 't',] - action: [SwitchToMode: Scroll,] key: [Ctrl: 's',] + - action: [SwitchToMode: Session,] + key: [Ctrl: 'o',] - action: [Quit,] key: [Ctrl: 'q',] - action: [NewPane: ] @@ -43,6 +44,8 @@ keybinds: key: [Ctrl: 'r', Char: "\n", Char: ' ',] - action: [SwitchToMode: Scroll,] key: [Ctrl: 's'] + - action: [SwitchToMode: Session,] + key: [Ctrl: 'o',] - action: [Quit] key: [Ctrl: 'q'] - action: [Resize: Left,] @@ -56,13 +59,13 @@ keybinds: - action: [NewPane: ,] key: [ Alt: 'n',] - action: [MoveFocus: Left,] - key: [ Alt: 'h', Left,] + key: [ Alt: 'h',] - action: [MoveFocus: Right,] - key: [ Alt: 'l', Right,] + key: [ Alt: 'l',] - action: [MoveFocus: Down,] - key: [ Alt: 'j', Down,] + key: [ Alt: 'j',] - action: [MoveFocus: Up,] - key: [ Alt: 'k', Up,] + key: [ Alt: 'k',] - action: [FocusPreviousPane,] key: [ Alt: '[',] - action: [FocusNextPane,] @@ -70,24 +73,26 @@ keybinds: pane: - action: [SwitchToMode: Locked,] key: [Ctrl: 'g'] - - action: [SwitchToMode: Pane,] - key: [Ctrl: 'p',] + - action: [SwitchToMode: Resize,] + key: [Ctrl: 'n',] - action: [SwitchToMode: Tab,] key: [Ctrl: 't',] - action: [SwitchToMode: Normal,] - key: [Ctrl: 'r', Char: "\n", Char: ' ',] + key: [Ctrl: 'p', Char: "\n", Char: ' ',] - action: [SwitchToMode: Scroll,] key: [Ctrl: 's'] + - action: [SwitchToMode: Session,] + key: [Ctrl: 'o',] - action: [Quit,] key: [Ctrl: 'q',] - action: [MoveFocus: Left,] - key: [ Alt: 'h', Left,] + key: [ Char: 'h', Left,] - action: [MoveFocus: Right,] - key: [ Alt: 'l', Right,] + key: [ Char: 'l', Right,] - action: [MoveFocus: Down,] - key: [ Alt: 'j', Down,] + key: [ Char: 'j', Down,] - action: [MoveFocus: Up,] - key: [ Alt: 'k', Up,] + key: [ Char: 'k', Up,] - action: [SwitchFocus,] key: [Char: 'p'] - action: [NewPane: ,] @@ -100,6 +105,8 @@ keybinds: key: [Char: 'x',] - action: [ToggleFocusFullscreen,] key: [Char: 'f',] + - action: [TogglePaneFrames,] + key: [Char: 'z',] - action: [FocusPreviousPane,] key: [ Alt: '[',] - action: [FocusNextPane,] @@ -109,10 +116,14 @@ keybinds: key: [Ctrl: 'g'] - action: [SwitchToMode: Pane,] key: [Ctrl: 'p',] + - action: [SwitchToMode: Resize,] + key: [Ctrl: 'n',] - action: [SwitchToMode: Normal,] - key: [Ctrl: 'r', Ctrl: 't', Char: "\n", Char: ' ',] + key: [Ctrl: 't', Char: "\n", Char: ' ',] - action: [SwitchToMode: Scroll,] key: [Ctrl: 's'] + - action: [SwitchToMode: Session,] + key: [Ctrl: 'o',] - action: [SwitchToMode: RenameTab, TabNameInput: [0],] key: [Char: 'r'] - action: [Quit,] @@ -122,17 +133,15 @@ keybinds: - action: [FocusNextPane,] key: [ Alt: ']',] - action: [GoToPreviousTab,] - key: [ Char: 'h',] - - action: [GoToNextTab,] - key: [ Char: 'l',] + key: [ Char: 'h', Left, Up, Char: 'k',] - action: [GoToNextTab,] - key: [ Char: 'j',] - - action: [GoToPreviousTab,] - key: [ Char: 'k',] + key: [ Char: 'l', Right,Down, Char: 'j'] - action: [NewTab: ,] key: [ Char: 'n',] - action: [CloseTab,] key: [ Char: 'x',] + - action: [ToggleActiveSyncTab] + key: [Char: 's'] - action: [MoveFocus: Left,] key: [ Alt: 'h',] - action: [MoveFocus: Right,] @@ -159,6 +168,8 @@ keybinds: key: [ Char: '8',] - action: [GoToTab: 9,] key: [ Char: '9',] + - action: [ToggleTab] + key: [ Char: "\t" ] scroll: - action: [SwitchToMode: Normal,] key: [Ctrl: 'r', Ctrl: 's', Char: ' ', @@ -169,6 +180,12 @@ keybinds: key: [Ctrl: 'g',] - action: [SwitchToMode: Pane,] key: [Ctrl: 'p',] + - action: [SwitchToMode: Session,] + key: [Ctrl: 'o',] + - action: [SwitchToMode: Resize,] + key: [Ctrl: 'n',] + - action: [ScrollToBottom, SwitchToMode: Normal,] + key: [Ctrl: 'c',] - action: [Quit,] key: [Ctrl: 'q',] - action: [ScrollDown,] @@ -176,9 +193,9 @@ keybinds: - action: [ScrollUp,] key: [Char: 'k', Up,] - action: [PageScrollDown,] - key: [Ctrl: 'f', PageDown,] + key: [Ctrl: 'f', PageDown, Right, Char: 'l',] - action: [PageScrollUp,] - key: [Ctrl: 'b', PageUp,] + key: [Ctrl: 'b', PageUp, Left, Char: 'h',] - action: [NewPane: ,] key: [ Alt: 'n',] - action: [MoveFocus: Left,] @@ -214,3 +231,27 @@ keybinds: key: [ Alt: '[',] - action: [FocusNextPane,] key: [ Alt: ']',] + session: + - action: [SwitchToMode: Locked,] + key: [Ctrl: 'g'] + - action: [SwitchToMode: Resize,] + key: [Ctrl: 'n',] + - action: [SwitchToMode: Pane,] + key: [Ctrl: 'p',] + - action: [SwitchToMode: Tab,] + key: [Ctrl: 't',] + - action: [SwitchToMode: Normal,] + key: [Ctrl: 'o', Char: "\n", Char: ' ',] + - action: [SwitchToMode: Scroll,] + key: [Ctrl: 's'] + - action: [Quit,] + key: [Ctrl: 'q',] + - action: [Detach,] + key: [Char: 'd',] + +# Choose what to do when zellij receives SIGTERM, SIGINT, SIGQUIT or SIGHUP +# eg. when terminal window with an active zellij session is closed +# Options: +# - detach (Default) +# - quit +#on_force_close: quit diff --git a/src/main.rs b/src/main.rs index 0a91ca652b..90eeb96b21 100644 --- a/src/main.rs +++ b/src/main.rs @@ -56,7 +56,6 @@ pub fn main() { }; if let Some(Command::Sessions(Sessions::Attach { session_name, - force, create, options, })) = opts.command.clone() @@ -73,22 +72,14 @@ pub fn main() { (ClientInfo::New(session_name.unwrap()), layout) } else { ( - ClientInfo::Attach( - session_name.unwrap(), - force, - config_options.clone(), - ), + ClientInfo::Attach(session_name.unwrap(), config_options.clone()), None, ) } } else { assert_session(session); ( - ClientInfo::Attach( - session_name.unwrap(), - force, - config_options.clone(), - ), + ClientInfo::Attach(session_name.unwrap(), config_options.clone()), None, ) } @@ -106,7 +97,7 @@ pub fn main() { } } ActiveSession::One(session_name) => ( - ClientInfo::Attach(session_name, force, config_options.clone()), + ClientInfo::Attach(session_name, config_options.clone()), None, ), ActiveSession::Many => { diff --git a/src/tests/e2e/cases.rs b/src/tests/e2e/cases.rs index 307986c1a3..77892463c2 100644 --- a/src/tests/e2e/cases.rs +++ b/src/tests/e2e/cases.rs @@ -153,20 +153,12 @@ pub fn cannot_split_terminals_vertically_when_active_terminal_is_too_small() { }, }) .add_step(Step { - name: "Send text to terminal", - instruction: |mut remote_terminal: RemoteTerminal| -> bool { - // this is just normal input that should be sent into the one terminal so that we can make - // sure we silently failed to split in the previous step - remote_terminal.send_key("Hi!".as_bytes()); - true - }, - }) - .add_step(Step { - name: "Wait for text to appear", + name: "Make sure only one pane appears", instruction: |remote_terminal: RemoteTerminal| -> bool { let mut step_is_complete = false; - if remote_terminal.cursor_position_is(6, 2) && remote_terminal.snapshot_contains("Hi!") + if remote_terminal.cursor_position_is(3, 2) && remote_terminal.snapshot_contains("...") { + // ... is the truncated tip line step_is_complete = true; } step_is_complete @@ -917,3 +909,85 @@ pub fn start_without_pane_frames() { .run_all_steps(); assert_snapshot!(last_snapshot); } + +#[test] +#[ignore] +pub fn mirrored_sessions() { + let fake_win_size = Size { + cols: 120, + rows: 24, + }; + let mut test_attempts = 10; + let session_name = "mirrored_sessions"; + let mut last_snapshot = None; + loop { + // we run this test in a loop because there are some edge cases (especially in the CI) + // where the second runner times out and then we also need to restart the first runner + // if no test timed out, we break the loop and assert the snapshot + let mut first_runner = + RemoteRunner::new_with_session_name("mirrored_sessions", fake_win_size, session_name) + .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(&PANE_MODE); + remote_terminal.send_key(&SPLIT_RIGHT_IN_PANE_MODE); + // back to normal mode after split + remote_terminal.send_key(&ENTER); + step_is_complete = true; + } + step_is_complete + }, + }) + .add_step(Step { + name: "Wait for new pane to open", + instruction: |remote_terminal: RemoteTerminal| -> bool { + let mut step_is_complete = false; + if remote_terminal.cursor_position_is(63, 2) + && remote_terminal.tip_appears() + { + // cursor is in the newly opened second pane + step_is_complete = true; + } + step_is_complete + }, + }); + first_runner.run_all_steps(); + + let mut second_runner = + RemoteRunner::new_existing_session("mirrored_sessions", fake_win_size, session_name) + .add_step(Step { + name: "Make sure session appears correctly", + instruction: |remote_terminal: RemoteTerminal| -> bool { + let mut step_is_complete = false; + if remote_terminal.cursor_position_is(63, 2) + && remote_terminal.tip_appears() + { + // cursor is in the newly opened second pane + step_is_complete = true; + } + step_is_complete + }, + }); + let last_test_snapshot = second_runner.run_all_steps(); + + if (first_runner.test_timed_out || second_runner.test_timed_out) && test_attempts >= 0 { + test_attempts -= 1; + continue; + } else { + last_snapshot = Some(last_test_snapshot); + break; + } + } + match last_snapshot { + Some(last_snapshot) => { + assert_snapshot!(last_snapshot); + } + None => { + panic!("test timed out before completing"); + } + } +} diff --git a/src/tests/e2e/remote_runner.rs b/src/tests/e2e/remote_runner.rs index 7c4587f3ea..924babf686 100644 --- a/src/tests/e2e/remote_runner.rs +++ b/src/tests/e2e/remote_runner.rs @@ -10,7 +10,7 @@ use std::net::TcpStream; use std::path::Path; -const ZELLIJ_EXECUTABLE_LOCATION: &str = "/usr/src/zellij/x86_64-unknown-linux-musl/debug/zellij"; +const ZELLIJ_EXECUTABLE_LOCATION: &str = "/usr/src/zellij/x86_64-unknown-linux-musl/release/zellij"; const ZELLIJ_LAYOUT_PATH: &str = "/usr/src/zellij/fixtures/layouts"; const CONNECTION_STRING: &str = "127.0.0.1:2222"; const CONNECTION_USERNAME: &str = "test"; @@ -24,7 +24,7 @@ fn ssh_connect() -> ssh2::Session { sess.handshake().unwrap(); sess.userauth_password(CONNECTION_USERNAME, CONNECTION_PASSWORD) .unwrap(); - sess.set_timeout(20000); + sess.set_timeout(3000); sess } @@ -58,6 +58,27 @@ fn start_zellij(channel: &mut ssh2::Channel) { channel.flush().unwrap(); } +fn start_zellij_in_session(channel: &mut ssh2::Channel, session_name: &str) { + stop_zellij(channel); + channel + .write_all( + format!( + "{} --session {}\n", + ZELLIJ_EXECUTABLE_LOCATION, session_name + ) + .as_bytes(), + ) + .unwrap(); + channel.flush().unwrap(); +} + +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()) + .unwrap(); + channel.flush().unwrap(); +} + fn start_zellij_without_frames(channel: &mut ssh2::Channel) { stop_zellij(channel); channel @@ -188,6 +209,9 @@ pub struct RemoteRunner { win_size: Size, layout_file_name: Option<&'static str>, without_frames: bool, + session_name: Option, + attach_to_existing: bool, + pub test_timed_out: bool, } impl RemoteRunner { @@ -216,10 +240,89 @@ impl RemoteRunner { test_name, currently_running_step: None, current_step_index: 0, - retries_left: 3, + retries_left: 10, + win_size, + layout_file_name: None, + without_frames: false, + session_name: None, + attach_to_existing: false, + test_timed_out: false, + } + } + pub fn new_with_session_name( + test_name: &'static str, + win_size: Size, + session_name: &str, + ) -> Self { + let sess = ssh_connect(); + let mut channel = sess.channel_session().unwrap(); + let vte_parser = vte::Parser::new(); + let mut rows = Dimension::fixed(win_size.rows); + let mut cols = Dimension::fixed(win_size.cols); + rows.set_inner(win_size.rows); + cols.set_inner(win_size.cols); + let pane_geom = PaneGeom { + x: 0, + y: 0, + rows, + cols, + }; + let terminal_output = TerminalPane::new(0, pane_geom, Palette::default(), 0); // 0 is the pane index + setup_remote_environment(&mut channel, win_size); + start_zellij_in_session(&mut channel, &session_name); + RemoteRunner { + steps: vec![], + channel, + terminal_output, + vte_parser, + test_name, + currently_running_step: None, + current_step_index: 0, + retries_left: 10, + win_size, + layout_file_name: None, + without_frames: false, + session_name: Some(String::from(session_name)), + attach_to_existing: false, + test_timed_out: false, + } + } + pub fn new_existing_session( + test_name: &'static str, + win_size: Size, + session_name: &str, + ) -> Self { + let sess = ssh_connect(); + let mut channel = sess.channel_session().unwrap(); + let vte_parser = vte::Parser::new(); + let mut rows = Dimension::fixed(win_size.rows); + let mut cols = Dimension::fixed(win_size.cols); + rows.set_inner(win_size.rows); + cols.set_inner(win_size.cols); + let pane_geom = PaneGeom { + x: 0, + y: 0, + rows, + cols, + }; + let terminal_output = TerminalPane::new(0, pane_geom, Palette::default(), 0); // 0 is the pane index + setup_remote_environment(&mut channel, win_size); + attach_to_existing_session(&mut channel, &session_name); + RemoteRunner { + steps: vec![], + channel, + terminal_output, + vte_parser, + test_name, + currently_running_step: None, + current_step_index: 0, + retries_left: 10, win_size, layout_file_name: None, without_frames: false, + session_name: Some(String::from(session_name)), + attach_to_existing: true, + test_timed_out: false, } } pub fn new_without_frames(test_name: &'static str, win_size: Size) -> Self { @@ -247,10 +350,13 @@ impl RemoteRunner { test_name, currently_running_step: None, current_step_index: 0, - retries_left: 3, + retries_left: 10, win_size, layout_file_name: None, without_frames: true, + session_name: None, + attach_to_existing: false, + test_timed_out: false, } } pub fn new_with_layout( @@ -283,10 +389,13 @@ impl RemoteRunner { test_name, currently_running_step: None, current_step_index: 0, - retries_left: 3, + retries_left: 10, win_size, layout_file_name: Some(layout_file_name), without_frames: false, + session_name: None, + attach_to_existing: false, + test_timed_out: false, } } pub fn add_step(mut self, step: Step) -> Self { @@ -296,16 +405,6 @@ impl RemoteRunner { pub fn replace_steps(&mut self, steps: Vec) { self.steps = steps; } - fn current_remote_terminal_state(&mut self) -> RemoteTerminal { - let current_snapshot = self.get_current_snapshot(); - let (cursor_x, cursor_y) = self.terminal_output.cursor_coordinates().unwrap_or((0, 0)); - RemoteTerminal { - cursor_x, - cursor_y, - current_snapshot, - channel: &mut self.channel, - } - } pub fn run_next_step(&mut self) { if let Some(next_step) = self.steps.get(self.current_step_index) { let current_snapshot = take_snapshot(&mut self.terminal_output); @@ -341,6 +440,24 @@ impl RemoteRunner { new_runner.replace_steps(self.steps.clone()); drop(std::mem::replace(self, new_runner)); self.run_all_steps() + } else if self.session_name.is_some() { + let mut new_runner = if self.attach_to_existing { + RemoteRunner::new_existing_session( + self.test_name, + self.win_size, + &self.session_name.as_ref().unwrap(), + ) + } else { + RemoteRunner::new_with_session_name( + self.test_name, + self.win_size, + &self.session_name.as_ref().unwrap(), + ) + }; + new_runner.retries_left = self.retries_left - 1; + new_runner.replace_steps(self.steps.clone()); + drop(std::mem::replace(self, new_runner)); + self.run_all_steps() } else { let mut new_runner = RemoteRunner::new(self.test_name, self.win_size); new_runner.retries_left = self.retries_left - 1; @@ -349,22 +466,6 @@ impl RemoteRunner { self.run_all_steps() } } - fn display_informative_error(&mut self) { - let test_name = self.test_name; - let current_step_name = self.currently_running_step.as_ref().cloned(); - match current_step_name { - Some(current_step) => { - let remote_terminal = self.current_remote_terminal_state(); - eprintln!("Timed out waiting for data on the SSH channel for test {}. Was waiting for step: {}", test_name, current_step); - eprintln!("{:?}", remote_terminal); - } - None => { - let remote_terminal = self.current_remote_terminal_state(); - eprintln!("Timed out waiting for data on the SSH channel for test {}. Haven't begun running steps yet.", test_name); - eprintln!("{:?}", remote_terminal); - } - } - } pub fn run_all_steps(&mut self) -> String { // returns the last snapshot loop { @@ -381,23 +482,16 @@ impl RemoteRunner { break; } } - Err(e) => { - if e.kind() == std::io::ErrorKind::TimedOut { - if self.retries_left > 0 { - return self.restart_test(); - } - self.display_informative_error(); - panic!("Timed out waiting for test"); + Err(_e) => { + if self.retries_left > 0 { + return self.restart_test(); } - panic!("Error while reading remote session: {}", e); + self.test_timed_out = true; } } } take_snapshot(&mut self.terminal_output) } - pub fn get_current_snapshot(&mut self) -> String { - take_snapshot(&mut self.terminal_output) - } } impl Drop for RemoteRunner { diff --git a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__cannot_split_terminals_vertically_when_active_terminal_is_too_small.snap b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__cannot_split_terminals_vertically_when_active_terminal_is_too_small.snap index d37f23e330..796a596b9f 100644 --- a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__cannot_split_terminals_vertically_when_active_terminal_is_too_small.snap +++ b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__cannot_split_terminals_vertically_when_active_terminal_is_too_small.snap @@ -5,7 +5,7 @@ expression: last_snapshot --- Zellij ┌──────┐ -│$ Hi!█│ +│$ █ │ │ │ │ │ │ │ @@ -22,4 +22,4 @@ expression: last_snapshot │ │ └──────┘ Ctrl + - + ... diff --git a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__mirrored_sessions.snap b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__mirrored_sessions.snap new file mode 100644 index 0000000000..3223680c87 --- /dev/null +++ b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__mirrored_sessions.snap @@ -0,0 +1,29 @@ +--- +source: src/tests/e2e/cases.rs +expression: last_snapshot + +--- + Zellij (mirrored_sessions)  Tab #1  +┌ Pane #1 ─────────────────────────────────────────────────┐┌ Pane #2 ─────────────────────────────────────────────────┐ +│$ ││$ █ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +└──────────────────────────────────────────────────────────┘└──────────────────────────────────────────────────────────┘ + Ctrl + LOCK 

PANE  TAB  RESIZE  SCROLL  SESSION  QUIT  + Tip: Alt + n => open new pane. Alt + [] or hjkl => navigate between panes. diff --git a/zellij-client/Cargo.toml b/zellij-client/Cargo.toml index b7b61425d6..4c580f13be 100644 --- a/zellij-client/Cargo.toml +++ b/zellij-client/Cargo.toml @@ -12,6 +12,7 @@ license = "MIT" mio = "0.7.11" termbg = "0.2.3" zellij-utils = { path = "../zellij-utils/", version = "0.18.0" } +zellij-tile = { path = "../zellij-tile/", version = "0.18.0" } log = "0.4.14" [dev-dependencies] diff --git a/zellij-client/src/input_handler.rs b/zellij-client/src/input_handler.rs index 217b10f975..e78f59c1a3 100644 --- a/zellij-client/src/input_handler.rs +++ b/zellij-client/src/input_handler.rs @@ -8,15 +8,16 @@ use zellij_utils::{ termion, zellij_tile, }; -use crate::{os_input_output::ClientOsApi, ClientInstruction, CommandIsExecuting}; +use crate::{ + os_input_output::ClientOsApi, ClientInstruction, CommandIsExecuting, InputInstruction, +}; use zellij_utils::{ - channels::{SenderWithContext, OPENCALLS}, - errors::ContextType, + channels::{Receiver, SenderWithContext, OPENCALLS}, + errors::{ContextType, ErrorContext}, input::{actions::Action, cast_termion_key, config::Config, keybinds::Keybinds}, ipc::{ClientToServerMsg, ExitReason}, }; -use termion::input::TermReadEventsAndRaw; use zellij_tile::data::{InputMode, Key}; /// Handles the dispatching of [`Action`]s according to the current @@ -31,6 +32,7 @@ struct InputHandler { send_client_instructions: SenderWithContext, should_exit: bool, pasting: bool, + receive_input_instructions: Receiver<(InputInstruction, ErrorContext)>, } impl InputHandler { @@ -42,6 +44,7 @@ impl InputHandler { options: Options, send_client_instructions: SenderWithContext, mode: InputMode, + receive_input_instructions: Receiver<(InputInstruction, ErrorContext)>, ) -> Self { InputHandler { mode, @@ -52,6 +55,7 @@ impl InputHandler { send_client_instructions, should_exit: false, pasting: false, + receive_input_instructions, } } @@ -71,10 +75,9 @@ impl InputHandler { if self.should_exit { break; } - let stdin_buffer = self.os_input.read_from_stdin(); - for key_result in stdin_buffer.events_and_raw() { - match key_result { - Ok((event, raw_bytes)) => match event { + match self.receive_input_instructions.recv() { + Ok((InputInstruction::KeyEvent(event, raw_bytes), _error_context)) => { + match event { termion::event::Event::Key(key) => { let key = cast_termion_key(key); self.handle_key(&key, raw_bytes); @@ -101,9 +104,12 @@ impl InputHandler { self.handle_unknown_key(raw_bytes); } } - }, - Err(err) => panic!("Encountered read error: {:?}", err), + } + } + Ok((InputInstruction::SwitchToMode(input_mode), _error_context)) => { + self.mode = input_mode; } + Err(err) => panic!("Encountered read error: {:?}", err), } } } @@ -179,6 +185,8 @@ impl InputHandler { should_break = true; } Action::SwitchToMode(mode) => { + // this is an optimistic update, we should get a SwitchMode instruction from the + // server later that atomically changes the mode as well self.mode = mode; self.os_input .send_to_server(ClientToServerMsg::Action(action)); @@ -224,6 +232,7 @@ pub(crate) fn input_loop( command_is_executing: CommandIsExecuting, send_client_instructions: SenderWithContext, default_mode: InputMode, + receive_input_instructions: Receiver<(InputInstruction, ErrorContext)>, ) { let _handler = InputHandler::new( os_input, @@ -232,6 +241,7 @@ pub(crate) fn input_loop( options, send_client_instructions, default_mode, + receive_input_instructions, ) .handle_input(); } diff --git a/zellij-client/src/lib.rs b/zellij-client/src/lib.rs index bd2c637688..92b87e7014 100644 --- a/zellij-client/src/lib.rs +++ b/zellij-client/src/lib.rs @@ -14,12 +14,15 @@ use crate::{ command_is_executing::CommandIsExecuting, input_handler::input_loop, os_input_output::ClientOsApi, }; +use termion::input::TermReadEventsAndRaw; +use zellij_tile::data::InputMode; use zellij_utils::{ channels::{self, ChannelWithContext, SenderWithContext}, consts::{SESSION_NAME, ZELLIJ_IPC_PIPE}, errors::{ClientContext, ContextType, ErrorInstruction}, input::{actions::Action, config::Config, options::Options}, ipc::{ClientAttributes, ClientToServerMsg, ExitReason, ServerToClientMsg}, + termion, }; use zellij_utils::{cli::CliArgs, input::layout::LayoutFromYaml}; @@ -30,6 +33,7 @@ pub(crate) enum ClientInstruction { Render(String), UnblockInputThread, Exit(ExitReason), + SwitchToMode(InputMode), } impl From for ClientInstruction { @@ -38,6 +42,9 @@ impl From for ClientInstruction { ServerToClientMsg::Exit(e) => ClientInstruction::Exit(e), ServerToClientMsg::Render(buffer) => ClientInstruction::Render(buffer), ServerToClientMsg::UnblockInputThread => ClientInstruction::UnblockInputThread, + ServerToClientMsg::SwitchToMode(input_mode) => { + ClientInstruction::SwitchToMode(input_mode) + } } } } @@ -49,6 +56,7 @@ impl From<&ClientInstruction> for ClientContext { ClientInstruction::Error(_) => ClientContext::Error, ClientInstruction::Render(_) => ClientContext::Render, ClientInstruction::UnblockInputThread => ClientContext::UnblockInputThread, + ClientInstruction::SwitchToMode(_) => ClientContext::SwitchToMode, } } } @@ -78,10 +86,16 @@ fn spawn_server(socket_path: &Path) -> io::Result<()> { #[derive(Debug, Clone)] pub enum ClientInfo { - Attach(String, bool, Options), + Attach(String, Options), New(String), } +#[derive(Debug, Clone)] +pub enum InputInstruction { + KeyEvent(termion::event::Event, Vec), + SwitchToMode(InputMode), +} + pub fn start_client( mut os_input: Box, opts: CliArgs, @@ -121,11 +135,11 @@ pub fn start_client( }; let first_msg = match info { - ClientInfo::Attach(name, force, config_options) => { + ClientInfo::Attach(name, config_options) => { SESSION_NAME.set(name).unwrap(); std::env::set_var(&"ZELLIJ_SESSION_NAME", SESSION_NAME.get().unwrap()); - ClientToServerMsg::AttachClient(client_attributes, force, config_options) + ClientToServerMsg::AttachClient(client_attributes, config_options) } ClientInfo::New(name) => { SESSION_NAME.set(name).unwrap(); @@ -138,6 +152,7 @@ pub fn start_client( Box::new(opts), Box::new(config_options.clone()), layout.unwrap(), + Some(config.plugins.clone()), ) } }; @@ -158,6 +173,11 @@ pub fn start_client( > = channels::bounded(50); let send_client_instructions = SenderWithContext::new(send_client_instructions); + let (send_input_instructions, receive_input_instructions): ChannelWithContext< + InputInstruction, + > = channels::bounded(50); + let send_input_instructions = SenderWithContext::new(send_input_instructions); + std::panic::set_hook({ use zellij_utils::errors::handle_panic; let send_client_instructions = send_client_instructions.clone(); @@ -170,6 +190,22 @@ pub fn start_client( let _stdin_thread = thread::Builder::new() .name("stdin_handler".to_string()) + .spawn({ + let os_input = os_input.clone(); + let send_input_instructions = send_input_instructions.clone(); + move || loop { + let stdin_buffer = os_input.read_from_stdin(); + for key_result in stdin_buffer.events_and_raw() { + let (key_event, raw_bytes) = key_result.unwrap(); + send_input_instructions + .send(InputInstruction::KeyEvent(key_event, raw_bytes)) + .unwrap(); + } + } + }); + + let _input_thread = thread::Builder::new() + .name("input_handler".to_string()) .spawn({ let send_client_instructions = send_client_instructions.clone(); let command_is_executing = command_is_executing.clone(); @@ -183,6 +219,7 @@ pub fn start_client( command_is_executing, send_client_instructions, default_mode, + receive_input_instructions, ) } }); @@ -238,12 +275,13 @@ pub fn start_client( os_input.disable_mouse(); let error = format!( "{}\n{}{}", - goto_start_of_last_line, restore_snapshot, backtrace + restore_snapshot, goto_start_of_last_line, backtrace ); let _ = os_input .get_stdout_writer() .write(error.as_bytes()) .unwrap(); + let _ = os_input.get_stdout_writer().flush().unwrap(); std::process::exit(1); }; @@ -279,6 +317,11 @@ pub fn start_client( ClientInstruction::UnblockInputThread => { command_is_executing.unblock_input_thread(); } + ClientInstruction::SwitchToMode(input_mode) => { + send_input_instructions + .send(InputInstruction::SwitchToMode(input_mode)) + .unwrap(); + } } } diff --git a/zellij-client/src/unit/input_handler_tests.rs b/zellij-client/src/unit/input_handler_tests.rs index 17bb1b0f87..c2cf784b11 100644 --- a/zellij-client/src/unit/input_handler_tests.rs +++ b/zellij-client/src/unit/input_handler_tests.rs @@ -3,8 +3,11 @@ use zellij_utils::input::actions::{Action, Direction}; use zellij_utils::input::config::Config; use zellij_utils::input::options::Options; use zellij_utils::pane_size::Size; +use zellij_utils::termion::event::Event; +use zellij_utils::termion::event::Key; use zellij_utils::zellij_tile::data::Palette; +use crate::InputInstruction; use crate::{os_input_output::ClientOsApi, ClientInstruction, CommandIsExecuting}; use std::path::Path; @@ -67,14 +70,12 @@ pub mod commands { } struct FakeClientOsApi { - stdin_events: Arc>>>, events_sent_to_server: Arc>>, command_is_executing: Arc>, } impl FakeClientOsApi { pub fn new( - mut stdin_events: Vec>, events_sent_to_server: Arc>>, command_is_executing: CommandIsExecuting, ) -> Self { @@ -82,10 +83,7 @@ impl FakeClientOsApi { // Arc here because we need interior mutability, otherwise we'll have to change the // ClientOsApi trait, and that will cause a lot of havoc let command_is_executing = Arc::new(Mutex::new(command_is_executing)); - stdin_events.push(commands::QUIT.to_vec()); - let stdin_events = Arc::new(Mutex::new(stdin_events)); // this is also done for interior mutability FakeClientOsApi { - stdin_events, events_sent_to_server, command_is_executing, } @@ -106,11 +104,7 @@ impl ClientOsApi for FakeClientOsApi { unimplemented!() } fn read_from_stdin(&self) -> Vec { - let mut stdin_events = self.stdin_events.lock().unwrap(); - if stdin_events.is_empty() { - panic!("ran out of stdin events!"); - } - stdin_events.remove(0) + unimplemented!() } fn box_clone(&self) -> Box { unimplemented!() @@ -156,11 +150,10 @@ fn extract_actions_sent_to_server( #[test] pub fn quit_breaks_input_loop() { - let stdin_events = vec![]; + let stdin_events = vec![(commands::QUIT.to_vec(), Event::Key(Key::Ctrl('q')))]; let events_sent_to_server = Arc::new(Mutex::new(vec![])); let command_is_executing = CommandIsExecuting::new(); let client_os_api = Box::new(FakeClientOsApi::new( - stdin_events, events_sent_to_server.clone(), command_is_executing.clone(), )); @@ -172,6 +165,16 @@ pub fn quit_breaks_input_loop() { > = channels::bounded(50); let send_client_instructions = SenderWithContext::new(send_client_instructions); + let (send_input_instructions, receive_input_instructions): ChannelWithContext< + InputInstruction, + > = channels::bounded(50); + let send_input_instructions = SenderWithContext::new(send_input_instructions); + for event in stdin_events { + send_input_instructions + .send(InputInstruction::KeyEvent(event.1, event.0)) + .unwrap(); + } + let default_mode = InputMode::Normal; input_loop( client_os_api, @@ -180,6 +183,7 @@ pub fn quit_breaks_input_loop() { command_is_executing, send_client_instructions, default_mode, + receive_input_instructions, ); let expected_actions_sent_to_server = vec![Action::Quit]; let received_actions = extract_actions_sent_to_server(events_sent_to_server); @@ -190,12 +194,18 @@ pub fn quit_breaks_input_loop() { } #[test] -pub fn move_focus_left_in_pane_mode() { - let stdin_events = vec![commands::MOVE_FOCUS_LEFT_IN_NORMAL_MODE.to_vec()]; +pub fn move_focus_left_in_normal_mode() { + let stdin_events = vec![ + ( + commands::MOVE_FOCUS_LEFT_IN_NORMAL_MODE.to_vec(), + Event::Key(Key::Alt('h')), + ), + (commands::QUIT.to_vec(), Event::Key(Key::Ctrl('q'))), + ]; + let events_sent_to_server = Arc::new(Mutex::new(vec![])); let command_is_executing = CommandIsExecuting::new(); let client_os_api = Box::new(FakeClientOsApi::new( - stdin_events, events_sent_to_server.clone(), command_is_executing.clone(), )); @@ -207,6 +217,16 @@ pub fn move_focus_left_in_pane_mode() { > = channels::bounded(50); let send_client_instructions = SenderWithContext::new(send_client_instructions); + let (send_input_instructions, receive_input_instructions): ChannelWithContext< + InputInstruction, + > = channels::bounded(50); + let send_input_instructions = SenderWithContext::new(send_input_instructions); + for event in stdin_events { + send_input_instructions + .send(InputInstruction::KeyEvent(event.1, event.0)) + .unwrap(); + } + let default_mode = InputMode::Normal; input_loop( client_os_api, @@ -215,6 +235,7 @@ pub fn move_focus_left_in_pane_mode() { command_is_executing, send_client_instructions, default_mode, + receive_input_instructions, ); let expected_actions_sent_to_server = vec![Action::MoveFocusOrTab(Direction::Left), Action::Quit]; @@ -228,14 +249,23 @@ pub fn move_focus_left_in_pane_mode() { #[test] pub fn bracketed_paste() { let stdin_events = vec![ - commands::BRACKETED_PASTE_START.to_vec(), - commands::MOVE_FOCUS_LEFT_IN_NORMAL_MODE.to_vec(), - commands::BRACKETED_PASTE_END.to_vec(), + ( + commands::BRACKETED_PASTE_START.to_vec(), + Event::Unsupported(commands::BRACKETED_PASTE_START.to_vec()), + ), + ( + commands::MOVE_FOCUS_LEFT_IN_NORMAL_MODE.to_vec(), + Event::Key(Key::Alt('h')), + ), + ( + commands::BRACKETED_PASTE_END.to_vec(), + Event::Unsupported(commands::BRACKETED_PASTE_END.to_vec()), + ), + (commands::QUIT.to_vec(), Event::Key(Key::Ctrl('q'))), ]; let events_sent_to_server = Arc::new(Mutex::new(vec![])); let command_is_executing = CommandIsExecuting::new(); let client_os_api = Box::new(FakeClientOsApi::new( - stdin_events, events_sent_to_server.clone(), command_is_executing.clone(), )); @@ -247,6 +277,16 @@ pub fn bracketed_paste() { > = channels::bounded(50); let send_client_instructions = SenderWithContext::new(send_client_instructions); + let (send_input_instructions, receive_input_instructions): ChannelWithContext< + InputInstruction, + > = channels::bounded(50); + let send_input_instructions = SenderWithContext::new(send_input_instructions); + for event in stdin_events { + send_input_instructions + .send(InputInstruction::KeyEvent(event.1, event.0)) + .unwrap(); + } + let default_mode = InputMode::Normal; input_loop( client_os_api, @@ -255,6 +295,7 @@ pub fn bracketed_paste() { command_is_executing, send_client_instructions, default_mode, + receive_input_instructions, ); let expected_actions_sent_to_server = vec![ Action::Write(commands::BRACKETED_PASTE_START.to_vec()), diff --git a/zellij-server/Cargo.toml b/zellij-server/Cargo.toml index bc0abe3102..c9f98ecf12 100644 --- a/zellij-server/Cargo.toml +++ b/zellij-server/Cargo.toml @@ -16,6 +16,7 @@ byteorder = "1.4.3" daemonize = "0.4.1" serde_json = "1.0" unicode-width = "0.1.8" +url = "2.2.2" wasmer = "1.0.0" wasmer-wasi = "1.0.0" cassowary = "0.3.0" diff --git a/zellij-server/src/lib.rs b/zellij-server/src/lib.rs index 419aae4f6c..34b39fd8a7 100644 --- a/zellij-server/src/lib.rs +++ b/zellij-server/src/lib.rs @@ -11,11 +11,13 @@ mod ui; mod wasm_vm; use log::info; +use std::collections::HashMap; use std::{ path::PathBuf, sync::{Arc, Mutex, RwLock}, thread, }; +use zellij_utils::pane_size::Size; use zellij_utils::zellij_tile; use wasmer::Store; @@ -38,35 +40,32 @@ use zellij_utils::{ get_mode_info, layout::LayoutFromYaml, options::Options, + plugins::PluginsConfig, }, - ipc::{ClientAttributes, ClientToServerMsg, ExitReason, ServerToClientMsg}, + ipc::{ClientAttributes, ExitReason, ServerToClientMsg}, setup::get_default_data_dir, }; +pub(crate) type ClientId = u16; + /// Instructions related to server-side application #[derive(Debug, Clone)] pub(crate) enum ServerInstruction { - NewClient(ClientAttributes, Box, Box, LayoutFromYaml), + NewClient( + ClientAttributes, + Box, + Box, + LayoutFromYaml, + ClientId, + Option, + ), Render(Option), UnblockInputThread, - ClientExit, + ClientExit(ClientId), + RemoveClient(ClientId), Error(String), - DetachSession, - AttachClient(ClientAttributes, bool, Options), -} - -impl From for ServerInstruction { - fn from(instruction: ClientToServerMsg) -> Self { - match instruction { - ClientToServerMsg::NewClient(attrs, opts, options, layout) => { - ServerInstruction::NewClient(attrs, opts, options, layout) - } - ClientToServerMsg::AttachClient(attrs, force, options) => { - ServerInstruction::AttachClient(attrs, force, options) - } - _ => unreachable!(), - } - } + DetachSession(ClientId), + AttachClient(ClientAttributes, Options, ClientId), } impl From<&ServerInstruction> for ServerContext { @@ -75,9 +74,10 @@ impl From<&ServerInstruction> for ServerContext { ServerInstruction::NewClient(..) => ServerContext::NewClient, ServerInstruction::Render(_) => ServerContext::Render, ServerInstruction::UnblockInputThread => ServerContext::UnblockInputThread, - ServerInstruction::ClientExit => ServerContext::ClientExit, + ServerInstruction::ClientExit(..) => ServerContext::ClientExit, + ServerInstruction::RemoveClient(..) => ServerContext::RemoveClient, ServerInstruction::Error(_) => ServerContext::Error, - ServerInstruction::DetachSession => ServerContext::DetachSession, + ServerInstruction::DetachSession(..) => ServerContext::DetachSession, ServerInstruction::AttachClient(..) => ServerContext::AttachClient, } } @@ -110,14 +110,67 @@ impl Drop for SessionMetaData { } } -#[derive(Copy, Clone, Debug, Eq, PartialEq)] -pub(crate) enum SessionState { - Attached, - Detached, - Uninitialized, +macro_rules! remove_client { + ($client_id:expr, $os_input:expr, $session_state:expr) => { + $os_input.remove_client($client_id); + $session_state.write().unwrap().remove_client($client_id); + }; +} + +#[derive(Clone, Debug, PartialEq)] +pub(crate) struct SessionState { + clients: HashMap>, +} + +impl SessionState { + pub fn new() -> Self { + SessionState { + clients: HashMap::new(), + } + } + pub fn new_client(&mut self) -> ClientId { + let mut clients: Vec = self.clients.keys().copied().collect(); + clients.sort_unstable(); + let next_client_id = clients.last().unwrap_or(&0) + 1; + self.clients.insert(next_client_id, None); + next_client_id + } + pub fn remove_client(&mut self, client_id: ClientId) { + self.clients.remove(&client_id); + } + pub fn set_client_size(&mut self, client_id: ClientId, size: Size) { + self.clients.insert(client_id, Some(size)); + } + pub fn min_client_terminal_size(&self) -> Option { + // None if there are no client sizes + let mut rows: Vec = self + .clients + .values() + .filter_map(|size| size.map(|size| size.rows)) + .collect(); + rows.sort_unstable(); + let mut cols: Vec = self + .clients + .values() + .filter_map(|size| size.map(|size| size.cols)) + .collect(); + cols.sort_unstable(); + let min_rows = rows.first(); + let min_cols = cols.first(); + match (min_rows, min_cols) { + (Some(min_rows), Some(min_cols)) => Some(Size { + rows: *min_rows, + cols: *min_cols, + }), + _ => None, + } + } + pub fn client_ids(&self) -> Vec { + self.clients.keys().copied().collect() + } } -pub fn start_server(os_input: Box, socket_path: PathBuf) { +pub fn start_server(mut os_input: Box, socket_path: PathBuf) { info!("Starting Zellij server!"); daemonize::Daemonize::new() .working_directory(std::env::current_dir().unwrap()) @@ -130,7 +183,7 @@ pub fn start_server(os_input: Box, socket_path: PathBuf) { let (to_server, server_receiver): ChannelWithContext = channels::bounded(50); let to_server = SenderWithContext::new(to_server); let session_data: Arc>> = Arc::new(RwLock::new(None)); - let session_state = Arc::new(RwLock::new(SessionState::Uninitialized)); + let session_state = Arc::new(RwLock::new(SessionState::new())); std::panic::set_hook({ use zellij_utils::errors::handle_panic; @@ -163,7 +216,8 @@ pub fn start_server(os_input: Box, socket_path: PathBuf) { match stream { Ok(stream) => { let mut os_input = os_input.clone(); - os_input.update_receiver(stream); + let client_id = session_state.write().unwrap().new_client(); + let receiver = os_input.new_client(client_id, stream); let session_data = session_data.clone(); let session_state = session_state.clone(); let to_server = to_server.clone(); @@ -176,6 +230,8 @@ pub fn start_server(os_input: Box, socket_path: PathBuf) { session_state, os_input, to_server, + receiver, + client_id, ) }) .unwrap(), @@ -193,18 +249,31 @@ pub fn start_server(os_input: Box, socket_path: PathBuf) { let (instruction, mut err_ctx) = server_receiver.recv().unwrap(); err_ctx.add_call(ContextType::IPCServer((&instruction).into())); match instruction { - ServerInstruction::NewClient(client_attributes, opts, config_options, layout) => { + ServerInstruction::NewClient( + client_attributes, + opts, + config_options, + layout, + client_id, + plugins, + ) => { let session = init_session( os_input.clone(), - opts, - config_options.clone(), to_server.clone(), client_attributes, session_state.clone(), - layout.clone(), + SessionOptions { + opts, + layout: layout.clone(), + plugins, + config_options: config_options.clone(), + }, ); *session_data.write().unwrap() = Some(session); - *session_state.write().unwrap() = SessionState::Attached; + session_state + .write() + .unwrap() + .set_client_size(client_id, client_attributes.size); let default_shell = config_options.default_shell.map(|shell| { TerminalAction::RunCommand(RunCommand { @@ -232,17 +301,26 @@ pub fn start_server(os_input: Box, socket_path: PathBuf) { spawn_tabs(None); } } - ServerInstruction::AttachClient(attrs, _, options) => { - *session_state.write().unwrap() = SessionState::Attached; + ServerInstruction::AttachClient(attrs, options, client_id) => { let rlock = session_data.read().unwrap(); let session_data = rlock.as_ref().unwrap(); + session_state + .write() + .unwrap() + .set_client_size(client_id, attrs.size); + let min_size = session_state + .read() + .unwrap() + .min_client_terminal_size() + .unwrap(); session_data .senders - .send_to_screen(ScreenInstruction::TerminalResize(attrs.size)) + .send_to_screen(ScreenInstruction::TerminalResize(min_size)) .unwrap(); let default_mode = options.default_mode.unwrap_or_default(); let mode_info = get_mode_info(default_mode, attrs.palette, session_data.capabilities); + let mode = mode_info.mode; session_data .senders .send_to_screen(ScreenInstruction::ChangeMode(mode_info.clone())) @@ -254,37 +332,86 @@ pub fn start_server(os_input: Box, socket_path: PathBuf) { Event::ModeUpdate(mode_info), )) .unwrap(); + for client_id in session_state.read().unwrap().clients.keys() { + os_input.send_to_client(*client_id, ServerToClientMsg::SwitchToMode(mode)); + } } ServerInstruction::UnblockInputThread => { - if *session_state.read().unwrap() == SessionState::Attached { - os_input.send_to_client(ServerToClientMsg::UnblockInputThread); + for client_id in session_state.read().unwrap().clients.keys() { + os_input.send_to_client(*client_id, ServerToClientMsg::UnblockInputThread); } } - ServerInstruction::ClientExit => { - os_input.send_to_client(ServerToClientMsg::Exit(ExitReason::Normal)); - break; + ServerInstruction::ClientExit(client_id) => { + os_input.send_to_client(client_id, ServerToClientMsg::Exit(ExitReason::Normal)); + remove_client!(client_id, os_input, session_state); + if let Some(min_size) = session_state.read().unwrap().min_client_terminal_size() { + session_data + .write() + .unwrap() + .as_ref() + .unwrap() + .senders + .send_to_screen(ScreenInstruction::TerminalResize(min_size)) + .unwrap(); + } + if session_state.read().unwrap().clients.is_empty() { + *session_data.write().unwrap() = None; + break; + } + } + ServerInstruction::RemoveClient(client_id) => { + remove_client!(client_id, os_input, session_state); + if let Some(min_size) = session_state.read().unwrap().min_client_terminal_size() { + session_data + .write() + .unwrap() + .as_ref() + .unwrap() + .senders + .send_to_screen(ScreenInstruction::TerminalResize(min_size)) + .unwrap(); + } } - ServerInstruction::DetachSession => { - *session_state.write().unwrap() = SessionState::Detached; - os_input.send_to_client(ServerToClientMsg::Exit(ExitReason::Normal)); - os_input.remove_client_sender(); + ServerInstruction::DetachSession(client_id) => { + os_input.send_to_client(client_id, ServerToClientMsg::Exit(ExitReason::Normal)); + remove_client!(client_id, os_input, session_state); + if let Some(min_size) = session_state.read().unwrap().min_client_terminal_size() { + session_data + .write() + .unwrap() + .as_ref() + .unwrap() + .senders + .send_to_screen(ScreenInstruction::TerminalResize(min_size)) + .unwrap(); + } } - ServerInstruction::Render(output) => { - if *session_state.read().unwrap() == SessionState::Attached { - // Here output is of the type Option sent by screen thread. - // If `Some(_)`- unwrap it and forward it to the client to render. - // If `None`- Send an exit instruction. This is the case when the user closes last Tab/Pane. - if let Some(op) = output { - os_input.send_to_client(ServerToClientMsg::Render(op)); - } else { - os_input.send_to_client(ServerToClientMsg::Exit(ExitReason::Normal)); - break; + ServerInstruction::Render(mut output) => { + let client_ids = session_state.read().unwrap().client_ids(); + // Here the output is of the type Option sent by screen thread. + // If `Some(_)`- unwrap it and forward it to the clients to render. + // If `None`- Send an exit instruction. This is the case when a user closes the last Tab/Pane. + if let Some(op) = output.as_mut() { + for client_id in client_ids { + os_input.send_to_client(client_id, ServerToClientMsg::Render(op.clone())); + } + } else { + for client_id in client_ids { + os_input + .send_to_client(client_id, ServerToClientMsg::Exit(ExitReason::Normal)); + remove_client!(client_id, os_input, session_state); } + break; } } ServerInstruction::Error(backtrace) => { - if *session_state.read().unwrap() == SessionState::Attached { - os_input.send_to_client(ServerToClientMsg::Exit(ExitReason::Error(backtrace))); + let client_ids = session_state.read().unwrap().client_ids(); + for client_id in client_ids { + os_input.send_to_client( + client_id, + ServerToClientMsg::Exit(ExitReason::Error(backtrace.clone())), + ); + remove_client!(client_id, os_input, session_state); } break; } @@ -302,15 +429,26 @@ pub fn start_server(os_input: Box, socket_path: PathBuf) { drop(std::fs::remove_file(&socket_path)); } +pub struct SessionOptions { + pub opts: Box, + pub config_options: Box, + pub layout: LayoutFromYaml, + pub plugins: Option, +} + fn init_session( os_input: Box, - opts: Box, - config_options: Box, to_server: SenderWithContext, client_attributes: ClientAttributes, session_state: Arc>, - layout: LayoutFromYaml, + options: SessionOptions, ) -> SessionMetaData { + let SessionOptions { + opts, + config_options, + layout, + plugins, + } = options; let (to_screen, screen_receiver): ChannelWithContext = channels::unbounded(); let to_screen = SenderWithContext::new(to_screen); @@ -394,7 +532,7 @@ fn init_session( ); let store = Store::default(); - move || wasm_thread_main(plugin_bus, store, data_dir) + move || wasm_thread_main(plugin_bus, store, data_dir, plugins.unwrap_or_default()) }) .unwrap(); SessionMetaData { diff --git a/zellij-server/src/os_input_output.rs b/zellij-server/src/os_input_output.rs index e24b24b41a..09c5e9d700 100644 --- a/zellij-server/src/os_input_output.rs +++ b/zellij-server/src/os_input_output.rs @@ -1,3 +1,5 @@ +use std::collections::HashMap; + #[cfg(target_os = "macos")] use darwin_libproc; @@ -22,12 +24,8 @@ use nix::unistd::{self, ForkResult}; use signal_hook::consts::*; use zellij_tile::data::Palette; use zellij_utils::{ - errors::ErrorContext, input::command::{RunCommand, TerminalAction}, - ipc::{ - ClientToServerMsg, ExitReason, IpcReceiverWithContext, IpcSenderWithContext, - ServerToClientMsg, - }, + ipc::{ClientToServerMsg, IpcReceiverWithContext, IpcSenderWithContext, ServerToClientMsg}, shared::default_palette, }; @@ -37,6 +35,8 @@ use byteorder::{BigEndian, ByteOrder}; pub use nix::unistd::Pid; +use crate::ClientId; + pub(crate) fn set_terminal_size_using_fd(fd: RawFd, columns: u16, rows: u16) { // TODO: do this with the nix ioctl use libc::ioctl; @@ -230,8 +230,7 @@ pub fn spawn_terminal( #[derive(Clone)] pub struct ServerOsInputOutput { orig_termios: Arc>, - receive_instructions_from_client: Option>>>, - send_instructions_to_client: Arc>>>, + client_senders: Arc>>>, } // async fn in traits is not supported by rust, so dtolnay's excellent async_trait macro is being @@ -285,22 +284,13 @@ pub trait ServerOsApi: Send + Sync { fn force_kill(&self, pid: Pid) -> Result<(), nix::Error>; /// Returns a [`Box`] pointer to this [`ServerOsApi`] struct. fn box_clone(&self) -> Box; - /// Receives a message on server-side IPC channel - fn recv_from_client(&self) -> (ClientToServerMsg, ErrorContext); - /// Sends a message to client - fn send_to_client(&self, msg: ServerToClientMsg); - /// Adds a sender to client - fn add_client_sender(&self); - /// Send to the temporary client - // A temporary client is the one that hasn't been registered as a client yet. - // Only the corresponding router thread has access to send messages to it. - // This can be the case when the client cannot attach to the session, - // so it tries to connect and then exits, hence temporary. - fn send_to_temp_client(&self, msg: ServerToClientMsg); - /// Removes the sender to client - fn remove_client_sender(&self); - /// Update the receiver socket for the client - fn update_receiver(&mut self, stream: LocalSocketStream); + fn send_to_client(&self, client_id: ClientId, msg: ServerToClientMsg); + fn new_client( + &mut self, + client_id: ClientId, + stream: LocalSocketStream, + ) -> IpcReceiverWithContext; + fn remove_client(&mut self, client_id: ClientId); fn load_palette(&self) -> Palette; /// Returns the current working directory for a given pid fn get_cwd(&self, pid: Pid) -> Option; @@ -340,55 +330,29 @@ impl ServerOsApi for ServerOsInputOutput { let _ = kill(pid, Some(Signal::SIGKILL)); Ok(()) } - fn recv_from_client(&self) -> (ClientToServerMsg, ErrorContext) { - self.receive_instructions_from_client - .as_ref() - .unwrap() - .lock() - .unwrap() - .recv() - } - fn send_to_client(&self, msg: ServerToClientMsg) { - self.send_instructions_to_client - .lock() - .unwrap() - .as_mut() - .unwrap() - .send(msg); - } - fn add_client_sender(&self) { - let sender = self - .receive_instructions_from_client - .as_ref() - .unwrap() - .lock() - .unwrap() - .get_sender(); - let old_sender = self - .send_instructions_to_client - .lock() - .unwrap() - .replace(sender); - if let Some(mut sender) = old_sender { - sender.send(ServerToClientMsg::Exit(ExitReason::ForceDetached)); + fn send_to_client(&self, client_id: ClientId, msg: ServerToClientMsg) { + if let Some(sender) = self.client_senders.lock().unwrap().get_mut(&client_id) { + sender.send(msg); } } - fn send_to_temp_client(&self, msg: ServerToClientMsg) { - self.receive_instructions_from_client - .as_ref() - .unwrap() + fn new_client( + &mut self, + client_id: ClientId, + stream: LocalSocketStream, + ) -> IpcReceiverWithContext { + let receiver = IpcReceiverWithContext::new(stream); + let sender = receiver.get_sender(); + self.client_senders .lock() .unwrap() - .get_sender() - .send(msg); + .insert(client_id, sender); + receiver } - fn remove_client_sender(&self) { - assert!(self.send_instructions_to_client.lock().unwrap().is_some()); - *self.send_instructions_to_client.lock().unwrap() = None; - } - fn update_receiver(&mut self, stream: LocalSocketStream) { - self.receive_instructions_from_client = - Some(Arc::new(Mutex::new(IpcReceiverWithContext::new(stream)))); + fn remove_client(&mut self, client_id: ClientId) { + let mut client_senders = self.client_senders.lock().unwrap(); + if client_senders.contains_key(&client_id) { + client_senders.remove(&client_id); + } } fn load_palette(&self) -> Palette { default_palette() @@ -418,8 +382,7 @@ pub fn get_server_os_input() -> Result { let orig_termios = Arc::new(Mutex::new(current_termios)); Ok(ServerOsInputOutput { orig_termios, - receive_instructions_from_client: None, - send_instructions_to_client: Arc::new(Mutex::new(None)), + client_senders: Arc::new(Mutex::new(HashMap::new())), }) } diff --git a/zellij-server/src/pty.rs b/zellij-server/src/pty.rs index e37e60621e..761b1702fb 100644 --- a/zellij-server/src/pty.rs +++ b/zellij-server/src/pty.rs @@ -65,6 +65,8 @@ pub(crate) struct Pty { task_handles: HashMap>, } +use std::convert::TryFrom; + pub(crate) fn pty_thread_main(mut pty: Pty, layout: LayoutFromYaml) { loop { let (event, mut err_ctx) = pty.bus.recv().expect("failed to receive event on channel"); @@ -104,7 +106,10 @@ pub(crate) fn pty_thread_main(mut pty: Pty, layout: LayoutFromYaml) { }); let merged_layout = layout.template.clone().insert_tab_layout(tab_layout); - pty.spawn_terminals_for_layout(merged_layout.into(), terminal_action.clone()); + let layout: Layout = + Layout::try_from(merged_layout).unwrap_or_else(|err| panic!("{}", err)); + + pty.spawn_terminals_for_layout(layout, terminal_action.clone()); if let Some(tab_name) = tab_name { // clear current name at first diff --git a/zellij-server/src/route.rs b/zellij-server/src/route.rs index a88d04b09d..3d1a5960d9 100644 --- a/zellij-server/src/route.rs +++ b/zellij-server/src/route.rs @@ -13,14 +13,17 @@ use zellij_utils::{ command::TerminalAction, get_mode_info, }, - ipc::{ClientToServerMsg, ExitReason, ServerToClientMsg}, + ipc::{ClientToServerMsg, IpcReceiverWithContext, ServerToClientMsg}, }; +use crate::ClientId; + fn route_action( action: Action, session: &SessionMetaData, _os_input: &dyn ServerOsApi, to_server: &SenderWithContext, + client_id: ClientId, ) -> bool { let mut should_break = false; session @@ -241,11 +244,15 @@ fn route_action( .unwrap(); } Action::Quit => { - to_server.send(ServerInstruction::ClientExit).unwrap(); + to_server + .send(ServerInstruction::ClientExit(client_id)) + .unwrap(); should_break = true; } Action::Detach => { - to_server.send(ServerInstruction::DetachSession).unwrap(); + to_server + .send(ServerInstruction::DetachSession(client_id)) + .unwrap(); should_break = true; } Action::LeftClick(point) => { @@ -282,47 +289,75 @@ pub(crate) fn route_thread_main( session_state: Arc>, os_input: Box, to_server: SenderWithContext, + mut receiver: IpcReceiverWithContext, + client_id: ClientId, ) { loop { - let (instruction, err_ctx) = os_input.recv_from_client(); + let (instruction, err_ctx) = receiver.recv(); err_ctx.update_thread_ctx(); let rlocked_sessions = session_data.read().unwrap(); match instruction { ClientToServerMsg::Action(action) => { if let Some(rlocked_sessions) = rlocked_sessions.as_ref() { - if route_action(action, rlocked_sessions, &*os_input, &to_server) { + if let Action::SwitchToMode(input_mode) = action { + for client_id in session_state.read().unwrap().clients.keys() { + os_input.send_to_client( + *client_id, + ServerToClientMsg::SwitchToMode(input_mode), + ); + } + } + if route_action(action, rlocked_sessions, &*os_input, &to_server, client_id) { break; } } } ClientToServerMsg::TerminalResize(new_size) => { + session_state + .write() + .unwrap() + .set_client_size(client_id, new_size); + let min_size = session_state + .read() + .unwrap() + .min_client_terminal_size() + .unwrap(); rlocked_sessions .as_ref() .unwrap() .senders - .send_to_screen(ScreenInstruction::TerminalResize(new_size)) + .send_to_screen(ScreenInstruction::TerminalResize(min_size)) .unwrap(); } - ClientToServerMsg::NewClient(..) => { - if *session_state.read().unwrap() != SessionState::Uninitialized { - os_input.send_to_temp_client(ServerToClientMsg::Exit(ExitReason::Error( - "Cannot add new client".into(), - ))); - } else { - os_input.add_client_sender(); - to_server.send(instruction.into()).unwrap(); - } + ClientToServerMsg::NewClient( + client_attributes, + cli_args, + opts, + layout, + plugin_config, + ) => { + let new_client_instruction = ServerInstruction::NewClient( + client_attributes, + cli_args, + opts, + layout, + client_id, + plugin_config, + ); + to_server.send(new_client_instruction).unwrap(); } - ClientToServerMsg::AttachClient(_, force, _) => { - if *session_state.read().unwrap() == SessionState::Attached && !force { - os_input.send_to_temp_client(ServerToClientMsg::Exit(ExitReason::CannotAttach)); - } else { - os_input.add_client_sender(); - to_server.send(instruction.into()).unwrap(); - } + ClientToServerMsg::AttachClient(client_attributes, opts) => { + let attach_client_instruction = + ServerInstruction::AttachClient(client_attributes, opts, client_id); + to_server.send(attach_client_instruction).unwrap(); + } + ClientToServerMsg::ClientExited => { + // we don't unwrap this because we don't really care if there's an error here (eg. + // if the main server thread exited before this router thread did) + let _ = to_server.send(ServerInstruction::RemoveClient(client_id)); + break; } - ClientToServerMsg::ClientExited => break, } } } diff --git a/zellij-server/src/screen.rs b/zellij-server/src/screen.rs index 5e1addb5a1..e1e743f181 100644 --- a/zellij-server/src/screen.rs +++ b/zellij-server/src/screen.rs @@ -252,12 +252,10 @@ impl Screen { .unwrap(); if self.tabs.is_empty() { self.active_tab_index = None; - if *self.session_state.read().unwrap() == SessionState::Attached { - self.bus - .senders - .send_to_server(ServerInstruction::Render(None)) - .unwrap(); - } + self.bus + .senders + .send_to_server(ServerInstruction::Render(None)) + .unwrap(); } else { if let Some(tab) = self.get_active_tab() { tab.visible(false); @@ -288,9 +286,6 @@ impl Screen { /// Renders this [`Screen`], which amounts to rendering its active [`Tab`]. pub fn render(&mut self) { - if *self.session_state.read().unwrap() != SessionState::Attached { - return; - } if let Some(active_tab) = self.get_active_tab_mut() { if active_tab.get_active_pane().is_some() { active_tab.render(); @@ -374,6 +369,8 @@ impl Screen { position: tab.position, name: tab.name.clone(), active: active_tab_index == tab.index, + panes_to_hide: tab.panes_to_hide.len(), + is_fullscreen_active: tab.is_fullscreen_active(), is_sync_panes_active: tab.is_sync_panes_active(), }); } @@ -492,6 +489,7 @@ pub(crate) fn screen_thread_main( .senders .send_to_server(ServerInstruction::UnblockInputThread) .unwrap(); + screen.update_tabs(); } ScreenInstruction::HorizontalSplit(pid) => { screen.get_active_tab_mut().unwrap().horizontal_split(pid); @@ -500,6 +498,7 @@ pub(crate) fn screen_thread_main( .senders .send_to_server(ServerInstruction::UnblockInputThread) .unwrap(); + screen.update_tabs(); } ScreenInstruction::VerticalSplit(pid) => { screen.get_active_tab_mut().unwrap().vertical_split(pid); @@ -508,6 +507,7 @@ pub(crate) fn screen_thread_main( .senders .send_to_server(ServerInstruction::UnblockInputThread) .unwrap(); + screen.update_tabs(); } ScreenInstruction::WriteCharacter(bytes) => { let active_tab = screen.get_active_tab_mut().unwrap(); @@ -638,6 +638,7 @@ pub(crate) fn screen_thread_main( .get_active_tab_mut() .unwrap() .toggle_active_pane_fullscreen(); + screen.update_tabs(); } ScreenInstruction::TogglePaneFrames => { screen.draw_pane_frames = !screen.draw_pane_frames; diff --git a/zellij-server/src/tab.rs b/zellij-server/src/tab.rs index 62e0a2cb51..2e30734105 100644 --- a/zellij-server/src/tab.rs +++ b/zellij-server/src/tab.rs @@ -99,7 +99,7 @@ pub(crate) struct Tab { pub position: usize, pub name: String, panes: BTreeMap>, - panes_to_hide: HashSet, + pub panes_to_hide: HashSet, active_terminal: Option, max_panes: Option, viewport: Viewport, // includes all non-UI panes @@ -322,23 +322,18 @@ impl Tab { for (layout, position_and_size) in positions_and_size { // A plugin pane - if let Some(Run::Plugin(Some(plugin))) = &layout.run { + if let Some(Run::Plugin(run)) = layout.run.clone() { let (pid_tx, pid_rx) = channel(); + let pane_title = run.location.to_string(); self.senders - .send_to_plugin(PluginInstruction::Load( - pid_tx, - plugin.path.clone(), - tab_index, - plugin._allow_exec_host_cmd, - )) + .send_to_plugin(PluginInstruction::Load(pid_tx, run, tab_index)) .unwrap(); let pid = pid_rx.recv().unwrap(); - let title = String::from(plugin.path.as_path().as_os_str().to_string_lossy()); let mut new_plugin = PluginPane::new( pid, *position_and_size, self.senders.to_plugin.as_ref().unwrap().clone(), - title, + pane_title, ); new_plugin.set_borderless(layout.borderless); self.panes.insert(PaneId::Plugin(pid), Box::new(new_plugin)); @@ -663,6 +658,9 @@ impl Tab { self.toggle_fullscreen_is_active(); } } + pub fn is_fullscreen_active(&self) -> bool { + self.fullscreen_is_active + } pub fn toggle_fullscreen_is_active(&mut self) { self.fullscreen_is_active = !self.fullscreen_is_active; } @@ -730,12 +728,10 @@ impl Tab { } } pub fn render(&mut self) { - if self.active_terminal.is_none() - || *self.session_state.read().unwrap() != SessionState::Attached - { + if self.active_terminal.is_none() || self.session_state.read().unwrap().clients.is_empty() { // we might not have an active terminal if we closed the last pane // in that case, we should not render as the app is exiting - // or if this session is not attached to a client, we do not have to render + // or if there are no attached clients to this session return; } self.senders diff --git a/zellij-server/src/unit/screen_tests.rs b/zellij-server/src/unit/screen_tests.rs index aeea671baf..186bdf7f4d 100644 --- a/zellij-server/src/unit/screen_tests.rs +++ b/zellij-server/src/unit/screen_tests.rs @@ -3,12 +3,14 @@ use crate::zellij_tile::data::{ModeInfo, Palette}; use crate::{ os_input_output::{AsyncReader, ChildId, Pid, ServerOsApi}, thread_bus::Bus, - SessionState, + ClientId, SessionState, }; +use std::convert::TryInto; use std::path::PathBuf; use std::sync::{Arc, RwLock}; use zellij_utils::input::command::TerminalAction; use zellij_utils::input::layout::LayoutTemplate; +use zellij_utils::ipc::IpcReceiverWithContext; use zellij_utils::pane_size::Size; use std::os::unix::io::RawFd; @@ -17,7 +19,6 @@ use zellij_utils::ipc::ClientAttributes; use zellij_utils::nix; use zellij_utils::{ - errors::ErrorContext, interprocess::local_socket::LocalSocketStream, ipc::{ClientToServerMsg, ServerToClientMsg}, }; @@ -26,49 +27,44 @@ use zellij_utils::{ struct 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) { // noop } fn spawn_terminal(&self, _file_to_open: TerminalAction) -> (RawFd, ChildId) { unimplemented!() } - fn read_from_tty_stdout(&self, _fd: RawFd, _buf: &mut [u8]) -> Result { + fn read_from_tty_stdout(&self, fd: RawFd, buf: &mut [u8]) -> Result { unimplemented!() } - fn async_file_reader(&self, _fd: RawFd) -> Box { + fn async_file_reader(&self, fd: RawFd) -> Box { unimplemented!() } - fn write_to_tty_stdin(&self, _fd: RawFd, _buf: &[u8]) -> Result { + fn write_to_tty_stdin(&self, fd: RawFd, buf: &[u8]) -> Result { unimplemented!() } - fn tcdrain(&self, _fd: RawFd) -> Result<(), nix::Error> { + fn tcdrain(&self, fd: RawFd) -> Result<(), nix::Error> { unimplemented!() } - fn box_clone(&self) -> Box { - Box::new((*self).clone()) - } - fn force_kill(&self, _pid: Pid) -> Result<(), nix::Error> { - unimplemented!() - } - fn kill(&self, _pid: Pid) -> Result<(), nix::Error> { + fn kill(&self, pid: Pid) -> Result<(), nix::Error> { unimplemented!() } - fn recv_from_client(&self) -> (ClientToServerMsg, ErrorContext) { + fn force_kill(&self, pid: Pid) -> Result<(), nix::Error> { unimplemented!() } - fn send_to_client(&self, _msg: ServerToClientMsg) { - unimplemented!() - } - fn add_client_sender(&self) { - unimplemented!() + fn box_clone(&self) -> Box { + Box::new((*self).clone()) } - fn send_to_temp_client(&self, _msg: ServerToClientMsg) { + fn send_to_client(&self, client_id: ClientId, msg: ServerToClientMsg) { unimplemented!() } - fn remove_client_sender(&self) { + fn new_client( + &mut self, + client_id: ClientId, + stream: LocalSocketStream, + ) -> IpcReceiverWithContext { unimplemented!() } - fn update_receiver(&mut self, _stream: LocalSocketStream) { + fn remove_client(&mut self, client_id: ClientId) { unimplemented!() } fn load_palette(&self) -> Palette { @@ -89,7 +85,7 @@ fn create_new_screen(size: Size) -> Screen { }; let max_panes = None; let mode_info = ModeInfo::default(); - let session_state = Arc::new(RwLock::new(SessionState::Attached)); + let session_state = Arc::new(RwLock::new(SessionState::new())); Screen::new( bus, &client_attributes, @@ -101,7 +97,7 @@ fn create_new_screen(size: Size) -> Screen { } fn new_tab(screen: &mut Screen, pid: i32) { - screen.apply_layout(LayoutTemplate::default().into(), vec![pid]); + screen.apply_layout(LayoutTemplate::default().try_into().unwrap(), vec![pid]); } #[test] diff --git a/zellij-server/src/unit/tab_tests.rs b/zellij-server/src/unit/tab_tests.rs index de5c106704..17bd98beec 100644 --- a/zellij-server/src/unit/tab_tests.rs +++ b/zellij-server/src/unit/tab_tests.rs @@ -4,11 +4,13 @@ use crate::{ os_input_output::{AsyncReader, ChildId, Pid, ServerOsApi}, panes::PaneId, thread_bus::ThreadSenders, - SessionState, + ClientId, SessionState, }; +use std::convert::TryInto; use std::path::PathBuf; use std::sync::{Arc, RwLock}; use zellij_utils::input::layout::LayoutTemplate; +use zellij_utils::ipc::IpcReceiverWithContext; use zellij_utils::pane_size::Size; use std::os::unix::io::RawFd; @@ -16,58 +18,53 @@ use std::os::unix::io::RawFd; use zellij_utils::nix; use zellij_utils::{ - errors::ErrorContext, input::command::TerminalAction, interprocess::local_socket::LocalSocketStream, ipc::{ClientToServerMsg, ServerToClientMsg}, }; +#[derive(Clone)] struct 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) { // noop } fn spawn_terminal(&self, _file_to_open: TerminalAction) -> (RawFd, ChildId) { unimplemented!() } - fn read_from_tty_stdout(&self, _fd: RawFd, _buf: &mut [u8]) -> Result { + fn read_from_tty_stdout(&self, fd: RawFd, buf: &mut [u8]) -> Result { unimplemented!() } - fn async_file_reader(&self, _fd: RawFd) -> Box { + fn async_file_reader(&self, fd: RawFd) -> Box { unimplemented!() } - fn write_to_tty_stdin(&self, _fd: RawFd, _buf: &[u8]) -> Result { + fn write_to_tty_stdin(&self, fd: RawFd, buf: &[u8]) -> Result { unimplemented!() } - fn tcdrain(&self, _fd: RawFd) -> Result<(), nix::Error> { + fn tcdrain(&self, fd: RawFd) -> Result<(), nix::Error> { unimplemented!() } - fn box_clone(&self) -> Box { - unimplemented!() - } - fn force_kill(&self, _pid: Pid) -> Result<(), nix::Error> { + fn kill(&self, pid: Pid) -> Result<(), nix::Error> { unimplemented!() } - fn kill(&self, _pid: Pid) -> Result<(), nix::Error> { + fn force_kill(&self, pid: Pid) -> Result<(), nix::Error> { unimplemented!() } - fn recv_from_client(&self) -> (ClientToServerMsg, ErrorContext) { - unimplemented!() - } - fn send_to_client(&self, _msg: ServerToClientMsg) { - unimplemented!() - } - fn add_client_sender(&self) { - unimplemented!() + fn box_clone(&self) -> Box { + Box::new((*self).clone()) } - fn send_to_temp_client(&self, _msg: ServerToClientMsg) { + fn send_to_client(&self, client_id: ClientId, msg: ServerToClientMsg) { unimplemented!() } - fn remove_client_sender(&self) { + fn new_client( + &mut self, + client_id: ClientId, + stream: LocalSocketStream, + ) -> IpcReceiverWithContext { unimplemented!() } - fn update_receiver(&mut self, _stream: LocalSocketStream) { + fn remove_client(&mut self, client_id: ClientId) { unimplemented!() } fn load_palette(&self) -> Palette { @@ -87,7 +84,7 @@ fn create_new_tab(size: Size) -> Tab { let max_panes = None; let mode_info = ModeInfo::default(); let colors = Palette::default(); - let session_state = Arc::new(RwLock::new(SessionState::Attached)); + let session_state = Arc::new(RwLock::new(SessionState::new())); let mut tab = Tab::new( index, position, @@ -101,7 +98,11 @@ fn create_new_tab(size: Size) -> Tab { session_state, true, // draw pane frames ); - tab.apply_layout(LayoutTemplate::default().into(), vec![1], index); + tab.apply_layout( + LayoutTemplate::default().try_into().unwrap(), + vec![1], + index, + ); tab } diff --git a/zellij-server/src/wasm_vm.rs b/zellij-server/src/wasm_vm.rs index 7475bf46d8..1d67e8477e 100644 --- a/zellij-server/src/wasm_vm.rs +++ b/zellij-server/src/wasm_vm.rs @@ -1,7 +1,7 @@ -use log::{info, warn}; +use log::{debug, info, warn}; use std::collections::{HashMap, HashSet}; use std::fs; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::process; use std::str::FromStr; use std::sync::{mpsc::Sender, Arc, Mutex}; @@ -9,6 +9,7 @@ use std::thread; use std::time::{Duration, Instant}; use serde::{de::DeserializeOwned, Serialize}; +use url::Url; use wasmer::{ imports, ChainableNamedResolver, Function, ImportObject, Instance, Module, Store, Value, WasmerEnv, @@ -24,12 +25,17 @@ use crate::{ thread_bus::{Bus, ThreadSenders}, }; use zellij_utils::errors::{ContextType, PluginContext}; -use zellij_utils::{input::command::TerminalAction, serde, zellij_tile}; +use zellij_utils::{ + input::command::TerminalAction, + input::layout::RunPlugin, + input::plugins::{PluginConfig, PluginType, PluginsConfig}, + serde, zellij_tile, +}; #[derive(Clone, Debug)] pub(crate) enum PluginInstruction { - Load(Sender, PathBuf, usize, bool), // tx_pid, path_of_plugin , tab_index, allow_exec_host_cmd - Update(Option, Event), // Focused plugin / broadcast, event data + Load(Sender, RunPlugin, usize), // tx_pid, plugin metadata, tab_index + Update(Option, Event), // Focused plugin / broadcast, event data Render(Sender, u32, usize, usize), // String buffer, plugin id, rows, cols Unload(u32), Exit, @@ -50,82 +56,62 @@ impl From<&PluginInstruction> for PluginContext { #[derive(WasmerEnv, Clone)] pub(crate) struct PluginEnv { pub plugin_id: u32, - pub tab_index: usize, + pub plugin: PluginConfig, pub senders: ThreadSenders, pub wasi_env: WasiEnv, pub subscriptions: Arc>>, - // FIXME: Once permission system is ready, this could be removed - pub _allow_exec_host_cmd: bool, plugin_own_data_dir: PathBuf, } // Thread main -------------------------------------------------------------------------------------------------------- -pub(crate) fn wasm_thread_main(bus: Bus, store: Store, data_dir: PathBuf) { +pub(crate) fn wasm_thread_main( + bus: Bus, + store: Store, + data_dir: PathBuf, + plugins: PluginsConfig, +) { info!("Wasm main thread starts"); + let mut plugin_id = 0; let mut plugin_map = HashMap::new(); let plugin_dir = data_dir.join("plugins/"); let plugin_global_data_dir = plugin_dir.join("data"); fs::create_dir_all(plugin_global_data_dir.as_path()).unwrap(); + for plugin in plugins.iter() { + if let PluginType::Headless = plugin.run { + let (instance, plugin_env) = start_plugin( + plugin_id, + plugin, + 0, + &bus, + &store, + &data_dir, + &plugin_global_data_dir, + ); + plugin_map.insert(plugin_id, (instance, plugin_env)); + plugin_id += 1; + } + } + loop { let (event, mut err_ctx) = bus.recv().expect("failed to receive event on channel"); err_ctx.add_call(ContextType::Plugin((&event).into())); match event { - PluginInstruction::Load(pid_tx, path, tab_index, _allow_exec_host_cmd) => { - let wasm_bytes = fs::read(&path) - .or_else(|_| fs::read(&path.with_extension("wasm"))) - .or_else(|_| fs::read(&plugin_dir.join(&path).with_extension("wasm"))) - .unwrap_or_else(|_| panic!("cannot find plugin {}", &path.display())); - - // FIXME: Cache this compiled module on disk. I could use `(de)serialize_to_file()` for that - let module = Module::new(&store, &wasm_bytes).unwrap(); - - let output = Pipe::new(); - let input = Pipe::new(); - let stderr = LoggingPipe::new( - path.as_path().file_name().unwrap().to_str().unwrap(), - plugin_id, - ); + PluginInstruction::Load(pid_tx, run, tab_index) => { + let plugin = plugins + .get(&run) + .unwrap_or_else(|| panic!("Plugin {:?} could not be resolved", run)); - let plugin_name = path.as_path().file_stem().unwrap(); - let plugin_own_data_dir = plugin_global_data_dir.join(plugin_name); - - let mut wasi_env = WasiState::new("Zellij") - .env("CLICOLOR_FORCE", "1") - .map_dir("/host", ".") - .unwrap() - .map_dir("/data", plugin_own_data_dir.as_path()) - .unwrap() - .stdin(Box::new(input)) - .stdout(Box::new(output)) - .stderr(Box::new(stderr)) - .finalize() - .unwrap(); - - let wasi = wasi_env.import_object(&module).unwrap(); - - if _allow_exec_host_cmd { - info!("Plugin({:?}) is able to run any host command, this may lead to some security issues!", path); - } - - let plugin_env = PluginEnv { + let (instance, plugin_env) = start_plugin( plugin_id, + &plugin, tab_index, - senders: bus.senders.clone(), - wasi_env, - subscriptions: Arc::new(Mutex::new(HashSet::new())), - _allow_exec_host_cmd, - plugin_own_data_dir, - }; - - let zellij = zellij_exports(&store, &plugin_env); - let instance = Instance::new(&module, &zellij.chain_back(wasi)).unwrap(); - - let start = instance.exports.get_function("_start").unwrap(); - - // This eventually calls the `.load()` method - start.call(&[]).unwrap(); + &bus, + &store, + &data_dir, + &plugin_global_data_dir, + ); plugin_map.insert(plugin_id, (instance, plugin_env)); pid_tx.send(plugin_id).unwrap(); @@ -149,7 +135,6 @@ pub(crate) fn wasm_thread_main(bus: Bus, store: Store, data_d buf_tx.send(String::new()).unwrap(); } else { let (instance, plugin_env) = plugin_map.get(&pid).unwrap(); - let render = instance.exports.get_function("render").unwrap(); render @@ -171,6 +156,71 @@ pub(crate) fn wasm_thread_main(bus: Bus, store: Store, data_d fs::remove_dir_all(plugin_global_data_dir.as_path()).unwrap(); } +fn start_plugin( + plugin_id: u32, + plugin: &PluginConfig, + tab_index: usize, + bus: &Bus, + store: &Store, + data_dir: &Path, + plugin_global_data_dir: &Path, +) -> (Instance, PluginEnv) { + if plugin._allow_exec_host_cmd { + info!( + "Plugin({:?}) is able to run any host command, this may lead to some security issues!", + plugin.path + ); + } + + let wasm_bytes = plugin + .resolve_wasm_bytes(&data_dir.join("plugins/")) + .unwrap_or_else(|| panic!("Cannot resolve wasm bytes for plugin {:?}", plugin)); + + // FIXME: Cache this compiled module on disk. I could use `(de)serialize_to_file()` for that + let module = Module::new(store, &wasm_bytes).unwrap(); + + let output = Pipe::new(); + let input = Pipe::new(); + let stderr = LoggingPipe::new(&plugin.location.to_string(), plugin_id); + let plugin_own_data_dir = plugin_global_data_dir.join(Url::from(&plugin.location).to_string()); + fs::create_dir_all(&plugin_own_data_dir).unwrap(); + + let mut wasi_env = WasiState::new("Zellij") + .env("CLICOLOR_FORCE", "1") + .map_dir("/host", ".") + .unwrap() + .map_dir("/data", plugin_own_data_dir.as_path()) + .unwrap() + .stdin(Box::new(input)) + .stdout(Box::new(output)) + .stderr(Box::new(stderr)) + .finalize() + .unwrap(); + + let wasi = wasi_env.import_object(&module).unwrap(); + let mut plugin = plugin.clone(); + plugin.set_tab_index(tab_index); + + let plugin_env = PluginEnv { + plugin_id, + plugin, + senders: bus.senders.clone(), + wasi_env, + subscriptions: Arc::new(Mutex::new(HashSet::new())), + plugin_own_data_dir, + }; + + let zellij = zellij_exports(store, &plugin_env); + let instance = Instance::new(&module, &zellij.chain_back(wasi)).unwrap(); + + let start = instance.exports.get_function("_start").unwrap(); + + // This eventually calls the `.load()` method + start.call(&[]).unwrap(); + + (instance, plugin_env) +} + // Plugin API --------------------------------------------------------------------------------------------------------- pub(crate) fn zellij_exports(store: &Store, plugin_env: &PluginEnv) -> ImportObject { @@ -209,15 +259,25 @@ fn host_unsubscribe(plugin_env: &PluginEnv) { } fn host_set_selectable(plugin_env: &PluginEnv, selectable: i32) { - let selectable = selectable != 0; - plugin_env - .senders - .send_to_screen(ScreenInstruction::SetSelectable( - PaneId::Plugin(plugin_env.plugin_id), - selectable, - plugin_env.tab_index, - )) - .unwrap() + match plugin_env.plugin.run { + PluginType::Pane(Some(tab_index)) => { + let selectable = selectable != 0; + plugin_env + .senders + .send_to_screen(ScreenInstruction::SetSelectable( + PaneId::Plugin(plugin_env.plugin_id), + selectable, + tab_index, + )) + .unwrap() + } + _ => { + debug!( + "{} - Calling method 'host_set_selectable' does nothing for headless plugins", + plugin_env.plugin.location + ) + } + } } fn host_get_plugin_ids(plugin_env: &PluginEnv) { @@ -272,7 +332,7 @@ fn host_exec_cmd(plugin_env: &PluginEnv) { let command = cmdline.remove(0); // Bail out if we're forbidden to run command - if !plugin_env._allow_exec_host_cmd { + if !plugin_env.plugin._allow_exec_host_cmd { warn!("This plugin isn't allow to run command in host side, skip running this command: '{cmd} {args}'.", cmd = command, args = cmdline.join(" ")); return; diff --git a/zellij-tile/src/data.rs b/zellij-tile/src/data.rs index 44708627fa..3e5062b925 100644 --- a/zellij-tile/src/data.rs +++ b/zellij-tile/src/data.rs @@ -1,4 +1,5 @@ use serde::{Deserialize, Serialize}; +use std::fmt; use std::str::FromStr; use strum_macros::{EnumDiscriminants, EnumIter, EnumString, ToString}; @@ -160,6 +161,8 @@ pub struct TabInfo { pub position: usize, pub name: String, pub active: bool, + pub panes_to_hide: usize, + pub is_fullscreen_active: bool, pub is_sync_panes_active: bool, } @@ -169,6 +172,28 @@ pub struct PluginIds { pub zellij_pid: u32, } +/// Tag used to identify the plugin in layout and config yaml files +#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)] +pub struct PluginTag(String); + +impl PluginTag { + pub fn new(url: impl Into) -> Self { + PluginTag(url.into()) + } +} + +impl From for String { + fn from(tag: PluginTag) -> Self { + tag.0 + } +} + +impl fmt::Display for PluginTag { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} + #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)] pub struct PluginCapabilities { pub arrow_fonts: bool, diff --git a/zellij-tile/src/lib.rs b/zellij-tile/src/lib.rs index af7d8ca12a..96483c3c39 100644 --- a/zellij-tile/src/lib.rs +++ b/zellij-tile/src/lib.rs @@ -27,7 +27,9 @@ macro_rules! register_plugin { #[no_mangle] pub fn update() { STATE.with(|state| { - state.borrow_mut().update($crate::shim::object_from_stdin()); + state + .borrow_mut() + .update($crate::shim::object_from_stdin().unwrap()); }); } diff --git a/zellij-tile/src/shim.rs b/zellij-tile/src/shim.rs index bdd6f4fb18..6904d3f7d8 100644 --- a/zellij-tile/src/shim.rs +++ b/zellij-tile/src/shim.rs @@ -24,7 +24,7 @@ pub fn set_selectable(selectable: bool) { // Query Functions pub fn get_plugin_ids() -> PluginIds { unsafe { host_get_plugin_ids() }; - object_from_stdin() + object_from_stdin().unwrap() } // Host Functions @@ -45,10 +45,10 @@ pub fn exec_cmd(cmd: &[&str]) { // Internal Functions #[doc(hidden)] -pub fn object_from_stdin() -> T { +pub fn object_from_stdin() -> Result { let mut json = String::new(); io::stdin().read_line(&mut json).unwrap(); - serde_json::from_str(&json).unwrap() + serde_json::from_str(&json) } #[doc(hidden)] diff --git a/zellij-utils/Cargo.toml b/zellij-utils/Cargo.toml index 66e8c1cf4d..8751edbd8a 100644 --- a/zellij-utils/Cargo.toml +++ b/zellij-utils/Cargo.toml @@ -21,11 +21,13 @@ nix = "0.19.1" once_cell = "1.7.2" serde = { version = "1.0", features = ["derive"] } serde_yaml = "0.8" +serde_json = "1.0" signal-hook = "0.3" strip-ansi-escapes = "0.1.0" structopt = "0.3" strum = "0.20.0" termion = "1.5.0" +url = { version = "2.2.2", features = ["serde"] } vte = "0.10.1" zellij-tile = { path = "../zellij-tile/", version = "0.18.0" } log = "0.4.14" diff --git a/zellij-utils/assets/config/default.yaml b/zellij-utils/assets/config/default.yaml index 60b4069ebf..c00fdfbedb 100644 --- a/zellij-utils/assets/config/default.yaml +++ b/zellij-utils/assets/config/default.yaml @@ -248,6 +248,13 @@ keybinds: key: [Ctrl: 'q',] - action: [Detach,] key: [Char: 'd',] +plugins: + - path: tab-bar + tag: tab-bar + - path: status-bar + tag: status-bar + - path: strider + tag: strider # Choose what to do when zellij receives SIGTERM, SIGINT, SIGQUIT or SIGHUP # eg. when terminal window with an active zellij session is closed diff --git a/zellij-utils/assets/layouts/default.yaml b/zellij-utils/assets/layouts/default.yaml index 549dea24b9..0688e54bbc 100644 --- a/zellij-utils/assets/layouts/default.yaml +++ b/zellij-utils/assets/layouts/default.yaml @@ -8,7 +8,7 @@ template: Fixed: 1 run: plugin: - path: tab-bar + location: "zellij:tab-bar" - direction: Vertical body: true - direction: Vertical @@ -17,6 +17,6 @@ template: Fixed: 2 run: plugin: - path: status-bar + location: "zellij:status-bar" tabs: - direction: Vertical diff --git a/zellij-utils/assets/layouts/disable-status-bar.yaml b/zellij-utils/assets/layouts/disable-status-bar.yaml index e97bb8f1e9..1077939803 100644 --- a/zellij-utils/assets/layouts/disable-status-bar.yaml +++ b/zellij-utils/assets/layouts/disable-status-bar.yaml @@ -8,6 +8,6 @@ template: Fixed: 1 run: plugin: - path: tab-bar + location: "zellij:tab-bar" - direction: Vertical body: true diff --git a/zellij-utils/assets/layouts/strider.yaml b/zellij-utils/assets/layouts/strider.yaml index ccb2a5748a..26e1eba4f7 100644 --- a/zellij-utils/assets/layouts/strider.yaml +++ b/zellij-utils/assets/layouts/strider.yaml @@ -8,7 +8,7 @@ template: Fixed: 1 run: plugin: - path: tab-bar + location: "zellij:tab-bar" - direction: Vertical body: true - direction: Vertical @@ -17,7 +17,7 @@ template: Fixed: 2 run: plugin: - path: status-bar + location: "zellij:status-bar" tabs: - direction: Vertical parts: @@ -26,5 +26,5 @@ tabs: Percent: 20 run: plugin: - path: strider + location: "zellij:strider" - direction: Horizontal diff --git a/zellij-utils/src/cli.rs b/zellij-utils/src/cli.rs index eb52589ae2..452033b8a9 100644 --- a/zellij-utils/src/cli.rs +++ b/zellij-utils/src/cli.rs @@ -81,11 +81,6 @@ pub enum Sessions { /// Name of the session to attach to. session_name: Option, - /// Force attach- session will detach from the other - /// zellij client (if any) and attach to this. - #[structopt(long, short)] - force: bool, - /// Create a session if one does not exist. #[structopt(short, long)] create: bool, diff --git a/zellij-utils/src/errors.rs b/zellij-utils/src/errors.rs index 87c3bb4dcf..dc9a97890e 100644 --- a/zellij-utils/src/errors.rs +++ b/zellij-utils/src/errors.rs @@ -60,8 +60,37 @@ where ), }; + let one_line_backtrace = match (info.location(), msg) { + (Some(location), Some(msg)) => format!( + "{}\n\u{1b}[0;0mError: \u{1b}[0;31mthread '{}' panicked at '{}': {}:{}\n\u{1b}[0;0m", + err_ctx, + thread, + msg, + location.file(), + location.line(), + ), + (Some(location), None) => format!( + "{}\n\u{1b}[0;0mError: \u{1b}[0;31mthread '{}' panicked: {}:{}\n\u{1b}[0;0m", + err_ctx, + thread, + location.file(), + location.line(), + ), + (None, Some(msg)) => format!( + "{}\n\u{1b}[0;0mError: \u{1b}[0;31mthread '{}' panicked at '{}'\n\u{1b}[0;0m", + err_ctx, thread, msg + ), + (None, None) => format!( + "{}\n\u{1b}[0;0mError: \u{1b}[0;31mthread '{}' panicked\n\u{1b}[0;0m", + err_ctx, thread + ), + }; + if thread == "main" { - println!("{}", backtrace); + // here we only show the first line because the backtrace is not readable otherwise + // a better solution would be to escape raw mode before we do this, but it's not trivial + // to get os_input here + println!("\u{1b}[2J{}", one_line_backtrace); process::exit(1); } else { let _ = sender.send(T::error(backtrace)); @@ -262,6 +291,7 @@ pub enum ClientContext { UnblockInputThread, Render, ServerError, + SwitchToMode, } /// Stack call representations corresponding to the different types of [`ServerInstruction`]s. @@ -271,6 +301,7 @@ pub enum ServerContext { Render, UnblockInputThread, ClientExit, + RemoveClient, Error, DetachSession, AttachClient, diff --git a/zellij-utils/src/input/config.rs b/zellij-utils/src/input/config.rs index 0339d4dce8..e55798e752 100644 --- a/zellij-utils/src/input/config.rs +++ b/zellij-utils/src/input/config.rs @@ -5,15 +5,16 @@ use std::fs::File; use std::io::{self, Read}; use std::path::{Path, PathBuf}; +use serde::{Deserialize, Serialize}; +use std::convert::{TryFrom, TryInto}; + use super::keybinds::{Keybinds, KeybindsFromYaml}; use super::options::Options; +use super::plugins::{PluginsConfig, PluginsConfigError, PluginsConfigFromYaml}; use super::theme::ThemesFromYaml; use crate::cli::{CliArgs, Command}; use crate::setup; -use serde::{Deserialize, Serialize}; -use std::convert::TryFrom; - const DEFAULT_CONFIG_FILE_NAME: &str = "config.yaml"; type ConfigResult = Result; @@ -25,6 +26,8 @@ pub struct ConfigFromYaml { pub options: Option, pub keybinds: Option, pub themes: Option, + #[serde(default)] + pub plugins: PluginsConfigFromYaml, } /// Main configuration. @@ -33,6 +36,7 @@ pub struct Config { pub keybinds: Keybinds, pub options: Options, pub themes: Option, + pub plugins: PluginsConfig, } #[derive(Debug)] @@ -45,9 +49,10 @@ pub enum ConfigError { IoPath(io::Error, PathBuf), // Internal Deserialization Error FromUtf8(std::string::FromUtf8Error), - // Missing the tab section in the layout. - Layout(LayoutMissingTabSectionError), - LayoutPartAndTab(LayoutPartAndTabError), + // Naming a part in a tab is unsupported + LayoutNameInTab(LayoutNameInTabError), + // Plugins have a semantic error, usually trying to parse two of the same tag + PluginsError(PluginsConfigError), } impl Default for Config { @@ -55,11 +60,13 @@ impl Default for Config { let keybinds = Keybinds::default(); let options = Options::default(); let themes = None; + let plugins = PluginsConfig::default(); Config { keybinds, options, themes, + plugins, } } } @@ -107,9 +114,11 @@ impl Config { let keybinds = Keybinds::get_default_keybinds_with_config(config.keybinds); let options = Options::from_yaml(config.options); let themes = config.themes; + let plugins = PluginsConfig::get_plugins_with_default(config.plugins.try_into()?); Ok(Config { keybinds, options, + plugins, themes, }) } @@ -130,79 +139,42 @@ impl Config { } /// Gets default configuration from assets - // TODO Deserialize the Configuration from bytes &[u8], + // TODO Deserialize the Config from bytes &[u8], // once serde-yaml supports zero-copy pub fn from_default_assets() -> ConfigResult { - Self::from_yaml(String::from_utf8(setup::DEFAULT_CONFIG.to_vec())?.as_str()) + let cfg = String::from_utf8(setup::DEFAULT_CONFIG.to_vec())?; + Self::from_yaml(cfg.as_str()) } } // TODO: Split errors up into separate modules #[derive(Debug, Clone)] -pub struct LayoutMissingTabSectionError; -#[derive(Debug, Clone)] -pub struct LayoutPartAndTabError; +pub struct LayoutNameInTabError; -impl fmt::Display for LayoutMissingTabSectionError { +impl fmt::Display for LayoutNameInTabError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!( f, - "MissingTabSectionError: -There needs to be exactly one `tabs` section specified in the layout file, for example: + "LayoutNameInTabError: +The `parts` inside the `tabs` can't be named. For example: --- -direction: Horizontal -parts: - - direction: Vertical - - direction: Vertical - tabs: - - direction: Vertical - - direction: Vertical - - direction: Vertical -" - ) - } -} - -impl std::error::Error for LayoutMissingTabSectionError { - fn description(&self) -> &str { - "One tab must be specified per Layout." - } -} - -impl fmt::Display for LayoutPartAndTabError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!( - f, - "LayoutPartAndTabError: -The `tabs` and `parts` section should not be specified on the same level in the layout file, for example: ---- -direction: Horizontal -parts: - - direction: Vertical - - direction: Vertical tabs: - direction: Vertical - - direction: Vertical - - direction: Vertical - -should rather be specified as: ---- -direction: Horizontal -parts: - - direction: Vertical - - direction: Vertical - tabs: - - direction: Vertical + name: main + parts: - direction: Vertical + name: section # <== The part section can't be named. - direction: Vertical + - direction: Vertical + name: test " ) } } -impl std::error::Error for LayoutPartAndTabError { +impl std::error::Error for LayoutNameInTabError { fn description(&self) -> &str { - "The `tabs` and parts section should not be specified on the same level." + "The `parts` inside the `tabs` can't be named." } } @@ -215,12 +187,10 @@ impl Display for ConfigError { } ConfigError::Serde(ref err) => write!(formatter, "Deserialization error: {}", err), ConfigError::FromUtf8(ref err) => write!(formatter, "FromUtf8Error: {}", err), - ConfigError::Layout(ref err) => { - write!(formatter, "There was an error in the layout file, {}", err) - } - ConfigError::LayoutPartAndTab(ref err) => { + ConfigError::LayoutNameInTab(ref err) => { write!(formatter, "There was an error in the layout file, {}", err) } + ConfigError::PluginsError(ref err) => write!(formatter, "PluginsError: {}", err), } } } @@ -232,8 +202,8 @@ impl std::error::Error for ConfigError { ConfigError::IoPath(ref err, _) => Some(err), ConfigError::Serde(ref err) => Some(err), ConfigError::FromUtf8(ref err) => Some(err), - ConfigError::Layout(ref err) => Some(err), - ConfigError::LayoutPartAndTab(ref err) => Some(err), + ConfigError::LayoutNameInTab(ref err) => Some(err), + ConfigError::PluginsError(ref err) => Some(err), } } } @@ -256,15 +226,15 @@ impl From for ConfigError { } } -impl From for ConfigError { - fn from(err: LayoutMissingTabSectionError) -> ConfigError { - ConfigError::Layout(err) +impl From for ConfigError { + fn from(err: LayoutNameInTabError) -> ConfigError { + ConfigError::LayoutNameInTab(err) } } -impl From for ConfigError { - fn from(err: LayoutPartAndTabError) -> ConfigError { - ConfigError::LayoutPartAndTab(err) +impl From for ConfigError { + fn from(err: PluginsConfigError) -> ConfigError { + ConfigError::PluginsError(err) } } diff --git a/zellij-utils/src/input/layout.rs b/zellij-utils/src/input/layout.rs index 98fdb0d12b..0336141510 100644 --- a/zellij-utils/src/input/layout.rs +++ b/zellij-utils/src/input/layout.rs @@ -9,20 +9,27 @@ // If plugins should be able to depend on the layout system // then [`zellij-utils`] could be a proper place. use crate::{ - input::{command::RunCommand, config::ConfigError}, + input::{ + command::RunCommand, + config::{ConfigError, LayoutNameInTabError}, + }, pane_size::{Dimension, PaneGeom}, setup, }; use crate::{serde, serde_yaml}; +use super::plugins::{PluginTag, PluginsConfigError}; use serde::{Deserialize, Serialize}; +use std::convert::{TryFrom, TryInto}; use std::vec::Vec; use std::{ cmp::max, + fmt, fs, ops::Not, path::{Path, PathBuf}, }; use std::{fs::File, io::prelude::*}; +use url::Url; #[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone, Copy)] #[serde(crate = "self::serde")] @@ -53,17 +60,68 @@ pub enum SplitSize { #[serde(crate = "self::serde")] pub enum Run { #[serde(rename = "plugin")] - Plugin(Option), + Plugin(RunPlugin), #[serde(rename = "command")] Command(RunCommand), } -#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq, Eq)] +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] +#[serde(crate = "self::serde")] +pub enum RunFromYaml { + #[serde(rename = "plugin")] + Plugin(RunPluginFromYaml), + #[serde(rename = "command")] + Command(RunCommand), +} + +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] +#[serde(crate = "self::serde")] +pub struct RunPluginFromYaml { + #[serde(default)] + pub _allow_exec_host_cmd: bool, + pub location: Url, +} + +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] #[serde(crate = "self::serde")] pub struct RunPlugin { - pub path: PathBuf, #[serde(default)] pub _allow_exec_host_cmd: bool, + pub location: RunPluginLocation, +} + +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] +#[serde(crate = "self::serde")] +pub enum RunPluginLocation { + File(PathBuf), + Zellij(PluginTag), +} + +impl From<&RunPluginLocation> for Url { + fn from(location: &RunPluginLocation) -> Self { + let url = match location { + RunPluginLocation::File(path) => format!( + "file:{}", + path.clone().into_os_string().into_string().unwrap() + ), + RunPluginLocation::Zellij(tag) => format!("zellij:{}", tag), + }; + Self::parse(&url).unwrap() + } +} + +impl fmt::Display for RunPluginLocation { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + match self { + Self::File(path) => write!( + f, + "{}", + path.clone().into_os_string().into_string().unwrap() + ), + + Self::Zellij(tag) => write!(f, "{}", tag), + } + } } // The layout struct ultimately used to build the layouts. @@ -106,7 +164,12 @@ impl LayoutFromYaml { let layout: Option = serde_yaml::from_str(&layout)?; match layout { - Some(layout) => Ok(layout), + Some(layout) => { + for tab in layout.tabs.clone() { + tab.check()?; + } + Ok(layout) + } None => Ok(LayoutFromYaml::default()), } } @@ -185,7 +248,7 @@ pub struct LayoutTemplate { #[serde(default)] pub body: bool, pub split_size: Option, - pub run: Option, + pub run: Option, } impl LayoutTemplate { @@ -220,15 +283,28 @@ impl LayoutTemplate { #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] #[serde(crate = "self::serde")] pub struct TabLayout { + #[serde(default)] pub direction: Direction, #[serde(default)] pub borderless: bool, #[serde(default)] pub parts: Vec, pub split_size: Option, - pub run: Option, #[serde(default)] pub name: String, + pub run: Option, +} + +impl TabLayout { + fn check(&self) -> Result { + for part in self.parts.iter() { + part.check()?; + if !part.name.is_empty() { + return Err(ConfigError::LayoutNameInTab(LayoutNameInTabError)); + } + } + Ok(self.clone()) + } } impl Layout { @@ -270,25 +346,23 @@ impl Layout { split_space(space, self) } - pub fn merge_tab_layout(&mut self, tab: TabLayout) { - self.parts.push(tab.into()); - } - pub fn merge_layout_parts(&mut self, mut parts: Vec) { self.parts.append(&mut parts); } - fn from_vec_tab_layout(tab_layout: Vec) -> Vec { + fn from_vec_tab_layout(tab_layout: Vec) -> Result, ConfigError> { tab_layout .iter() - .map(|tab_layout| Layout::from(tab_layout.to_owned())) + .map(|tab_layout| Layout::try_from(tab_layout.to_owned())) .collect() } - fn from_vec_template_layout(layout_template: Vec) -> Vec { + fn from_vec_template_layout( + layout_template: Vec, + ) -> Result, ConfigError> { layout_template .iter() - .map(|layout_template| Layout::from(layout_template.to_owned())) + .map(|layout_template| Layout::try_from(layout_template.to_owned())) .collect() } } @@ -387,15 +461,55 @@ fn split_space(space_to_split: &PaneGeom, layout: &Layout) -> Vec<(Layout, PaneG pane_positions } -impl From for Layout { - fn from(tab: TabLayout) -> Self { - Layout { +impl TryFrom for RunPluginLocation { + type Error = PluginsConfigError; + + fn try_from(url: Url) -> Result { + match url.scheme() { + "zellij" => Ok(Self::Zellij(PluginTag::new(url.path()))), + "file" => { + let path = PathBuf::from(url.path()); + let canonicalize = |p: &Path| { + fs::canonicalize(p) + .map_err(|_| PluginsConfigError::InvalidPluginLocation(p.to_owned())) + }; + canonicalize(&path) + .or_else(|_| match path.strip_prefix("/") { + Ok(path) => canonicalize(path), + Err(_) => Err(PluginsConfigError::InvalidPluginLocation(path.to_owned())), + }) + .map(Self::File) + } + _ => Err(PluginsConfigError::InvalidUrl(url)), + } + } +} + +impl TryFrom for Run { + type Error = PluginsConfigError; + + fn try_from(run: RunFromYaml) -> Result { + match run { + RunFromYaml::Command(command) => Ok(Run::Command(command)), + RunFromYaml::Plugin(plugin) => Ok(Run::Plugin(RunPlugin { + _allow_exec_host_cmd: plugin._allow_exec_host_cmd, + location: plugin.location.try_into()?, + })), + } + } +} + +impl TryFrom for Layout { + type Error = ConfigError; + + fn try_from(tab: TabLayout) -> Result { + Ok(Layout { direction: tab.direction, borderless: tab.borderless, - parts: Self::from_vec_tab_layout(tab.parts), + parts: Self::from_vec_tab_layout(tab.parts)?, split_size: tab.split_size, - run: tab.run, - } + run: tab.run.map(Run::try_from).transpose()?, + }) } } @@ -412,15 +526,22 @@ impl From for LayoutTemplate { } } -impl From for Layout { - fn from(template: LayoutTemplate) -> Self { - Layout { +impl TryFrom for Layout { + type Error = ConfigError; + + fn try_from(template: LayoutTemplate) -> Result { + Ok(Layout { direction: template.direction, borderless: template.borderless, - parts: Self::from_vec_template_layout(template.parts), + parts: Self::from_vec_template_layout(template.parts)?, split_size: template.split_size, - run: template.run, - } + run: template + .run + .map(Run::try_from) + // FIXME: This is just Result::transpose but that method is unstable, when it + // stabalizes we should swap this out. + .map_or(Ok(None), |r| r.map(Some))?, + }) } } @@ -467,6 +588,12 @@ impl Default for LayoutFromYaml { } } +impl Default for Direction { + fn default() -> Self { + Direction::Horizontal + } +} + // The unit test location. #[cfg(test)] #[path = "./unit/layout_test.rs"] diff --git a/zellij-utils/src/input/mod.rs b/zellij-utils/src/input/mod.rs index 68aa9dd79b..409c9afae7 100644 --- a/zellij-utils/src/input/mod.rs +++ b/zellij-utils/src/input/mod.rs @@ -7,6 +7,7 @@ pub mod keybinds; pub mod layout; pub mod mouse; pub mod options; +pub mod plugins; pub mod theme; use termion::input::TermRead; diff --git a/zellij-utils/src/input/plugins.rs b/zellij-utils/src/input/plugins.rs new file mode 100644 index 0000000000..931d9f7803 --- /dev/null +++ b/zellij-utils/src/input/plugins.rs @@ -0,0 +1,315 @@ +//! Plugins configuration metadata +use std::borrow::Borrow; +use std::collections::HashMap; +use std::convert::TryFrom; +use std::fmt::{self, Display}; +use std::fs; +use std::path::{Path, PathBuf}; + +use lazy_static::lazy_static; +use serde::{Deserialize, Serialize}; +use url::Url; + +use super::config::ConfigFromYaml; +use super::layout::{RunPlugin, RunPluginLocation}; +use crate::setup; +pub use zellij_tile::data::PluginTag; + +lazy_static! { + static ref DEFAULT_CONFIG_PLUGINS: PluginsConfig = { + let cfg = String::from_utf8(setup::DEFAULT_CONFIG.to_vec()).unwrap(); + let cfg_yaml: ConfigFromYaml = serde_yaml::from_str(cfg.as_str()).unwrap(); + PluginsConfig::try_from(cfg_yaml.plugins).unwrap() + }; +} + +#[derive(Clone, Debug, Default, PartialEq, Deserialize, Serialize)] +pub struct PluginsConfigFromYaml(Vec); + +/// Used in the config struct for plugin metadata +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +pub struct PluginsConfig(HashMap); + +impl PluginsConfig { + pub fn new() -> Self { + Self(HashMap::new()) + } + + /// Entrypoint from the config module + pub fn get_plugins_with_default(user_plugins: Self) -> Self { + let mut base_plugins = DEFAULT_CONFIG_PLUGINS.clone(); + base_plugins.0.extend(user_plugins.0); + base_plugins + } + + /// Get plugin config from run configuration specified in layout files. + pub fn get(&self, run: impl Borrow) -> Option { + let run = run.borrow(); + match &run.location { + RunPluginLocation::File(path) => Some(PluginConfig { + path: path.clone(), + run: PluginType::Pane(None), + _allow_exec_host_cmd: run._allow_exec_host_cmd, + location: run.location.clone(), + }), + RunPluginLocation::Zellij(tag) => self.0.get(tag).cloned().map(|plugin| PluginConfig { + _allow_exec_host_cmd: run._allow_exec_host_cmd, + ..plugin + }), + } + } + + pub fn iter(&self) -> impl Iterator { + self.0.values() + } +} + +impl Default for PluginsConfig { + fn default() -> Self { + Self::get_plugins_with_default(PluginsConfig::new()) + } +} + +impl TryFrom for PluginsConfig { + type Error = PluginsConfigError; + + fn try_from(yaml: PluginsConfigFromYaml) -> Result { + let mut plugins = HashMap::new(); + for plugin in yaml.0 { + if plugins.contains_key(&plugin.tag) { + return Err(PluginsConfigError::DuplicatePlugins(plugin.tag)); + } + plugins.insert(plugin.tag.clone(), plugin.into()); + } + + Ok(PluginsConfig(plugins)) + } +} + +impl From for PluginConfig { + fn from(plugin: PluginConfigFromYaml) -> Self { + PluginConfig { + path: plugin.path, + run: match plugin.run { + PluginTypeFromYaml::Pane => PluginType::Pane(None), + PluginTypeFromYaml::Headless => PluginType::Headless, + }, + _allow_exec_host_cmd: plugin._allow_exec_host_cmd, + location: RunPluginLocation::Zellij(plugin.tag), + } + } +} + +/// Plugin metadata +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +pub struct PluginConfig { + /// Path of the plugin, see resolve_wasm_bytes for resolution semantics + pub path: PathBuf, + /// Plugin type + pub run: PluginType, + /// Allow command execution from plugin + pub _allow_exec_host_cmd: bool, + /// Original location of the + pub location: RunPluginLocation, +} + +impl PluginConfig { + /// Resolve wasm plugin bytes for the plugin path and given plugin directory. Attempts to first + /// resolve the plugin path as an absolute path, then adds a ".wasm" extension to the path and + /// resolves that, finally we use the plugin directoy joined with the path with an appended + /// ".wasm" extension. So if our path is "tab-bar" and the given plugin dir is + /// "/home/bob/.zellij/plugins" the lookup chain will be this: + /// + /// ```bash + /// /tab-bar + /// /tab-bar.wasm + /// /home/bob/.zellij/plugins/tab-bar.wasm + /// ``` + /// + pub fn resolve_wasm_bytes(&self, plugin_dir: &Path) -> Option> { + fs::read(&self.path) + .or_else(|_| fs::read(&self.path.with_extension("wasm"))) + .or_else(|_| fs::read(plugin_dir.join(&self.path).with_extension("wasm"))) + .ok() + } + + /// Sets the tab index inside of the plugin type of the run field. + pub fn set_tab_index(&mut self, tab_index: usize) { + match self.run { + PluginType::Pane(..) => { + self.run = PluginType::Pane(Some(tab_index)); + } + PluginType::Headless => {} + } + } +} + +/// Type of the plugin. Defaults to Pane. +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "kebab-case")] +pub enum PluginType { + // TODO: A plugin with output thats cloned across every pane in a tab, or across the entire + // application might be useful + // Tab + // Static + /// Starts immediately when Zellij is started and runs without a visible pane + Headless, + /// Runs once per pane declared inside a layout file + Pane(Option), // tab_index +} + +impl Default for PluginType { + fn default() -> Self { + Self::Pane(None) + } +} + +#[derive(Clone, Debug, Default, PartialEq, Deserialize, Serialize)] +pub struct PluginConfigFromYaml { + pub path: PathBuf, + pub tag: PluginTag, + #[serde(default)] + pub run: PluginTypeFromYaml, + #[serde(default)] + pub config: serde_yaml::Value, + #[serde(default)] + pub _allow_exec_host_cmd: bool, +} + +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "kebab-case")] +pub enum PluginTypeFromYaml { + Headless, + Pane, +} + +impl Default for PluginTypeFromYaml { + fn default() -> Self { + Self::Pane + } +} + +#[derive(Debug, PartialEq)] +pub enum PluginsConfigError { + DuplicatePlugins(PluginTag), + InvalidUrl(Url), + InvalidPluginLocation(PathBuf), +} + +impl std::error::Error for PluginsConfigError {} +impl Display for PluginsConfigError { + fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + match self { + PluginsConfigError::DuplicatePlugins(tag) => write!( + formatter, + "Duplication in plugin tag names is not allowed: '{}'", + String::from(tag.clone()) + ), + PluginsConfigError::InvalidUrl(url) => write!( + formatter, + "Only 'file:' and 'zellij:' url schemes are supported for plugin lookup. '{}' does not match either.", + url + ), + PluginsConfigError::InvalidPluginLocation(path) => write!( + formatter, + "Could not find plugin at the path: '{:?}'", path + ), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::input::config::ConfigError; + use std::convert::TryInto; + + #[test] + fn run_plugin_permissions_are_inherited() -> Result<(), ConfigError> { + let yaml_plugins: PluginsConfigFromYaml = serde_yaml::from_str( + " + - path: boo.wasm + tag: boo + _allow_exec_host_cmd: false + ", + )?; + let plugins = PluginsConfig::try_from(yaml_plugins)?; + + assert_eq!( + plugins.get(RunPlugin { + _allow_exec_host_cmd: true, + location: RunPluginLocation::Zellij(PluginTag::new("boo")) + }), + Some(PluginConfig { + _allow_exec_host_cmd: true, + path: PathBuf::from("boo.wasm"), + location: RunPluginLocation::Zellij(PluginTag::new("boo")), + run: PluginType::Pane(None), + }) + ); + + Ok(()) + } + + #[test] + fn try_from_yaml_fails_when_duplicate_tag_names_are_present() -> Result<(), ConfigError> { + let ConfigFromYaml { plugins, .. } = serde_yaml::from_str( + " + plugins: + - path: /foo/bar/baz.wasm + tag: boo + - path: /foo/bar/boo.wasm + tag: boo + ", + )?; + + assert_eq!( + PluginsConfig::try_from(plugins), + Err(PluginsConfigError::DuplicatePlugins(PluginTag::new("boo"))) + ); + + Ok(()) + } + + #[test] + fn default_plugins() -> Result<(), ConfigError> { + let ConfigFromYaml { plugins, .. } = serde_yaml::from_str( + " + plugins: + - path: boo.wasm + tag: boo + ", + )?; + let plugins = PluginsConfig::get_plugins_with_default(plugins.try_into()?); + + assert_eq!(plugins.iter().collect::>().len(), 4); + Ok(()) + } + + #[test] + fn default_plugins_allow_overriding() -> Result<(), ConfigError> { + let ConfigFromYaml { plugins, .. } = serde_yaml::from_str( + " + plugins: + - path: boo.wasm + tag: tab-bar + ", + )?; + let plugins = PluginsConfig::get_plugins_with_default(plugins.try_into()?); + + assert_eq!( + plugins.get(RunPlugin { + _allow_exec_host_cmd: false, + location: RunPluginLocation::Zellij(PluginTag::new("tab-bar")) + }), + Some(PluginConfig { + _allow_exec_host_cmd: false, + path: PathBuf::from("boo.wasm"), + location: RunPluginLocation::Zellij(PluginTag::new("tab-bar")), + run: PluginType::Pane(None), + }) + ); + + Ok(()) + } +} diff --git a/zellij-utils/src/input/unit/fixtures/layouts/three-panes-with-tab-and-default-plugins.yaml b/zellij-utils/src/input/unit/fixtures/layouts/three-panes-with-tab-and-default-plugins.yaml index ae54a0c9bf..8148fd2008 100644 --- a/zellij-utils/src/input/unit/fixtures/layouts/three-panes-with-tab-and-default-plugins.yaml +++ b/zellij-utils/src/input/unit/fixtures/layouts/three-panes-with-tab-and-default-plugins.yaml @@ -7,7 +7,7 @@ template: Fixed: 1 run: plugin: - path: tab-bar + location: "zellij:tab-bar" - direction: Horizontal body: true - direction: Vertical @@ -15,8 +15,7 @@ template: Fixed: 2 run: plugin: - path: status-bar - + location: "zellij:status-bar" tabs: - direction: Vertical parts: diff --git a/zellij-utils/src/input/unit/layout_test.rs b/zellij-utils/src/input/unit/layout_test.rs index 1b696b0a1e..dc8969251a 100644 --- a/zellij-utils/src/input/unit/layout_test.rs +++ b/zellij-utils/src/input/unit/layout_test.rs @@ -1,4 +1,5 @@ use super::super::layout::*; +use std::convert::TryInto; fn layout_test_dir(layout: String) -> PathBuf { let root = Path::new(env!("CARGO_MANIFEST_DIR")); @@ -45,10 +46,10 @@ fn default_layout_merged_correctly() { borderless: true, parts: vec![], split_size: Some(SplitSize::Fixed(1)), - run: Some(Run::Plugin(Some(RunPlugin { - path: "tab-bar".into(), - ..Default::default() - }))), + run: Some(Run::Plugin(RunPlugin { + location: RunPluginLocation::Zellij(PluginTag::new("tab-bar")), + _allow_exec_host_cmd: false, + })), }, Layout { direction: Direction::Vertical, @@ -62,16 +63,16 @@ fn default_layout_merged_correctly() { borderless: true, parts: vec![], split_size: Some(SplitSize::Fixed(2)), - run: Some(Run::Plugin(Some(RunPlugin { - path: "status-bar".into(), - ..Default::default() - }))), + run: Some(Run::Plugin(RunPlugin { + location: RunPluginLocation::Zellij(PluginTag::new("status-bar")), + _allow_exec_host_cmd: false, + })), }, ], split_size: None, run: None, }; - assert_eq!(merged_layout, tab_layout.into()); + assert_eq!(merged_layout, tab_layout.try_into().unwrap()); } #[test] @@ -89,10 +90,10 @@ fn default_layout_new_tab_correct() { borderless: true, parts: vec![], split_size: Some(SplitSize::Fixed(1)), - run: Some(Run::Plugin(Some(RunPlugin { - path: "tab-bar".into(), - ..Default::default() - }))), + run: Some(Run::Plugin(RunPlugin { + location: RunPluginLocation::Zellij(PluginTag::new("tab-bar")), + _allow_exec_host_cmd: false, + })), }, Layout { direction: Direction::Horizontal, @@ -106,16 +107,16 @@ fn default_layout_new_tab_correct() { borderless: true, parts: vec![], split_size: Some(SplitSize::Fixed(2)), - run: Some(Run::Plugin(Some(RunPlugin { - path: "status-bar".into(), - ..Default::default() - }))), + run: Some(Run::Plugin(RunPlugin { + location: RunPluginLocation::Zellij(PluginTag::new("status-bar")), + _allow_exec_host_cmd: false, + })), }, ], split_size: None, run: None, }; - assert_eq!(merged_layout, tab_layout.into()); + assert_eq!(merged_layout, tab_layout.try_into().unwrap()); } #[test] @@ -207,7 +208,7 @@ fn three_panes_with_tab_merged_correctly() { split_size: None, run: None, }; - assert_eq!(merged_layout, tab_layout.into()); + assert_eq!(merged_layout, tab_layout.try_into().unwrap()); } #[test] @@ -229,7 +230,7 @@ fn three_panes_with_tab_new_tab_is_correct() { split_size: None, run: None, }; - assert_eq!(merged_layout, tab_layout.into()); + assert_eq!(merged_layout, tab_layout.try_into().unwrap()); } #[test] @@ -265,10 +266,10 @@ fn three_panes_with_tab_and_default_plugins_merged_correctly() { borderless: false, parts: vec![], split_size: Some(SplitSize::Fixed(1)), - run: Some(Run::Plugin(Some(RunPlugin { - path: "tab-bar".into(), - ..Default::default() - }))), + run: Some(Run::Plugin(RunPlugin { + location: RunPluginLocation::Zellij(PluginTag::new("tab-bar")), + _allow_exec_host_cmd: false, + })), }, Layout { direction: Direction::Vertical, @@ -312,16 +313,16 @@ fn three_panes_with_tab_and_default_plugins_merged_correctly() { borderless: false, parts: vec![], split_size: Some(SplitSize::Fixed(2)), - run: Some(Run::Plugin(Some(RunPlugin { - path: "status-bar".into(), - ..Default::default() - }))), + run: Some(Run::Plugin(RunPlugin { + location: RunPluginLocation::Zellij(PluginTag::new("status-bar")), + _allow_exec_host_cmd: false, + })), }, ], split_size: None, run: None, }; - assert_eq!(merged_layout, tab_layout.into()); + assert_eq!(merged_layout, tab_layout.try_into().unwrap()); } #[test] @@ -339,10 +340,10 @@ fn three_panes_with_tab_and_default_plugins_new_tab_is_correct() { borderless: false, parts: vec![], split_size: Some(SplitSize::Fixed(1)), - run: Some(Run::Plugin(Some(RunPlugin { - path: "tab-bar".into(), - ..Default::default() - }))), + run: Some(Run::Plugin(RunPlugin { + location: RunPluginLocation::Zellij(PluginTag::new("tab-bar")), + _allow_exec_host_cmd: false, + })), }, Layout { direction: Direction::Horizontal, @@ -356,16 +357,16 @@ fn three_panes_with_tab_and_default_plugins_new_tab_is_correct() { borderless: false, parts: vec![], split_size: Some(SplitSize::Fixed(2)), - run: Some(Run::Plugin(Some(RunPlugin { - path: "status-bar".into(), - ..Default::default() - }))), + run: Some(Run::Plugin(RunPlugin { + location: RunPluginLocation::Zellij(PluginTag::new("status-bar")), + _allow_exec_host_cmd: false, + })), }, ], split_size: None, run: None, }; - assert_eq!(merged_layout, tab_layout.into()); + assert_eq!(merged_layout, tab_layout.try_into().unwrap()); } #[test] @@ -473,7 +474,7 @@ fn deeply_nested_tab_merged_correctly() { split_size: None, run: None, }; - assert_eq!(merged_layout, tab_layout.into()); + assert_eq!(merged_layout, tab_layout.try_into().unwrap()); } #[test] @@ -523,7 +524,7 @@ fn three_tabs_tab_one_merged_correctly() { run: None, }; - assert_eq!(merged_layout, tab_layout.into()); + assert_eq!(merged_layout, tab_layout.try_into().unwrap()); } #[test] @@ -573,7 +574,7 @@ fn three_tabs_tab_two_merged_correctly() { run: None, }; - assert_eq!(merged_layout, tab_layout.into()); + assert_eq!(merged_layout, tab_layout.try_into().unwrap()); } #[test] @@ -622,7 +623,7 @@ fn three_tabs_tab_three_merged_correctly() { split_size: None, run: None, }; - assert_eq!(merged_layout, tab_layout.into()); + assert_eq!(merged_layout, tab_layout.try_into().unwrap()); } #[test] @@ -660,7 +661,7 @@ fn no_tabs_merged_correctly() { run: None, }; - assert_eq!(merged_layout, tab_layout.into()); + assert_eq!(merged_layout, tab_layout.try_into().unwrap()); } #[test] @@ -716,5 +717,5 @@ fn no_layout_template_merged_correctly() { borderless: false, }; - assert_eq!(merged_layout, tab_layout.into()); + assert_eq!(merged_layout, tab_layout.try_into().unwrap()); } diff --git a/zellij-utils/src/ipc.rs b/zellij-utils/src/ipc.rs index a7cda9772e..5c327022fb 100644 --- a/zellij-utils/src/ipc.rs +++ b/zellij-utils/src/ipc.rs @@ -3,7 +3,7 @@ use crate::{ cli::CliArgs, errors::{get_current_ctx, ErrorContext}, - input::{actions::Action, layout::LayoutFromYaml, options::Options}, + input::{actions::Action, layout::LayoutFromYaml, options::Options, plugins::PluginsConfig}, pane_size::Size, }; use interprocess::local_socket::LocalSocketStream; @@ -16,7 +16,7 @@ use std::{ os::unix::io::{AsRawFd, FromRawFd}, }; -use zellij_tile::data::Palette; +use zellij_tile::data::{InputMode, Palette}; type SessionId = u64; @@ -58,8 +58,14 @@ pub enum ClientToServerMsg { // Disconnect from the session we're connected to DisconnectFromSession,*/ TerminalResize(Size), - NewClient(ClientAttributes, Box, Box, LayoutFromYaml), - AttachClient(ClientAttributes, bool, Options), + NewClient( + ClientAttributes, + Box, + Box, + LayoutFromYaml, + Option, + ), + AttachClient(ClientAttributes, Options), Action(Action), ClientExited, } @@ -74,6 +80,7 @@ pub enum ServerToClientMsg { Render(String), UnblockInputThread, Exit(ExitReason), + SwitchToMode(InputMode), } #[derive(Serialize, Deserialize, Debug, Clone)] @@ -120,7 +127,9 @@ impl IpcSenderWithContext { pub fn send(&mut self, msg: T) { let err_ctx = get_current_ctx(); bincode::serialize_into(&mut self.sender, &(msg, err_ctx)).unwrap(); - self.sender.flush().unwrap(); + // TODO: unwrapping here can cause issues when the server disconnects which we don't mind + // do we need to handle errors here in other cases? + let _ = self.sender.flush(); } /// Returns an [`IpcReceiverWithContext`] with the same socket as this sender. diff --git a/zellij-utils/src/setup.rs b/zellij-utils/src/setup.rs index 0d9855abaf..7300146101 100644 --- a/zellij-utils/src/setup.rs +++ b/zellij-utils/src/setup.rs @@ -188,7 +188,6 @@ impl Setup { return Err(e); } }; - //.map(|layout| layout.template); if let Some(Command::Setup(ref setup)) = &opts.command { setup.from_cli(opts, &config_options).map_or_else(