diff --git a/CHANGELOG.md b/CHANGELOG.md index 1da63b00..316de361 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,46 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) ## [Unreleased] +* Fix: Properly open new pane with CWD also when switching to a new tab (https://github.com/zellij-org/zellij/pull/729) +* 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) + +## [0.17.0] - 2021-09-15 +* New panes/tabs now open in CWD of focused pane (https://github.com/zellij-org/zellij/pull/691) +* Fix bug when opening new tab the new pane's viewport would sometimes be calculated incorrectly (https://github.com/zellij-org/zellij/pull/683) +* Fix bug when in some cases closing a tab would not clear the previous pane's contents (https://github.com/zellij-org/zellij/pull/684) +* Fix bug where tabs would sometimes be created with the wrong index in their name (https://github.com/zellij-org/zellij/pull/686) +* Fix bug where wide chars would mess up pane titles (https://github.com/zellij-org/zellij/pull/698) +* Fix various borderless-frame in viewport bugs (https://github.com/zellij-org/zellij/pull/697) +* Fix example configuration file (https://github.com/zellij-org/zellij/pull/693) +* Fix various tab bar responsiveness issues (https://github.com/zellij-org/zellij/pull/703) +* Allow plugins to run system commands (https://github.com/zellij-org/zellij/pull/666) + * This has also added a temporary new permission flag that needs to be specified in the layout. This is a breaking change: + ```yaml + ... + plugin: strider + ... + ``` + has become: + ```yaml + plugin: + path: strider + ``` + A plugin can be given command executing permission with: + ```yaml + plugin: + path: strider + _allow_exec_host_cmd: true + ``` +* Use the unicode width in tab-bar plugin, for tab names (https://github.com/zellij-org/zellij/pull/709) +* Fix automated builds that make use of the `setup` subcommand (https://github.com/zellij-org/zellij/pull/711) +* Add option to specify a tabs name in the tab `layout` file (https://github.com/zellij-org/zellij/pull/715) +* Improve handling of empty valid `yaml` files (https://github.com/zellij-org/zellij/pull/716) +* Add options subcommand to attach (https://github.com/zellij-org/zellij/pull/718) +* Fix: do not pad empty pane frame title (https://github.com/zellij-org/zellij/pull/724) +* Fix: Do not overflow empty lines when resizing panes (https://github.com/zellij-org/zellij/pull/725) + ## [0.16.0] - 2021-08-31 * Plugins don't crash zellij anymore on receiving mouse events (https://github.com/zellij-org/zellij/pull/620) diff --git a/Cargo.lock b/Cargo.lock index 6c2cdcd3..66a0b3cb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,11 +4,11 @@ version = 3 [[package]] name = "addr2line" -version = "0.16.0" +version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e61f2b7f93d2c7d2b08263acaa4a363b3e276806c68af6134c44f523bf1aacd" +checksum = "e7a2e47a1fbe209ee101dd6d61285226744c6c8d3c21c8dc878ba6cb9f467f3a" dependencies = [ - "gimli 0.25.0", + "gimli 0.24.0", ] [[package]] @@ -19,9 +19,9 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "aho-corasick" -version = "0.7.18" +version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" +checksum = "7404febffaa47dac81aa44dba71523c9d069b1bdc50a77db41195149e17f68e5" dependencies = [ "memchr", ] @@ -227,16 +227,16 @@ checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" [[package]] name = "backtrace" -version = "0.3.61" +version = "0.3.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7a905d892734eea339e896738c14b9afce22b5318f64b951e70bf3844419b01" +checksum = "4717cfcbfaa661a0fd48f8453951837ae7e8f81e481fbb136e3202d72805a744" dependencies = [ "addr2line", "cc", "cfg-if 1.0.0", "libc", "miniz_oxide", - "object 0.26.0", + "object 0.24.0", "rustc-demangle", ] @@ -621,6 +621,26 @@ dependencies = [ "syn", ] +[[package]] +name = "darwin-libproc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc629b7cf42586fee31dae31f9ab73fa5ff5f0170016aa61be5fcbc12a90c516" +dependencies = [ + "darwin-libproc-sys", + "libc", + "memchr", +] + +[[package]] +name = "darwin-libproc-sys" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef0aa083b94c54aa4cfd9bbfd37856714c139d1dc511af80270558c7ba3b4816" +dependencies = [ + "libc", +] + [[package]] name = "derivative" version = "2.2.0" @@ -897,9 +917,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.25.0" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0a01e0497841a3b2db4f8afa483cce65f7e96a3498bd6c541734792aeac8fe7" +checksum = "0e4075386626662786ddb0ec9081e7c7eeb1ba31951f447ca780ef9f5d568189" [[package]] name = "gloo-timers" @@ -1188,9 +1208,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.4.0" +version = "2.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b16bd47d9e329435e309c58469fe0791c2d0d1ba96ec0954152a5ae2b04387dc" +checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525" [[package]] name = "memmap2" @@ -1325,12 +1345,9 @@ dependencies = [ [[package]] name = "object" -version = "0.26.0" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c55827317fb4c08822499848a14237d2874d6f139828893017237e7ab93eb386" -dependencies = [ - "memchr", -] +checksum = "1a5b3dd1c072ee7963717671d1ca129f1048fda25edea6b752bfc71ac8854170" [[package]] name = "once_cell" @@ -1675,9 +1692,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.5.4" +version = "1.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" +checksum = "2a26af418b574bd56588335b3a3659a65725d4e636eb1016c2f9e3b38c7cc759" dependencies = [ "aho-corasick", "memchr", @@ -1981,6 +1998,7 @@ version = "0.1.0" dependencies = [ "ansi_term 0.12.1", "colored", + "unicode-width", "zellij-tile", "zellij-tile-utils", ] @@ -2622,7 +2640,7 @@ dependencies = [ [[package]] name = "zellij" -version = "0.17.0" +version = "0.18.0" dependencies = [ "insta", "log", @@ -2636,7 +2654,7 @@ dependencies = [ [[package]] name = "zellij-client" -version = "0.17.0" +version = "0.18.0" dependencies = [ "insta", "log", @@ -2647,14 +2665,16 @@ dependencies = [ [[package]] name = "zellij-server" -version = "0.17.0" +version = "0.18.0" dependencies = [ "ansi_term 0.12.1", "async-trait", "base64", + "byteorder", "cassowary", "chrono", "daemonize", + "darwin-libproc", "insta", "log", "serde_json", @@ -2667,7 +2687,7 @@ dependencies = [ [[package]] name = "zellij-tile" -version = "0.17.0" +version = "0.18.0" dependencies = [ "serde", "serde_json", @@ -2677,14 +2697,14 @@ dependencies = [ [[package]] name = "zellij-tile-utils" -version = "0.17.0" +version = "0.18.0" dependencies = [ "ansi_term 0.12.1", ] [[package]] name = "zellij-utils" -version = "0.17.0" +version = "0.18.0" dependencies = [ "async-std", "backtrace", @@ -2707,6 +2727,7 @@ dependencies = [ "strum", "tempfile", "termion", + "unicode-width", "vte 0.10.1", "zellij-tile", ] diff --git a/Cargo.toml b/Cargo.toml index e5a7af57..738b4642 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "zellij" -version = "0.17.0" +version = "0.18.0" authors = ["Aram Drevekenin "] edition = "2018" description = "A terminal workspace with batteries included" @@ -14,9 +14,9 @@ resolver = "2" [dependencies] names = "0.11.0" -zellij-client = { path = "zellij-client/", version = "0.17.0" } -zellij-server = { path = "zellij-server/", version = "0.17.0" } -zellij-utils = { path = "zellij-utils/", version = "0.17.0" } +zellij-client = { path = "zellij-client/", version = "0.18.0" } +zellij-server = { path = "zellij-server/", version = "0.18.0" } +zellij-utils = { path = "zellij-utils/", version = "0.18.0" } log = "0.4.14" [dev-dependencies] diff --git a/README.md b/README.md index 916f6720..ac36035e 100644 --- a/README.md +++ b/README.md @@ -13,9 +13,9 @@

Discord Chat + Zellij documentation

- # What is this? [Zellij](https://en.wikipedia.org/wiki/Zellij) is a workspace aimed at developers, ops-oriented people and anyone who loves the terminal. diff --git a/assets/plugins/status-bar.wasm b/assets/plugins/status-bar.wasm index d1ad39e9..56edbb36 100644 Binary files a/assets/plugins/status-bar.wasm and b/assets/plugins/status-bar.wasm differ diff --git a/assets/plugins/strider.wasm b/assets/plugins/strider.wasm index b57557a7..3baf9127 100644 Binary files a/assets/plugins/strider.wasm and b/assets/plugins/strider.wasm differ diff --git a/assets/plugins/tab-bar.wasm b/assets/plugins/tab-bar.wasm index 896512fe..6756e7c1 100644 Binary files a/assets/plugins/tab-bar.wasm and b/assets/plugins/tab-bar.wasm differ diff --git a/default-plugins/tab-bar/Cargo.toml b/default-plugins/tab-bar/Cargo.toml index dd91bdb5..7fd6593d 100644 --- a/default-plugins/tab-bar/Cargo.toml +++ b/default-plugins/tab-bar/Cargo.toml @@ -8,5 +8,6 @@ license = "MIT" [dependencies] colored = "2" ansi_term = "0.12" +unicode-width = "0.1.8" zellij-tile = { path = "../../zellij-tile" } zellij-tile-utils = { path = "../../zellij-tile-utils" } \ No newline at end of file diff --git a/default-plugins/tab-bar/src/line.rs b/default-plugins/tab-bar/src/line.rs index 4cdc02c4..af78b0fc 100644 --- a/default-plugins/tab-bar/src/line.rs +++ b/default-plugins/tab-bar/src/line.rs @@ -1,4 +1,5 @@ use ansi_term::ANSIStrings; +use unicode_width::UnicodeWidthStr; use crate::{LinePart, ARROW_SEPARATOR}; use zellij_tile::prelude::*; @@ -8,39 +9,82 @@ fn get_current_title_len(current_title: &[LinePart]) -> usize { current_title.iter().map(|p| p.len).sum() } +// move elements from before_active and after_active into tabs_to_render while they fit in cols +// adds collapsed_tabs to the left and right if there's left over tabs that don't fit fn populate_tabs_in_tab_line( tabs_before_active: &mut Vec, tabs_after_active: &mut Vec, tabs_to_render: &mut Vec, cols: usize, + palette: Palette, + capabilities: PluginCapabilities, ) { - let mut take_next_tab_from_tabs_after = true; + let mut middle_size = get_current_title_len(tabs_to_render); + + let mut total_left = 0; + let mut total_right = 0; loop { - if tabs_before_active.is_empty() && tabs_after_active.is_empty() { + let left_count = tabs_before_active.len(); + let right_count = tabs_after_active.len(); + let collapsed_left = left_more_message(left_count, palette, tab_separator(capabilities)); + let collapsed_right = right_more_message(right_count, palette, tab_separator(capabilities)); + + let total_size = collapsed_left.len + middle_size + collapsed_right.len; + + if total_size > cols { + // break and dont add collapsed tabs to tabs_to_render, they will not fit break; } - let current_title_len = get_current_title_len(tabs_to_render); - if current_title_len >= cols { - break; - } - let should_take_next_tab = take_next_tab_from_tabs_after; - let can_take_next_tab = !tabs_after_active.is_empty() - && tabs_after_active.get(0).unwrap().len + current_title_len <= cols; - let can_take_previous_tab = !tabs_before_active.is_empty() - && tabs_before_active.last().unwrap().len + current_title_len <= cols; - if should_take_next_tab && can_take_next_tab { - let next_tab = tabs_after_active.remove(0); - tabs_to_render.push(next_tab); - take_next_tab_from_tabs_after = false; - } else if can_take_previous_tab { - let previous_tab = tabs_before_active.pop().unwrap(); - tabs_to_render.insert(0, previous_tab); - take_next_tab_from_tabs_after = true; - } else if can_take_next_tab { - let next_tab = tabs_after_active.remove(0); - tabs_to_render.push(next_tab); - take_next_tab_from_tabs_after = false; + + let left = if let Some(tab) = tabs_before_active.last() { + tab.len } else { + usize::MAX + }; + + let right = if let Some(tab) = tabs_after_active.first() { + tab.len + } else { + usize::MAX + }; + + // total size is shortened if the next tab to be added is the last one, as that will remove the collapsed tab + let size_by_adding_left = + left.saturating_add(total_size) + .saturating_sub(if left_count == 1 { + collapsed_left.len + } else { + 0 + }); + let size_by_adding_right = + right + .saturating_add(total_size) + .saturating_sub(if right_count == 1 { + collapsed_right.len + } else { + 0 + }); + + let left_fits = size_by_adding_left <= cols; + let right_fits = size_by_adding_right <= cols; + // active tab is kept in the middle by adding to the side that + // has less width, or if the tab on the other side doesn' fit + if (total_left <= total_right || !right_fits) && left_fits { + // add left tab + let tab = tabs_before_active.pop().unwrap(); + middle_size += tab.len; + total_left += tab.len; + tabs_to_render.insert(0, tab); + } else if right_fits { + // add right tab + let tab = tabs_after_active.remove(0); + middle_size += tab.len; + total_right += tab.len; + tabs_to_render.push(tab); + } else { + // there's either no space to add more tabs or no more tabs to add, so we're done + tabs_to_render.insert(0, collapsed_left); + tabs_to_render.push(collapsed_right); break; } } @@ -56,7 +100,8 @@ fn left_more_message(tab_count_to_the_left: usize, palette: Palette, separator: " ← +many ".to_string() }; // 238 - let more_text_len = more_text.chars().count() + 2; // 2 for the arrows + // chars length plus separator length on both sides + let more_text_len = more_text.width() + 2 * separator.width(); let left_separator = style!(palette.cyan, palette.orange).paint(separator); let more_styled_text = style!(palette.black, palette.orange) .bold() @@ -85,7 +130,8 @@ fn right_more_message( } else { " +many → ".to_string() }; - let more_text_len = more_text.chars().count() + 1; // 2 for the arrow + // chars length plus separator length on both sides + let more_text_len = more_text.width() + 2 * separator.width(); let left_separator = style!(palette.cyan, palette.orange).paint(separator); let more_styled_text = style!(palette.black, palette.orange) .bold() @@ -101,48 +147,6 @@ fn right_more_message( } } -fn add_previous_tabs_msg( - tabs_before_active: &mut Vec, - tabs_to_render: &mut Vec, - title_bar: &mut Vec, - cols: usize, - palette: Palette, - separator: &str, -) { - while get_current_title_len(tabs_to_render) - + left_more_message(tabs_before_active.len(), palette, separator).len - >= cols - && !tabs_to_render.is_empty() - { - tabs_before_active.push(tabs_to_render.remove(0)); - } - - let left_more_message = left_more_message(tabs_before_active.len(), palette, separator); - if left_more_message.len <= cols { - title_bar.push(left_more_message); - } -} - -fn add_next_tabs_msg( - tabs_after_active: &mut Vec, - title_bar: &mut Vec, - cols: usize, - palette: Palette, - separator: &str, -) { - while get_current_title_len(title_bar) - + right_more_message(tabs_after_active.len(), palette, separator).len - >= cols - && !title_bar.is_empty() - { - tabs_after_active.insert(0, title_bar.pop().unwrap()); - } - let right_more_message = right_more_message(tabs_after_active.len(), palette, separator); - if right_more_message.len < cols { - title_bar.push(right_more_message); - } -} - fn tab_line_prefix(session_name: Option<&str>, palette: Palette, cols: usize) -> Vec { let prefix_text = " Zellij ".to_string(); @@ -156,7 +160,7 @@ fn tab_line_prefix(session_name: Option<&str>, palette: Palette, cols: usize) -> }]; if let Some(name) = session_name { let name_part = format!("({}) ", name); - let name_part_len = name_part.chars().count(); + let name_part_len = name_part.width(); let name_part_styled_text = style!(palette.white, palette.cyan).bold().paint(name_part); if cols.saturating_sub(prefix_text_len) >= name_part_len { parts.push(LinePart { @@ -184,7 +188,6 @@ pub fn tab_line( palette: Palette, capabilities: PluginCapabilities, ) -> Vec { - let mut tabs_to_render = Vec::new(); let mut tabs_after_active = all_tabs.split_off(active_tab_index); let mut tabs_before_active = all_tabs; let active_tab = if !tabs_after_active.is_empty() { @@ -194,38 +197,22 @@ pub fn tab_line( }; let mut prefix = tab_line_prefix(session_name, palette, cols); let prefix_len = get_current_title_len(&prefix); - if prefix_len + active_tab.len <= cols { - tabs_to_render.push(active_tab); + + // if active tab alone won't fit in cols, don't draw any tabs + if prefix_len + active_tab.len > cols { + return prefix; } + let mut tabs_to_render = vec![active_tab]; + populate_tabs_in_tab_line( &mut tabs_before_active, &mut tabs_after_active, &mut tabs_to_render, cols.saturating_sub(prefix_len), + palette, + capabilities, ); - - let mut tab_line: Vec = vec![]; - if !tabs_before_active.is_empty() { - add_previous_tabs_msg( - &mut tabs_before_active, - &mut tabs_to_render, - &mut tab_line, - cols.saturating_sub(prefix_len), - palette, - tab_separator(capabilities), - ); - } - tab_line.append(&mut tabs_to_render); - if !tabs_after_active.is_empty() { - add_next_tabs_msg( - &mut tabs_after_active, - &mut tab_line, - cols.saturating_sub(prefix_len), - palette, - tab_separator(capabilities), - ); - } - prefix.append(&mut tab_line); + prefix.append(&mut tabs_to_render); prefix } diff --git a/default-plugins/tab-bar/src/main.rs b/default-plugins/tab-bar/src/main.rs index d4111cfa..85078126 100644 --- a/default-plugins/tab-bar/src/main.rs +++ b/default-plugins/tab-bar/src/main.rs @@ -65,7 +65,7 @@ impl ZellijPlugin for State { self.mode_info.session_name.as_deref(), all_tabs, active_tab_index, - cols, + cols.saturating_sub(1), self.mode_info.palette, self.mode_info.capabilities, ); diff --git a/default-plugins/tab-bar/src/tab.rs b/default-plugins/tab-bar/src/tab.rs index 9edfba13..fdb49855 100644 --- a/default-plugins/tab-bar/src/tab.rs +++ b/default-plugins/tab-bar/src/tab.rs @@ -1,11 +1,12 @@ use crate::{line::tab_separator, LinePart}; use ansi_term::ANSIStrings; +use unicode_width::UnicodeWidthStr; use zellij_tile::prelude::*; use zellij_tile_utils::style; pub fn active_tab(text: String, palette: Palette, separator: &str) -> LinePart { let left_separator = style!(palette.cyan, palette.green).paint(separator); - let tab_text_len = text.chars().count() + 4; // 2 for left and right separators, 2 for the text padding + let tab_text_len = text.width() + 2 + separator.width() * 2; // 2 for left and right separators, 2 for the text padding let tab_styled_text = style!(palette.black, palette.green) .bold() .paint(format!(" {} ", text)); @@ -22,7 +23,7 @@ pub fn active_tab(text: String, palette: Palette, separator: &str) -> LinePart { pub fn non_active_tab(text: String, palette: Palette, separator: &str) -> LinePart { let left_separator = style!(palette.cyan, palette.fg).paint(separator); - let tab_text_len = text.chars().count() + 4; // 2 for left and right separators, 2 for the padding + let tab_text_len = text.width() + 2 + separator.width() * 2; // 2 for left and right separators, 2 for the text padding let tab_styled_text = style!(palette.black, palette.fg) .bold() .paint(format!(" {} ", text)); diff --git a/example/default.yaml b/example/default.yaml index 9b6c8904..1a72a634 100644 --- a/example/default.yaml +++ b/example/default.yaml @@ -129,7 +129,7 @@ keybinds: key: [ Char: 'j',] - action: [GoToPreviousTab,] key: [ Char: 'k',] - - action: [NewTab,] + - action: [NewTab: ,] key: [ Char: 'n',] - action: [CloseTab,] key: [ Char: 'x',] diff --git a/src/main.rs b/src/main.rs index f0d00f77..0a91ca65 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,12 +4,15 @@ mod sessions; mod tests; use crate::install::populate_data_dir; -use sessions::{assert_session, assert_session_ne, get_active_session, list_sessions}; +use sessions::{ + assert_session, assert_session_ne, get_active_session, get_sessions, list_sessions, + print_sessions, session_exists, ActiveSession, +}; use std::process; use zellij_client::{os_input_output::get_client_os_input, start_client, ClientInfo}; use zellij_server::{os_input_output::get_server_os_input, start_server}; use zellij_utils::{ - cli::{CliArgs, Command, Sessions}, + cli::{CliArgs, Command, SessionCommand, Sessions}, consts::{ZELLIJ_TMP_DIR, ZELLIJ_TMP_LOG_DIR}, logging::*, setup::{get_default_data_dir, Setup}, @@ -36,6 +39,14 @@ pub fn main() { }; start_server(Box::new(os_input), path); } else { + let (config, layout, config_options) = match Setup::from_options(&opts) { + Ok(results) => results, + Err(e) => { + eprintln!("{}", e); + process::exit(1); + } + }; + let os_input = match get_client_os_input() { Ok(os_input) => os_input, Err(e) => { @@ -44,40 +55,77 @@ pub fn main() { } }; if let Some(Command::Sessions(Sessions::Attach { - mut session_name, + session_name, force, + create, + options, })) = opts.command.clone() { - if let Some(session) = session_name.as_ref() { - assert_session(session); - } else { - session_name = Some(get_active_session()); - } + let config_options = match options { + Some(SessionCommand::Options(o)) => config_options.merge(o), + None => config_options, + }; - let (config, _, config_options) = match Setup::from_options(&opts) { - Ok(results) => results, - Err(e) => { - eprintln!("{}", e); - process::exit(1); + let (client, attach_layout) = match session_name.as_ref() { + Some(session) => { + if create { + if !session_exists(session).unwrap() { + (ClientInfo::New(session_name.unwrap()), layout) + } else { + ( + ClientInfo::Attach( + session_name.unwrap(), + force, + config_options.clone(), + ), + None, + ) + } + } else { + assert_session(session); + ( + ClientInfo::Attach( + session_name.unwrap(), + force, + config_options.clone(), + ), + None, + ) + } } + None => match get_active_session() { + ActiveSession::None => { + if create { + ( + ClientInfo::New(names::Generator::default().next().unwrap()), + layout, + ) + } else { + println!("No active zellij sessions found."); + process::exit(1); + } + } + ActiveSession::One(session_name) => ( + ClientInfo::Attach(session_name, force, config_options.clone()), + None, + ), + ActiveSession::Many => { + println!("Please specify the session name to attach to. The following sessions are active:"); + print_sessions(get_sessions().unwrap()); + process::exit(1); + } + }, }; start_client( Box::new(os_input), opts, config, - ClientInfo::Attach(session_name.unwrap(), force, config_options), - None, + config_options, + client, + attach_layout, ); } else { - let (config, layout, _) = match Setup::from_options(&opts) { - Ok(results) => results, - Err(e) => { - eprintln!("{}", e); - process::exit(1); - } - }; - let session_name = opts .session .clone() @@ -93,6 +141,7 @@ pub fn main() { Box::new(os_input), opts, config, + config_options, ClientInfo::New(session_name), layout, ); diff --git a/src/sessions.rs b/src/sessions.rs index fb1a9635..9f1e20f0 100644 --- a/src/sessions.rs +++ b/src/sessions.rs @@ -6,7 +6,7 @@ use zellij_utils::{ ipc::{ClientToServerMsg, IpcSenderWithContext}, }; -fn get_sessions() -> Result, io::ErrorKind> { +pub(crate) fn get_sessions() -> Result, io::ErrorKind> { match fs::read_dir(&*ZELLIJ_SOCK_DIR) { Ok(files) => { let mut sessions = Vec::new(); @@ -47,7 +47,7 @@ fn assert_socket(name: &str) -> bool { } } -fn print_sessions(sessions: Vec) { +pub(crate) fn print_sessions(sessions: Vec) { let curr_session = std::env::var("ZELLIJ_SESSION_NAME").unwrap_or_else(|_| "".into()); sessions.iter().for_each(|session| { let suffix = if curr_session == *session { @@ -59,22 +59,29 @@ fn print_sessions(sessions: Vec) { }) } -pub(crate) fn get_active_session() -> String { +pub(crate) enum ActiveSession { + None, + One(String), + Many, +} + +pub(crate) fn get_active_session() -> ActiveSession { match get_sessions() { Ok(mut sessions) => { if sessions.len() == 1 { - return sessions.pop().unwrap(); + return ActiveSession::One(sessions.pop().unwrap()); } if sessions.is_empty() { - println!("No active zellij sessions found."); + ActiveSession::None } else { - println!("Please specify the session name to attach to. The following sessions are active:"); - print_sessions(sessions); + ActiveSession::Many } } - Err(e) => eprintln!("Error occured: {:?}", e), + Err(e) => { + eprintln!("Error occured: {:?}", e); + process::exit(1); + } } - process::exit(1); } pub(crate) fn list_sessions() { @@ -95,15 +102,30 @@ pub(crate) fn list_sessions() { process::exit(exit_code); } -pub(crate) fn assert_session(name: &str) { - match get_sessions() { +pub(crate) fn session_exists(name: &str) -> Result { + return match get_sessions() { Ok(sessions) => { if sessions.iter().any(|s| s == name) { - return; + return Ok(true); } - println!("No session named {:?} found.", name); + Ok(false) + } + Err(e) => Err(e), + }; +} + +pub(crate) fn assert_session(name: &str) { + match session_exists(name) { + Ok(result) => { + if result { + return; + } else { + println!("No session named {:?} found.", name); + } + } + Err(e) => { + eprintln!("Error occured: {:?}", e); } - Err(e) => eprintln!("Error occured: {:?}", e), }; process::exit(1); } diff --git a/src/tests/e2e/cases.rs b/src/tests/e2e/cases.rs index dda7d96a..307986c1 100644 --- a/src/tests/e2e/cases.rs +++ b/src/tests/e2e/cases.rs @@ -157,7 +157,7 @@ pub fn cannot_split_terminals_vertically_when_active_terminal_is_too_small() { 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()); + remote_terminal.send_key("Hi!".as_bytes()); true }, }) @@ -205,26 +205,26 @@ pub fn scrolling_inside_a_pane() { 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 - remote_terminal.send_key(&format!("{:0<56}", "line1 ").as_bytes()); - remote_terminal.send_key(&format!("{:0<58}", "line2 ").as_bytes()); - remote_terminal.send_key(&format!("{:0<58}", "line3 ").as_bytes()); - remote_terminal.send_key(&format!("{:0<58}", "line4 ").as_bytes()); - remote_terminal.send_key(&format!("{:0<58}", "line5 ").as_bytes()); - remote_terminal.send_key(&format!("{:0<58}", "line6 ").as_bytes()); - remote_terminal.send_key(&format!("{:0<58}", "line7 ").as_bytes()); - remote_terminal.send_key(&format!("{:0<58}", "line8 ").as_bytes()); - remote_terminal.send_key(&format!("{:0<58}", "line9 ").as_bytes()); - remote_terminal.send_key(&format!("{:0<58}", "line10 ").as_bytes()); - remote_terminal.send_key(&format!("{:0<58}", "line11 ").as_bytes()); - remote_terminal.send_key(&format!("{:0<58}", "line12 ").as_bytes()); - remote_terminal.send_key(&format!("{:0<58}", "line13 ").as_bytes()); - remote_terminal.send_key(&format!("{:0<58}", "line14 ").as_bytes()); - remote_terminal.send_key(&format!("{:0<58}", "line15 ").as_bytes()); - remote_terminal.send_key(&format!("{:0<58}", "line16 ").as_bytes()); - remote_terminal.send_key(&format!("{:0<58}", "line17 ").as_bytes()); - remote_terminal.send_key(&format!("{:0<58}", "line18 ").as_bytes()); - remote_terminal.send_key(&format!("{:0<58}", "line19 ").as_bytes()); - remote_terminal.send_key(&format!("{:0<57}", "line20 ").as_bytes()); + remote_terminal.send_key(format!("{:0<56}", "line1 ").as_bytes()); + remote_terminal.send_key(format!("{:0<58}", "line2 ").as_bytes()); + remote_terminal.send_key(format!("{:0<58}", "line3 ").as_bytes()); + remote_terminal.send_key(format!("{:0<58}", "line4 ").as_bytes()); + remote_terminal.send_key(format!("{:0<58}", "line5 ").as_bytes()); + remote_terminal.send_key(format!("{:0<58}", "line6 ").as_bytes()); + remote_terminal.send_key(format!("{:0<58}", "line7 ").as_bytes()); + remote_terminal.send_key(format!("{:0<58}", "line8 ").as_bytes()); + remote_terminal.send_key(format!("{:0<58}", "line9 ").as_bytes()); + remote_terminal.send_key(format!("{:0<58}", "line10 ").as_bytes()); + remote_terminal.send_key(format!("{:0<58}", "line11 ").as_bytes()); + remote_terminal.send_key(format!("{:0<58}", "line12 ").as_bytes()); + remote_terminal.send_key(format!("{:0<58}", "line13 ").as_bytes()); + remote_terminal.send_key(format!("{:0<58}", "line14 ").as_bytes()); + remote_terminal.send_key(format!("{:0<58}", "line15 ").as_bytes()); + remote_terminal.send_key(format!("{:0<58}", "line16 ").as_bytes()); + remote_terminal.send_key(format!("{:0<58}", "line17 ").as_bytes()); + remote_terminal.send_key(format!("{:0<58}", "line18 ").as_bytes()); + remote_terminal.send_key(format!("{:0<58}", "line19 ").as_bytes()); + remote_terminal.send_key(format!("{:0<57}", "line20 ").as_bytes()); step_is_complete = true; } step_is_complete @@ -574,7 +574,7 @@ pub fn lock_mode() { if remote_terminal.snapshot_contains("INTERFACE LOCKED") { remote_terminal.send_key(&TAB_MODE); remote_terminal.send_key(&NEW_TAB_IN_TAB_MODE); - remote_terminal.send_key(&"abc".as_bytes()); + remote_terminal.send_key("abc".as_bytes()); step_is_complete = true; } step_is_complete @@ -675,7 +675,7 @@ pub fn detach_and_attach_session() { let mut step_is_complete = false; if remote_terminal.cursor_position_is(63, 2) && remote_terminal.tip_appears() { // new pane has been opened and focused - remote_terminal.send_key(&"I am some text".as_bytes()); + remote_terminal.send_key("I am some text".as_bytes()); step_is_complete = true; } step_is_complete @@ -825,26 +825,26 @@ pub fn scrolling_inside_a_pane_with_mouse() { 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 - remote_terminal.send_key(&format!("{:0<56}", "line1 ").as_bytes()); - remote_terminal.send_key(&format!("{:0<58}", "line2 ").as_bytes()); - remote_terminal.send_key(&format!("{:0<58}", "line3 ").as_bytes()); - remote_terminal.send_key(&format!("{:0<58}", "line4 ").as_bytes()); - remote_terminal.send_key(&format!("{:0<58}", "line5 ").as_bytes()); - remote_terminal.send_key(&format!("{:0<58}", "line6 ").as_bytes()); - remote_terminal.send_key(&format!("{:0<58}", "line7 ").as_bytes()); - remote_terminal.send_key(&format!("{:0<58}", "line8 ").as_bytes()); - remote_terminal.send_key(&format!("{:0<58}", "line9 ").as_bytes()); - remote_terminal.send_key(&format!("{:0<58}", "line10 ").as_bytes()); - remote_terminal.send_key(&format!("{:0<58}", "line11 ").as_bytes()); - remote_terminal.send_key(&format!("{:0<58}", "line12 ").as_bytes()); - remote_terminal.send_key(&format!("{:0<58}", "line13 ").as_bytes()); - remote_terminal.send_key(&format!("{:0<58}", "line14 ").as_bytes()); - remote_terminal.send_key(&format!("{:0<58}", "line15 ").as_bytes()); - remote_terminal.send_key(&format!("{:0<58}", "line16 ").as_bytes()); - remote_terminal.send_key(&format!("{:0<58}", "line17 ").as_bytes()); - remote_terminal.send_key(&format!("{:0<58}", "line18 ").as_bytes()); - remote_terminal.send_key(&format!("{:0<58}", "line19 ").as_bytes()); - remote_terminal.send_key(&format!("{:0<57}", "line20 ").as_bytes()); + remote_terminal.send_key(format!("{:0<56}", "line1 ").as_bytes()); + remote_terminal.send_key(format!("{:0<58}", "line2 ").as_bytes()); + remote_terminal.send_key(format!("{:0<58}", "line3 ").as_bytes()); + remote_terminal.send_key(format!("{:0<58}", "line4 ").as_bytes()); + remote_terminal.send_key(format!("{:0<58}", "line5 ").as_bytes()); + remote_terminal.send_key(format!("{:0<58}", "line6 ").as_bytes()); + remote_terminal.send_key(format!("{:0<58}", "line7 ").as_bytes()); + remote_terminal.send_key(format!("{:0<58}", "line8 ").as_bytes()); + remote_terminal.send_key(format!("{:0<58}", "line9 ").as_bytes()); + remote_terminal.send_key(format!("{:0<58}", "line10 ").as_bytes()); + remote_terminal.send_key(format!("{:0<58}", "line11 ").as_bytes()); + remote_terminal.send_key(format!("{:0<58}", "line12 ").as_bytes()); + remote_terminal.send_key(format!("{:0<58}", "line13 ").as_bytes()); + remote_terminal.send_key(format!("{:0<58}", "line14 ").as_bytes()); + remote_terminal.send_key(format!("{:0<58}", "line15 ").as_bytes()); + remote_terminal.send_key(format!("{:0<58}", "line16 ").as_bytes()); + remote_terminal.send_key(format!("{:0<58}", "line17 ").as_bytes()); + remote_terminal.send_key(format!("{:0<58}", "line18 ").as_bytes()); + remote_terminal.send_key(format!("{:0<58}", "line19 ").as_bytes()); + remote_terminal.send_key(format!("{:0<57}", "line20 ").as_bytes()); step_is_complete = true; } step_is_complete diff --git a/src/tests/e2e/remote_runner.rs b/src/tests/e2e/remote_runner.rs index de9c35ed..7c4587f3 100644 --- a/src/tests/e2e/remote_runner.rs +++ b/src/tests/e2e/remote_runner.rs @@ -34,9 +34,7 @@ fn setup_remote_environment(channel: &mut ssh2::Channel, win_size: Size) { .request_pty("xterm", None, Some((columns, rows, 0, 0))) .unwrap(); channel.shell().unwrap(); - channel - .write_all(format!("export PS1=\"$ \"\n").as_bytes()) - .unwrap(); + channel.write_all("export PS1=\"$ \"\n".as_bytes()).unwrap(); channel.flush().unwrap(); } @@ -154,7 +152,7 @@ impl<'a> RemoteTerminal<'a> { format!("x: {}, y: {}", self.cursor_x, self.cursor_y) } pub fn send_key(&mut self, key: &[u8]) { - self.channel.write(key).unwrap(); + self.channel.write_all(key).unwrap(); self.channel.flush().unwrap(); } pub fn change_size(&mut self, cols: u32, rows: u32) { diff --git a/test-template.yaml b/test-template.yaml new file mode 100644 index 00000000..b9c496d4 --- /dev/null +++ b/test-template.yaml @@ -0,0 +1,20 @@ +--- +template: + direction: Horizontal + parts: + - direction: Vertical + borderless: true + split_size: + Fixed: 1 + run: + plugin: tab-bar + - direction: Vertical + borderless: true + - direction: Vertical + borderless: true + - direction: Vertical + borderless: true + split_size: + Fixed: 2 + run: + plugin: status-bar diff --git a/zellij-client/Cargo.toml b/zellij-client/Cargo.toml index 271fb716..b7b61425 100644 --- a/zellij-client/Cargo.toml +++ b/zellij-client/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "zellij-client" -version = "0.17.0" +version = "0.18.0" authors = ["Kunal Mohan "] edition = "2018" description = "The client-side library for Zellij" @@ -11,7 +11,7 @@ license = "MIT" [dependencies] mio = "0.7.11" termbg = "0.2.3" -zellij-utils = { path = "../zellij-utils/", version = "0.17.0" } +zellij-utils = { path = "../zellij-utils/", version = "0.18.0" } log = "0.4.14" [dev-dependencies] diff --git a/zellij-client/src/lib.rs b/zellij-client/src/lib.rs index 7d67a01a..bd2c6376 100644 --- a/zellij-client/src/lib.rs +++ b/zellij-client/src/lib.rs @@ -86,6 +86,7 @@ pub fn start_client( mut os_input: Box, opts: CliArgs, config: Config, + config_options: Options, info: ClientInfo, layout: Option, ) { @@ -105,7 +106,6 @@ pub fn start_client( .unwrap(); std::env::set_var(&"ZELLIJ", "0"); - let config_options = Options::from_cli(&config.options, opts.command.clone()); let palette = config.themes.clone().map_or_else( || os_input.load_palette(), |t| { diff --git a/zellij-client/src/unit/input_handler_tests.rs b/zellij-client/src/unit/input_handler_tests.rs index a9b86a75..17bb1b0f 100644 --- a/zellij-client/src/unit/input_handler_tests.rs +++ b/zellij-client/src/unit/input_handler_tests.rs @@ -110,8 +110,7 @@ impl ClientOsApi for FakeClientOsApi { if stdin_events.is_empty() { panic!("ran out of stdin events!"); } - let next_event = stdin_events.remove(0); - next_event + stdin_events.remove(0) } fn box_clone(&self) -> Box { unimplemented!() @@ -174,14 +173,14 @@ pub fn quit_breaks_input_loop() { let send_client_instructions = SenderWithContext::new(send_client_instructions); let default_mode = InputMode::Normal; - drop(input_loop( + input_loop( client_os_api, config, options, command_is_executing, send_client_instructions, default_mode, - )); + ); let expected_actions_sent_to_server = vec![Action::Quit]; let received_actions = extract_actions_sent_to_server(events_sent_to_server); assert_eq!( @@ -192,8 +191,7 @@ pub fn quit_breaks_input_loop() { #[test] pub fn move_focus_left_in_pane_mode() { - let mut stdin_events = vec![]; - stdin_events.push(commands::MOVE_FOCUS_LEFT_IN_NORMAL_MODE.to_vec()); + let stdin_events = vec![commands::MOVE_FOCUS_LEFT_IN_NORMAL_MODE.to_vec()]; let events_sent_to_server = Arc::new(Mutex::new(vec![])); let command_is_executing = CommandIsExecuting::new(); let client_os_api = Box::new(FakeClientOsApi::new( @@ -210,14 +208,14 @@ pub fn move_focus_left_in_pane_mode() { let send_client_instructions = SenderWithContext::new(send_client_instructions); let default_mode = InputMode::Normal; - drop(input_loop( + input_loop( client_os_api, config, options, command_is_executing, send_client_instructions, default_mode, - )); + ); let expected_actions_sent_to_server = vec![Action::MoveFocusOrTab(Direction::Left), Action::Quit]; let received_actions = extract_actions_sent_to_server(events_sent_to_server); @@ -250,14 +248,14 @@ pub fn bracketed_paste() { let send_client_instructions = SenderWithContext::new(send_client_instructions); let default_mode = InputMode::Normal; - drop(input_loop( + input_loop( client_os_api, config, options, command_is_executing, send_client_instructions, default_mode, - )); + ); let expected_actions_sent_to_server = vec![ Action::Write(commands::BRACKETED_PASTE_START.to_vec()), Action::Write(commands::MOVE_FOCUS_LEFT_IN_NORMAL_MODE.to_vec()), // keys were directly written to server and not interpreted diff --git a/zellij-server/Cargo.toml b/zellij-server/Cargo.toml index fe56b742..bc0abe31 100644 --- a/zellij-server/Cargo.toml +++ b/zellij-server/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "zellij-server" -version = "0.17.0" +version = "0.18.0" authors = ["Kunal Mohan "] edition = "2018" description = "The server-side library for Zellij" @@ -12,17 +12,21 @@ license = "MIT" ansi_term = "0.12.1" async-trait = "0.1.50" base64 = "0.13.0" +byteorder = "1.4.3" daemonize = "0.4.1" serde_json = "1.0" unicode-width = "0.1.8" wasmer = "1.0.0" wasmer-wasi = "1.0.0" cassowary = "0.3.0" -zellij-utils = { path = "../zellij-utils/", version = "0.17.0" } +zellij-utils = { path = "../zellij-utils/", version = "0.18.0" } log = "0.4.14" typetag = "0.1.7" chrono = "0.4.19" +[target.'cfg(target_os = "macos")'.dependencies] +darwin-libproc = "0.2.0" + [dev-dependencies] insta = "1.6.0" diff --git a/zellij-server/src/lib.rs b/zellij-server/src/lib.rs index 9073ed3a..419aae4f 100644 --- a/zellij-server/src/lib.rs +++ b/zellij-server/src/lib.rs @@ -170,19 +170,13 @@ pub fn start_server(os_input: Box, socket_path: PathBuf) { thread_handles.lock().unwrap().push( thread::Builder::new() .name("server_router".to_string()) - .spawn({ - let session_data = session_data.clone(); - let os_input = os_input.clone(); - let to_server = to_server.clone(); - - move || { - route_thread_main( - session_data, - session_state, - os_input, - to_server, - ) - } + .spawn(move || { + route_thread_main( + session_data, + session_state, + os_input, + to_server, + ) }) .unwrap(), ); @@ -267,7 +261,6 @@ pub fn start_server(os_input: Box, socket_path: PathBuf) { } } ServerInstruction::ClientExit => { - *session_data.write().unwrap() = None; os_input.send_to_client(ServerToClientMsg::Exit(ExitReason::Normal)); break; } @@ -297,6 +290,10 @@ pub fn start_server(os_input: Box, socket_path: PathBuf) { } } } + + // Drop cached session data before exit. + *session_data.write().unwrap() = None; + thread_handles .lock() .unwrap() diff --git a/zellij-server/src/logging_pipe.rs b/zellij-server/src/logging_pipe.rs index f1fae6b8..1edb2954 100644 --- a/zellij-server/src/logging_pipe.rs +++ b/zellij-server/src/logging_pipe.rs @@ -149,7 +149,7 @@ mod logging_pipe_test { let test_buffer = "Testing write".as_bytes(); - pipe.write(test_buffer).expect("Err write"); + pipe.write_all(test_buffer).expect("Err write"); pipe.flush().expect("Err flush"); assert_eq!(pipe.buffer.len(), test_buffer.len()); @@ -161,7 +161,7 @@ mod logging_pipe_test { let test_buffer = "Testing write \n".as_bytes(); - pipe.write(test_buffer).expect("Err write"); + pipe.write_all(test_buffer).expect("Err write"); pipe.flush().expect("Err flush"); assert_eq!(pipe.buffer.len(), 0); @@ -174,7 +174,7 @@ mod logging_pipe_test { let test_buffer = "Testing write \n".as_bytes(); let test_buffer2 = "And the rest".as_bytes(); - pipe.write( + pipe.write_all( [ test_buffer, test_buffer, @@ -197,7 +197,7 @@ mod logging_pipe_test { let test_buffer = "Testing write \n".as_bytes(); - pipe.write( + pipe.write_all( [ test_buffer, test_buffer, @@ -223,7 +223,7 @@ mod logging_pipe_test { // make sure it's not valid utf-8 string if we drop last symbol assert!(std::str::from_utf8(&test_buffer[..test_buffer.len() - 1]).is_err()); - pipe.write(&test_buffer[..test_buffer.len() - 1]) + pipe.write_all(&test_buffer[..test_buffer.len() - 1]) .expect("Err write"); pipe.flush().expect("Err flush"); @@ -237,7 +237,7 @@ mod logging_pipe_test { let mut pipe = LoggingPipe::new("TestPipe", 0); let test_buffer = "Testing write \n".as_bytes(); - pipe.write( + pipe.write_all( [test_buffer, test_buffer, b"\n", b"\n", b"\n"] .concat() .as_slice(), diff --git a/zellij-server/src/os_input_output.rs b/zellij-server/src/os_input_output.rs index 05980805..e24b24b4 100644 --- a/zellij-server/src/os_input_output.rs +++ b/zellij-server/src/os_input_output.rs @@ -1,4 +1,8 @@ +#[cfg(target_os = "macos")] +use darwin_libproc; + use std::env; +use std::fs; use std::os::unix::io::RawFd; use std::os::unix::process::CommandExt; use std::path::PathBuf; @@ -10,7 +14,7 @@ use zellij_utils::{async_std, interprocess, libc, nix, signal_hook, zellij_tile} use async_std::fs::File as AsyncFile; use async_std::os::unix::io::FromRawFd; use interprocess::local_socket::LocalSocketStream; -use nix::pty::{forkpty, Winsize}; +use nix::pty::{forkpty, ForkptyResult, Winsize}; use nix::sys::signal::{kill, Signal}; use nix::sys::termios; use nix::sys::wait::waitpid; @@ -29,6 +33,7 @@ use zellij_utils::{ use async_std::io::ReadExt; pub use async_trait::async_trait; +use byteorder::{BigEndian, ByteOrder}; pub use nix::unistd::Pid; @@ -92,44 +97,94 @@ fn handle_command_exit(mut child: Child) { } } +fn handle_fork_pty( + fork_pty_res: ForkptyResult, + cmd: RunCommand, + parent_fd: RawFd, + child_fd: RawFd, +) -> (RawFd, ChildId) { + let pid_primary = fork_pty_res.master; + let (pid_secondary, pid_shell) = match fork_pty_res.fork_result { + ForkResult::Parent { child } => { + let pid_shell = read_from_pipe(parent_fd, child_fd); + (child, pid_shell) + } + ForkResult::Child => { + let child = unsafe { + let command = &mut Command::new(cmd.command); + if let Some(current_dir) = cmd.cwd { + command.current_dir(current_dir); + } + command + .args(&cmd.args) + .pre_exec(|| -> std::io::Result<()> { + // this is the "unsafe" part, for more details please see: + // https://doc.rust-lang.org/std/os/unix/process/trait.CommandExt.html#notes-and-safety + unistd::setpgid(Pid::from_raw(0), Pid::from_raw(0)) + .expect("failed to create a new process group"); + Ok(()) + }) + .spawn() + .expect("failed to spawn") + }; + unistd::tcsetpgrp(0, Pid::from_raw(child.id() as i32)) + .expect("faled to set child's forceground process group"); + write_to_pipe(child.id(), parent_fd, child_fd); + handle_command_exit(child); + ::std::process::exit(0); + } + }; + + ( + pid_primary, + ChildId { + primary: pid_secondary, + shell: pid_shell.map(|pid| Pid::from_raw(pid as i32)), + }, + ) +} + /// Spawns a new terminal from the parent terminal with [`termios`](termios::Termios) /// `orig_termios`. /// -fn handle_terminal(cmd: RunCommand, orig_termios: termios::Termios) -> (RawFd, Pid) { - let (pid_primary, pid_secondary): (RawFd, Pid) = { - match forkpty(None, Some(&orig_termios)) { - Ok(fork_pty_res) => { - let pid_primary = fork_pty_res.master; - let pid_secondary = match fork_pty_res.fork_result { - ForkResult::Parent { child } => child, - ForkResult::Child => { - let child = unsafe { - Command::new(cmd.command) - .args(&cmd.args) - .pre_exec(|| -> std::io::Result<()> { - // this is the "unsafe" part, for more details please see: - // https://doc.rust-lang.org/std/os/unix/process/trait.CommandExt.html#notes-and-safety - unistd::setpgid(Pid::from_raw(0), Pid::from_raw(0)) - .expect("failed to create a new process group"); - Ok(()) - }) - .spawn() - .expect("failed to spawn") - }; - unistd::tcsetpgrp(0, Pid::from_raw(child.id() as i32)) - .expect("faled to set child's forceground process group"); - handle_command_exit(child); - ::std::process::exit(0); - } - }; - (pid_primary, pid_secondary) - } - Err(e) => { - panic!("failed to fork {:?}", e); - } +fn handle_terminal(cmd: RunCommand, orig_termios: termios::Termios) -> (RawFd, ChildId) { + // Create a pipe to allow the child the communicate the shell's pid to it's + // parent. + let (parent_fd, child_fd) = unistd::pipe().expect("failed to create pipe"); + match forkpty(None, Some(&orig_termios)) { + Ok(fork_pty_res) => handle_fork_pty(fork_pty_res, cmd, parent_fd, child_fd), + Err(e) => { + panic!("failed to fork {:?}", e); } - }; - (pid_primary, pid_secondary) + } +} + +/// Write to a pipe given both file descriptors +fn write_to_pipe(data: u32, parent_fd: RawFd, child_fd: RawFd) { + let mut buff = [0; 4]; + BigEndian::write_u32(&mut buff, data); + if unistd::close(parent_fd).is_err() { + return; + } + if unistd::write(child_fd, &buff).is_err() { + return; + } + unistd::close(child_fd).unwrap_or_default(); +} + +/// Read from a pipe given both file descriptors +fn read_from_pipe(parent_fd: RawFd, child_fd: RawFd) -> Option { + let mut buffer = [0; 4]; + if unistd::close(child_fd).is_err() { + return None; + } + if unistd::read(parent_fd, &mut buffer).is_err() { + return None; + } + if unistd::close(parent_fd).is_err() { + return None; + } + Some(u32::from_be_bytes(buffer)) } /// If a [`TerminalAction::OpenFile(file)`] is given, the text editor specified by environment variable `EDITOR` @@ -145,11 +200,11 @@ fn handle_terminal(cmd: RunCommand, orig_termios: termios::Termios) -> (RawFd, P /// This function will panic if both the `EDITOR` and `VISUAL` environment variables are not /// set. pub fn spawn_terminal( - terminal_action: Option, + terminal_action: TerminalAction, orig_termios: termios::Termios, -) -> (RawFd, Pid) { +) -> (RawFd, ChildId) { let cmd = match terminal_action { - Some(TerminalAction::OpenFile(file_to_open)) => { + TerminalAction::OpenFile(file_to_open) => { if env::var("EDITOR").is_err() && env::var("VISUAL").is_err() { panic!("Can't edit files if an editor is not defined. To fix: define the EDITOR or VISUAL environment variables with the path to your editor (eg. /usr/bin/vim)"); } @@ -160,15 +215,13 @@ pub fn spawn_terminal( .into_os_string() .into_string() .expect("Not valid Utf8 Encoding")]; - RunCommand { command, args } - } - Some(TerminalAction::RunCommand(command)) => command, - None => { - let command = - PathBuf::from(env::var("SHELL").expect("Could not find the SHELL variable")); - let args = vec![]; - RunCommand { command, args } + RunCommand { + command, + args, + cwd: None, + } } + TerminalAction::RunCommand(command) => command, }; handle_terminal(cmd, orig_termios) @@ -214,8 +267,10 @@ impl AsyncReader for RawFdAsyncReader { pub trait ServerOsApi: Send + Sync { /// Sets the size of the terminal associated to file descriptor `fd`. fn set_terminal_size_using_fd(&self, fd: RawFd, cols: u16, rows: u16); - /// Spawn a new terminal, with a terminal action. - fn spawn_terminal(&self, terminal_action: Option) -> (RawFd, Pid); + /// Spawn a new terminal, with a terminal action. The returned tuple contains the master file + /// descriptor of the forked psuedo terminal and a [ChildId] struct containing process id's for + /// the forked child process. + fn spawn_terminal(&self, terminal_action: TerminalAction) -> (RawFd, ChildId); /// Read bytes from the standard output of the virtual terminal referred to by `fd`. fn read_from_tty_stdout(&self, fd: RawFd, buf: &mut [u8]) -> Result; /// Creates an `AsyncReader` that can be used to read from `fd` in an async context @@ -247,6 +302,8 @@ pub trait ServerOsApi: Send + Sync { /// Update the receiver socket for the client fn update_receiver(&mut self, stream: LocalSocketStream); fn load_palette(&self) -> Palette; + /// Returns the current working directory for a given pid + fn get_cwd(&self, pid: Pid) -> Option; } impl ServerOsApi for ServerOsInputOutput { @@ -255,7 +312,7 @@ impl ServerOsApi for ServerOsInputOutput { set_terminal_size_using_fd(fd, cols, rows); } } - fn spawn_terminal(&self, terminal_action: Option) -> (RawFd, Pid) { + fn spawn_terminal(&self, terminal_action: TerminalAction) -> (RawFd, ChildId) { let orig_termios = self.orig_termios.lock().unwrap(); spawn_terminal(terminal_action, orig_termios.clone()) } @@ -336,6 +393,18 @@ impl ServerOsApi for ServerOsInputOutput { fn load_palette(&self) -> Palette { default_palette() } + #[cfg(target_os = "macos")] + fn get_cwd(&self, pid: Pid) -> Option { + darwin_libproc::pid_cwd(pid.as_raw()).ok() + } + #[cfg(target_os = "linux")] + fn get_cwd(&self, pid: Pid) -> Option { + fs::read_link(format!("/proc/{}/cwd", pid)).ok() + } + #[cfg(all(not(target_os = "linux"), not(target_os = "macos")))] + fn get_cwd(&self, _pid: Pid) -> Option { + None + } } impl Clone for Box { @@ -353,3 +422,13 @@ pub fn get_server_os_input() -> Result { send_instructions_to_client: Arc::new(Mutex::new(None)), }) } + +/// Process id's for forked terminals +#[derive(Debug)] +pub struct ChildId { + /// Primary process id of a forked terminal + pub primary: Pid, + /// Process id of the command running inside the forked terminal, usually a shell. The primary + /// field is it's parent process id. + pub shell: Option, +} diff --git a/zellij-server/src/panes/grid.rs b/zellij-server/src/panes/grid.rs index 5e3479ee..2d2354c6 100644 --- a/zellij-server/src/panes/grid.rs +++ b/zellij-server/src/panes/grid.rs @@ -527,8 +527,8 @@ impl Grid { for (i, line) in self.viewport.iter().enumerate() { if line.is_canonical { canonical_lines_traversed += 1; + y_coordinates = i; if canonical_lines_traversed == canonical_line_index + 1 { - y_coordinates = i; break; } } @@ -628,6 +628,23 @@ impl Grid { } } } + + // trim lines after the last empty space that has no following character, because + // terminals don't trim empty lines + for line in viewport_canonical_lines.iter_mut() { + let mut trim_at = None; + for (index, character) in line.columns.iter().enumerate() { + if character.character != EMPTY_TERMINAL_CHARACTER.character { + trim_at = None; + } else if trim_at.is_none() { + trim_at = Some(index); + } + } + if let Some(trim_at) = trim_at { + line.columns.truncate(trim_at); + } + } + let mut new_viewport_rows = vec![]; for mut canonical_line in viewport_canonical_lines { let mut canonical_line_parts: Vec = vec![]; @@ -658,9 +675,11 @@ impl Grid { } new_viewport_rows.append(&mut canonical_line_parts); } + self.viewport = new_viewport_rows; let mut new_cursor_y = self.canonical_line_y_coordinates(cursor_canonical_line_index); + let new_cursor_x = (cursor_index_in_canonical_line / new_columns) + (cursor_index_in_canonical_line % new_columns); let current_viewport_row_count = self.viewport.len(); diff --git a/zellij-server/src/panes/terminal_pane.rs b/zellij-server/src/panes/terminal_pane.rs index 092ec15e..ae867237 100644 --- a/zellij-server/src/panes/terminal_pane.rs +++ b/zellij-server/src/panes/terminal_pane.rs @@ -253,7 +253,9 @@ impl Pane for TerminalPane { color: self.frame_color, }; if &frame != last_frame { - vte_output.push_str(&frame.render()); + if !self.borderless { + vte_output.push_str(&frame.render()); + } self.frame = Some(frame); } } diff --git a/zellij-server/src/panes/unit/grid_tests.rs b/zellij-server/src/panes/unit/grid_tests.rs index b10565fe..34236f7e 100644 --- a/zellij-server/src/panes/unit/grid_tests.rs +++ b/zellij-server/src/panes/unit/grid_tests.rs @@ -8,9 +8,8 @@ fn read_fixture(fixture_name: &str) -> Vec { path_to_file.push("tests"); path_to_file.push("fixtures"); path_to_file.push(fixture_name); - let content = std::fs::read(path_to_file) - .unwrap_or_else(|_| panic!("could not read fixture {:?}", &fixture_name)); - content + std::fs::read(path_to_file) + .unwrap_or_else(|_| panic!("could not read fixture {:?}", &fixture_name)) } #[test] diff --git a/zellij-server/src/panes/unit/snapshots/zellij_server__panes__grid__grid_tests__clear_scroll_region.snap b/zellij-server/src/panes/unit/snapshots/zellij_server__panes__grid__grid_tests__clear_scroll_region.snap index b53826df..e5c302a0 100644 --- a/zellij-server/src/panes/unit/snapshots/zellij_server__panes__grid__grid_tests__clear_scroll_region.snap +++ b/zellij-server/src/panes/unit/snapshots/zellij_server__panes__grid__grid_tests__clear_scroll_region.snap @@ -3,7 +3,7 @@ source: zellij-server/src/panes/./unit/grid_tests.rs expression: "format!(\"{:?}\", grid)" --- -00 (C): Welcome to fish, the friendly interactive shell +00 (C): Welcome to fish, the friendly interactive shell 01 (C): ⋊> ~/c/mosaic on main ⨯ vim some-file 15:07:22 02 (C): ⋊> ~/c/mosaic on main ⨯ 15:07:29 diff --git a/zellij-server/src/panes/unit/snapshots/zellij_server__panes__grid__grid_tests__emacs_longbuf.snap b/zellij-server/src/panes/unit/snapshots/zellij_server__panes__grid__grid_tests__emacs_longbuf.snap index 5c08c323..71cc9bc8 100644 --- a/zellij-server/src/panes/unit/snapshots/zellij_server__panes__grid__grid_tests__emacs_longbuf.snap +++ b/zellij-server/src/panes/unit/snapshots/zellij_server__panes__grid__grid_tests__emacs_longbuf.snap @@ -3,8 +3,8 @@ source: zellij-server/src/panes/./unit/grid_tests.rs expression: "format!(\"{:?}\", grid)" --- -00 (C): ➜ mosaic git:(mosaic#130) emacs -01 (C): ➜ mosaic git:(mosaic#130) emacs -nw +00 (C): ➜ mosaic git:(mosaic#130) emacs +01 (C): ➜ mosaic git:(mosaic#130) emacs -nw 02 (C): ➜ mosaic git:(mosaic#130) exit 03 (C): diff --git a/zellij-server/src/pty.rs b/zellij-server/src/pty.rs index e28729b7..e37e6062 100644 --- a/zellij-server/src/pty.rs +++ b/zellij-server/src/pty.rs @@ -1,5 +1,5 @@ use crate::{ - os_input_output::{AsyncReader, Pid, ServerOsApi}, + os_input_output::{AsyncReader, ChildId, ServerOsApi}, panes::PaneId, screen::ScreenInstruction, thread_bus::{Bus, ThreadSenders}, @@ -12,14 +12,16 @@ use async_std::{ }; use std::{ collections::HashMap, + env, os::unix::io::RawFd, + path::PathBuf, time::{Duration, Instant}, }; use zellij_utils::{ async_std, errors::{get_current_ctx, ContextType, PtyContext}, input::{ - command::TerminalAction, + command::{RunCommand, TerminalAction}, layout::{Layout, LayoutFromYaml, Run, TabLayout}, }, logging::debug_to_file, @@ -33,6 +35,7 @@ pub(crate) enum PtyInstruction { SpawnTerminal(Option), SpawnTerminalVertically(Option), SpawnTerminalHorizontally(Option), + UpdateActivePane(Option), NewTab(Option, Option), ClosePane(PaneId), CloseTab(Vec), @@ -45,6 +48,7 @@ impl From<&PtyInstruction> for PtyContext { PtyInstruction::SpawnTerminal(_) => PtyContext::SpawnTerminal, PtyInstruction::SpawnTerminalVertically(_) => PtyContext::SpawnTerminalVertically, PtyInstruction::SpawnTerminalHorizontally(_) => PtyContext::SpawnTerminalHorizontally, + PtyInstruction::UpdateActivePane(_) => PtyContext::UpdateActivePane, PtyInstruction::ClosePane(_) => PtyContext::ClosePane, PtyInstruction::CloseTab(_) => PtyContext::CloseTab, PtyInstruction::NewTab(..) => PtyContext::NewTab, @@ -54,8 +58,9 @@ impl From<&PtyInstruction> for PtyContext { } pub(crate) struct Pty { + pub active_pane: Option, pub bus: Bus, - pub id_to_child_pid: HashMap, + pub id_to_child_pid: HashMap, debug_to_file: bool, task_handles: HashMap>, } @@ -86,9 +91,32 @@ pub(crate) fn pty_thread_main(mut pty: Pty, layout: LayoutFromYaml) { .send_to_screen(ScreenInstruction::HorizontalSplit(PaneId::Terminal(pid))) .unwrap(); } + PtyInstruction::UpdateActivePane(pane_id) => { + pty.set_active_pane(pane_id); + } PtyInstruction::NewTab(terminal_action, tab_layout) => { + let tab_name = tab_layout.as_ref().and_then(|layout| { + if layout.name.is_empty() { + None + } else { + Some(layout.name.clone()) + } + }); + let merged_layout = layout.template.clone().insert_tab_layout(tab_layout); pty.spawn_terminals_for_layout(merged_layout.into(), terminal_action.clone()); + + if let Some(tab_name) = tab_name { + // clear current name at first + pty.bus + .senders + .send_to_screen(ScreenInstruction::UpdateTabName(vec![0])) + .unwrap(); + pty.bus + .senders + .send_to_screen(ScreenInstruction::UpdateTabName(tab_name.into_bytes())) + .unwrap(); + } } PtyInstruction::ClosePane(id) => { pty.close_pane(id); @@ -208,14 +236,30 @@ fn stream_terminal_bytes( impl Pty { pub fn new(bus: Bus, debug_to_file: bool) -> Self { Pty { + active_pane: None, bus, id_to_child_pid: HashMap::new(), debug_to_file, task_handles: HashMap::new(), } } + pub fn get_default_terminal(&self) -> TerminalAction { + TerminalAction::RunCommand(RunCommand { + args: vec![], + command: PathBuf::from(env::var("SHELL").expect("Could not find the SHELL variable")), + cwd: self + .active_pane + .and_then(|pane| match pane { + PaneId::Plugin(..) => None, + PaneId::Terminal(id) => self.id_to_child_pid.get(&id).and_then(|id| id.shell), + }) + .and_then(|id| self.bus.os_input.as_ref().map(|input| input.get_cwd(id))) + .flatten(), + }) + } pub fn spawn_terminal(&mut self, terminal_action: Option) -> RawFd { - let (pid_primary, pid_secondary): (RawFd, Pid) = self + let terminal_action = terminal_action.unwrap_or_else(|| self.get_default_terminal()); + let (pid_primary, child_id): (RawFd, ChildId) = self .bus .os_input .as_mut() @@ -228,7 +272,7 @@ impl Pty { self.debug_to_file, ); self.task_handles.insert(pid_primary, task_handle); - self.id_to_child_pid.insert(pid_primary, pid_secondary); + self.id_to_child_pid.insert(pid_primary, child_id); pid_primary } pub fn spawn_terminals_for_layout( @@ -236,29 +280,26 @@ impl Pty { layout: Layout, default_shell: Option, ) { + let default_shell = default_shell.unwrap_or_else(|| self.get_default_terminal()); let extracted_run_instructions = layout.extract_run_instructions(); let mut new_pane_pids = vec![]; for run_instruction in extracted_run_instructions { match run_instruction { Some(Run::Command(command)) => { let cmd = TerminalAction::RunCommand(command); - let (pid_primary, pid_secondary): (RawFd, Pid) = self - .bus - .os_input - .as_mut() - .unwrap() - .spawn_terminal(Some(cmd)); - self.id_to_child_pid.insert(pid_primary, pid_secondary); + let (pid_primary, child_id): (RawFd, ChildId) = + self.bus.os_input.as_mut().unwrap().spawn_terminal(cmd); + self.id_to_child_pid.insert(pid_primary, child_id); new_pane_pids.push(pid_primary); } None => { - let (pid_primary, pid_secondary): (RawFd, Pid) = self + let (pid_primary, child_id): (RawFd, ChildId) = self .bus .os_input .as_mut() .unwrap() .spawn_terminal(default_shell.clone()); - self.id_to_child_pid.insert(pid_primary, pid_secondary); + self.id_to_child_pid.insert(pid_primary, child_id); new_pane_pids.push(pid_primary); } // Investigate moving plugin loading to here. @@ -285,10 +326,15 @@ impl Pty { pub fn close_pane(&mut self, id: PaneId) { match id { PaneId::Terminal(id) => { - let child_pid = self.id_to_child_pid.remove(&id).unwrap(); + let pids = self.id_to_child_pid.remove(&id).unwrap(); let handle = self.task_handles.remove(&id).unwrap(); task::block_on(async { - self.bus.os_input.as_mut().unwrap().kill(child_pid).unwrap(); + self.bus + .os_input + .as_mut() + .unwrap() + .kill(pids.primary) + .unwrap(); let timeout = Duration::from_millis(100); match async_timeout(timeout, handle.cancel()).await { Ok(_) => {} @@ -297,7 +343,7 @@ impl Pty { .os_input .as_mut() .unwrap() - .force_kill(child_pid) + .force_kill(pids.primary) .unwrap(); } }; @@ -315,6 +361,9 @@ impl Pty { self.close_pane(id); }); } + pub fn set_active_pane(&mut self, pane_id: Option) { + self.active_pane = pane_id; + } } impl Drop for Pty { diff --git a/zellij-server/src/screen.rs b/zellij-server/src/screen.rs index 432b15fc..5e1addb5 100644 --- a/zellij-server/src/screen.rs +++ b/zellij-server/src/screen.rs @@ -188,22 +188,37 @@ impl Screen { } } + /// A helper function to switch to a new tab at specified position. + fn switch_active_tab(&mut self, new_tab_pos: usize) { + if let Some(new_tab) = self.tabs.values().find(|t| t.position == new_tab_pos) { + let current_tab = self.get_active_tab().unwrap(); + + // If new active tab is same as the current one, do nothing. + if current_tab.position == new_tab_pos { + return; + } + + current_tab.visible(false); + let new_tab_index = new_tab.index; + let new_tab = self.get_indexed_tab_mut(new_tab_index).unwrap(); + new_tab.set_force_render(); + new_tab.visible(true); + + let old_active_index = self.active_tab_index.replace(new_tab_index); + self.tab_history.retain(|&e| e != Some(new_tab_pos)); + self.tab_history.push(old_active_index); + + self.update_tabs(); + self.render(); + } + } + /// Sets this [`Screen`]'s active [`Tab`] to the next tab. pub fn switch_tab_next(&mut self) { let active_tab_pos = self.get_active_tab().unwrap().position; let new_tab_pos = (active_tab_pos + 1) % self.tabs.len(); - for tab in self.tabs.values_mut() { - if tab.position == new_tab_pos { - tab.set_force_render(); - self.tab_history.retain(|&e| e != Some(tab.index)); - self.tab_history.push(self.active_tab_index); - self.active_tab_index = Some(tab.index); - break; - } - } - self.update_tabs(); - self.render(); + self.switch_active_tab(new_tab_pos); } /// Sets this [`Screen`]'s active [`Tab`] to the previous tab. @@ -214,32 +229,12 @@ impl Screen { } else { active_tab_pos - 1 }; - for tab in self.tabs.values_mut() { - if tab.position == new_tab_pos { - tab.set_force_render(); - self.tab_history.retain(|&e| e != Some(tab.index)); - self.tab_history.push(self.active_tab_index); - self.active_tab_index = Some(tab.index); - break; - } - } - self.update_tabs(); - self.render(); + + self.switch_active_tab(new_tab_pos); } - pub fn go_to_tab(&mut self, mut tab_index: usize) { - tab_index -= 1; - let active_tab_index = self.get_active_tab().unwrap().index; - if let Some(t) = self.tabs.values_mut().find(|t| t.position == tab_index) { - if t.index != active_tab_index { - t.set_force_render(); - self.tab_history.retain(|&e| e != Some(t.index)); - self.tab_history.push(self.active_tab_index); - self.active_tab_index = Some(t.index); - self.update_tabs(); - self.render(); - } - } + pub fn go_to_tab(&mut self, tab_index: usize) { + self.switch_active_tab(tab_index - 1); } /// Closes this [`Screen`]'s active [`Tab`], exiting the application if it happens @@ -264,10 +259,14 @@ impl Screen { .unwrap(); } } else { + if let Some(tab) = self.get_active_tab() { + tab.visible(false); + } self.active_tab_index = self.tab_history.pop().unwrap(); for t in self.tabs.values_mut() { - if t.position == self.active_tab_index.unwrap() { - t.set_force_render() + if t.index == self.active_tab_index.unwrap() { + t.set_force_render(); + t.visible(true); } if t.position > active_tab.position { t.position -= 1; @@ -357,8 +356,12 @@ impl Screen { self.draw_pane_frames, ); tab.apply_layout(layout, new_pids, tab_index); - self.tab_history.push(self.active_tab_index); - self.active_tab_index = Some(tab_index); + if let Some(active_tab) = self.get_active_tab() { + active_tab.visible(false); + } + self.tab_history + .push(self.active_tab_index.replace(tab_index)); + tab.visible(true); self.tabs.insert(tab_index, tab); self.update_tabs(); } diff --git a/zellij-server/src/tab.rs b/zellij-server/src/tab.rs index 16e196a1..62e0a2cb 100644 --- a/zellij-server/src/tab.rs +++ b/zellij-server/src/tab.rs @@ -268,7 +268,7 @@ impl Tab { let panes = BTreeMap::new(); let name = if name.is_empty() { - format!("Tab #{}", position + 1) + format!("Tab #{}", index + 1) } else { name }; @@ -325,10 +325,15 @@ impl Tab { if let Some(Run::Plugin(Some(plugin))) = &layout.run { let (pid_tx, pid_rx) = channel(); self.senders - .send_to_plugin(PluginInstruction::Load(pid_tx, plugin.clone(), tab_index)) + .send_to_plugin(PluginInstruction::Load( + pid_tx, + plugin.path.clone(), + tab_index, + plugin._allow_exec_host_cmd, + )) .unwrap(); let pid = pid_rx.recv().unwrap(); - let title = String::from(plugin.as_path().as_os_str().to_string_lossy()); + let title = String::from(plugin.path.as_path().as_os_str().to_string_lossy()); let mut new_plugin = PluginPane::new( pid, *position_and_size, @@ -684,22 +689,35 @@ impl Tab { } pub fn set_pane_frames(&mut self, draw_pane_frames: bool) { self.draw_pane_frames = draw_pane_frames; + self.should_clear_display_before_rendering = true; + let viewport = self.viewport; for (pane_id, pane) in self.panes.iter_mut() { - pane.set_frame(draw_pane_frames); - if draw_pane_frames { + if !pane.borderless() { + pane.set_frame(draw_pane_frames); + } + + #[allow(clippy::if_same_then_else)] + if draw_pane_frames & !pane.borderless() { + // there's definitely a frame around this pane, offset its contents pane.set_content_offset(Offset::frame(1)); + } else if draw_pane_frames && pane.borderless() { + // there's no frame around this pane, and the tab isn't handling the boundaries + // between panes (they each draw their own frames as they please) + // this one doesn't - do not offset its content + pane.set_content_offset(Offset::default()); + } else if !is_inside_viewport(&viewport, pane) { + // this pane is outside the viewport and has no border - it should not have an offset + pane.set_content_offset(Offset::default()); } else { + // no draw_pane_frames and this pane should have a separation to other panes + // according to its position in the viewport (eg. no separation if its at the + // viewport bottom) - offset its content accordingly let position_and_size = pane.current_geom(); let (pane_columns_offset, pane_rows_offset) = pane_content_offset(&position_and_size, &self.viewport); pane.set_content_offset(Offset::shift(pane_rows_offset, pane_columns_offset)); } - // FIXME: this should also override the above logic - if pane.borderless() { - pane.set_content_offset(Offset::default()); - } - // FIXME: This, and all other `set_terminal_size_using_fd` calls, would be best in // `TerminalPane::reflow_lines` if let PaneId::Terminal(pid) = pane_id { @@ -720,6 +738,9 @@ impl Tab { // or if this session is not attached to a client, we do not have to render return; } + self.senders + .send_to_pty(PtyInstruction::UpdateActivePane(self.active_terminal)) + .unwrap(); let mut output = String::new(); let mut boundaries = Boundaries::new(self.viewport); let hide_cursor = "\u{1b}[?25l"; @@ -1739,16 +1760,16 @@ impl Tab { } let active_terminal_id = self.get_active_pane_id().unwrap(); let terminal_ids: Vec = self.get_selectable_panes().map(|(&pid, _)| pid).collect(); // TODO: better, no allocations - let first_terminal = terminal_ids.get(0).unwrap(); let active_terminal_id_position = terminal_ids .iter() .position(|id| id == &active_terminal_id) .unwrap(); - if let Some(next_terminal) = terminal_ids.get(active_terminal_id_position + 1) { - self.active_terminal = Some(*next_terminal); - } else { - self.active_terminal = Some(*first_terminal); - } + let active_terminal = terminal_ids + .get(active_terminal_id_position + 1) + .or_else(|| terminal_ids.get(0)) + .copied(); + + self.active_terminal = active_terminal; self.render(); } pub fn focus_next_pane(&mut self) { @@ -1767,16 +1788,17 @@ impl Tab { a_pane.y().cmp(&b_pane.y()) } }); - let first_pane = panes.get(0).unwrap(); let active_pane_position = panes .iter() .position(|(id, _)| *id == &active_pane_id) // TODO: better .unwrap(); - if let Some(next_pane) = panes.get(active_pane_position + 1) { - self.active_terminal = Some(*next_pane.0); - } else { - self.active_terminal = Some(*first_pane.0); - } + + let active_terminal = panes + .get(active_pane_position + 1) + .or_else(|| panes.get(0)) + .map(|p| *p.0); + + self.active_terminal = active_terminal; self.render(); } pub fn focus_previous_pane(&mut self) { @@ -1800,11 +1822,13 @@ impl Tab { .iter() .position(|(id, _)| *id == &active_pane_id) // TODO: better .unwrap(); - if active_pane_position == 0 { - self.active_terminal = Some(*last_pane.0); + + let active_terminal = if active_pane_position == 0 { + Some(*last_pane.0) } else { - self.active_terminal = Some(*panes.get(active_pane_position - 1).unwrap().0); - } + Some(*panes.get(active_pane_position - 1).unwrap().0) + }; + self.active_terminal = active_terminal; self.render(); } // returns a boolean that indicates whether the focus moved @@ -1816,7 +1840,7 @@ impl Tab { return false; } let active_terminal = self.get_active_pane(); - if let Some(active) = active_terminal { + let updated_active_terminal = if let Some(active) = active_terminal { let terminals = self.get_selectable_panes(); let next_index = terminals .enumerate() @@ -1839,13 +1863,12 @@ impl Tab { self.render(); return true; } - None => { - self.active_terminal = Some(active.pid()); - } + None => Some(active.pid()), } } else { - self.active_terminal = Some(active_terminal.unwrap().pid()); - } + Some(active_terminal.unwrap().pid()) + }; + self.active_terminal = updated_active_terminal; false } pub fn move_focus_down(&mut self) { @@ -1856,7 +1879,7 @@ impl Tab { return; } let active_terminal = self.get_active_pane(); - if let Some(active) = active_terminal { + let updated_active_terminal = if let Some(active) = active_terminal { let terminals = self.get_selectable_panes(); let next_index = terminals .enumerate() @@ -1875,15 +1898,14 @@ impl Tab { let next_active_pane = self.panes.get_mut(&p).unwrap(); next_active_pane.set_should_render(true); - self.active_terminal = Some(p); - } - None => { - self.active_terminal = Some(active.pid()); + Some(p) } + None => Some(active.pid()), } } else { - self.active_terminal = Some(active_terminal.unwrap().pid()); - } + Some(active_terminal.unwrap().pid()) + }; + self.active_terminal = updated_active_terminal; self.render(); } pub fn move_focus_up(&mut self) { @@ -1894,7 +1916,7 @@ impl Tab { return; } let active_terminal = self.get_active_pane(); - if let Some(active) = active_terminal { + let updated_active_terminal = if let Some(active) = active_terminal { let terminals = self.get_selectable_panes(); let next_index = terminals .enumerate() @@ -1913,15 +1935,14 @@ impl Tab { let next_active_pane = self.panes.get_mut(&p).unwrap(); next_active_pane.set_should_render(true); - self.active_terminal = Some(p); - } - None => { - self.active_terminal = Some(active.pid()); + Some(p) } + None => Some(active.pid()), } } else { - self.active_terminal = Some(active_terminal.unwrap().pid()); - } + Some(active_terminal.unwrap().pid()) + }; + self.active_terminal = updated_active_terminal; self.render(); } // returns a boolean that indicates whether the focus moved @@ -1933,7 +1954,7 @@ impl Tab { return false; } let active_terminal = self.get_active_pane(); - if let Some(active) = active_terminal { + let updated_active_terminal = if let Some(active) = active_terminal { let terminals = self.get_selectable_panes(); let next_index = terminals .enumerate() @@ -1956,13 +1977,12 @@ impl Tab { self.render(); return true; } - None => { - self.active_terminal = Some(active.pid()); - } + None => Some(active.pid()), } } else { - self.active_terminal = Some(active_terminal.unwrap().pid()); - } + Some(active_terminal.unwrap().pid()) + }; + self.active_terminal = updated_active_terminal; false } fn horizontal_borders(&self, terminals: &[PaneId]) -> HashSet { @@ -1981,6 +2001,7 @@ impl Tab { borders }) } + fn panes_to_the_left_between_aligning_borders(&self, id: PaneId) -> Option> { if let Some(terminal) = self.panes.get(&id) { let upper_close_border = terminal.y(); @@ -2107,7 +2128,7 @@ impl Tab { if let Some(pane) = self.panes.get_mut(&id) { pane.set_selectable(selectable); if self.get_active_pane_id() == Some(id) && !selectable { - self.active_terminal = self.next_active_pane(&self.get_pane_ids()) + self.active_terminal = self.next_active_pane(&self.get_pane_ids()); } } self.render(); @@ -2345,10 +2366,9 @@ impl Tab { .unwrap(); } fn is_inside_viewport(&self, pane_id: &PaneId) -> bool { - let pane_position_and_size = self.panes.get(pane_id).unwrap().current_geom(); - pane_position_and_size.y >= self.viewport.y - && pane_position_and_size.y + pane_position_and_size.rows.as_usize() - <= self.viewport.y + self.viewport.rows + // this is mostly separated to an outside function in order to allow us to pass a clone to + // it sometimes when we need to get around the borrow checker + is_inside_viewport(&self.viewport, self.panes.get(pane_id).unwrap()) } fn offset_viewport(&mut self, position_and_size: &Viewport) { if position_and_size.x == self.viewport.x @@ -2376,6 +2396,29 @@ impl Tab { } } } + + pub fn visible(&self, visible: bool) { + let pids_in_this_tab = self.panes.keys().filter_map(|p| match p { + PaneId::Plugin(pid) => Some(pid), + _ => None, + }); + for pid in pids_in_this_tab { + self.senders + .send_to_plugin(PluginInstruction::Update( + Some(*pid), + Event::Visible(visible), + )) + .unwrap(); + } + } +} + +#[allow(clippy::borrowed_box)] +fn is_inside_viewport(viewport: &Viewport, pane: &Box) -> bool { + let pane_position_and_size = pane.current_geom(); + pane_position_and_size.y >= viewport.y + && pane_position_and_size.y + pane_position_and_size.rows.as_usize() + <= viewport.y + viewport.rows } #[cfg(test)] diff --git a/zellij-server/src/ui/pane_boundaries_frame.rs b/zellij-server/src/ui/pane_boundaries_frame.rs index d3cd6d5a..a283a6b3 100644 --- a/zellij-server/src/ui/pane_boundaries_frame.rs +++ b/zellij-server/src/ui/pane_boundaries_frame.rs @@ -4,6 +4,8 @@ use ansi_term::Style; use zellij_utils::pane_size::Viewport; use zellij_utils::zellij_tile::prelude::PaletteColor; +use unicode_width::{UnicodeWidthChar, UnicodeWidthStr}; + fn color_string(character: &str, color: Option) -> String { match color { Some(color) => match color { @@ -33,11 +35,11 @@ impl PaneFrame { let full_indication = format!(" {}/{} ", self.scroll_position.0, self.scroll_position.1); let short_indication = format!(" {} ", self.scroll_position.0); - if prefix.chars().count() + full_indication.chars().count() <= max_length { + if prefix.width() + full_indication.width() <= max_length { Some(format!("{}{}", prefix, full_indication)) - } else if full_indication.chars().count() <= max_length { + } else if full_indication.width() <= max_length { Some(full_indication) - } else if short_indication.chars().count() <= max_length { + } else if short_indication.width() <= max_length { Some(short_indication) } else { None @@ -50,30 +52,43 @@ impl PaneFrame { let middle_truncated_sign = "[..]"; let middle_truncated_sign_long = "[...]"; let full_text = format!(" {} ", &self.title); - if max_length <= 6 { + if max_length <= 6 || self.title.is_empty() { None - } else if full_text.chars().count() <= max_length { + } else if full_text.width() <= max_length { Some(full_text) } else { - let length_of_each_half = (max_length - middle_truncated_sign.chars().count()) / 2; - let first_part: String = full_text.chars().take(length_of_each_half).collect(); - let second_part: String = full_text - .chars() - .skip(full_text.chars().count() - length_of_each_half) - .collect(); - let title_left_side = if first_part.chars().count() - + middle_truncated_sign.chars().count() - + second_part.chars().count() - < max_length - { - // this means we lost 1 character when dividing the total length into halves - format!( - "{}{}{}", - first_part, middle_truncated_sign_long, second_part - ) - } else { - format!("{}{}{}", first_part, middle_truncated_sign, second_part) - }; + let length_of_each_half = (max_length - middle_truncated_sign.width()) / 2; + + let mut first_part: String = String::with_capacity(length_of_each_half); + for char in full_text.chars() { + if first_part.width() + char.width().unwrap_or(0) > length_of_each_half { + break; + } else { + first_part.push(char); + } + } + + let mut second_part: String = String::with_capacity(length_of_each_half); + for char in full_text.chars().rev() { + if second_part.width() + char.width().unwrap_or(0) > length_of_each_half { + break; + } else { + second_part.insert(0, char); + } + } + + let title_left_side = + if first_part.width() + middle_truncated_sign.width() + second_part.width() + < max_length + { + // this means we lost 1 character when dividing the total length into halves + format!( + "{}{}{}", + first_part, middle_truncated_sign_long, second_part + ) + } else { + format!("{}{}{}", first_part, middle_truncated_sign, second_part) + }; Some(title_left_side) } } @@ -83,15 +98,13 @@ impl PaneFrame { let right_boundary = boundary_type::TOP_RIGHT; let left_side = self.render_title_left_side(total_title_length); let right_side = left_side.as_ref().and_then(|left_side| { - let space_left = total_title_length.saturating_sub(left_side.chars().count() + 1); // 1 for a middle separator + let space_left = total_title_length.saturating_sub(left_side.width() + 1); // 1 for a middle separator self.render_title_right_side(space_left) }); let title_text = match (left_side, right_side) { (Some(left_side), Some(right_side)) => { let mut middle = String::new(); - for _ in - (left_side.chars().count() + right_side.chars().count())..total_title_length - { + for _ in (left_side.width() + right_side.width())..total_title_length { middle.push_str(boundary_type::HORIZONTAL); } format!( @@ -101,7 +114,7 @@ impl PaneFrame { } (Some(left_side), None) => { let mut middle_padding = String::new(); - for _ in left_side.chars().count()..total_title_length { + for _ in left_side.width()..total_title_length { middle_padding.push_str(boundary_type::HORIZONTAL); } format!( diff --git a/zellij-server/src/unit/screen_tests.rs b/zellij-server/src/unit/screen_tests.rs index 2d080886..aeea671b 100644 --- a/zellij-server/src/unit/screen_tests.rs +++ b/zellij-server/src/unit/screen_tests.rs @@ -1,10 +1,11 @@ use super::{Screen, ScreenInstruction}; use crate::zellij_tile::data::{ModeInfo, Palette}; use crate::{ - os_input_output::{AsyncReader, Pid, ServerOsApi}, + os_input_output::{AsyncReader, ChildId, Pid, ServerOsApi}, thread_bus::Bus, SessionState, }; +use std::path::PathBuf; use std::sync::{Arc, RwLock}; use zellij_utils::input::command::TerminalAction; use zellij_utils::input::layout::LayoutTemplate; @@ -28,7 +29,7 @@ impl ServerOsApi for FakeInputOutput { fn set_terminal_size_using_fd(&self, _fd: RawFd, _cols: u16, _rows: u16) { // noop } - fn spawn_terminal(&self, _file_to_open: Option) -> (RawFd, Pid) { + fn spawn_terminal(&self, _file_to_open: TerminalAction) -> (RawFd, ChildId) { unimplemented!() } fn read_from_tty_stdout(&self, _fd: RawFd, _buf: &mut [u8]) -> Result { @@ -73,14 +74,19 @@ impl ServerOsApi for FakeInputOutput { fn load_palette(&self) -> Palette { unimplemented!() } + fn get_cwd(&self, _pid: Pid) -> Option { + unimplemented!() + } } fn create_new_screen(size: Size) -> Screen { let mut bus: Bus = Bus::empty(); let fake_os_input = FakeInputOutput {}; bus.os_input = Some(Box::new(fake_os_input)); - let mut client_attributes = ClientAttributes::default(); - client_attributes.size = size; + let client_attributes = ClientAttributes { + size, + ..Default::default() + }; let max_panes = None; let mode_info = ModeInfo::default(); let session_state = Arc::new(RwLock::new(SessionState::Attached)); diff --git a/zellij-server/src/unit/tab_tests.rs b/zellij-server/src/unit/tab_tests.rs index ea96df62..de5c1067 100644 --- a/zellij-server/src/unit/tab_tests.rs +++ b/zellij-server/src/unit/tab_tests.rs @@ -1,11 +1,12 @@ use super::Tab; use crate::zellij_tile::data::{ModeInfo, Palette}; use crate::{ - os_input_output::{AsyncReader, Pid, ServerOsApi}, + os_input_output::{AsyncReader, ChildId, Pid, ServerOsApi}, panes::PaneId, thread_bus::ThreadSenders, SessionState, }; +use std::path::PathBuf; use std::sync::{Arc, RwLock}; use zellij_utils::input::layout::LayoutTemplate; use zellij_utils::pane_size::Size; @@ -27,7 +28,7 @@ impl ServerOsApi for FakeInputOutput { fn set_terminal_size_using_fd(&self, _fd: RawFd, _cols: u16, _rows: u16) { // noop } - fn spawn_terminal(&self, _file_to_open: Option) -> (RawFd, Pid) { + fn spawn_terminal(&self, _file_to_open: TerminalAction) -> (RawFd, ChildId) { unimplemented!() } fn read_from_tty_stdout(&self, _fd: RawFd, _buf: &mut [u8]) -> Result { @@ -72,6 +73,9 @@ impl ServerOsApi for FakeInputOutput { fn load_palette(&self) -> Palette { unimplemented!() } + fn get_cwd(&self, _pid: Pid) -> Option { + unimplemented!() + } } fn create_new_tab(size: Size) -> Tab { diff --git a/zellij-server/src/wasm_vm.rs b/zellij-server/src/wasm_vm.rs index e8d9344e..7475bf46 100644 --- a/zellij-server/src/wasm_vm.rs +++ b/zellij-server/src/wasm_vm.rs @@ -1,4 +1,4 @@ -use log::info; +use log::{info, warn}; use std::collections::{HashMap, HashSet}; use std::fs; use std::path::PathBuf; @@ -28,8 +28,8 @@ use zellij_utils::{input::command::TerminalAction, serde, zellij_tile}; #[derive(Clone, Debug)] pub(crate) enum PluginInstruction { - Load(Sender, PathBuf, usize), // tx_pid, path_of_plugin , tab_index - Update(Option, Event), // Focused plugin / broadcast, event data + Load(Sender, PathBuf, usize, bool), // tx_pid, path_of_plugin , tab_index, allow_exec_host_cmd + Update(Option, Event), // Focused plugin / broadcast, event data Render(Sender, u32, usize, usize), // String buffer, plugin id, rows, cols Unload(u32), Exit, @@ -54,6 +54,9 @@ pub(crate) struct PluginEnv { 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 -------------------------------------------------------------------------------------------------------- @@ -61,12 +64,15 @@ pub(crate) fn wasm_thread_main(bus: Bus, store: Store, data_d 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(); + 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) => { - let plugin_dir = data_dir.join("plugins/"); + 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"))) @@ -81,15 +87,15 @@ pub(crate) fn wasm_thread_main(bus: Bus, store: Store, data_d path.as_path().file_name().unwrap().to_str().unwrap(), plugin_id, ); + + 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") - .preopen(|p| { - p.directory(".") // FIXME: Change this to a more meaningful dir - .alias(".") - .read(true) - .write(true) - .create(true) - }) + .map_dir("/host", ".") + .unwrap() + .map_dir("/data", plugin_own_data_dir.as_path()) .unwrap() .stdin(Box::new(input)) .stdout(Box::new(output)) @@ -99,12 +105,18 @@ pub(crate) fn wasm_thread_main(bus: Bus, store: Store, data_d 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 { plugin_id, 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); @@ -147,10 +159,16 @@ pub(crate) fn wasm_thread_main(bus: Bus, store: Store, data_d buf_tx.send(wasi_read_string(&plugin_env.wasi_env)).unwrap(); } } - PluginInstruction::Unload(pid) => drop(plugin_map.remove(&pid)), + PluginInstruction::Unload(pid) => { + info!("Bye from plugin {}", &pid); + // TODO: remove plugin's own data directory + drop(plugin_map.remove(&pid)); + } PluginInstruction::Exit => break, } } + info!("wasm main thread exits"); + fs::remove_dir_all(plugin_global_data_dir.as_path()).unwrap(); } // Plugin API --------------------------------------------------------------------------------------------------------- @@ -174,6 +192,7 @@ pub(crate) fn zellij_exports(store: &Store, plugin_env: &PluginEnv) -> ImportObj host_get_plugin_ids, host_open_file, host_set_timeout, + host_exec_cmd, } } @@ -248,6 +267,24 @@ fn host_set_timeout(plugin_env: &PluginEnv, secs: f64) { }); } +fn host_exec_cmd(plugin_env: &PluginEnv) { + let mut cmdline: Vec = wasi_read_object(&plugin_env.wasi_env); + let command = cmdline.remove(0); + + // Bail out if we're forbidden to run command + if !plugin_env._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; + } + + // Here, we don't wait the command to finish + process::Command::new(command) + .args(cmdline) + .spawn() + .unwrap(); +} + // Helper Functions --------------------------------------------------------------------------------------------------- pub fn wasi_read_string(wasi_env: &WasiEnv) -> String { diff --git a/zellij-tile-utils/Cargo.toml b/zellij-tile-utils/Cargo.toml index 79c9763f..6da76ce8 100644 --- a/zellij-tile-utils/Cargo.toml +++ b/zellij-tile-utils/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "zellij-tile-utils" -version = "0.17.0" +version = "0.18.0" authors = ["denis "] edition = "2018" description = "A utility library for Zellij plugins" diff --git a/zellij-tile/Cargo.toml b/zellij-tile/Cargo.toml index 0ee63e7f..2399e17e 100644 --- a/zellij-tile/Cargo.toml +++ b/zellij-tile/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "zellij-tile" -version = "0.17.0" +version = "0.18.0" authors = ["Brooks J Rady "] edition = "2018" description = "A small client-side library for writing Zellij plugins" diff --git a/zellij-tile/src/data.rs b/zellij-tile/src/data.rs index b22be626..44708627 100644 --- a/zellij-tile/src/data.rs +++ b/zellij-tile/src/data.rs @@ -35,6 +35,7 @@ pub enum Event { Timer(f64), CopyToClipboard, InputReceived, + Visible(bool), } /// Describes the different input modes, which change the way that keystrokes will be interpreted. diff --git a/zellij-tile/src/shim.rs b/zellij-tile/src/shim.rs index b514b55a..bdd6f4fb 100644 --- a/zellij-tile/src/shim.rs +++ b/zellij-tile/src/shim.rs @@ -37,6 +37,10 @@ pub fn open_file(path: &Path) { pub fn set_timeout(secs: f64) { unsafe { host_set_timeout(secs) }; } +pub fn exec_cmd(cmd: &[&str]) { + object_to_stdout(&cmd); + unsafe { host_exec_cmd() }; +} // Internal Functions @@ -60,4 +64,5 @@ extern "C" { fn host_get_plugin_ids(); fn host_open_file(); fn host_set_timeout(secs: f64); + fn host_exec_cmd(); } diff --git a/zellij-utils/Cargo.toml b/zellij-utils/Cargo.toml index 6e4b72cc..66e8c1cf 100644 --- a/zellij-utils/Cargo.toml +++ b/zellij-utils/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "zellij-utils" -version = "0.17.0" +version = "0.18.0" authors = ["Kunal Mohan "] edition = "2018" description = "A utility library for Zellij client and server" @@ -27,9 +27,10 @@ structopt = "0.3" strum = "0.20.0" termion = "1.5.0" vte = "0.10.1" -zellij-tile = { path = "../zellij-tile/", version = "0.17.0" } +zellij-tile = { path = "../zellij-tile/", version = "0.18.0" } log = "0.4.14" log4rs = "1.0.0" +unicode-width = "0.1.8" [dependencies.async-std] version = "1.3.0" diff --git a/zellij-utils/assets/config/default.yaml b/zellij-utils/assets/config/default.yaml index ef9711b4..60b4069e 100644 --- a/zellij-utils/assets/config/default.yaml +++ b/zellij-utils/assets/config/default.yaml @@ -182,6 +182,8 @@ keybinds: key: [Ctrl: 'p',] - action: [SwitchToMode: Session,] key: [Ctrl: 'o',] + - action: [SwitchToMode: Resize,] + key: [Ctrl: 'n',] - action: [ScrollToBottom, SwitchToMode: Normal,] key: [Ctrl: 'c',] - action: [Quit,] diff --git a/zellij-utils/assets/layouts/default.yaml b/zellij-utils/assets/layouts/default.yaml index e0a28da1..549dea24 100644 --- a/zellij-utils/assets/layouts/default.yaml +++ b/zellij-utils/assets/layouts/default.yaml @@ -7,7 +7,8 @@ template: split_size: Fixed: 1 run: - plugin: tab-bar + plugin: + path: tab-bar - direction: Vertical body: true - direction: Vertical @@ -15,6 +16,7 @@ template: split_size: Fixed: 2 run: - plugin: status-bar + plugin: + path: 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 a58ef4cf..e97bb8f1 100644 --- a/zellij-utils/assets/layouts/disable-status-bar.yaml +++ b/zellij-utils/assets/layouts/disable-status-bar.yaml @@ -7,6 +7,7 @@ template: split_size: Fixed: 1 run: - plugin: tab-bar + plugin: + path: tab-bar - direction: Vertical body: true diff --git a/zellij-utils/assets/layouts/strider.yaml b/zellij-utils/assets/layouts/strider.yaml index 96e3c290..ccb2a574 100644 --- a/zellij-utils/assets/layouts/strider.yaml +++ b/zellij-utils/assets/layouts/strider.yaml @@ -7,7 +7,8 @@ template: split_size: Fixed: 1 run: - plugin: tab-bar + plugin: + path: tab-bar - direction: Vertical body: true - direction: Vertical @@ -15,7 +16,8 @@ template: split_size: Fixed: 2 run: - plugin: status-bar + plugin: + path: status-bar tabs: - direction: Vertical parts: @@ -23,5 +25,6 @@ tabs: split_size: Percent: 20 run: - plugin: strider + plugin: + path: strider - direction: Horizontal diff --git a/zellij-utils/src/cli.rs b/zellij-utils/src/cli.rs index 2ac0e986..eb52589a 100644 --- a/zellij-utils/src/cli.rs +++ b/zellij-utils/src/cli.rs @@ -62,6 +62,13 @@ pub enum Command { Sessions(Sessions), } +#[derive(Debug, StructOpt, Clone, Serialize, Deserialize)] +pub enum SessionCommand { + /// Change the behaviour of zellij + #[structopt(name = "options")] + Options(Options), +} + #[derive(Debug, StructOpt, Clone, Serialize, Deserialize)] pub enum Sessions { /// List active sessions @@ -78,5 +85,13 @@ pub enum Sessions { /// 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, + + /// Change the behaviour of zellij + #[structopt(subcommand, name = "options")] + options: Option, }, } diff --git a/zellij-utils/src/errors.rs b/zellij-utils/src/errors.rs index 65a54c56..87c3bb4d 100644 --- a/zellij-utils/src/errors.rs +++ b/zellij-utils/src/errors.rs @@ -237,6 +237,7 @@ pub enum PtyContext { SpawnTerminal, SpawnTerminalVertically, SpawnTerminalHorizontally, + UpdateActivePane, NewTab, ClosePane, CloseTab, diff --git a/zellij-utils/src/input/command.rs b/zellij-utils/src/input/command.rs index 2054f208..debd5bc5 100644 --- a/zellij-utils/src/input/command.rs +++ b/zellij-utils/src/input/command.rs @@ -15,6 +15,8 @@ pub struct RunCommand { pub command: PathBuf, #[serde(default)] pub args: Vec, + #[serde(default)] + pub cwd: Option, } /// Intermediate representation @@ -25,6 +27,8 @@ pub struct RunCommandAction { #[serde(default)] pub args: Vec, #[serde(default)] + pub cwd: Option, + #[serde(default)] pub direction: Option, } @@ -33,6 +37,7 @@ impl From for RunCommand { RunCommand { command: action.command, args: action.args, + cwd: action.cwd, } } } diff --git a/zellij-utils/src/input/config.rs b/zellij-utils/src/input/config.rs index 930fe07a..0339d4dc 100644 --- a/zellij-utils/src/input/config.rs +++ b/zellij-utils/src/input/config.rs @@ -99,16 +99,21 @@ impl TryFrom<&CliArgs> for Config { impl Config { /// Uses defaults, but lets config override them. pub fn from_yaml(yaml_config: &str) -> ConfigResult { - let config_from_yaml: ConfigFromYaml = serde_yaml::from_str(yaml_config)?; - let keybinds = Keybinds::get_default_keybinds_with_config(config_from_yaml.keybinds); - let options = Options::from_yaml(config_from_yaml.options); - let themes = config_from_yaml.themes; + let config_from_yaml: Option = serde_yaml::from_str(yaml_config)?; - Ok(Config { - keybinds, - options, - themes, - }) + match config_from_yaml { + None => Ok(Config::default()), + Some(config) => { + let keybinds = Keybinds::get_default_keybinds_with_config(config.keybinds); + let options = Options::from_yaml(config.options); + let themes = config.themes; + Ok(Config { + keybinds, + options, + themes, + }) + } + } } /// Deserializes from given path. @@ -275,8 +280,10 @@ mod config_test { #[test] fn try_from_cli_args_with_config() { let arbitrary_config = PathBuf::from("nonexistent.yaml"); - let mut opts = CliArgs::default(); - opts.config = Some(arbitrary_config); + let opts = CliArgs { + config: Some(arbitrary_config), + ..Default::default() + }; println!("OPTS= {:?}", opts); let result = Config::try_from(&opts); assert!(result.is_err()); @@ -285,11 +292,13 @@ mod config_test { #[test] fn try_from_cli_args_with_option_clean() { use crate::setup::Setup; - let mut opts = CliArgs::default(); - opts.command = Some(Command::Setup(Setup { - clean: true, - ..Setup::default() - })); + let opts = CliArgs { + command: Some(Command::Setup(Setup { + clean: true, + ..Setup::default() + })), + ..Default::default() + }; let result = Config::try_from(&opts); assert!(result.is_ok()); } diff --git a/zellij-utils/src/input/layout.rs b/zellij-utils/src/input/layout.rs index be71130c..98fdb0d1 100644 --- a/zellij-utils/src/input/layout.rs +++ b/zellij-utils/src/input/layout.rs @@ -53,11 +53,19 @@ pub enum SplitSize { #[serde(crate = "self::serde")] pub enum Run { #[serde(rename = "plugin")] - Plugin(Option), + Plugin(Option), #[serde(rename = "command")] Command(RunCommand), } +#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq, Eq)] +#[serde(crate = "self::serde")] +pub struct RunPlugin { + pub path: PathBuf, + #[serde(default)] + pub _allow_exec_host_cmd: bool, +} + // The layout struct ultimately used to build the layouts. #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] #[serde(crate = "self::serde")] @@ -95,9 +103,12 @@ impl LayoutFromYaml { let mut layout = String::new(); layout_file.read_to_string(&mut layout)?; - let layout: LayoutFromYaml = serde_yaml::from_str(&layout)?; + let layout: Option = serde_yaml::from_str(&layout)?; - Ok(layout) + match layout { + Some(layout) => Ok(layout), + None => Ok(LayoutFromYaml::default()), + } } // It wants to use Path here, but that doesn't compile. @@ -216,6 +227,8 @@ pub struct TabLayout { pub parts: Vec, pub split_size: Option, pub run: Option, + #[serde(default)] + pub name: String, } impl Layout { @@ -419,6 +432,7 @@ impl Default for TabLayout { parts: vec![], split_size: None, run: None, + name: String::new(), } } } 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 88046395..ae54a0c9 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 @@ -6,14 +6,16 @@ template: split_size: Fixed: 1 run: - plugin: tab-bar + plugin: + path: tab-bar - direction: Horizontal body: true - direction: Vertical split_size: Fixed: 2 run: - plugin: status-bar + plugin: + path: status-bar tabs: - direction: Vertical diff --git a/zellij-utils/src/input/unit/keybinds_test.rs b/zellij-utils/src/input/unit/keybinds_test.rs index e8a776d7..6800bdf5 100644 --- a/zellij-utils/src/input/unit/keybinds_test.rs +++ b/zellij-utils/src/input/unit/keybinds_test.rs @@ -92,7 +92,7 @@ fn merge_keybinds_overwrites_same_keys() { let mut keybinds_self = Keybinds::new(); keybinds_self .0 - .insert(InputMode::Normal, mode_keybinds_self.clone()); + .insert(InputMode::Normal, mode_keybinds_self); let mut keybinds_other = Keybinds::new(); keybinds_other .0 @@ -152,7 +152,7 @@ fn no_unbind_unbinds_none() { fn last_keybind_is_taken() { let actions_1 = vec![Action::NoOp, Action::NewTab(None)]; let keyaction_1 = KeyActionFromYaml { - action: actions_1.clone(), + action: actions_1, key: vec![Key::F(1), Key::Backspace, Key::Char('t')], }; let actions_2 = vec![Action::GoToTab(1)]; @@ -184,7 +184,7 @@ fn last_keybind_overwrites() { let mut expected = ModeKeybinds::new(); expected.0.insert(Key::F(1), actions_2.clone()); - expected.0.insert(Key::Backspace, actions_1.clone()); + expected.0.insert(Key::Backspace, actions_1); expected.0.insert(Key::Char('t'), actions_2); assert_eq!(expected, ModeKeybinds::from(vec![keyaction_1, keyaction_2])); diff --git a/zellij-utils/src/input/unit/layout_test.rs b/zellij-utils/src/input/unit/layout_test.rs index abce56a4..1b696b0a 100644 --- a/zellij-utils/src/input/unit/layout_test.rs +++ b/zellij-utils/src/input/unit/layout_test.rs @@ -45,7 +45,10 @@ fn default_layout_merged_correctly() { borderless: true, parts: vec![], split_size: Some(SplitSize::Fixed(1)), - run: Some(Run::Plugin(Some("tab-bar".into()))), + run: Some(Run::Plugin(Some(RunPlugin { + path: "tab-bar".into(), + ..Default::default() + }))), }, Layout { direction: Direction::Vertical, @@ -59,7 +62,10 @@ fn default_layout_merged_correctly() { borderless: true, parts: vec![], split_size: Some(SplitSize::Fixed(2)), - run: Some(Run::Plugin(Some("status-bar".into()))), + run: Some(Run::Plugin(Some(RunPlugin { + path: "status-bar".into(), + ..Default::default() + }))), }, ], split_size: None, @@ -83,7 +89,10 @@ fn default_layout_new_tab_correct() { borderless: true, parts: vec![], split_size: Some(SplitSize::Fixed(1)), - run: Some(Run::Plugin(Some("tab-bar".into()))), + run: Some(Run::Plugin(Some(RunPlugin { + path: "tab-bar".into(), + ..Default::default() + }))), }, Layout { direction: Direction::Horizontal, @@ -97,7 +106,10 @@ fn default_layout_new_tab_correct() { borderless: true, parts: vec![], split_size: Some(SplitSize::Fixed(2)), - run: Some(Run::Plugin(Some("status-bar".into()))), + run: Some(Run::Plugin(Some(RunPlugin { + path: "status-bar".into(), + ..Default::default() + }))), }, ], split_size: None, @@ -253,7 +265,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("tab-bar".into()))), + run: Some(Run::Plugin(Some(RunPlugin { + path: "tab-bar".into(), + ..Default::default() + }))), }, Layout { direction: Direction::Vertical, @@ -297,7 +312,10 @@ 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("status-bar".into()))), + run: Some(Run::Plugin(Some(RunPlugin { + path: "status-bar".into(), + ..Default::default() + }))), }, ], split_size: None, @@ -321,7 +339,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("tab-bar".into()))), + run: Some(Run::Plugin(Some(RunPlugin { + path: "tab-bar".into(), + ..Default::default() + }))), }, Layout { direction: Direction::Horizontal, @@ -335,7 +356,10 @@ 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("status-bar".into()))), + run: Some(Run::Plugin(Some(RunPlugin { + path: "status-bar".into(), + ..Default::default() + }))), }, ], split_size: None, diff --git a/zellij-utils/src/shared.rs b/zellij-utils/src/shared.rs index 4b3ba974..06d1378d 100644 --- a/zellij-utils/src/shared.rs +++ b/zellij-utils/src/shared.rs @@ -7,6 +7,7 @@ use std::os::unix::fs::PermissionsExt; use std::path::Path; use std::{fs, io}; use strip_ansi_escapes::strip; +use unicode_width::UnicodeWidthStr; use zellij_tile::data::{Palette, PaletteColor, PaletteSource, ThemeHue}; const UNIX_PERMISSIONS: u32 = 0o700; @@ -18,10 +19,7 @@ pub fn set_permissions(path: &Path) -> io::Result<()> { } pub fn ansi_len(s: &str) -> usize { - from_utf8(&strip(s.as_bytes()).unwrap()) - .unwrap() - .chars() - .count() + from_utf8(&strip(s.as_bytes()).unwrap()).unwrap().width() } pub fn adjust_to_size(s: &str, rows: usize, columns: usize) -> String {