Merge branch 'main' of github.com:zellij-org/zellij

This commit is contained in:
Brooks J Rady 2021-09-19 16:25:52 +01:00
commit 1a1c10a226
54 changed files with 986 additions and 511 deletions

View file

@ -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/) The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
## [Unreleased] ## [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 ## [0.16.0] - 2021-08-31
* Plugins don't crash zellij anymore on receiving mouse events (https://github.com/zellij-org/zellij/pull/620) * Plugins don't crash zellij anymore on receiving mouse events (https://github.com/zellij-org/zellij/pull/620)

71
Cargo.lock generated
View file

@ -4,11 +4,11 @@ version = 3
[[package]] [[package]]
name = "addr2line" name = "addr2line"
version = "0.16.0" version = "0.15.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e61f2b7f93d2c7d2b08263acaa4a363b3e276806c68af6134c44f523bf1aacd" checksum = "e7a2e47a1fbe209ee101dd6d61285226744c6c8d3c21c8dc878ba6cb9f467f3a"
dependencies = [ dependencies = [
"gimli 0.25.0", "gimli 0.24.0",
] ]
[[package]] [[package]]
@ -19,9 +19,9 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]] [[package]]
name = "aho-corasick" name = "aho-corasick"
version = "0.7.18" version = "0.7.15"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" checksum = "7404febffaa47dac81aa44dba71523c9d069b1bdc50a77db41195149e17f68e5"
dependencies = [ dependencies = [
"memchr", "memchr",
] ]
@ -227,16 +227,16 @@ checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
[[package]] [[package]]
name = "backtrace" name = "backtrace"
version = "0.3.61" version = "0.3.59"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7a905d892734eea339e896738c14b9afce22b5318f64b951e70bf3844419b01" checksum = "4717cfcbfaa661a0fd48f8453951837ae7e8f81e481fbb136e3202d72805a744"
dependencies = [ dependencies = [
"addr2line", "addr2line",
"cc", "cc",
"cfg-if 1.0.0", "cfg-if 1.0.0",
"libc", "libc",
"miniz_oxide", "miniz_oxide",
"object 0.26.0", "object 0.24.0",
"rustc-demangle", "rustc-demangle",
] ]
@ -621,6 +621,26 @@ dependencies = [
"syn", "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]] [[package]]
name = "derivative" name = "derivative"
version = "2.2.0" version = "2.2.0"
@ -897,9 +917,9 @@ dependencies = [
[[package]] [[package]]
name = "gimli" name = "gimli"
version = "0.25.0" version = "0.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0a01e0497841a3b2db4f8afa483cce65f7e96a3498bd6c541734792aeac8fe7" checksum = "0e4075386626662786ddb0ec9081e7c7eeb1ba31951f447ca780ef9f5d568189"
[[package]] [[package]]
name = "gloo-timers" name = "gloo-timers"
@ -1188,9 +1208,9 @@ dependencies = [
[[package]] [[package]]
name = "memchr" name = "memchr"
version = "2.4.0" version = "2.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b16bd47d9e329435e309c58469fe0791c2d0d1ba96ec0954152a5ae2b04387dc" checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525"
[[package]] [[package]]
name = "memmap2" name = "memmap2"
@ -1325,12 +1345,9 @@ dependencies = [
[[package]] [[package]]
name = "object" name = "object"
version = "0.26.0" version = "0.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c55827317fb4c08822499848a14237d2874d6f139828893017237e7ab93eb386" checksum = "1a5b3dd1c072ee7963717671d1ca129f1048fda25edea6b752bfc71ac8854170"
dependencies = [
"memchr",
]
[[package]] [[package]]
name = "once_cell" name = "once_cell"
@ -1675,9 +1692,9 @@ dependencies = [
[[package]] [[package]]
name = "regex" name = "regex"
version = "1.5.4" version = "1.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" checksum = "2a26af418b574bd56588335b3a3659a65725d4e636eb1016c2f9e3b38c7cc759"
dependencies = [ dependencies = [
"aho-corasick", "aho-corasick",
"memchr", "memchr",
@ -1981,6 +1998,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"ansi_term 0.12.1", "ansi_term 0.12.1",
"colored", "colored",
"unicode-width",
"zellij-tile", "zellij-tile",
"zellij-tile-utils", "zellij-tile-utils",
] ]
@ -2622,7 +2640,7 @@ dependencies = [
[[package]] [[package]]
name = "zellij" name = "zellij"
version = "0.17.0" version = "0.18.0"
dependencies = [ dependencies = [
"insta", "insta",
"log", "log",
@ -2636,7 +2654,7 @@ dependencies = [
[[package]] [[package]]
name = "zellij-client" name = "zellij-client"
version = "0.17.0" version = "0.18.0"
dependencies = [ dependencies = [
"insta", "insta",
"log", "log",
@ -2647,14 +2665,16 @@ dependencies = [
[[package]] [[package]]
name = "zellij-server" name = "zellij-server"
version = "0.17.0" version = "0.18.0"
dependencies = [ dependencies = [
"ansi_term 0.12.1", "ansi_term 0.12.1",
"async-trait", "async-trait",
"base64", "base64",
"byteorder",
"cassowary", "cassowary",
"chrono", "chrono",
"daemonize", "daemonize",
"darwin-libproc",
"insta", "insta",
"log", "log",
"serde_json", "serde_json",
@ -2667,7 +2687,7 @@ dependencies = [
[[package]] [[package]]
name = "zellij-tile" name = "zellij-tile"
version = "0.17.0" version = "0.18.0"
dependencies = [ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
@ -2677,14 +2697,14 @@ dependencies = [
[[package]] [[package]]
name = "zellij-tile-utils" name = "zellij-tile-utils"
version = "0.17.0" version = "0.18.0"
dependencies = [ dependencies = [
"ansi_term 0.12.1", "ansi_term 0.12.1",
] ]
[[package]] [[package]]
name = "zellij-utils" name = "zellij-utils"
version = "0.17.0" version = "0.18.0"
dependencies = [ dependencies = [
"async-std", "async-std",
"backtrace", "backtrace",
@ -2707,6 +2727,7 @@ dependencies = [
"strum", "strum",
"tempfile", "tempfile",
"termion", "termion",
"unicode-width",
"vte 0.10.1", "vte 0.10.1",
"zellij-tile", "zellij-tile",
] ]

View file

@ -1,6 +1,6 @@
[package] [package]
name = "zellij" name = "zellij"
version = "0.17.0" version = "0.18.0"
authors = ["Aram Drevekenin <aram@poor.dev>"] authors = ["Aram Drevekenin <aram@poor.dev>"]
edition = "2018" edition = "2018"
description = "A terminal workspace with batteries included" description = "A terminal workspace with batteries included"
@ -14,9 +14,9 @@ resolver = "2"
[dependencies] [dependencies]
names = "0.11.0" names = "0.11.0"
zellij-client = { path = "zellij-client/", version = "0.17.0" } zellij-client = { path = "zellij-client/", version = "0.18.0" }
zellij-server = { path = "zellij-server/", version = "0.17.0" } zellij-server = { path = "zellij-server/", version = "0.18.0" }
zellij-utils = { path = "zellij-utils/", version = "0.17.0" } zellij-utils = { path = "zellij-utils/", version = "0.18.0" }
log = "0.4.14" log = "0.4.14"
[dev-dependencies] [dev-dependencies]

View file

@ -13,9 +13,9 @@
<p align="center"> <p align="center">
<a href="https://discord.gg/CrUAFH3"><img alt="Discord Chat" src="https://img.shields.io/discord/771367133715628073"></a> <a href="https://discord.gg/CrUAFH3"><img alt="Discord Chat" src="https://img.shields.io/discord/771367133715628073"></a>
<a href="https://zellij.dev/documentation/"><img alt="Zellij documentation" src="https://img.shields.io/badge/zellij-documentation-fc0060"></a>
</p> </p>
# What is this? # 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. [Zellij](https://en.wikipedia.org/wiki/Zellij) is a workspace aimed at developers, ops-oriented people and anyone who loves the terminal.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -8,5 +8,6 @@ license = "MIT"
[dependencies] [dependencies]
colored = "2" colored = "2"
ansi_term = "0.12" ansi_term = "0.12"
unicode-width = "0.1.8"
zellij-tile = { path = "../../zellij-tile" } zellij-tile = { path = "../../zellij-tile" }
zellij-tile-utils = { path = "../../zellij-tile-utils" } zellij-tile-utils = { path = "../../zellij-tile-utils" }

View file

@ -1,4 +1,5 @@
use ansi_term::ANSIStrings; use ansi_term::ANSIStrings;
use unicode_width::UnicodeWidthStr;
use crate::{LinePart, ARROW_SEPARATOR}; use crate::{LinePart, ARROW_SEPARATOR};
use zellij_tile::prelude::*; 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() 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( fn populate_tabs_in_tab_line(
tabs_before_active: &mut Vec<LinePart>, tabs_before_active: &mut Vec<LinePart>,
tabs_after_active: &mut Vec<LinePart>, tabs_after_active: &mut Vec<LinePart>,
tabs_to_render: &mut Vec<LinePart>, tabs_to_render: &mut Vec<LinePart>,
cols: usize, 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 { 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; break;
} }
let current_title_len = get_current_title_len(tabs_to_render);
if current_title_len >= cols { let left = if let Some(tab) = tabs_before_active.last() {
break; tab.len
}
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;
} else { } 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; break;
} }
} }
@ -56,7 +100,8 @@ fn left_more_message(tab_count_to_the_left: usize, palette: Palette, separator:
" ← +many ".to_string() " ← +many ".to_string()
}; };
// 238 // 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 left_separator = style!(palette.cyan, palette.orange).paint(separator);
let more_styled_text = style!(palette.black, palette.orange) let more_styled_text = style!(palette.black, palette.orange)
.bold() .bold()
@ -85,7 +130,8 @@ fn right_more_message(
} else { } else {
" +many → ".to_string() " +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 left_separator = style!(palette.cyan, palette.orange).paint(separator);
let more_styled_text = style!(palette.black, palette.orange) let more_styled_text = style!(palette.black, palette.orange)
.bold() .bold()
@ -101,48 +147,6 @@ fn right_more_message(
} }
} }
fn add_previous_tabs_msg(
tabs_before_active: &mut Vec<LinePart>,
tabs_to_render: &mut Vec<LinePart>,
title_bar: &mut Vec<LinePart>,
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<LinePart>,
title_bar: &mut Vec<LinePart>,
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<LinePart> { fn tab_line_prefix(session_name: Option<&str>, palette: Palette, cols: usize) -> Vec<LinePart> {
let prefix_text = " Zellij ".to_string(); 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 { if let Some(name) = session_name {
let name_part = format!("({}) ", 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); let name_part_styled_text = style!(palette.white, palette.cyan).bold().paint(name_part);
if cols.saturating_sub(prefix_text_len) >= name_part_len { if cols.saturating_sub(prefix_text_len) >= name_part_len {
parts.push(LinePart { parts.push(LinePart {
@ -184,7 +188,6 @@ pub fn tab_line(
palette: Palette, palette: Palette,
capabilities: PluginCapabilities, capabilities: PluginCapabilities,
) -> Vec<LinePart> { ) -> Vec<LinePart> {
let mut tabs_to_render = Vec::new();
let mut tabs_after_active = all_tabs.split_off(active_tab_index); let mut tabs_after_active = all_tabs.split_off(active_tab_index);
let mut tabs_before_active = all_tabs; let mut tabs_before_active = all_tabs;
let active_tab = if !tabs_after_active.is_empty() { 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 mut prefix = tab_line_prefix(session_name, palette, cols);
let prefix_len = get_current_title_len(&prefix); 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( populate_tabs_in_tab_line(
&mut tabs_before_active, &mut tabs_before_active,
&mut tabs_after_active, &mut tabs_after_active,
&mut tabs_to_render, &mut tabs_to_render,
cols.saturating_sub(prefix_len), cols.saturating_sub(prefix_len),
palette,
capabilities,
); );
prefix.append(&mut tabs_to_render);
let mut tab_line: Vec<LinePart> = 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 prefix
} }

View file

@ -65,7 +65,7 @@ impl ZellijPlugin for State {
self.mode_info.session_name.as_deref(), self.mode_info.session_name.as_deref(),
all_tabs, all_tabs,
active_tab_index, active_tab_index,
cols, cols.saturating_sub(1),
self.mode_info.palette, self.mode_info.palette,
self.mode_info.capabilities, self.mode_info.capabilities,
); );

View file

@ -1,11 +1,12 @@
use crate::{line::tab_separator, LinePart}; use crate::{line::tab_separator, LinePart};
use ansi_term::ANSIStrings; use ansi_term::ANSIStrings;
use unicode_width::UnicodeWidthStr;
use zellij_tile::prelude::*; use zellij_tile::prelude::*;
use zellij_tile_utils::style; use zellij_tile_utils::style;
pub fn active_tab(text: String, palette: Palette, separator: &str) -> LinePart { pub fn active_tab(text: String, palette: Palette, separator: &str) -> LinePart {
let left_separator = style!(palette.cyan, palette.green).paint(separator); 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) let tab_styled_text = style!(palette.black, palette.green)
.bold() .bold()
.paint(format!(" {} ", text)); .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 { pub fn non_active_tab(text: String, palette: Palette, separator: &str) -> LinePart {
let left_separator = style!(palette.cyan, palette.fg).paint(separator); 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) let tab_styled_text = style!(palette.black, palette.fg)
.bold() .bold()
.paint(format!(" {} ", text)); .paint(format!(" {} ", text));

View file

@ -129,7 +129,7 @@ keybinds:
key: [ Char: 'j',] key: [ Char: 'j',]
- action: [GoToPreviousTab,] - action: [GoToPreviousTab,]
key: [ Char: 'k',] key: [ Char: 'k',]
- action: [NewTab,] - action: [NewTab: ,]
key: [ Char: 'n',] key: [ Char: 'n',]
- action: [CloseTab,] - action: [CloseTab,]
key: [ Char: 'x',] key: [ Char: 'x',]

View file

@ -4,12 +4,15 @@ mod sessions;
mod tests; mod tests;
use crate::install::populate_data_dir; 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 std::process;
use zellij_client::{os_input_output::get_client_os_input, start_client, ClientInfo}; 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_server::{os_input_output::get_server_os_input, start_server};
use zellij_utils::{ use zellij_utils::{
cli::{CliArgs, Command, Sessions}, cli::{CliArgs, Command, SessionCommand, Sessions},
consts::{ZELLIJ_TMP_DIR, ZELLIJ_TMP_LOG_DIR}, consts::{ZELLIJ_TMP_DIR, ZELLIJ_TMP_LOG_DIR},
logging::*, logging::*,
setup::{get_default_data_dir, Setup}, setup::{get_default_data_dir, Setup},
@ -36,6 +39,14 @@ pub fn main() {
}; };
start_server(Box::new(os_input), path); start_server(Box::new(os_input), path);
} else { } 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() { let os_input = match get_client_os_input() {
Ok(os_input) => os_input, Ok(os_input) => os_input,
Err(e) => { Err(e) => {
@ -44,40 +55,77 @@ pub fn main() {
} }
}; };
if let Some(Command::Sessions(Sessions::Attach { if let Some(Command::Sessions(Sessions::Attach {
mut session_name, session_name,
force, force,
create,
options,
})) = opts.command.clone() })) = opts.command.clone()
{ {
if let Some(session) = session_name.as_ref() { let config_options = match options {
assert_session(session); Some(SessionCommand::Options(o)) => config_options.merge(o),
} else { None => config_options,
session_name = Some(get_active_session()); };
}
let (config, _, config_options) = match Setup::from_options(&opts) { let (client, attach_layout) = match session_name.as_ref() {
Ok(results) => results, Some(session) => {
Err(e) => { if create {
eprintln!("{}", e); if !session_exists(session).unwrap() {
process::exit(1); (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( start_client(
Box::new(os_input), Box::new(os_input),
opts, opts,
config, config,
ClientInfo::Attach(session_name.unwrap(), force, config_options), config_options,
None, client,
attach_layout,
); );
} else { } else {
let (config, layout, _) = match Setup::from_options(&opts) {
Ok(results) => results,
Err(e) => {
eprintln!("{}", e);
process::exit(1);
}
};
let session_name = opts let session_name = opts
.session .session
.clone() .clone()
@ -93,6 +141,7 @@ pub fn main() {
Box::new(os_input), Box::new(os_input),
opts, opts,
config, config,
config_options,
ClientInfo::New(session_name), ClientInfo::New(session_name),
layout, layout,
); );

View file

@ -6,7 +6,7 @@ use zellij_utils::{
ipc::{ClientToServerMsg, IpcSenderWithContext}, ipc::{ClientToServerMsg, IpcSenderWithContext},
}; };
fn get_sessions() -> Result<Vec<String>, io::ErrorKind> { pub(crate) fn get_sessions() -> Result<Vec<String>, io::ErrorKind> {
match fs::read_dir(&*ZELLIJ_SOCK_DIR) { match fs::read_dir(&*ZELLIJ_SOCK_DIR) {
Ok(files) => { Ok(files) => {
let mut sessions = Vec::new(); let mut sessions = Vec::new();
@ -47,7 +47,7 @@ fn assert_socket(name: &str) -> bool {
} }
} }
fn print_sessions(sessions: Vec<String>) { pub(crate) fn print_sessions(sessions: Vec<String>) {
let curr_session = std::env::var("ZELLIJ_SESSION_NAME").unwrap_or_else(|_| "".into()); let curr_session = std::env::var("ZELLIJ_SESSION_NAME").unwrap_or_else(|_| "".into());
sessions.iter().for_each(|session| { sessions.iter().for_each(|session| {
let suffix = if curr_session == *session { let suffix = if curr_session == *session {
@ -59,22 +59,29 @@ fn print_sessions(sessions: Vec<String>) {
}) })
} }
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() { match get_sessions() {
Ok(mut sessions) => { Ok(mut sessions) => {
if sessions.len() == 1 { if sessions.len() == 1 {
return sessions.pop().unwrap(); return ActiveSession::One(sessions.pop().unwrap());
} }
if sessions.is_empty() { if sessions.is_empty() {
println!("No active zellij sessions found."); ActiveSession::None
} else { } else {
println!("Please specify the session name to attach to. The following sessions are active:"); ActiveSession::Many
print_sessions(sessions);
} }
} }
Err(e) => eprintln!("Error occured: {:?}", e), Err(e) => {
eprintln!("Error occured: {:?}", e);
process::exit(1);
}
} }
process::exit(1);
} }
pub(crate) fn list_sessions() { pub(crate) fn list_sessions() {
@ -95,15 +102,30 @@ pub(crate) fn list_sessions() {
process::exit(exit_code); process::exit(exit_code);
} }
pub(crate) fn assert_session(name: &str) { pub(crate) fn session_exists(name: &str) -> Result<bool, io::ErrorKind> {
match get_sessions() { return match get_sessions() {
Ok(sessions) => { Ok(sessions) => {
if sessions.iter().any(|s| s == name) { 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); process::exit(1);
} }

View file

@ -157,7 +157,7 @@ pub fn cannot_split_terminals_vertically_when_active_terminal_is_too_small() {
instruction: |mut remote_terminal: RemoteTerminal| -> bool { instruction: |mut remote_terminal: RemoteTerminal| -> bool {
// this is just normal input that should be sent into the one terminal so that we can make // 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 // 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 true
}, },
}) })
@ -205,26 +205,26 @@ pub fn scrolling_inside_a_pane() {
let mut step_is_complete = false; let mut step_is_complete = false;
if remote_terminal.cursor_position_is(63, 2) && remote_terminal.tip_appears() { if remote_terminal.cursor_position_is(63, 2) && remote_terminal.tip_appears() {
// cursor is in the newly opened second pane // cursor is in the newly opened second pane
remote_terminal.send_key(&format!("{:0<56}", "line1 ").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}", "line2 ").as_bytes());
remote_terminal.send_key(&format!("{:0<58}", "line3 ").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}", "line4 ").as_bytes());
remote_terminal.send_key(&format!("{:0<58}", "line5 ").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}", "line6 ").as_bytes());
remote_terminal.send_key(&format!("{:0<58}", "line7 ").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}", "line8 ").as_bytes());
remote_terminal.send_key(&format!("{:0<58}", "line9 ").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}", "line10 ").as_bytes());
remote_terminal.send_key(&format!("{:0<58}", "line11 ").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}", "line12 ").as_bytes());
remote_terminal.send_key(&format!("{:0<58}", "line13 ").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}", "line14 ").as_bytes());
remote_terminal.send_key(&format!("{:0<58}", "line15 ").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}", "line16 ").as_bytes());
remote_terminal.send_key(&format!("{:0<58}", "line17 ").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}", "line18 ").as_bytes());
remote_terminal.send_key(&format!("{:0<58}", "line19 ").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<57}", "line20 ").as_bytes());
step_is_complete = true; step_is_complete = true;
} }
step_is_complete step_is_complete
@ -574,7 +574,7 @@ pub fn lock_mode() {
if remote_terminal.snapshot_contains("INTERFACE LOCKED") { if remote_terminal.snapshot_contains("INTERFACE LOCKED") {
remote_terminal.send_key(&TAB_MODE); remote_terminal.send_key(&TAB_MODE);
remote_terminal.send_key(&NEW_TAB_IN_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 = true;
} }
step_is_complete step_is_complete
@ -675,7 +675,7 @@ pub fn detach_and_attach_session() {
let mut step_is_complete = false; let mut step_is_complete = false;
if remote_terminal.cursor_position_is(63, 2) && remote_terminal.tip_appears() { if remote_terminal.cursor_position_is(63, 2) && remote_terminal.tip_appears() {
// new pane has been opened and focused // 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 = true;
} }
step_is_complete step_is_complete
@ -825,26 +825,26 @@ pub fn scrolling_inside_a_pane_with_mouse() {
let mut step_is_complete = false; let mut step_is_complete = false;
if remote_terminal.cursor_position_is(63, 2) && remote_terminal.tip_appears() { if remote_terminal.cursor_position_is(63, 2) && remote_terminal.tip_appears() {
// cursor is in the newly opened second pane // cursor is in the newly opened second pane
remote_terminal.send_key(&format!("{:0<56}", "line1 ").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}", "line2 ").as_bytes());
remote_terminal.send_key(&format!("{:0<58}", "line3 ").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}", "line4 ").as_bytes());
remote_terminal.send_key(&format!("{:0<58}", "line5 ").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}", "line6 ").as_bytes());
remote_terminal.send_key(&format!("{:0<58}", "line7 ").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}", "line8 ").as_bytes());
remote_terminal.send_key(&format!("{:0<58}", "line9 ").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}", "line10 ").as_bytes());
remote_terminal.send_key(&format!("{:0<58}", "line11 ").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}", "line12 ").as_bytes());
remote_terminal.send_key(&format!("{:0<58}", "line13 ").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}", "line14 ").as_bytes());
remote_terminal.send_key(&format!("{:0<58}", "line15 ").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}", "line16 ").as_bytes());
remote_terminal.send_key(&format!("{:0<58}", "line17 ").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}", "line18 ").as_bytes());
remote_terminal.send_key(&format!("{:0<58}", "line19 ").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<57}", "line20 ").as_bytes());
step_is_complete = true; step_is_complete = true;
} }
step_is_complete step_is_complete

View file

@ -34,9 +34,7 @@ fn setup_remote_environment(channel: &mut ssh2::Channel, win_size: Size) {
.request_pty("xterm", None, Some((columns, rows, 0, 0))) .request_pty("xterm", None, Some((columns, rows, 0, 0)))
.unwrap(); .unwrap();
channel.shell().unwrap(); channel.shell().unwrap();
channel channel.write_all("export PS1=\"$ \"\n".as_bytes()).unwrap();
.write_all(format!("export PS1=\"$ \"\n").as_bytes())
.unwrap();
channel.flush().unwrap(); channel.flush().unwrap();
} }
@ -154,7 +152,7 @@ impl<'a> RemoteTerminal<'a> {
format!("x: {}, y: {}", self.cursor_x, self.cursor_y) format!("x: {}, y: {}", self.cursor_x, self.cursor_y)
} }
pub fn send_key(&mut self, key: &[u8]) { pub fn send_key(&mut self, key: &[u8]) {
self.channel.write(key).unwrap(); self.channel.write_all(key).unwrap();
self.channel.flush().unwrap(); self.channel.flush().unwrap();
} }
pub fn change_size(&mut self, cols: u32, rows: u32) { pub fn change_size(&mut self, cols: u32, rows: u32) {

20
test-template.yaml Normal file
View file

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

View file

@ -1,6 +1,6 @@
[package] [package]
name = "zellij-client" name = "zellij-client"
version = "0.17.0" version = "0.18.0"
authors = ["Kunal Mohan <kunalmohan99@gmail.com>"] authors = ["Kunal Mohan <kunalmohan99@gmail.com>"]
edition = "2018" edition = "2018"
description = "The client-side library for Zellij" description = "The client-side library for Zellij"
@ -11,7 +11,7 @@ license = "MIT"
[dependencies] [dependencies]
mio = "0.7.11" mio = "0.7.11"
termbg = "0.2.3" 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" log = "0.4.14"
[dev-dependencies] [dev-dependencies]

View file

@ -86,6 +86,7 @@ pub fn start_client(
mut os_input: Box<dyn ClientOsApi>, mut os_input: Box<dyn ClientOsApi>,
opts: CliArgs, opts: CliArgs,
config: Config, config: Config,
config_options: Options,
info: ClientInfo, info: ClientInfo,
layout: Option<LayoutFromYaml>, layout: Option<LayoutFromYaml>,
) { ) {
@ -105,7 +106,6 @@ pub fn start_client(
.unwrap(); .unwrap();
std::env::set_var(&"ZELLIJ", "0"); 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( let palette = config.themes.clone().map_or_else(
|| os_input.load_palette(), || os_input.load_palette(),
|t| { |t| {

View file

@ -110,8 +110,7 @@ impl ClientOsApi for FakeClientOsApi {
if stdin_events.is_empty() { if stdin_events.is_empty() {
panic!("ran out of stdin events!"); panic!("ran out of stdin events!");
} }
let next_event = stdin_events.remove(0); stdin_events.remove(0)
next_event
} }
fn box_clone(&self) -> Box<dyn ClientOsApi> { fn box_clone(&self) -> Box<dyn ClientOsApi> {
unimplemented!() unimplemented!()
@ -174,14 +173,14 @@ pub fn quit_breaks_input_loop() {
let send_client_instructions = SenderWithContext::new(send_client_instructions); let send_client_instructions = SenderWithContext::new(send_client_instructions);
let default_mode = InputMode::Normal; let default_mode = InputMode::Normal;
drop(input_loop( input_loop(
client_os_api, client_os_api,
config, config,
options, options,
command_is_executing, command_is_executing,
send_client_instructions, send_client_instructions,
default_mode, default_mode,
)); );
let expected_actions_sent_to_server = vec![Action::Quit]; let expected_actions_sent_to_server = vec![Action::Quit];
let received_actions = extract_actions_sent_to_server(events_sent_to_server); let received_actions = extract_actions_sent_to_server(events_sent_to_server);
assert_eq!( assert_eq!(
@ -192,8 +191,7 @@ pub fn quit_breaks_input_loop() {
#[test] #[test]
pub fn move_focus_left_in_pane_mode() { pub fn move_focus_left_in_pane_mode() {
let mut stdin_events = vec![]; let stdin_events = vec![commands::MOVE_FOCUS_LEFT_IN_NORMAL_MODE.to_vec()];
stdin_events.push(commands::MOVE_FOCUS_LEFT_IN_NORMAL_MODE.to_vec());
let events_sent_to_server = Arc::new(Mutex::new(vec![])); let events_sent_to_server = Arc::new(Mutex::new(vec![]));
let command_is_executing = CommandIsExecuting::new(); let command_is_executing = CommandIsExecuting::new();
let client_os_api = Box::new(FakeClientOsApi::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 send_client_instructions = SenderWithContext::new(send_client_instructions);
let default_mode = InputMode::Normal; let default_mode = InputMode::Normal;
drop(input_loop( input_loop(
client_os_api, client_os_api,
config, config,
options, options,
command_is_executing, command_is_executing,
send_client_instructions, send_client_instructions,
default_mode, default_mode,
)); );
let expected_actions_sent_to_server = let expected_actions_sent_to_server =
vec![Action::MoveFocusOrTab(Direction::Left), Action::Quit]; vec![Action::MoveFocusOrTab(Direction::Left), Action::Quit];
let received_actions = extract_actions_sent_to_server(events_sent_to_server); 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 send_client_instructions = SenderWithContext::new(send_client_instructions);
let default_mode = InputMode::Normal; let default_mode = InputMode::Normal;
drop(input_loop( input_loop(
client_os_api, client_os_api,
config, config,
options, options,
command_is_executing, command_is_executing,
send_client_instructions, send_client_instructions,
default_mode, default_mode,
)); );
let expected_actions_sent_to_server = vec![ let expected_actions_sent_to_server = vec![
Action::Write(commands::BRACKETED_PASTE_START.to_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 Action::Write(commands::MOVE_FOCUS_LEFT_IN_NORMAL_MODE.to_vec()), // keys were directly written to server and not interpreted

View file

@ -1,6 +1,6 @@
[package] [package]
name = "zellij-server" name = "zellij-server"
version = "0.17.0" version = "0.18.0"
authors = ["Kunal Mohan <kunalmohan99@gmail.com>"] authors = ["Kunal Mohan <kunalmohan99@gmail.com>"]
edition = "2018" edition = "2018"
description = "The server-side library for Zellij" description = "The server-side library for Zellij"
@ -12,17 +12,21 @@ license = "MIT"
ansi_term = "0.12.1" ansi_term = "0.12.1"
async-trait = "0.1.50" async-trait = "0.1.50"
base64 = "0.13.0" base64 = "0.13.0"
byteorder = "1.4.3"
daemonize = "0.4.1" daemonize = "0.4.1"
serde_json = "1.0" serde_json = "1.0"
unicode-width = "0.1.8" unicode-width = "0.1.8"
wasmer = "1.0.0" wasmer = "1.0.0"
wasmer-wasi = "1.0.0" wasmer-wasi = "1.0.0"
cassowary = "0.3.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" log = "0.4.14"
typetag = "0.1.7" typetag = "0.1.7"
chrono = "0.4.19" chrono = "0.4.19"
[target.'cfg(target_os = "macos")'.dependencies]
darwin-libproc = "0.2.0"
[dev-dependencies] [dev-dependencies]
insta = "1.6.0" insta = "1.6.0"

View file

@ -170,19 +170,13 @@ pub fn start_server(os_input: Box<dyn ServerOsApi>, socket_path: PathBuf) {
thread_handles.lock().unwrap().push( thread_handles.lock().unwrap().push(
thread::Builder::new() thread::Builder::new()
.name("server_router".to_string()) .name("server_router".to_string())
.spawn({ .spawn(move || {
let session_data = session_data.clone(); route_thread_main(
let os_input = os_input.clone(); session_data,
let to_server = to_server.clone(); session_state,
os_input,
move || { to_server,
route_thread_main( )
session_data,
session_state,
os_input,
to_server,
)
}
}) })
.unwrap(), .unwrap(),
); );
@ -267,7 +261,6 @@ pub fn start_server(os_input: Box<dyn ServerOsApi>, socket_path: PathBuf) {
} }
} }
ServerInstruction::ClientExit => { ServerInstruction::ClientExit => {
*session_data.write().unwrap() = None;
os_input.send_to_client(ServerToClientMsg::Exit(ExitReason::Normal)); os_input.send_to_client(ServerToClientMsg::Exit(ExitReason::Normal));
break; break;
} }
@ -297,6 +290,10 @@ pub fn start_server(os_input: Box<dyn ServerOsApi>, socket_path: PathBuf) {
} }
} }
} }
// Drop cached session data before exit.
*session_data.write().unwrap() = None;
thread_handles thread_handles
.lock() .lock()
.unwrap() .unwrap()

View file

@ -149,7 +149,7 @@ mod logging_pipe_test {
let test_buffer = "Testing write".as_bytes(); 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"); pipe.flush().expect("Err flush");
assert_eq!(pipe.buffer.len(), test_buffer.len()); assert_eq!(pipe.buffer.len(), test_buffer.len());
@ -161,7 +161,7 @@ mod logging_pipe_test {
let test_buffer = "Testing write \n".as_bytes(); 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"); pipe.flush().expect("Err flush");
assert_eq!(pipe.buffer.len(), 0); assert_eq!(pipe.buffer.len(), 0);
@ -174,7 +174,7 @@ mod logging_pipe_test {
let test_buffer = "Testing write \n".as_bytes(); let test_buffer = "Testing write \n".as_bytes();
let test_buffer2 = "And the rest".as_bytes(); let test_buffer2 = "And the rest".as_bytes();
pipe.write( pipe.write_all(
[ [
test_buffer, test_buffer,
test_buffer, test_buffer,
@ -197,7 +197,7 @@ mod logging_pipe_test {
let test_buffer = "Testing write \n".as_bytes(); let test_buffer = "Testing write \n".as_bytes();
pipe.write( pipe.write_all(
[ [
test_buffer, test_buffer,
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 // 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()); 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"); .expect("Err write");
pipe.flush().expect("Err flush"); pipe.flush().expect("Err flush");
@ -237,7 +237,7 @@ mod logging_pipe_test {
let mut pipe = LoggingPipe::new("TestPipe", 0); let mut pipe = LoggingPipe::new("TestPipe", 0);
let test_buffer = "Testing write \n".as_bytes(); let test_buffer = "Testing write \n".as_bytes();
pipe.write( pipe.write_all(
[test_buffer, test_buffer, b"\n", b"\n", b"\n"] [test_buffer, test_buffer, b"\n", b"\n", b"\n"]
.concat() .concat()
.as_slice(), .as_slice(),

View file

@ -1,4 +1,8 @@
#[cfg(target_os = "macos")]
use darwin_libproc;
use std::env; use std::env;
use std::fs;
use std::os::unix::io::RawFd; use std::os::unix::io::RawFd;
use std::os::unix::process::CommandExt; use std::os::unix::process::CommandExt;
use std::path::PathBuf; 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::fs::File as AsyncFile;
use async_std::os::unix::io::FromRawFd; use async_std::os::unix::io::FromRawFd;
use interprocess::local_socket::LocalSocketStream; 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::signal::{kill, Signal};
use nix::sys::termios; use nix::sys::termios;
use nix::sys::wait::waitpid; use nix::sys::wait::waitpid;
@ -29,6 +33,7 @@ use zellij_utils::{
use async_std::io::ReadExt; use async_std::io::ReadExt;
pub use async_trait::async_trait; pub use async_trait::async_trait;
use byteorder::{BigEndian, ByteOrder};
pub use nix::unistd::Pid; 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) /// Spawns a new terminal from the parent terminal with [`termios`](termios::Termios)
/// `orig_termios`. /// `orig_termios`.
/// ///
fn handle_terminal(cmd: RunCommand, orig_termios: termios::Termios) -> (RawFd, Pid) { fn handle_terminal(cmd: RunCommand, orig_termios: termios::Termios) -> (RawFd, ChildId) {
let (pid_primary, pid_secondary): (RawFd, Pid) = { // Create a pipe to allow the child the communicate the shell's pid to it's
match forkpty(None, Some(&orig_termios)) { // parent.
Ok(fork_pty_res) => { let (parent_fd, child_fd) = unistd::pipe().expect("failed to create pipe");
let pid_primary = fork_pty_res.master; match forkpty(None, Some(&orig_termios)) {
let pid_secondary = match fork_pty_res.fork_result { Ok(fork_pty_res) => handle_fork_pty(fork_pty_res, cmd, parent_fd, child_fd),
ForkResult::Parent { child } => child, Err(e) => {
ForkResult::Child => { panic!("failed to fork {:?}", e);
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);
}
} }
}; }
(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<u32> {
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` /// 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 /// This function will panic if both the `EDITOR` and `VISUAL` environment variables are not
/// set. /// set.
pub fn spawn_terminal( pub fn spawn_terminal(
terminal_action: Option<TerminalAction>, terminal_action: TerminalAction,
orig_termios: termios::Termios, orig_termios: termios::Termios,
) -> (RawFd, Pid) { ) -> (RawFd, ChildId) {
let cmd = match terminal_action { 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() { 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)"); 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_os_string()
.into_string() .into_string()
.expect("Not valid Utf8 Encoding")]; .expect("Not valid Utf8 Encoding")];
RunCommand { command, args } RunCommand {
} command,
Some(TerminalAction::RunCommand(command)) => command, args,
None => { cwd: None,
let command = }
PathBuf::from(env::var("SHELL").expect("Could not find the SHELL variable"));
let args = vec![];
RunCommand { command, args }
} }
TerminalAction::RunCommand(command) => command,
}; };
handle_terminal(cmd, orig_termios) handle_terminal(cmd, orig_termios)
@ -214,8 +267,10 @@ impl AsyncReader for RawFdAsyncReader {
pub trait ServerOsApi: Send + Sync { pub trait ServerOsApi: Send + Sync {
/// Sets the size of the terminal associated to file descriptor `fd`. /// Sets the size of the terminal associated to file descriptor `fd`.
fn set_terminal_size_using_fd(&self, fd: RawFd, cols: u16, rows: u16); fn set_terminal_size_using_fd(&self, fd: RawFd, cols: u16, rows: u16);
/// Spawn a new terminal, with a terminal action. /// Spawn a new terminal, with a terminal action. The returned tuple contains the master file
fn spawn_terminal(&self, terminal_action: Option<TerminalAction>) -> (RawFd, Pid); /// 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`. /// 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<usize, nix::Error>; fn read_from_tty_stdout(&self, fd: RawFd, buf: &mut [u8]) -> Result<usize, nix::Error>;
/// Creates an `AsyncReader` that can be used to read from `fd` in an async context /// 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 /// Update the receiver socket for the client
fn update_receiver(&mut self, stream: LocalSocketStream); fn update_receiver(&mut self, stream: LocalSocketStream);
fn load_palette(&self) -> Palette; fn load_palette(&self) -> Palette;
/// Returns the current working directory for a given pid
fn get_cwd(&self, pid: Pid) -> Option<PathBuf>;
} }
impl ServerOsApi for ServerOsInputOutput { impl ServerOsApi for ServerOsInputOutput {
@ -255,7 +312,7 @@ impl ServerOsApi for ServerOsInputOutput {
set_terminal_size_using_fd(fd, cols, rows); set_terminal_size_using_fd(fd, cols, rows);
} }
} }
fn spawn_terminal(&self, terminal_action: Option<TerminalAction>) -> (RawFd, Pid) { fn spawn_terminal(&self, terminal_action: TerminalAction) -> (RawFd, ChildId) {
let orig_termios = self.orig_termios.lock().unwrap(); let orig_termios = self.orig_termios.lock().unwrap();
spawn_terminal(terminal_action, orig_termios.clone()) spawn_terminal(terminal_action, orig_termios.clone())
} }
@ -336,6 +393,18 @@ impl ServerOsApi for ServerOsInputOutput {
fn load_palette(&self) -> Palette { fn load_palette(&self) -> Palette {
default_palette() default_palette()
} }
#[cfg(target_os = "macos")]
fn get_cwd(&self, pid: Pid) -> Option<PathBuf> {
darwin_libproc::pid_cwd(pid.as_raw()).ok()
}
#[cfg(target_os = "linux")]
fn get_cwd(&self, pid: Pid) -> Option<PathBuf> {
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<PathBuf> {
None
}
} }
impl Clone for Box<dyn ServerOsApi> { impl Clone for Box<dyn ServerOsApi> {
@ -353,3 +422,13 @@ pub fn get_server_os_input() -> Result<ServerOsInputOutput, nix::Error> {
send_instructions_to_client: Arc::new(Mutex::new(None)), 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<Pid>,
}

View file

@ -527,8 +527,8 @@ impl Grid {
for (i, line) in self.viewport.iter().enumerate() { for (i, line) in self.viewport.iter().enumerate() {
if line.is_canonical { if line.is_canonical {
canonical_lines_traversed += 1; canonical_lines_traversed += 1;
y_coordinates = i;
if canonical_lines_traversed == canonical_line_index + 1 { if canonical_lines_traversed == canonical_line_index + 1 {
y_coordinates = i;
break; 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![]; let mut new_viewport_rows = vec![];
for mut canonical_line in viewport_canonical_lines { for mut canonical_line in viewport_canonical_lines {
let mut canonical_line_parts: Vec<Row> = vec![]; let mut canonical_line_parts: Vec<Row> = vec![];
@ -658,9 +675,11 @@ impl Grid {
} }
new_viewport_rows.append(&mut canonical_line_parts); new_viewport_rows.append(&mut canonical_line_parts);
} }
self.viewport = new_viewport_rows; self.viewport = new_viewport_rows;
let mut new_cursor_y = self.canonical_line_y_coordinates(cursor_canonical_line_index); 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) let new_cursor_x = (cursor_index_in_canonical_line / new_columns)
+ (cursor_index_in_canonical_line % new_columns); + (cursor_index_in_canonical_line % new_columns);
let current_viewport_row_count = self.viewport.len(); let current_viewport_row_count = self.viewport.len();

View file

@ -253,7 +253,9 @@ impl Pane for TerminalPane {
color: self.frame_color, color: self.frame_color,
}; };
if &frame != last_frame { if &frame != last_frame {
vte_output.push_str(&frame.render()); if !self.borderless {
vte_output.push_str(&frame.render());
}
self.frame = Some(frame); self.frame = Some(frame);
} }
} }

View file

@ -8,9 +8,8 @@ fn read_fixture(fixture_name: &str) -> Vec<u8> {
path_to_file.push("tests"); path_to_file.push("tests");
path_to_file.push("fixtures"); path_to_file.push("fixtures");
path_to_file.push(fixture_name); path_to_file.push(fixture_name);
let content = std::fs::read(path_to_file) std::fs::read(path_to_file)
.unwrap_or_else(|_| panic!("could not read fixture {:?}", &fixture_name)); .unwrap_or_else(|_| panic!("could not read fixture {:?}", &fixture_name))
content
} }
#[test] #[test]

View file

@ -1,5 +1,5 @@
use crate::{ use crate::{
os_input_output::{AsyncReader, Pid, ServerOsApi}, os_input_output::{AsyncReader, ChildId, ServerOsApi},
panes::PaneId, panes::PaneId,
screen::ScreenInstruction, screen::ScreenInstruction,
thread_bus::{Bus, ThreadSenders}, thread_bus::{Bus, ThreadSenders},
@ -12,14 +12,16 @@ use async_std::{
}; };
use std::{ use std::{
collections::HashMap, collections::HashMap,
env,
os::unix::io::RawFd, os::unix::io::RawFd,
path::PathBuf,
time::{Duration, Instant}, time::{Duration, Instant},
}; };
use zellij_utils::{ use zellij_utils::{
async_std, async_std,
errors::{get_current_ctx, ContextType, PtyContext}, errors::{get_current_ctx, ContextType, PtyContext},
input::{ input::{
command::TerminalAction, command::{RunCommand, TerminalAction},
layout::{Layout, LayoutFromYaml, Run, TabLayout}, layout::{Layout, LayoutFromYaml, Run, TabLayout},
}, },
logging::debug_to_file, logging::debug_to_file,
@ -33,6 +35,7 @@ pub(crate) enum PtyInstruction {
SpawnTerminal(Option<TerminalAction>), SpawnTerminal(Option<TerminalAction>),
SpawnTerminalVertically(Option<TerminalAction>), SpawnTerminalVertically(Option<TerminalAction>),
SpawnTerminalHorizontally(Option<TerminalAction>), SpawnTerminalHorizontally(Option<TerminalAction>),
UpdateActivePane(Option<PaneId>),
NewTab(Option<TerminalAction>, Option<TabLayout>), NewTab(Option<TerminalAction>, Option<TabLayout>),
ClosePane(PaneId), ClosePane(PaneId),
CloseTab(Vec<PaneId>), CloseTab(Vec<PaneId>),
@ -45,6 +48,7 @@ impl From<&PtyInstruction> for PtyContext {
PtyInstruction::SpawnTerminal(_) => PtyContext::SpawnTerminal, PtyInstruction::SpawnTerminal(_) => PtyContext::SpawnTerminal,
PtyInstruction::SpawnTerminalVertically(_) => PtyContext::SpawnTerminalVertically, PtyInstruction::SpawnTerminalVertically(_) => PtyContext::SpawnTerminalVertically,
PtyInstruction::SpawnTerminalHorizontally(_) => PtyContext::SpawnTerminalHorizontally, PtyInstruction::SpawnTerminalHorizontally(_) => PtyContext::SpawnTerminalHorizontally,
PtyInstruction::UpdateActivePane(_) => PtyContext::UpdateActivePane,
PtyInstruction::ClosePane(_) => PtyContext::ClosePane, PtyInstruction::ClosePane(_) => PtyContext::ClosePane,
PtyInstruction::CloseTab(_) => PtyContext::CloseTab, PtyInstruction::CloseTab(_) => PtyContext::CloseTab,
PtyInstruction::NewTab(..) => PtyContext::NewTab, PtyInstruction::NewTab(..) => PtyContext::NewTab,
@ -54,8 +58,9 @@ impl From<&PtyInstruction> for PtyContext {
} }
pub(crate) struct Pty { pub(crate) struct Pty {
pub active_pane: Option<PaneId>,
pub bus: Bus<PtyInstruction>, pub bus: Bus<PtyInstruction>,
pub id_to_child_pid: HashMap<RawFd, Pid>, pub id_to_child_pid: HashMap<RawFd, ChildId>,
debug_to_file: bool, debug_to_file: bool,
task_handles: HashMap<RawFd, JoinHandle<()>>, task_handles: HashMap<RawFd, JoinHandle<()>>,
} }
@ -86,9 +91,32 @@ pub(crate) fn pty_thread_main(mut pty: Pty, layout: LayoutFromYaml) {
.send_to_screen(ScreenInstruction::HorizontalSplit(PaneId::Terminal(pid))) .send_to_screen(ScreenInstruction::HorizontalSplit(PaneId::Terminal(pid)))
.unwrap(); .unwrap();
} }
PtyInstruction::UpdateActivePane(pane_id) => {
pty.set_active_pane(pane_id);
}
PtyInstruction::NewTab(terminal_action, tab_layout) => { 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); let merged_layout = layout.template.clone().insert_tab_layout(tab_layout);
pty.spawn_terminals_for_layout(merged_layout.into(), terminal_action.clone()); 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) => { PtyInstruction::ClosePane(id) => {
pty.close_pane(id); pty.close_pane(id);
@ -208,14 +236,30 @@ fn stream_terminal_bytes(
impl Pty { impl Pty {
pub fn new(bus: Bus<PtyInstruction>, debug_to_file: bool) -> Self { pub fn new(bus: Bus<PtyInstruction>, debug_to_file: bool) -> Self {
Pty { Pty {
active_pane: None,
bus, bus,
id_to_child_pid: HashMap::new(), id_to_child_pid: HashMap::new(),
debug_to_file, debug_to_file,
task_handles: HashMap::new(), 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<TerminalAction>) -> RawFd { pub fn spawn_terminal(&mut self, terminal_action: Option<TerminalAction>) -> 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 .bus
.os_input .os_input
.as_mut() .as_mut()
@ -228,7 +272,7 @@ impl Pty {
self.debug_to_file, self.debug_to_file,
); );
self.task_handles.insert(pid_primary, task_handle); 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 pid_primary
} }
pub fn spawn_terminals_for_layout( pub fn spawn_terminals_for_layout(
@ -236,29 +280,26 @@ impl Pty {
layout: Layout, layout: Layout,
default_shell: Option<TerminalAction>, default_shell: Option<TerminalAction>,
) { ) {
let default_shell = default_shell.unwrap_or_else(|| self.get_default_terminal());
let extracted_run_instructions = layout.extract_run_instructions(); let extracted_run_instructions = layout.extract_run_instructions();
let mut new_pane_pids = vec![]; let mut new_pane_pids = vec![];
for run_instruction in extracted_run_instructions { for run_instruction in extracted_run_instructions {
match run_instruction { match run_instruction {
Some(Run::Command(command)) => { Some(Run::Command(command)) => {
let cmd = TerminalAction::RunCommand(command); let cmd = TerminalAction::RunCommand(command);
let (pid_primary, pid_secondary): (RawFd, Pid) = self let (pid_primary, child_id): (RawFd, ChildId) =
.bus self.bus.os_input.as_mut().unwrap().spawn_terminal(cmd);
.os_input self.id_to_child_pid.insert(pid_primary, child_id);
.as_mut()
.unwrap()
.spawn_terminal(Some(cmd));
self.id_to_child_pid.insert(pid_primary, pid_secondary);
new_pane_pids.push(pid_primary); new_pane_pids.push(pid_primary);
} }
None => { None => {
let (pid_primary, pid_secondary): (RawFd, Pid) = self let (pid_primary, child_id): (RawFd, ChildId) = self
.bus .bus
.os_input .os_input
.as_mut() .as_mut()
.unwrap() .unwrap()
.spawn_terminal(default_shell.clone()); .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); new_pane_pids.push(pid_primary);
} }
// Investigate moving plugin loading to here. // Investigate moving plugin loading to here.
@ -285,10 +326,15 @@ impl Pty {
pub fn close_pane(&mut self, id: PaneId) { pub fn close_pane(&mut self, id: PaneId) {
match id { match id {
PaneId::Terminal(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(); let handle = self.task_handles.remove(&id).unwrap();
task::block_on(async { 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); let timeout = Duration::from_millis(100);
match async_timeout(timeout, handle.cancel()).await { match async_timeout(timeout, handle.cancel()).await {
Ok(_) => {} Ok(_) => {}
@ -297,7 +343,7 @@ impl Pty {
.os_input .os_input
.as_mut() .as_mut()
.unwrap() .unwrap()
.force_kill(child_pid) .force_kill(pids.primary)
.unwrap(); .unwrap();
} }
}; };
@ -315,6 +361,9 @@ impl Pty {
self.close_pane(id); self.close_pane(id);
}); });
} }
pub fn set_active_pane(&mut self, pane_id: Option<PaneId>) {
self.active_pane = pane_id;
}
} }
impl Drop for Pty { impl Drop for Pty {

View file

@ -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. /// Sets this [`Screen`]'s active [`Tab`] to the next tab.
pub fn switch_tab_next(&mut self) { pub fn switch_tab_next(&mut self) {
let active_tab_pos = self.get_active_tab().unwrap().position; let active_tab_pos = self.get_active_tab().unwrap().position;
let new_tab_pos = (active_tab_pos + 1) % self.tabs.len(); let new_tab_pos = (active_tab_pos + 1) % self.tabs.len();
for tab in self.tabs.values_mut() { self.switch_active_tab(new_tab_pos);
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();
} }
/// Sets this [`Screen`]'s active [`Tab`] to the previous tab. /// Sets this [`Screen`]'s active [`Tab`] to the previous tab.
@ -214,32 +229,12 @@ impl Screen {
} else { } else {
active_tab_pos - 1 active_tab_pos - 1
}; };
for tab in self.tabs.values_mut() {
if tab.position == new_tab_pos { self.switch_active_tab(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();
} }
pub fn go_to_tab(&mut self, mut tab_index: usize) { pub fn go_to_tab(&mut self, tab_index: usize) {
tab_index -= 1; self.switch_active_tab(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();
}
}
} }
/// Closes this [`Screen`]'s active [`Tab`], exiting the application if it happens /// Closes this [`Screen`]'s active [`Tab`], exiting the application if it happens
@ -264,10 +259,14 @@ impl Screen {
.unwrap(); .unwrap();
} }
} else { } else {
if let Some(tab) = self.get_active_tab() {
tab.visible(false);
}
self.active_tab_index = self.tab_history.pop().unwrap(); self.active_tab_index = self.tab_history.pop().unwrap();
for t in self.tabs.values_mut() { for t in self.tabs.values_mut() {
if t.position == self.active_tab_index.unwrap() { if t.index == self.active_tab_index.unwrap() {
t.set_force_render() t.set_force_render();
t.visible(true);
} }
if t.position > active_tab.position { if t.position > active_tab.position {
t.position -= 1; t.position -= 1;
@ -357,8 +356,12 @@ impl Screen {
self.draw_pane_frames, self.draw_pane_frames,
); );
tab.apply_layout(layout, new_pids, tab_index); tab.apply_layout(layout, new_pids, tab_index);
self.tab_history.push(self.active_tab_index); if let Some(active_tab) = self.get_active_tab() {
self.active_tab_index = Some(tab_index); 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.tabs.insert(tab_index, tab);
self.update_tabs(); self.update_tabs();
} }

View file

@ -268,7 +268,7 @@ impl Tab {
let panes = BTreeMap::new(); let panes = BTreeMap::new();
let name = if name.is_empty() { let name = if name.is_empty() {
format!("Tab #{}", position + 1) format!("Tab #{}", index + 1)
} else { } else {
name name
}; };
@ -325,10 +325,15 @@ impl Tab {
if let Some(Run::Plugin(Some(plugin))) = &layout.run { if let Some(Run::Plugin(Some(plugin))) = &layout.run {
let (pid_tx, pid_rx) = channel(); let (pid_tx, pid_rx) = channel();
self.senders 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(); .unwrap();
let pid = pid_rx.recv().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( let mut new_plugin = PluginPane::new(
pid, pid,
*position_and_size, *position_and_size,
@ -684,22 +689,35 @@ impl Tab {
} }
pub fn set_pane_frames(&mut self, draw_pane_frames: bool) { pub fn set_pane_frames(&mut self, draw_pane_frames: bool) {
self.draw_pane_frames = draw_pane_frames; 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() { for (pane_id, pane) in self.panes.iter_mut() {
pane.set_frame(draw_pane_frames); if !pane.borderless() {
if draw_pane_frames { 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)); 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 { } 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 position_and_size = pane.current_geom();
let (pane_columns_offset, pane_rows_offset) = let (pane_columns_offset, pane_rows_offset) =
pane_content_offset(&position_and_size, &self.viewport); pane_content_offset(&position_and_size, &self.viewport);
pane.set_content_offset(Offset::shift(pane_rows_offset, pane_columns_offset)); 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 // FIXME: This, and all other `set_terminal_size_using_fd` calls, would be best in
// `TerminalPane::reflow_lines` // `TerminalPane::reflow_lines`
if let PaneId::Terminal(pid) = pane_id { 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 // or if this session is not attached to a client, we do not have to render
return; return;
} }
self.senders
.send_to_pty(PtyInstruction::UpdateActivePane(self.active_terminal))
.unwrap();
let mut output = String::new(); let mut output = String::new();
let mut boundaries = Boundaries::new(self.viewport); let mut boundaries = Boundaries::new(self.viewport);
let hide_cursor = "\u{1b}[?25l"; let hide_cursor = "\u{1b}[?25l";
@ -1739,16 +1760,16 @@ impl Tab {
} }
let active_terminal_id = self.get_active_pane_id().unwrap(); let active_terminal_id = self.get_active_pane_id().unwrap();
let terminal_ids: Vec<PaneId> = self.get_selectable_panes().map(|(&pid, _)| pid).collect(); // TODO: better, no allocations let terminal_ids: Vec<PaneId> = 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 let active_terminal_id_position = terminal_ids
.iter() .iter()
.position(|id| id == &active_terminal_id) .position(|id| id == &active_terminal_id)
.unwrap(); .unwrap();
if let Some(next_terminal) = terminal_ids.get(active_terminal_id_position + 1) { let active_terminal = terminal_ids
self.active_terminal = Some(*next_terminal); .get(active_terminal_id_position + 1)
} else { .or_else(|| terminal_ids.get(0))
self.active_terminal = Some(*first_terminal); .copied();
}
self.active_terminal = active_terminal;
self.render(); self.render();
} }
pub fn focus_next_pane(&mut self) { pub fn focus_next_pane(&mut self) {
@ -1767,16 +1788,17 @@ impl Tab {
a_pane.y().cmp(&b_pane.y()) a_pane.y().cmp(&b_pane.y())
} }
}); });
let first_pane = panes.get(0).unwrap();
let active_pane_position = panes let active_pane_position = panes
.iter() .iter()
.position(|(id, _)| *id == &active_pane_id) // TODO: better .position(|(id, _)| *id == &active_pane_id) // TODO: better
.unwrap(); .unwrap();
if let Some(next_pane) = panes.get(active_pane_position + 1) {
self.active_terminal = Some(*next_pane.0); let active_terminal = panes
} else { .get(active_pane_position + 1)
self.active_terminal = Some(*first_pane.0); .or_else(|| panes.get(0))
} .map(|p| *p.0);
self.active_terminal = active_terminal;
self.render(); self.render();
} }
pub fn focus_previous_pane(&mut self) { pub fn focus_previous_pane(&mut self) {
@ -1800,11 +1822,13 @@ impl Tab {
.iter() .iter()
.position(|(id, _)| *id == &active_pane_id) // TODO: better .position(|(id, _)| *id == &active_pane_id) // TODO: better
.unwrap(); .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 { } 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(); self.render();
} }
// returns a boolean that indicates whether the focus moved // returns a boolean that indicates whether the focus moved
@ -1816,7 +1840,7 @@ impl Tab {
return false; return false;
} }
let active_terminal = self.get_active_pane(); 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 terminals = self.get_selectable_panes();
let next_index = terminals let next_index = terminals
.enumerate() .enumerate()
@ -1839,13 +1863,12 @@ impl Tab {
self.render(); self.render();
return true; return true;
} }
None => { None => Some(active.pid()),
self.active_terminal = Some(active.pid());
}
} }
} else { } else {
self.active_terminal = Some(active_terminal.unwrap().pid()); Some(active_terminal.unwrap().pid())
} };
self.active_terminal = updated_active_terminal;
false false
} }
pub fn move_focus_down(&mut self) { pub fn move_focus_down(&mut self) {
@ -1856,7 +1879,7 @@ impl Tab {
return; return;
} }
let active_terminal = self.get_active_pane(); 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 terminals = self.get_selectable_panes();
let next_index = terminals let next_index = terminals
.enumerate() .enumerate()
@ -1875,15 +1898,14 @@ impl Tab {
let next_active_pane = self.panes.get_mut(&p).unwrap(); let next_active_pane = self.panes.get_mut(&p).unwrap();
next_active_pane.set_should_render(true); next_active_pane.set_should_render(true);
self.active_terminal = Some(p); Some(p)
}
None => {
self.active_terminal = Some(active.pid());
} }
None => Some(active.pid()),
} }
} else { } else {
self.active_terminal = Some(active_terminal.unwrap().pid()); Some(active_terminal.unwrap().pid())
} };
self.active_terminal = updated_active_terminal;
self.render(); self.render();
} }
pub fn move_focus_up(&mut self) { pub fn move_focus_up(&mut self) {
@ -1894,7 +1916,7 @@ impl Tab {
return; return;
} }
let active_terminal = self.get_active_pane(); 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 terminals = self.get_selectable_panes();
let next_index = terminals let next_index = terminals
.enumerate() .enumerate()
@ -1913,15 +1935,14 @@ impl Tab {
let next_active_pane = self.panes.get_mut(&p).unwrap(); let next_active_pane = self.panes.get_mut(&p).unwrap();
next_active_pane.set_should_render(true); next_active_pane.set_should_render(true);
self.active_terminal = Some(p); Some(p)
}
None => {
self.active_terminal = Some(active.pid());
} }
None => Some(active.pid()),
} }
} else { } else {
self.active_terminal = Some(active_terminal.unwrap().pid()); Some(active_terminal.unwrap().pid())
} };
self.active_terminal = updated_active_terminal;
self.render(); self.render();
} }
// returns a boolean that indicates whether the focus moved // returns a boolean that indicates whether the focus moved
@ -1933,7 +1954,7 @@ impl Tab {
return false; return false;
} }
let active_terminal = self.get_active_pane(); 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 terminals = self.get_selectable_panes();
let next_index = terminals let next_index = terminals
.enumerate() .enumerate()
@ -1956,13 +1977,12 @@ impl Tab {
self.render(); self.render();
return true; return true;
} }
None => { None => Some(active.pid()),
self.active_terminal = Some(active.pid());
}
} }
} else { } else {
self.active_terminal = Some(active_terminal.unwrap().pid()); Some(active_terminal.unwrap().pid())
} };
self.active_terminal = updated_active_terminal;
false false
} }
fn horizontal_borders(&self, terminals: &[PaneId]) -> HashSet<usize> { fn horizontal_borders(&self, terminals: &[PaneId]) -> HashSet<usize> {
@ -1981,6 +2001,7 @@ impl Tab {
borders borders
}) })
} }
fn panes_to_the_left_between_aligning_borders(&self, id: PaneId) -> Option<Vec<PaneId>> { fn panes_to_the_left_between_aligning_borders(&self, id: PaneId) -> Option<Vec<PaneId>> {
if let Some(terminal) = self.panes.get(&id) { if let Some(terminal) = self.panes.get(&id) {
let upper_close_border = terminal.y(); let upper_close_border = terminal.y();
@ -2107,7 +2128,7 @@ impl Tab {
if let Some(pane) = self.panes.get_mut(&id) { if let Some(pane) = self.panes.get_mut(&id) {
pane.set_selectable(selectable); pane.set_selectable(selectable);
if self.get_active_pane_id() == Some(id) && !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(); self.render();
@ -2345,10 +2366,9 @@ impl Tab {
.unwrap(); .unwrap();
} }
fn is_inside_viewport(&self, pane_id: &PaneId) -> bool { fn is_inside_viewport(&self, pane_id: &PaneId) -> bool {
let pane_position_and_size = self.panes.get(pane_id).unwrap().current_geom(); // this is mostly separated to an outside function in order to allow us to pass a clone to
pane_position_and_size.y >= self.viewport.y // it sometimes when we need to get around the borrow checker
&& pane_position_and_size.y + pane_position_and_size.rows.as_usize() is_inside_viewport(&self.viewport, self.panes.get(pane_id).unwrap())
<= self.viewport.y + self.viewport.rows
} }
fn offset_viewport(&mut self, position_and_size: &Viewport) { fn offset_viewport(&mut self, position_and_size: &Viewport) {
if position_and_size.x == self.viewport.x 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<dyn Pane>) -> 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)] #[cfg(test)]

View file

@ -4,6 +4,8 @@ use ansi_term::Style;
use zellij_utils::pane_size::Viewport; use zellij_utils::pane_size::Viewport;
use zellij_utils::zellij_tile::prelude::PaletteColor; use zellij_utils::zellij_tile::prelude::PaletteColor;
use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
fn color_string(character: &str, color: Option<PaletteColor>) -> String { fn color_string(character: &str, color: Option<PaletteColor>) -> String {
match color { match color {
Some(color) => match color { Some(color) => match color {
@ -33,11 +35,11 @@ impl PaneFrame {
let full_indication = let full_indication =
format!(" {}/{} ", self.scroll_position.0, self.scroll_position.1); format!(" {}/{} ", self.scroll_position.0, self.scroll_position.1);
let short_indication = format!(" {} ", self.scroll_position.0); 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)) Some(format!("{}{}", prefix, full_indication))
} else if full_indication.chars().count() <= max_length { } else if full_indication.width() <= max_length {
Some(full_indication) Some(full_indication)
} else if short_indication.chars().count() <= max_length { } else if short_indication.width() <= max_length {
Some(short_indication) Some(short_indication)
} else { } else {
None None
@ -50,30 +52,43 @@ impl PaneFrame {
let middle_truncated_sign = "[..]"; let middle_truncated_sign = "[..]";
let middle_truncated_sign_long = "[...]"; let middle_truncated_sign_long = "[...]";
let full_text = format!(" {} ", &self.title); let full_text = format!(" {} ", &self.title);
if max_length <= 6 { if max_length <= 6 || self.title.is_empty() {
None None
} else if full_text.chars().count() <= max_length { } else if full_text.width() <= max_length {
Some(full_text) Some(full_text)
} else { } else {
let length_of_each_half = (max_length - middle_truncated_sign.chars().count()) / 2; let length_of_each_half = (max_length - middle_truncated_sign.width()) / 2;
let first_part: String = full_text.chars().take(length_of_each_half).collect();
let second_part: String = full_text let mut first_part: String = String::with_capacity(length_of_each_half);
.chars() for char in full_text.chars() {
.skip(full_text.chars().count() - length_of_each_half) if first_part.width() + char.width().unwrap_or(0) > length_of_each_half {
.collect(); break;
let title_left_side = if first_part.chars().count() } else {
+ middle_truncated_sign.chars().count() first_part.push(char);
+ second_part.chars().count() }
< max_length }
{
// this means we lost 1 character when dividing the total length into halves let mut second_part: String = String::with_capacity(length_of_each_half);
format!( for char in full_text.chars().rev() {
"{}{}{}", if second_part.width() + char.width().unwrap_or(0) > length_of_each_half {
first_part, middle_truncated_sign_long, second_part break;
) } else {
} else { second_part.insert(0, char);
format!("{}{}{}", first_part, middle_truncated_sign, second_part) }
}; }
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) Some(title_left_side)
} }
} }
@ -83,15 +98,13 @@ impl PaneFrame {
let right_boundary = boundary_type::TOP_RIGHT; let right_boundary = boundary_type::TOP_RIGHT;
let left_side = self.render_title_left_side(total_title_length); let left_side = self.render_title_left_side(total_title_length);
let right_side = left_side.as_ref().and_then(|left_side| { 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) self.render_title_right_side(space_left)
}); });
let title_text = match (left_side, right_side) { let title_text = match (left_side, right_side) {
(Some(left_side), Some(right_side)) => { (Some(left_side), Some(right_side)) => {
let mut middle = String::new(); let mut middle = String::new();
for _ in for _ in (left_side.width() + right_side.width())..total_title_length {
(left_side.chars().count() + right_side.chars().count())..total_title_length
{
middle.push_str(boundary_type::HORIZONTAL); middle.push_str(boundary_type::HORIZONTAL);
} }
format!( format!(
@ -101,7 +114,7 @@ impl PaneFrame {
} }
(Some(left_side), None) => { (Some(left_side), None) => {
let mut middle_padding = String::new(); 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); middle_padding.push_str(boundary_type::HORIZONTAL);
} }
format!( format!(

View file

@ -1,10 +1,11 @@
use super::{Screen, ScreenInstruction}; use super::{Screen, ScreenInstruction};
use crate::zellij_tile::data::{ModeInfo, Palette}; use crate::zellij_tile::data::{ModeInfo, Palette};
use crate::{ use crate::{
os_input_output::{AsyncReader, Pid, ServerOsApi}, os_input_output::{AsyncReader, ChildId, Pid, ServerOsApi},
thread_bus::Bus, thread_bus::Bus,
SessionState, SessionState,
}; };
use std::path::PathBuf;
use std::sync::{Arc, RwLock}; use std::sync::{Arc, RwLock};
use zellij_utils::input::command::TerminalAction; use zellij_utils::input::command::TerminalAction;
use zellij_utils::input::layout::LayoutTemplate; 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) { fn set_terminal_size_using_fd(&self, _fd: RawFd, _cols: u16, _rows: u16) {
// noop // noop
} }
fn spawn_terminal(&self, _file_to_open: Option<TerminalAction>) -> (RawFd, Pid) { fn spawn_terminal(&self, _file_to_open: TerminalAction) -> (RawFd, ChildId) {
unimplemented!() unimplemented!()
} }
fn read_from_tty_stdout(&self, _fd: RawFd, _buf: &mut [u8]) -> Result<usize, nix::Error> { fn read_from_tty_stdout(&self, _fd: RawFd, _buf: &mut [u8]) -> Result<usize, nix::Error> {
@ -73,14 +74,19 @@ impl ServerOsApi for FakeInputOutput {
fn load_palette(&self) -> Palette { fn load_palette(&self) -> Palette {
unimplemented!() unimplemented!()
} }
fn get_cwd(&self, _pid: Pid) -> Option<PathBuf> {
unimplemented!()
}
} }
fn create_new_screen(size: Size) -> Screen { fn create_new_screen(size: Size) -> Screen {
let mut bus: Bus<ScreenInstruction> = Bus::empty(); let mut bus: Bus<ScreenInstruction> = Bus::empty();
let fake_os_input = FakeInputOutput {}; let fake_os_input = FakeInputOutput {};
bus.os_input = Some(Box::new(fake_os_input)); bus.os_input = Some(Box::new(fake_os_input));
let mut client_attributes = ClientAttributes::default(); let client_attributes = ClientAttributes {
client_attributes.size = size; size,
..Default::default()
};
let max_panes = None; let max_panes = None;
let mode_info = ModeInfo::default(); let mode_info = ModeInfo::default();
let session_state = Arc::new(RwLock::new(SessionState::Attached)); let session_state = Arc::new(RwLock::new(SessionState::Attached));

View file

@ -1,11 +1,12 @@
use super::Tab; use super::Tab;
use crate::zellij_tile::data::{ModeInfo, Palette}; use crate::zellij_tile::data::{ModeInfo, Palette};
use crate::{ use crate::{
os_input_output::{AsyncReader, Pid, ServerOsApi}, os_input_output::{AsyncReader, ChildId, Pid, ServerOsApi},
panes::PaneId, panes::PaneId,
thread_bus::ThreadSenders, thread_bus::ThreadSenders,
SessionState, SessionState,
}; };
use std::path::PathBuf;
use std::sync::{Arc, RwLock}; use std::sync::{Arc, RwLock};
use zellij_utils::input::layout::LayoutTemplate; use zellij_utils::input::layout::LayoutTemplate;
use zellij_utils::pane_size::Size; 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) { fn set_terminal_size_using_fd(&self, _fd: RawFd, _cols: u16, _rows: u16) {
// noop // noop
} }
fn spawn_terminal(&self, _file_to_open: Option<TerminalAction>) -> (RawFd, Pid) { fn spawn_terminal(&self, _file_to_open: TerminalAction) -> (RawFd, ChildId) {
unimplemented!() unimplemented!()
} }
fn read_from_tty_stdout(&self, _fd: RawFd, _buf: &mut [u8]) -> Result<usize, nix::Error> { fn read_from_tty_stdout(&self, _fd: RawFd, _buf: &mut [u8]) -> Result<usize, nix::Error> {
@ -72,6 +73,9 @@ impl ServerOsApi for FakeInputOutput {
fn load_palette(&self) -> Palette { fn load_palette(&self) -> Palette {
unimplemented!() unimplemented!()
} }
fn get_cwd(&self, _pid: Pid) -> Option<PathBuf> {
unimplemented!()
}
} }
fn create_new_tab(size: Size) -> Tab { fn create_new_tab(size: Size) -> Tab {

View file

@ -1,4 +1,4 @@
use log::info; use log::{info, warn};
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use std::fs; use std::fs;
use std::path::PathBuf; use std::path::PathBuf;
@ -28,8 +28,8 @@ use zellij_utils::{input::command::TerminalAction, serde, zellij_tile};
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub(crate) enum PluginInstruction { pub(crate) enum PluginInstruction {
Load(Sender<u32>, PathBuf, usize), // tx_pid, path_of_plugin , tab_index Load(Sender<u32>, PathBuf, usize, bool), // tx_pid, path_of_plugin , tab_index, allow_exec_host_cmd
Update(Option<u32>, Event), // Focused plugin / broadcast, event data Update(Option<u32>, Event), // Focused plugin / broadcast, event data
Render(Sender<String>, u32, usize, usize), // String buffer, plugin id, rows, cols Render(Sender<String>, u32, usize, usize), // String buffer, plugin id, rows, cols
Unload(u32), Unload(u32),
Exit, Exit,
@ -54,6 +54,9 @@ pub(crate) struct PluginEnv {
pub senders: ThreadSenders, pub senders: ThreadSenders,
pub wasi_env: WasiEnv, pub wasi_env: WasiEnv,
pub subscriptions: Arc<Mutex<HashSet<EventType>>>, pub subscriptions: Arc<Mutex<HashSet<EventType>>>,
// FIXME: Once permission system is ready, this could be removed
pub _allow_exec_host_cmd: bool,
plugin_own_data_dir: PathBuf,
} }
// Thread main -------------------------------------------------------------------------------------------------------- // Thread main --------------------------------------------------------------------------------------------------------
@ -61,12 +64,15 @@ pub(crate) fn wasm_thread_main(bus: Bus<PluginInstruction>, store: Store, data_d
info!("Wasm main thread starts"); info!("Wasm main thread starts");
let mut plugin_id = 0; let mut plugin_id = 0;
let mut plugin_map = HashMap::new(); 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 { loop {
let (event, mut err_ctx) = bus.recv().expect("failed to receive event on channel"); let (event, mut err_ctx) = bus.recv().expect("failed to receive event on channel");
err_ctx.add_call(ContextType::Plugin((&event).into())); err_ctx.add_call(ContextType::Plugin((&event).into()));
match event { match event {
PluginInstruction::Load(pid_tx, path, tab_index) => { PluginInstruction::Load(pid_tx, path, tab_index, _allow_exec_host_cmd) => {
let plugin_dir = data_dir.join("plugins/");
let wasm_bytes = fs::read(&path) let wasm_bytes = fs::read(&path)
.or_else(|_| fs::read(&path.with_extension("wasm"))) .or_else(|_| fs::read(&path.with_extension("wasm")))
.or_else(|_| fs::read(&plugin_dir.join(&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<PluginInstruction>, store: Store, data_d
path.as_path().file_name().unwrap().to_str().unwrap(), path.as_path().file_name().unwrap().to_str().unwrap(),
plugin_id, 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") let mut wasi_env = WasiState::new("Zellij")
.env("CLICOLOR_FORCE", "1") .env("CLICOLOR_FORCE", "1")
.preopen(|p| { .map_dir("/host", ".")
p.directory(".") // FIXME: Change this to a more meaningful dir .unwrap()
.alias(".") .map_dir("/data", plugin_own_data_dir.as_path())
.read(true)
.write(true)
.create(true)
})
.unwrap() .unwrap()
.stdin(Box::new(input)) .stdin(Box::new(input))
.stdout(Box::new(output)) .stdout(Box::new(output))
@ -99,12 +105,18 @@ pub(crate) fn wasm_thread_main(bus: Bus<PluginInstruction>, store: Store, data_d
let wasi = wasi_env.import_object(&module).unwrap(); let wasi = wasi_env.import_object(&module).unwrap();
if _allow_exec_host_cmd {
info!("Plugin({:?}) is able to run any host command, this may lead to some security issues!", path);
}
let plugin_env = PluginEnv { let plugin_env = PluginEnv {
plugin_id, plugin_id,
tab_index, tab_index,
senders: bus.senders.clone(), senders: bus.senders.clone(),
wasi_env, wasi_env,
subscriptions: Arc::new(Mutex::new(HashSet::new())), subscriptions: Arc::new(Mutex::new(HashSet::new())),
_allow_exec_host_cmd,
plugin_own_data_dir,
}; };
let zellij = zellij_exports(&store, &plugin_env); let zellij = zellij_exports(&store, &plugin_env);
@ -147,10 +159,16 @@ pub(crate) fn wasm_thread_main(bus: Bus<PluginInstruction>, store: Store, data_d
buf_tx.send(wasi_read_string(&plugin_env.wasi_env)).unwrap(); 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, PluginInstruction::Exit => break,
} }
} }
info!("wasm main thread exits");
fs::remove_dir_all(plugin_global_data_dir.as_path()).unwrap();
} }
// Plugin API --------------------------------------------------------------------------------------------------------- // Plugin API ---------------------------------------------------------------------------------------------------------
@ -174,6 +192,7 @@ pub(crate) fn zellij_exports(store: &Store, plugin_env: &PluginEnv) -> ImportObj
host_get_plugin_ids, host_get_plugin_ids,
host_open_file, host_open_file,
host_set_timeout, 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<String> = 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 --------------------------------------------------------------------------------------------------- // Helper Functions ---------------------------------------------------------------------------------------------------
pub fn wasi_read_string(wasi_env: &WasiEnv) -> String { pub fn wasi_read_string(wasi_env: &WasiEnv) -> String {

View file

@ -1,6 +1,6 @@
[package] [package]
name = "zellij-tile-utils" name = "zellij-tile-utils"
version = "0.17.0" version = "0.18.0"
authors = ["denis <denismaximov98@gmail.com>"] authors = ["denis <denismaximov98@gmail.com>"]
edition = "2018" edition = "2018"
description = "A utility library for Zellij plugins" description = "A utility library for Zellij plugins"

View file

@ -1,6 +1,6 @@
[package] [package]
name = "zellij-tile" name = "zellij-tile"
version = "0.17.0" version = "0.18.0"
authors = ["Brooks J Rady <b.j.rady@gmail.com>"] authors = ["Brooks J Rady <b.j.rady@gmail.com>"]
edition = "2018" edition = "2018"
description = "A small client-side library for writing Zellij plugins" description = "A small client-side library for writing Zellij plugins"

View file

@ -35,6 +35,7 @@ pub enum Event {
Timer(f64), Timer(f64),
CopyToClipboard, CopyToClipboard,
InputReceived, InputReceived,
Visible(bool),
} }
/// Describes the different input modes, which change the way that keystrokes will be interpreted. /// Describes the different input modes, which change the way that keystrokes will be interpreted.

View file

@ -37,6 +37,10 @@ pub fn open_file(path: &Path) {
pub fn set_timeout(secs: f64) { pub fn set_timeout(secs: f64) {
unsafe { host_set_timeout(secs) }; unsafe { host_set_timeout(secs) };
} }
pub fn exec_cmd(cmd: &[&str]) {
object_to_stdout(&cmd);
unsafe { host_exec_cmd() };
}
// Internal Functions // Internal Functions
@ -60,4 +64,5 @@ extern "C" {
fn host_get_plugin_ids(); fn host_get_plugin_ids();
fn host_open_file(); fn host_open_file();
fn host_set_timeout(secs: f64); fn host_set_timeout(secs: f64);
fn host_exec_cmd();
} }

View file

@ -1,6 +1,6 @@
[package] [package]
name = "zellij-utils" name = "zellij-utils"
version = "0.17.0" version = "0.18.0"
authors = ["Kunal Mohan <kunalmohan99@gmail.com>"] authors = ["Kunal Mohan <kunalmohan99@gmail.com>"]
edition = "2018" edition = "2018"
description = "A utility library for Zellij client and server" description = "A utility library for Zellij client and server"
@ -27,9 +27,10 @@ structopt = "0.3"
strum = "0.20.0" strum = "0.20.0"
termion = "1.5.0" termion = "1.5.0"
vte = "0.10.1" 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" log = "0.4.14"
log4rs = "1.0.0" log4rs = "1.0.0"
unicode-width = "0.1.8"
[dependencies.async-std] [dependencies.async-std]
version = "1.3.0" version = "1.3.0"

View file

@ -182,6 +182,8 @@ keybinds:
key: [Ctrl: 'p',] key: [Ctrl: 'p',]
- action: [SwitchToMode: Session,] - action: [SwitchToMode: Session,]
key: [Ctrl: 'o',] key: [Ctrl: 'o',]
- action: [SwitchToMode: Resize,]
key: [Ctrl: 'n',]
- action: [ScrollToBottom, SwitchToMode: Normal,] - action: [ScrollToBottom, SwitchToMode: Normal,]
key: [Ctrl: 'c',] key: [Ctrl: 'c',]
- action: [Quit,] - action: [Quit,]

View file

@ -7,7 +7,8 @@ template:
split_size: split_size:
Fixed: 1 Fixed: 1
run: run:
plugin: tab-bar plugin:
path: tab-bar
- direction: Vertical - direction: Vertical
body: true body: true
- direction: Vertical - direction: Vertical
@ -15,6 +16,7 @@ template:
split_size: split_size:
Fixed: 2 Fixed: 2
run: run:
plugin: status-bar plugin:
path: status-bar
tabs: tabs:
- direction: Vertical - direction: Vertical

View file

@ -7,6 +7,7 @@ template:
split_size: split_size:
Fixed: 1 Fixed: 1
run: run:
plugin: tab-bar plugin:
path: tab-bar
- direction: Vertical - direction: Vertical
body: true body: true

View file

@ -7,7 +7,8 @@ template:
split_size: split_size:
Fixed: 1 Fixed: 1
run: run:
plugin: tab-bar plugin:
path: tab-bar
- direction: Vertical - direction: Vertical
body: true body: true
- direction: Vertical - direction: Vertical
@ -15,7 +16,8 @@ template:
split_size: split_size:
Fixed: 2 Fixed: 2
run: run:
plugin: status-bar plugin:
path: status-bar
tabs: tabs:
- direction: Vertical - direction: Vertical
parts: parts:
@ -23,5 +25,6 @@ tabs:
split_size: split_size:
Percent: 20 Percent: 20
run: run:
plugin: strider plugin:
path: strider
- direction: Horizontal - direction: Horizontal

View file

@ -62,6 +62,13 @@ pub enum Command {
Sessions(Sessions), 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)] #[derive(Debug, StructOpt, Clone, Serialize, Deserialize)]
pub enum Sessions { pub enum Sessions {
/// List active sessions /// List active sessions
@ -78,5 +85,13 @@ pub enum Sessions {
/// zellij client (if any) and attach to this. /// zellij client (if any) and attach to this.
#[structopt(long, short)] #[structopt(long, short)]
force: bool, 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<SessionCommand>,
}, },
} }

View file

@ -237,6 +237,7 @@ pub enum PtyContext {
SpawnTerminal, SpawnTerminal,
SpawnTerminalVertically, SpawnTerminalVertically,
SpawnTerminalHorizontally, SpawnTerminalHorizontally,
UpdateActivePane,
NewTab, NewTab,
ClosePane, ClosePane,
CloseTab, CloseTab,

View file

@ -15,6 +15,8 @@ pub struct RunCommand {
pub command: PathBuf, pub command: PathBuf,
#[serde(default)] #[serde(default)]
pub args: Vec<String>, pub args: Vec<String>,
#[serde(default)]
pub cwd: Option<PathBuf>,
} }
/// Intermediate representation /// Intermediate representation
@ -25,6 +27,8 @@ pub struct RunCommandAction {
#[serde(default)] #[serde(default)]
pub args: Vec<String>, pub args: Vec<String>,
#[serde(default)] #[serde(default)]
pub cwd: Option<PathBuf>,
#[serde(default)]
pub direction: Option<Direction>, pub direction: Option<Direction>,
} }
@ -33,6 +37,7 @@ impl From<RunCommandAction> for RunCommand {
RunCommand { RunCommand {
command: action.command, command: action.command,
args: action.args, args: action.args,
cwd: action.cwd,
} }
} }
} }

View file

@ -99,16 +99,21 @@ impl TryFrom<&CliArgs> for Config {
impl Config { impl Config {
/// Uses defaults, but lets config override them. /// Uses defaults, but lets config override them.
pub fn from_yaml(yaml_config: &str) -> ConfigResult { pub fn from_yaml(yaml_config: &str) -> ConfigResult {
let config_from_yaml: ConfigFromYaml = serde_yaml::from_str(yaml_config)?; let config_from_yaml: Option<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;
Ok(Config { match config_from_yaml {
keybinds, None => Ok(Config::default()),
options, Some(config) => {
themes, 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. /// Deserializes from given path.
@ -275,8 +280,10 @@ mod config_test {
#[test] #[test]
fn try_from_cli_args_with_config() { fn try_from_cli_args_with_config() {
let arbitrary_config = PathBuf::from("nonexistent.yaml"); let arbitrary_config = PathBuf::from("nonexistent.yaml");
let mut opts = CliArgs::default(); let opts = CliArgs {
opts.config = Some(arbitrary_config); config: Some(arbitrary_config),
..Default::default()
};
println!("OPTS= {:?}", opts); println!("OPTS= {:?}", opts);
let result = Config::try_from(&opts); let result = Config::try_from(&opts);
assert!(result.is_err()); assert!(result.is_err());
@ -285,11 +292,13 @@ mod config_test {
#[test] #[test]
fn try_from_cli_args_with_option_clean() { fn try_from_cli_args_with_option_clean() {
use crate::setup::Setup; use crate::setup::Setup;
let mut opts = CliArgs::default(); let opts = CliArgs {
opts.command = Some(Command::Setup(Setup { command: Some(Command::Setup(Setup {
clean: true, clean: true,
..Setup::default() ..Setup::default()
})); })),
..Default::default()
};
let result = Config::try_from(&opts); let result = Config::try_from(&opts);
assert!(result.is_ok()); assert!(result.is_ok());
} }

View file

@ -53,11 +53,19 @@ pub enum SplitSize {
#[serde(crate = "self::serde")] #[serde(crate = "self::serde")]
pub enum Run { pub enum Run {
#[serde(rename = "plugin")] #[serde(rename = "plugin")]
Plugin(Option<PathBuf>), Plugin(Option<RunPlugin>),
#[serde(rename = "command")] #[serde(rename = "command")]
Command(RunCommand), 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. // The layout struct ultimately used to build the layouts.
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
#[serde(crate = "self::serde")] #[serde(crate = "self::serde")]
@ -95,9 +103,12 @@ impl LayoutFromYaml {
let mut layout = String::new(); let mut layout = String::new();
layout_file.read_to_string(&mut layout)?; layout_file.read_to_string(&mut layout)?;
let layout: LayoutFromYaml = serde_yaml::from_str(&layout)?; let layout: Option<LayoutFromYaml> = 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. // It wants to use Path here, but that doesn't compile.
@ -216,6 +227,8 @@ pub struct TabLayout {
pub parts: Vec<TabLayout>, pub parts: Vec<TabLayout>,
pub split_size: Option<SplitSize>, pub split_size: Option<SplitSize>,
pub run: Option<Run>, pub run: Option<Run>,
#[serde(default)]
pub name: String,
} }
impl Layout { impl Layout {
@ -419,6 +432,7 @@ impl Default for TabLayout {
parts: vec![], parts: vec![],
split_size: None, split_size: None,
run: None, run: None,
name: String::new(),
} }
} }
} }

View file

@ -6,14 +6,16 @@ template:
split_size: split_size:
Fixed: 1 Fixed: 1
run: run:
plugin: tab-bar plugin:
path: tab-bar
- direction: Horizontal - direction: Horizontal
body: true body: true
- direction: Vertical - direction: Vertical
split_size: split_size:
Fixed: 2 Fixed: 2
run: run:
plugin: status-bar plugin:
path: status-bar
tabs: tabs:
- direction: Vertical - direction: Vertical

View file

@ -92,7 +92,7 @@ fn merge_keybinds_overwrites_same_keys() {
let mut keybinds_self = Keybinds::new(); let mut keybinds_self = Keybinds::new();
keybinds_self keybinds_self
.0 .0
.insert(InputMode::Normal, mode_keybinds_self.clone()); .insert(InputMode::Normal, mode_keybinds_self);
let mut keybinds_other = Keybinds::new(); let mut keybinds_other = Keybinds::new();
keybinds_other keybinds_other
.0 .0
@ -152,7 +152,7 @@ fn no_unbind_unbinds_none() {
fn last_keybind_is_taken() { fn last_keybind_is_taken() {
let actions_1 = vec![Action::NoOp, Action::NewTab(None)]; let actions_1 = vec![Action::NoOp, Action::NewTab(None)];
let keyaction_1 = KeyActionFromYaml { let keyaction_1 = KeyActionFromYaml {
action: actions_1.clone(), action: actions_1,
key: vec![Key::F(1), Key::Backspace, Key::Char('t')], key: vec![Key::F(1), Key::Backspace, Key::Char('t')],
}; };
let actions_2 = vec![Action::GoToTab(1)]; let actions_2 = vec![Action::GoToTab(1)];
@ -184,7 +184,7 @@ fn last_keybind_overwrites() {
let mut expected = ModeKeybinds::new(); let mut expected = ModeKeybinds::new();
expected.0.insert(Key::F(1), actions_2.clone()); 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); expected.0.insert(Key::Char('t'), actions_2);
assert_eq!(expected, ModeKeybinds::from(vec![keyaction_1, keyaction_2])); assert_eq!(expected, ModeKeybinds::from(vec![keyaction_1, keyaction_2]));

View file

@ -45,7 +45,10 @@ fn default_layout_merged_correctly() {
borderless: true, borderless: true,
parts: vec![], parts: vec![],
split_size: Some(SplitSize::Fixed(1)), 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 { Layout {
direction: Direction::Vertical, direction: Direction::Vertical,
@ -59,7 +62,10 @@ fn default_layout_merged_correctly() {
borderless: true, borderless: true,
parts: vec![], parts: vec![],
split_size: Some(SplitSize::Fixed(2)), 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, split_size: None,
@ -83,7 +89,10 @@ fn default_layout_new_tab_correct() {
borderless: true, borderless: true,
parts: vec![], parts: vec![],
split_size: Some(SplitSize::Fixed(1)), 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 { Layout {
direction: Direction::Horizontal, direction: Direction::Horizontal,
@ -97,7 +106,10 @@ fn default_layout_new_tab_correct() {
borderless: true, borderless: true,
parts: vec![], parts: vec![],
split_size: Some(SplitSize::Fixed(2)), 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, split_size: None,
@ -253,7 +265,10 @@ fn three_panes_with_tab_and_default_plugins_merged_correctly() {
borderless: false, borderless: false,
parts: vec![], parts: vec![],
split_size: Some(SplitSize::Fixed(1)), 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 { Layout {
direction: Direction::Vertical, direction: Direction::Vertical,
@ -297,7 +312,10 @@ fn three_panes_with_tab_and_default_plugins_merged_correctly() {
borderless: false, borderless: false,
parts: vec![], parts: vec![],
split_size: Some(SplitSize::Fixed(2)), 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, split_size: None,
@ -321,7 +339,10 @@ fn three_panes_with_tab_and_default_plugins_new_tab_is_correct() {
borderless: false, borderless: false,
parts: vec![], parts: vec![],
split_size: Some(SplitSize::Fixed(1)), 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 { Layout {
direction: Direction::Horizontal, direction: Direction::Horizontal,
@ -335,7 +356,10 @@ fn three_panes_with_tab_and_default_plugins_new_tab_is_correct() {
borderless: false, borderless: false,
parts: vec![], parts: vec![],
split_size: Some(SplitSize::Fixed(2)), 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, split_size: None,

View file

@ -7,6 +7,7 @@ use std::os::unix::fs::PermissionsExt;
use std::path::Path; use std::path::Path;
use std::{fs, io}; use std::{fs, io};
use strip_ansi_escapes::strip; use strip_ansi_escapes::strip;
use unicode_width::UnicodeWidthStr;
use zellij_tile::data::{Palette, PaletteColor, PaletteSource, ThemeHue}; use zellij_tile::data::{Palette, PaletteColor, PaletteSource, ThemeHue};
const UNIX_PERMISSIONS: u32 = 0o700; const UNIX_PERMISSIONS: u32 = 0o700;
@ -18,10 +19,7 @@ pub fn set_permissions(path: &Path) -> io::Result<()> {
} }
pub fn ansi_len(s: &str) -> usize { pub fn ansi_len(s: &str) -> usize {
from_utf8(&strip(s.as_bytes()).unwrap()) from_utf8(&strip(s.as_bytes()).unwrap()).unwrap().width()
.unwrap()
.chars()
.count()
} }
pub fn adjust_to_size(s: &str, rows: usize, columns: usize) -> String { pub fn adjust_to_size(s: &str, rows: usize, columns: usize) -> String {