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/)
## [Unreleased]
* Fix: Properly open new pane with CWD also when switching to a new tab (https://github.com/zellij-org/zellij/pull/729)
* Feature: Option to create a new session if attach fails (`zellij attach --create`) (https://github.com/zellij-org/zellij/pull/731)
* Feature: Added the new `Visible` event, allowing plugins to detect if they are visible in the current tab (https://github.com/zellij-org/zellij/pull/717)
* Feature: Plugins now have access to a data directory at `/data` the working directory is now mounted at `/host` instead of `.` (https://github.com/zellij-org/zellij/pull/723)
## [0.17.0] - 2021-09-15
* New panes/tabs now open in CWD of focused pane (https://github.com/zellij-org/zellij/pull/691)
* Fix bug when opening new tab the new pane's viewport would sometimes be calculated incorrectly (https://github.com/zellij-org/zellij/pull/683)
* Fix bug when in some cases closing a tab would not clear the previous pane's contents (https://github.com/zellij-org/zellij/pull/684)
* Fix bug where tabs would sometimes be created with the wrong index in their name (https://github.com/zellij-org/zellij/pull/686)
* Fix bug where wide chars would mess up pane titles (https://github.com/zellij-org/zellij/pull/698)
* Fix various borderless-frame in viewport bugs (https://github.com/zellij-org/zellij/pull/697)
* Fix example configuration file (https://github.com/zellij-org/zellij/pull/693)
* Fix various tab bar responsiveness issues (https://github.com/zellij-org/zellij/pull/703)
* Allow plugins to run system commands (https://github.com/zellij-org/zellij/pull/666)
* This has also added a temporary new permission flag that needs to be specified in the layout. This is a breaking change:
```yaml
...
plugin: strider
...
```
has become:
```yaml
plugin:
path: strider
```
A plugin can be given command executing permission with:
```yaml
plugin:
path: strider
_allow_exec_host_cmd: true
```
* Use the unicode width in tab-bar plugin, for tab names (https://github.com/zellij-org/zellij/pull/709)
* Fix automated builds that make use of the `setup` subcommand (https://github.com/zellij-org/zellij/pull/711)
* Add option to specify a tabs name in the tab `layout` file (https://github.com/zellij-org/zellij/pull/715)
* Improve handling of empty valid `yaml` files (https://github.com/zellij-org/zellij/pull/716)
* Add options subcommand to attach (https://github.com/zellij-org/zellij/pull/718)
* Fix: do not pad empty pane frame title (https://github.com/zellij-org/zellij/pull/724)
* Fix: Do not overflow empty lines when resizing panes (https://github.com/zellij-org/zellij/pull/725)
## [0.16.0] - 2021-08-31
* Plugins don't crash zellij anymore on receiving mouse events (https://github.com/zellij-org/zellij/pull/620)

71
Cargo.lock generated
View file

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

View file

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

View file

@ -13,9 +13,9 @@
<p align="center">
<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>
# 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.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

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

View file

@ -1,4 +1,5 @@
use ansi_term::ANSIStrings;
use unicode_width::UnicodeWidthStr;
use crate::{LinePart, ARROW_SEPARATOR};
use zellij_tile::prelude::*;
@ -8,39 +9,82 @@ fn get_current_title_len(current_title: &[LinePart]) -> usize {
current_title.iter().map(|p| p.len).sum()
}
// move elements from before_active and after_active into tabs_to_render while they fit in cols
// adds collapsed_tabs to the left and right if there's left over tabs that don't fit
fn populate_tabs_in_tab_line(
tabs_before_active: &mut Vec<LinePart>,
tabs_after_active: &mut Vec<LinePart>,
tabs_to_render: &mut Vec<LinePart>,
cols: usize,
palette: Palette,
capabilities: PluginCapabilities,
) {
let mut take_next_tab_from_tabs_after = true;
let mut middle_size = get_current_title_len(tabs_to_render);
let mut total_left = 0;
let mut total_right = 0;
loop {
if tabs_before_active.is_empty() && tabs_after_active.is_empty() {
let left_count = tabs_before_active.len();
let right_count = tabs_after_active.len();
let collapsed_left = left_more_message(left_count, palette, tab_separator(capabilities));
let collapsed_right = right_more_message(right_count, palette, tab_separator(capabilities));
let total_size = collapsed_left.len + middle_size + collapsed_right.len;
if total_size > cols {
// break and dont add collapsed tabs to tabs_to_render, they will not fit
break;
}
let current_title_len = get_current_title_len(tabs_to_render);
if current_title_len >= cols {
break;
}
let should_take_next_tab = take_next_tab_from_tabs_after;
let can_take_next_tab = !tabs_after_active.is_empty()
&& tabs_after_active.get(0).unwrap().len + current_title_len <= cols;
let can_take_previous_tab = !tabs_before_active.is_empty()
&& tabs_before_active.last().unwrap().len + current_title_len <= cols;
if should_take_next_tab && can_take_next_tab {
let next_tab = tabs_after_active.remove(0);
tabs_to_render.push(next_tab);
take_next_tab_from_tabs_after = false;
} else if can_take_previous_tab {
let previous_tab = tabs_before_active.pop().unwrap();
tabs_to_render.insert(0, previous_tab);
take_next_tab_from_tabs_after = true;
} else if can_take_next_tab {
let next_tab = tabs_after_active.remove(0);
tabs_to_render.push(next_tab);
take_next_tab_from_tabs_after = false;
let left = if let Some(tab) = tabs_before_active.last() {
tab.len
} else {
usize::MAX
};
let right = if let Some(tab) = tabs_after_active.first() {
tab.len
} else {
usize::MAX
};
// total size is shortened if the next tab to be added is the last one, as that will remove the collapsed tab
let size_by_adding_left =
left.saturating_add(total_size)
.saturating_sub(if left_count == 1 {
collapsed_left.len
} else {
0
});
let size_by_adding_right =
right
.saturating_add(total_size)
.saturating_sub(if right_count == 1 {
collapsed_right.len
} else {
0
});
let left_fits = size_by_adding_left <= cols;
let right_fits = size_by_adding_right <= cols;
// active tab is kept in the middle by adding to the side that
// has less width, or if the tab on the other side doesn' fit
if (total_left <= total_right || !right_fits) && left_fits {
// add left tab
let tab = tabs_before_active.pop().unwrap();
middle_size += tab.len;
total_left += tab.len;
tabs_to_render.insert(0, tab);
} else if right_fits {
// add right tab
let tab = tabs_after_active.remove(0);
middle_size += tab.len;
total_right += tab.len;
tabs_to_render.push(tab);
} else {
// there's either no space to add more tabs or no more tabs to add, so we're done
tabs_to_render.insert(0, collapsed_left);
tabs_to_render.push(collapsed_right);
break;
}
}
@ -56,7 +100,8 @@ fn left_more_message(tab_count_to_the_left: usize, palette: Palette, separator:
" ← +many ".to_string()
};
// 238
let more_text_len = more_text.chars().count() + 2; // 2 for the arrows
// chars length plus separator length on both sides
let more_text_len = more_text.width() + 2 * separator.width();
let left_separator = style!(palette.cyan, palette.orange).paint(separator);
let more_styled_text = style!(palette.black, palette.orange)
.bold()
@ -85,7 +130,8 @@ fn right_more_message(
} else {
" +many → ".to_string()
};
let more_text_len = more_text.chars().count() + 1; // 2 for the arrow
// chars length plus separator length on both sides
let more_text_len = more_text.width() + 2 * separator.width();
let left_separator = style!(palette.cyan, palette.orange).paint(separator);
let more_styled_text = style!(palette.black, palette.orange)
.bold()
@ -101,48 +147,6 @@ fn right_more_message(
}
}
fn add_previous_tabs_msg(
tabs_before_active: &mut Vec<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> {
let prefix_text = " Zellij ".to_string();
@ -156,7 +160,7 @@ fn tab_line_prefix(session_name: Option<&str>, palette: Palette, cols: usize) ->
}];
if let Some(name) = session_name {
let name_part = format!("({}) ", name);
let name_part_len = name_part.chars().count();
let name_part_len = name_part.width();
let name_part_styled_text = style!(palette.white, palette.cyan).bold().paint(name_part);
if cols.saturating_sub(prefix_text_len) >= name_part_len {
parts.push(LinePart {
@ -184,7 +188,6 @@ pub fn tab_line(
palette: Palette,
capabilities: PluginCapabilities,
) -> Vec<LinePart> {
let mut tabs_to_render = Vec::new();
let mut tabs_after_active = all_tabs.split_off(active_tab_index);
let mut tabs_before_active = all_tabs;
let active_tab = if !tabs_after_active.is_empty() {
@ -194,38 +197,22 @@ pub fn tab_line(
};
let mut prefix = tab_line_prefix(session_name, palette, cols);
let prefix_len = get_current_title_len(&prefix);
if prefix_len + active_tab.len <= cols {
tabs_to_render.push(active_tab);
// if active tab alone won't fit in cols, don't draw any tabs
if prefix_len + active_tab.len > cols {
return prefix;
}
let mut tabs_to_render = vec![active_tab];
populate_tabs_in_tab_line(
&mut tabs_before_active,
&mut tabs_after_active,
&mut tabs_to_render,
cols.saturating_sub(prefix_len),
palette,
capabilities,
);
let mut tab_line: Vec<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.append(&mut tabs_to_render);
prefix
}

View file

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

View file

@ -1,11 +1,12 @@
use crate::{line::tab_separator, LinePart};
use ansi_term::ANSIStrings;
use unicode_width::UnicodeWidthStr;
use zellij_tile::prelude::*;
use zellij_tile_utils::style;
pub fn active_tab(text: String, palette: Palette, separator: &str) -> LinePart {
let left_separator = style!(palette.cyan, palette.green).paint(separator);
let tab_text_len = text.chars().count() + 4; // 2 for left and right separators, 2 for the text padding
let tab_text_len = text.width() + 2 + separator.width() * 2; // 2 for left and right separators, 2 for the text padding
let tab_styled_text = style!(palette.black, palette.green)
.bold()
.paint(format!(" {} ", text));
@ -22,7 +23,7 @@ pub fn active_tab(text: String, palette: Palette, separator: &str) -> LinePart {
pub fn non_active_tab(text: String, palette: Palette, separator: &str) -> LinePart {
let left_separator = style!(palette.cyan, palette.fg).paint(separator);
let tab_text_len = text.chars().count() + 4; // 2 for left and right separators, 2 for the padding
let tab_text_len = text.width() + 2 + separator.width() * 2; // 2 for left and right separators, 2 for the text padding
let tab_styled_text = style!(palette.black, palette.fg)
.bold()
.paint(format!(" {} ", text));

View file

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

View file

@ -4,12 +4,15 @@ mod sessions;
mod tests;
use crate::install::populate_data_dir;
use sessions::{assert_session, assert_session_ne, get_active_session, list_sessions};
use sessions::{
assert_session, assert_session_ne, get_active_session, get_sessions, list_sessions,
print_sessions, session_exists, ActiveSession,
};
use std::process;
use zellij_client::{os_input_output::get_client_os_input, start_client, ClientInfo};
use zellij_server::{os_input_output::get_server_os_input, start_server};
use zellij_utils::{
cli::{CliArgs, Command, Sessions},
cli::{CliArgs, Command, SessionCommand, Sessions},
consts::{ZELLIJ_TMP_DIR, ZELLIJ_TMP_LOG_DIR},
logging::*,
setup::{get_default_data_dir, Setup},
@ -36,6 +39,14 @@ pub fn main() {
};
start_server(Box::new(os_input), path);
} else {
let (config, layout, config_options) = match Setup::from_options(&opts) {
Ok(results) => results,
Err(e) => {
eprintln!("{}", e);
process::exit(1);
}
};
let os_input = match get_client_os_input() {
Ok(os_input) => os_input,
Err(e) => {
@ -44,40 +55,77 @@ pub fn main() {
}
};
if let Some(Command::Sessions(Sessions::Attach {
mut session_name,
session_name,
force,
create,
options,
})) = opts.command.clone()
{
if let Some(session) = session_name.as_ref() {
assert_session(session);
} else {
session_name = Some(get_active_session());
}
let config_options = match options {
Some(SessionCommand::Options(o)) => config_options.merge(o),
None => config_options,
};
let (config, _, config_options) = match Setup::from_options(&opts) {
Ok(results) => results,
Err(e) => {
eprintln!("{}", e);
process::exit(1);
let (client, attach_layout) = match session_name.as_ref() {
Some(session) => {
if create {
if !session_exists(session).unwrap() {
(ClientInfo::New(session_name.unwrap()), layout)
} else {
(
ClientInfo::Attach(
session_name.unwrap(),
force,
config_options.clone(),
),
None,
)
}
} else {
assert_session(session);
(
ClientInfo::Attach(
session_name.unwrap(),
force,
config_options.clone(),
),
None,
)
}
}
None => match get_active_session() {
ActiveSession::None => {
if create {
(
ClientInfo::New(names::Generator::default().next().unwrap()),
layout,
)
} else {
println!("No active zellij sessions found.");
process::exit(1);
}
}
ActiveSession::One(session_name) => (
ClientInfo::Attach(session_name, force, config_options.clone()),
None,
),
ActiveSession::Many => {
println!("Please specify the session name to attach to. The following sessions are active:");
print_sessions(get_sessions().unwrap());
process::exit(1);
}
},
};
start_client(
Box::new(os_input),
opts,
config,
ClientInfo::Attach(session_name.unwrap(), force, config_options),
None,
config_options,
client,
attach_layout,
);
} else {
let (config, layout, _) = match Setup::from_options(&opts) {
Ok(results) => results,
Err(e) => {
eprintln!("{}", e);
process::exit(1);
}
};
let session_name = opts
.session
.clone()
@ -93,6 +141,7 @@ pub fn main() {
Box::new(os_input),
opts,
config,
config_options,
ClientInfo::New(session_name),
layout,
);

View file

@ -6,7 +6,7 @@ use zellij_utils::{
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) {
Ok(files) => {
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());
sessions.iter().for_each(|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() {
Ok(mut sessions) => {
if sessions.len() == 1 {
return sessions.pop().unwrap();
return ActiveSession::One(sessions.pop().unwrap());
}
if sessions.is_empty() {
println!("No active zellij sessions found.");
ActiveSession::None
} else {
println!("Please specify the session name to attach to. The following sessions are active:");
print_sessions(sessions);
ActiveSession::Many
}
}
Err(e) => eprintln!("Error occured: {:?}", e),
Err(e) => {
eprintln!("Error occured: {:?}", e);
process::exit(1);
}
}
process::exit(1);
}
pub(crate) fn list_sessions() {
@ -95,15 +102,30 @@ pub(crate) fn list_sessions() {
process::exit(exit_code);
}
pub(crate) fn assert_session(name: &str) {
match get_sessions() {
pub(crate) fn session_exists(name: &str) -> Result<bool, io::ErrorKind> {
return match get_sessions() {
Ok(sessions) => {
if sessions.iter().any(|s| s == name) {
return;
return Ok(true);
}
println!("No session named {:?} found.", name);
Ok(false)
}
Err(e) => Err(e),
};
}
pub(crate) fn assert_session(name: &str) {
match session_exists(name) {
Ok(result) => {
if result {
return;
} else {
println!("No session named {:?} found.", name);
}
}
Err(e) => {
eprintln!("Error occured: {:?}", e);
}
Err(e) => eprintln!("Error occured: {:?}", e),
};
process::exit(1);
}

View file

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

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)))
.unwrap();
channel.shell().unwrap();
channel
.write_all(format!("export PS1=\"$ \"\n").as_bytes())
.unwrap();
channel.write_all("export PS1=\"$ \"\n".as_bytes()).unwrap();
channel.flush().unwrap();
}
@ -154,7 +152,7 @@ impl<'a> RemoteTerminal<'a> {
format!("x: {}, y: {}", self.cursor_x, self.cursor_y)
}
pub fn send_key(&mut self, key: &[u8]) {
self.channel.write(key).unwrap();
self.channel.write_all(key).unwrap();
self.channel.flush().unwrap();
}
pub fn change_size(&mut self, cols: u32, rows: u32) {

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

View file

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

View file

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

View file

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

View file

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

View file

@ -149,7 +149,7 @@ mod logging_pipe_test {
let test_buffer = "Testing write".as_bytes();
pipe.write(test_buffer).expect("Err write");
pipe.write_all(test_buffer).expect("Err write");
pipe.flush().expect("Err flush");
assert_eq!(pipe.buffer.len(), test_buffer.len());
@ -161,7 +161,7 @@ mod logging_pipe_test {
let test_buffer = "Testing write \n".as_bytes();
pipe.write(test_buffer).expect("Err write");
pipe.write_all(test_buffer).expect("Err write");
pipe.flush().expect("Err flush");
assert_eq!(pipe.buffer.len(), 0);
@ -174,7 +174,7 @@ mod logging_pipe_test {
let test_buffer = "Testing write \n".as_bytes();
let test_buffer2 = "And the rest".as_bytes();
pipe.write(
pipe.write_all(
[
test_buffer,
test_buffer,
@ -197,7 +197,7 @@ mod logging_pipe_test {
let test_buffer = "Testing write \n".as_bytes();
pipe.write(
pipe.write_all(
[
test_buffer,
test_buffer,
@ -223,7 +223,7 @@ mod logging_pipe_test {
// make sure it's not valid utf-8 string if we drop last symbol
assert!(std::str::from_utf8(&test_buffer[..test_buffer.len() - 1]).is_err());
pipe.write(&test_buffer[..test_buffer.len() - 1])
pipe.write_all(&test_buffer[..test_buffer.len() - 1])
.expect("Err write");
pipe.flush().expect("Err flush");
@ -237,7 +237,7 @@ mod logging_pipe_test {
let mut pipe = LoggingPipe::new("TestPipe", 0);
let test_buffer = "Testing write \n".as_bytes();
pipe.write(
pipe.write_all(
[test_buffer, test_buffer, b"\n", b"\n", b"\n"]
.concat()
.as_slice(),

View file

@ -1,4 +1,8 @@
#[cfg(target_os = "macos")]
use darwin_libproc;
use std::env;
use std::fs;
use std::os::unix::io::RawFd;
use std::os::unix::process::CommandExt;
use std::path::PathBuf;
@ -10,7 +14,7 @@ use zellij_utils::{async_std, interprocess, libc, nix, signal_hook, zellij_tile}
use async_std::fs::File as AsyncFile;
use async_std::os::unix::io::FromRawFd;
use interprocess::local_socket::LocalSocketStream;
use nix::pty::{forkpty, Winsize};
use nix::pty::{forkpty, ForkptyResult, Winsize};
use nix::sys::signal::{kill, Signal};
use nix::sys::termios;
use nix::sys::wait::waitpid;
@ -29,6 +33,7 @@ use zellij_utils::{
use async_std::io::ReadExt;
pub use async_trait::async_trait;
use byteorder::{BigEndian, ByteOrder};
pub use nix::unistd::Pid;
@ -92,44 +97,94 @@ fn handle_command_exit(mut child: Child) {
}
}
fn handle_fork_pty(
fork_pty_res: ForkptyResult,
cmd: RunCommand,
parent_fd: RawFd,
child_fd: RawFd,
) -> (RawFd, ChildId) {
let pid_primary = fork_pty_res.master;
let (pid_secondary, pid_shell) = match fork_pty_res.fork_result {
ForkResult::Parent { child } => {
let pid_shell = read_from_pipe(parent_fd, child_fd);
(child, pid_shell)
}
ForkResult::Child => {
let child = unsafe {
let command = &mut Command::new(cmd.command);
if let Some(current_dir) = cmd.cwd {
command.current_dir(current_dir);
}
command
.args(&cmd.args)
.pre_exec(|| -> std::io::Result<()> {
// this is the "unsafe" part, for more details please see:
// https://doc.rust-lang.org/std/os/unix/process/trait.CommandExt.html#notes-and-safety
unistd::setpgid(Pid::from_raw(0), Pid::from_raw(0))
.expect("failed to create a new process group");
Ok(())
})
.spawn()
.expect("failed to spawn")
};
unistd::tcsetpgrp(0, Pid::from_raw(child.id() as i32))
.expect("faled to set child's forceground process group");
write_to_pipe(child.id(), parent_fd, child_fd);
handle_command_exit(child);
::std::process::exit(0);
}
};
(
pid_primary,
ChildId {
primary: pid_secondary,
shell: pid_shell.map(|pid| Pid::from_raw(pid as i32)),
},
)
}
/// Spawns a new terminal from the parent terminal with [`termios`](termios::Termios)
/// `orig_termios`.
///
fn handle_terminal(cmd: RunCommand, orig_termios: termios::Termios) -> (RawFd, Pid) {
let (pid_primary, pid_secondary): (RawFd, Pid) = {
match forkpty(None, Some(&orig_termios)) {
Ok(fork_pty_res) => {
let pid_primary = fork_pty_res.master;
let pid_secondary = match fork_pty_res.fork_result {
ForkResult::Parent { child } => child,
ForkResult::Child => {
let child = unsafe {
Command::new(cmd.command)
.args(&cmd.args)
.pre_exec(|| -> std::io::Result<()> {
// this is the "unsafe" part, for more details please see:
// https://doc.rust-lang.org/std/os/unix/process/trait.CommandExt.html#notes-and-safety
unistd::setpgid(Pid::from_raw(0), Pid::from_raw(0))
.expect("failed to create a new process group");
Ok(())
})
.spawn()
.expect("failed to spawn")
};
unistd::tcsetpgrp(0, Pid::from_raw(child.id() as i32))
.expect("faled to set child's forceground process group");
handle_command_exit(child);
::std::process::exit(0);
}
};
(pid_primary, pid_secondary)
}
Err(e) => {
panic!("failed to fork {:?}", e);
}
fn handle_terminal(cmd: RunCommand, orig_termios: termios::Termios) -> (RawFd, ChildId) {
// Create a pipe to allow the child the communicate the shell's pid to it's
// parent.
let (parent_fd, child_fd) = unistd::pipe().expect("failed to create pipe");
match forkpty(None, Some(&orig_termios)) {
Ok(fork_pty_res) => handle_fork_pty(fork_pty_res, cmd, parent_fd, child_fd),
Err(e) => {
panic!("failed to fork {:?}", e);
}
};
(pid_primary, pid_secondary)
}
}
/// Write to a pipe given both file descriptors
fn write_to_pipe(data: u32, parent_fd: RawFd, child_fd: RawFd) {
let mut buff = [0; 4];
BigEndian::write_u32(&mut buff, data);
if unistd::close(parent_fd).is_err() {
return;
}
if unistd::write(child_fd, &buff).is_err() {
return;
}
unistd::close(child_fd).unwrap_or_default();
}
/// Read from a pipe given both file descriptors
fn read_from_pipe(parent_fd: RawFd, child_fd: RawFd) -> Option<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`
@ -145,11 +200,11 @@ fn handle_terminal(cmd: RunCommand, orig_termios: termios::Termios) -> (RawFd, P
/// This function will panic if both the `EDITOR` and `VISUAL` environment variables are not
/// set.
pub fn spawn_terminal(
terminal_action: Option<TerminalAction>,
terminal_action: TerminalAction,
orig_termios: termios::Termios,
) -> (RawFd, Pid) {
) -> (RawFd, ChildId) {
let cmd = match terminal_action {
Some(TerminalAction::OpenFile(file_to_open)) => {
TerminalAction::OpenFile(file_to_open) => {
if env::var("EDITOR").is_err() && env::var("VISUAL").is_err() {
panic!("Can't edit files if an editor is not defined. To fix: define the EDITOR or VISUAL environment variables with the path to your editor (eg. /usr/bin/vim)");
}
@ -160,15 +215,13 @@ pub fn spawn_terminal(
.into_os_string()
.into_string()
.expect("Not valid Utf8 Encoding")];
RunCommand { command, args }
}
Some(TerminalAction::RunCommand(command)) => command,
None => {
let command =
PathBuf::from(env::var("SHELL").expect("Could not find the SHELL variable"));
let args = vec![];
RunCommand { command, args }
RunCommand {
command,
args,
cwd: None,
}
}
TerminalAction::RunCommand(command) => command,
};
handle_terminal(cmd, orig_termios)
@ -214,8 +267,10 @@ impl AsyncReader for RawFdAsyncReader {
pub trait ServerOsApi: Send + Sync {
/// Sets the size of the terminal associated to file descriptor `fd`.
fn set_terminal_size_using_fd(&self, fd: RawFd, cols: u16, rows: u16);
/// Spawn a new terminal, with a terminal action.
fn spawn_terminal(&self, terminal_action: Option<TerminalAction>) -> (RawFd, Pid);
/// Spawn a new terminal, with a terminal action. The returned tuple contains the master file
/// descriptor of the forked psuedo terminal and a [ChildId] struct containing process id's for
/// the forked child process.
fn spawn_terminal(&self, terminal_action: TerminalAction) -> (RawFd, ChildId);
/// Read bytes from the standard output of the virtual terminal referred to by `fd`.
fn read_from_tty_stdout(&self, fd: RawFd, buf: &mut [u8]) -> Result<usize, nix::Error>;
/// Creates an `AsyncReader` that can be used to read from `fd` in an async context
@ -247,6 +302,8 @@ pub trait ServerOsApi: Send + Sync {
/// Update the receiver socket for the client
fn update_receiver(&mut self, stream: LocalSocketStream);
fn load_palette(&self) -> Palette;
/// Returns the current working directory for a given pid
fn get_cwd(&self, pid: Pid) -> Option<PathBuf>;
}
impl ServerOsApi for ServerOsInputOutput {
@ -255,7 +312,7 @@ impl ServerOsApi for ServerOsInputOutput {
set_terminal_size_using_fd(fd, cols, rows);
}
}
fn spawn_terminal(&self, terminal_action: Option<TerminalAction>) -> (RawFd, Pid) {
fn spawn_terminal(&self, terminal_action: TerminalAction) -> (RawFd, ChildId) {
let orig_termios = self.orig_termios.lock().unwrap();
spawn_terminal(terminal_action, orig_termios.clone())
}
@ -336,6 +393,18 @@ impl ServerOsApi for ServerOsInputOutput {
fn load_palette(&self) -> Palette {
default_palette()
}
#[cfg(target_os = "macos")]
fn get_cwd(&self, pid: Pid) -> Option<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> {
@ -353,3 +422,13 @@ pub fn get_server_os_input() -> Result<ServerOsInputOutput, nix::Error> {
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() {
if line.is_canonical {
canonical_lines_traversed += 1;
y_coordinates = i;
if canonical_lines_traversed == canonical_line_index + 1 {
y_coordinates = i;
break;
}
}
@ -628,6 +628,23 @@ impl Grid {
}
}
}
// trim lines after the last empty space that has no following character, because
// terminals don't trim empty lines
for line in viewport_canonical_lines.iter_mut() {
let mut trim_at = None;
for (index, character) in line.columns.iter().enumerate() {
if character.character != EMPTY_TERMINAL_CHARACTER.character {
trim_at = None;
} else if trim_at.is_none() {
trim_at = Some(index);
}
}
if let Some(trim_at) = trim_at {
line.columns.truncate(trim_at);
}
}
let mut new_viewport_rows = vec![];
for mut canonical_line in viewport_canonical_lines {
let mut canonical_line_parts: Vec<Row> = vec![];
@ -658,9 +675,11 @@ impl Grid {
}
new_viewport_rows.append(&mut canonical_line_parts);
}
self.viewport = new_viewport_rows;
let mut new_cursor_y = self.canonical_line_y_coordinates(cursor_canonical_line_index);
let new_cursor_x = (cursor_index_in_canonical_line / new_columns)
+ (cursor_index_in_canonical_line % new_columns);
let current_viewport_row_count = self.viewport.len();

View file

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

View file

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

View file

@ -3,7 +3,7 @@ source: zellij-server/src/panes/./unit/grid_tests.rs
expression: "format!(\"{:?}\", grid)"
---
00 (C): Welcome to fish, the friendly interactive shell
00 (C): Welcome to fish, the friendly interactive shell
01 (C): ⋊> ~/c/mosaic on main vim some-file 15:07:22
02 (C): ⋊> ~/c/mosaic on main 15:07:29

View file

@ -3,8 +3,8 @@ source: zellij-server/src/panes/./unit/grid_tests.rs
expression: "format!(\"{:?}\", grid)"
---
00 (C): ➜ mosaic git:(mosaic#130) emacs
01 (C): ➜ mosaic git:(mosaic#130) emacs -nw
00 (C): ➜ mosaic git:(mosaic#130) emacs
01 (C): ➜ mosaic git:(mosaic#130) emacs -nw
02 (C): ➜ mosaic git:(mosaic#130) exit
03 (C):

View file

@ -1,5 +1,5 @@
use crate::{
os_input_output::{AsyncReader, Pid, ServerOsApi},
os_input_output::{AsyncReader, ChildId, ServerOsApi},
panes::PaneId,
screen::ScreenInstruction,
thread_bus::{Bus, ThreadSenders},
@ -12,14 +12,16 @@ use async_std::{
};
use std::{
collections::HashMap,
env,
os::unix::io::RawFd,
path::PathBuf,
time::{Duration, Instant},
};
use zellij_utils::{
async_std,
errors::{get_current_ctx, ContextType, PtyContext},
input::{
command::TerminalAction,
command::{RunCommand, TerminalAction},
layout::{Layout, LayoutFromYaml, Run, TabLayout},
},
logging::debug_to_file,
@ -33,6 +35,7 @@ pub(crate) enum PtyInstruction {
SpawnTerminal(Option<TerminalAction>),
SpawnTerminalVertically(Option<TerminalAction>),
SpawnTerminalHorizontally(Option<TerminalAction>),
UpdateActivePane(Option<PaneId>),
NewTab(Option<TerminalAction>, Option<TabLayout>),
ClosePane(PaneId),
CloseTab(Vec<PaneId>),
@ -45,6 +48,7 @@ impl From<&PtyInstruction> for PtyContext {
PtyInstruction::SpawnTerminal(_) => PtyContext::SpawnTerminal,
PtyInstruction::SpawnTerminalVertically(_) => PtyContext::SpawnTerminalVertically,
PtyInstruction::SpawnTerminalHorizontally(_) => PtyContext::SpawnTerminalHorizontally,
PtyInstruction::UpdateActivePane(_) => PtyContext::UpdateActivePane,
PtyInstruction::ClosePane(_) => PtyContext::ClosePane,
PtyInstruction::CloseTab(_) => PtyContext::CloseTab,
PtyInstruction::NewTab(..) => PtyContext::NewTab,
@ -54,8 +58,9 @@ impl From<&PtyInstruction> for PtyContext {
}
pub(crate) struct Pty {
pub active_pane: Option<PaneId>,
pub bus: Bus<PtyInstruction>,
pub id_to_child_pid: HashMap<RawFd, Pid>,
pub id_to_child_pid: HashMap<RawFd, ChildId>,
debug_to_file: bool,
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)))
.unwrap();
}
PtyInstruction::UpdateActivePane(pane_id) => {
pty.set_active_pane(pane_id);
}
PtyInstruction::NewTab(terminal_action, tab_layout) => {
let tab_name = tab_layout.as_ref().and_then(|layout| {
if layout.name.is_empty() {
None
} else {
Some(layout.name.clone())
}
});
let merged_layout = layout.template.clone().insert_tab_layout(tab_layout);
pty.spawn_terminals_for_layout(merged_layout.into(), terminal_action.clone());
if let Some(tab_name) = tab_name {
// clear current name at first
pty.bus
.senders
.send_to_screen(ScreenInstruction::UpdateTabName(vec![0]))
.unwrap();
pty.bus
.senders
.send_to_screen(ScreenInstruction::UpdateTabName(tab_name.into_bytes()))
.unwrap();
}
}
PtyInstruction::ClosePane(id) => {
pty.close_pane(id);
@ -208,14 +236,30 @@ fn stream_terminal_bytes(
impl Pty {
pub fn new(bus: Bus<PtyInstruction>, debug_to_file: bool) -> Self {
Pty {
active_pane: None,
bus,
id_to_child_pid: HashMap::new(),
debug_to_file,
task_handles: HashMap::new(),
}
}
pub fn get_default_terminal(&self) -> TerminalAction {
TerminalAction::RunCommand(RunCommand {
args: vec![],
command: PathBuf::from(env::var("SHELL").expect("Could not find the SHELL variable")),
cwd: self
.active_pane
.and_then(|pane| match pane {
PaneId::Plugin(..) => None,
PaneId::Terminal(id) => self.id_to_child_pid.get(&id).and_then(|id| id.shell),
})
.and_then(|id| self.bus.os_input.as_ref().map(|input| input.get_cwd(id)))
.flatten(),
})
}
pub fn spawn_terminal(&mut self, terminal_action: Option<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
.os_input
.as_mut()
@ -228,7 +272,7 @@ impl Pty {
self.debug_to_file,
);
self.task_handles.insert(pid_primary, task_handle);
self.id_to_child_pid.insert(pid_primary, pid_secondary);
self.id_to_child_pid.insert(pid_primary, child_id);
pid_primary
}
pub fn spawn_terminals_for_layout(
@ -236,29 +280,26 @@ impl Pty {
layout: Layout,
default_shell: Option<TerminalAction>,
) {
let default_shell = default_shell.unwrap_or_else(|| self.get_default_terminal());
let extracted_run_instructions = layout.extract_run_instructions();
let mut new_pane_pids = vec![];
for run_instruction in extracted_run_instructions {
match run_instruction {
Some(Run::Command(command)) => {
let cmd = TerminalAction::RunCommand(command);
let (pid_primary, pid_secondary): (RawFd, Pid) = self
.bus
.os_input
.as_mut()
.unwrap()
.spawn_terminal(Some(cmd));
self.id_to_child_pid.insert(pid_primary, pid_secondary);
let (pid_primary, child_id): (RawFd, ChildId) =
self.bus.os_input.as_mut().unwrap().spawn_terminal(cmd);
self.id_to_child_pid.insert(pid_primary, child_id);
new_pane_pids.push(pid_primary);
}
None => {
let (pid_primary, pid_secondary): (RawFd, Pid) = self
let (pid_primary, child_id): (RawFd, ChildId) = self
.bus
.os_input
.as_mut()
.unwrap()
.spawn_terminal(default_shell.clone());
self.id_to_child_pid.insert(pid_primary, pid_secondary);
self.id_to_child_pid.insert(pid_primary, child_id);
new_pane_pids.push(pid_primary);
}
// Investigate moving plugin loading to here.
@ -285,10 +326,15 @@ impl Pty {
pub fn close_pane(&mut self, id: PaneId) {
match id {
PaneId::Terminal(id) => {
let child_pid = self.id_to_child_pid.remove(&id).unwrap();
let pids = self.id_to_child_pid.remove(&id).unwrap();
let handle = self.task_handles.remove(&id).unwrap();
task::block_on(async {
self.bus.os_input.as_mut().unwrap().kill(child_pid).unwrap();
self.bus
.os_input
.as_mut()
.unwrap()
.kill(pids.primary)
.unwrap();
let timeout = Duration::from_millis(100);
match async_timeout(timeout, handle.cancel()).await {
Ok(_) => {}
@ -297,7 +343,7 @@ impl Pty {
.os_input
.as_mut()
.unwrap()
.force_kill(child_pid)
.force_kill(pids.primary)
.unwrap();
}
};
@ -315,6 +361,9 @@ impl Pty {
self.close_pane(id);
});
}
pub fn set_active_pane(&mut self, pane_id: Option<PaneId>) {
self.active_pane = pane_id;
}
}
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.
pub fn switch_tab_next(&mut self) {
let active_tab_pos = self.get_active_tab().unwrap().position;
let new_tab_pos = (active_tab_pos + 1) % self.tabs.len();
for tab in self.tabs.values_mut() {
if tab.position == new_tab_pos {
tab.set_force_render();
self.tab_history.retain(|&e| e != Some(tab.index));
self.tab_history.push(self.active_tab_index);
self.active_tab_index = Some(tab.index);
break;
}
}
self.update_tabs();
self.render();
self.switch_active_tab(new_tab_pos);
}
/// Sets this [`Screen`]'s active [`Tab`] to the previous tab.
@ -214,32 +229,12 @@ impl Screen {
} else {
active_tab_pos - 1
};
for tab in self.tabs.values_mut() {
if tab.position == new_tab_pos {
tab.set_force_render();
self.tab_history.retain(|&e| e != Some(tab.index));
self.tab_history.push(self.active_tab_index);
self.active_tab_index = Some(tab.index);
break;
}
}
self.update_tabs();
self.render();
self.switch_active_tab(new_tab_pos);
}
pub fn go_to_tab(&mut self, mut tab_index: usize) {
tab_index -= 1;
let active_tab_index = self.get_active_tab().unwrap().index;
if let Some(t) = self.tabs.values_mut().find(|t| t.position == tab_index) {
if t.index != active_tab_index {
t.set_force_render();
self.tab_history.retain(|&e| e != Some(t.index));
self.tab_history.push(self.active_tab_index);
self.active_tab_index = Some(t.index);
self.update_tabs();
self.render();
}
}
pub fn go_to_tab(&mut self, tab_index: usize) {
self.switch_active_tab(tab_index - 1);
}
/// Closes this [`Screen`]'s active [`Tab`], exiting the application if it happens
@ -264,10 +259,14 @@ impl Screen {
.unwrap();
}
} else {
if let Some(tab) = self.get_active_tab() {
tab.visible(false);
}
self.active_tab_index = self.tab_history.pop().unwrap();
for t in self.tabs.values_mut() {
if t.position == self.active_tab_index.unwrap() {
t.set_force_render()
if t.index == self.active_tab_index.unwrap() {
t.set_force_render();
t.visible(true);
}
if t.position > active_tab.position {
t.position -= 1;
@ -357,8 +356,12 @@ impl Screen {
self.draw_pane_frames,
);
tab.apply_layout(layout, new_pids, tab_index);
self.tab_history.push(self.active_tab_index);
self.active_tab_index = Some(tab_index);
if let Some(active_tab) = self.get_active_tab() {
active_tab.visible(false);
}
self.tab_history
.push(self.active_tab_index.replace(tab_index));
tab.visible(true);
self.tabs.insert(tab_index, tab);
self.update_tabs();
}

View file

@ -268,7 +268,7 @@ impl Tab {
let panes = BTreeMap::new();
let name = if name.is_empty() {
format!("Tab #{}", position + 1)
format!("Tab #{}", index + 1)
} else {
name
};
@ -325,10 +325,15 @@ impl Tab {
if let Some(Run::Plugin(Some(plugin))) = &layout.run {
let (pid_tx, pid_rx) = channel();
self.senders
.send_to_plugin(PluginInstruction::Load(pid_tx, plugin.clone(), tab_index))
.send_to_plugin(PluginInstruction::Load(
pid_tx,
plugin.path.clone(),
tab_index,
plugin._allow_exec_host_cmd,
))
.unwrap();
let pid = pid_rx.recv().unwrap();
let title = String::from(plugin.as_path().as_os_str().to_string_lossy());
let title = String::from(plugin.path.as_path().as_os_str().to_string_lossy());
let mut new_plugin = PluginPane::new(
pid,
*position_and_size,
@ -684,22 +689,35 @@ impl Tab {
}
pub fn set_pane_frames(&mut self, draw_pane_frames: bool) {
self.draw_pane_frames = draw_pane_frames;
self.should_clear_display_before_rendering = true;
let viewport = self.viewport;
for (pane_id, pane) in self.panes.iter_mut() {
pane.set_frame(draw_pane_frames);
if draw_pane_frames {
if !pane.borderless() {
pane.set_frame(draw_pane_frames);
}
#[allow(clippy::if_same_then_else)]
if draw_pane_frames & !pane.borderless() {
// there's definitely a frame around this pane, offset its contents
pane.set_content_offset(Offset::frame(1));
} else if draw_pane_frames && pane.borderless() {
// there's no frame around this pane, and the tab isn't handling the boundaries
// between panes (they each draw their own frames as they please)
// this one doesn't - do not offset its content
pane.set_content_offset(Offset::default());
} else if !is_inside_viewport(&viewport, pane) {
// this pane is outside the viewport and has no border - it should not have an offset
pane.set_content_offset(Offset::default());
} else {
// no draw_pane_frames and this pane should have a separation to other panes
// according to its position in the viewport (eg. no separation if its at the
// viewport bottom) - offset its content accordingly
let position_and_size = pane.current_geom();
let (pane_columns_offset, pane_rows_offset) =
pane_content_offset(&position_and_size, &self.viewport);
pane.set_content_offset(Offset::shift(pane_rows_offset, pane_columns_offset));
}
// FIXME: this should also override the above logic
if pane.borderless() {
pane.set_content_offset(Offset::default());
}
// FIXME: This, and all other `set_terminal_size_using_fd` calls, would be best in
// `TerminalPane::reflow_lines`
if let PaneId::Terminal(pid) = pane_id {
@ -720,6 +738,9 @@ impl Tab {
// or if this session is not attached to a client, we do not have to render
return;
}
self.senders
.send_to_pty(PtyInstruction::UpdateActivePane(self.active_terminal))
.unwrap();
let mut output = String::new();
let mut boundaries = Boundaries::new(self.viewport);
let hide_cursor = "\u{1b}[?25l";
@ -1739,16 +1760,16 @@ impl Tab {
}
let active_terminal_id = self.get_active_pane_id().unwrap();
let terminal_ids: Vec<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
.iter()
.position(|id| id == &active_terminal_id)
.unwrap();
if let Some(next_terminal) = terminal_ids.get(active_terminal_id_position + 1) {
self.active_terminal = Some(*next_terminal);
} else {
self.active_terminal = Some(*first_terminal);
}
let active_terminal = terminal_ids
.get(active_terminal_id_position + 1)
.or_else(|| terminal_ids.get(0))
.copied();
self.active_terminal = active_terminal;
self.render();
}
pub fn focus_next_pane(&mut self) {
@ -1767,16 +1788,17 @@ impl Tab {
a_pane.y().cmp(&b_pane.y())
}
});
let first_pane = panes.get(0).unwrap();
let active_pane_position = panes
.iter()
.position(|(id, _)| *id == &active_pane_id) // TODO: better
.unwrap();
if let Some(next_pane) = panes.get(active_pane_position + 1) {
self.active_terminal = Some(*next_pane.0);
} else {
self.active_terminal = Some(*first_pane.0);
}
let active_terminal = panes
.get(active_pane_position + 1)
.or_else(|| panes.get(0))
.map(|p| *p.0);
self.active_terminal = active_terminal;
self.render();
}
pub fn focus_previous_pane(&mut self) {
@ -1800,11 +1822,13 @@ impl Tab {
.iter()
.position(|(id, _)| *id == &active_pane_id) // TODO: better
.unwrap();
if active_pane_position == 0 {
self.active_terminal = Some(*last_pane.0);
let active_terminal = if active_pane_position == 0 {
Some(*last_pane.0)
} else {
self.active_terminal = Some(*panes.get(active_pane_position - 1).unwrap().0);
}
Some(*panes.get(active_pane_position - 1).unwrap().0)
};
self.active_terminal = active_terminal;
self.render();
}
// returns a boolean that indicates whether the focus moved
@ -1816,7 +1840,7 @@ impl Tab {
return false;
}
let active_terminal = self.get_active_pane();
if let Some(active) = active_terminal {
let updated_active_terminal = if let Some(active) = active_terminal {
let terminals = self.get_selectable_panes();
let next_index = terminals
.enumerate()
@ -1839,13 +1863,12 @@ impl Tab {
self.render();
return true;
}
None => {
self.active_terminal = Some(active.pid());
}
None => Some(active.pid()),
}
} else {
self.active_terminal = Some(active_terminal.unwrap().pid());
}
Some(active_terminal.unwrap().pid())
};
self.active_terminal = updated_active_terminal;
false
}
pub fn move_focus_down(&mut self) {
@ -1856,7 +1879,7 @@ impl Tab {
return;
}
let active_terminal = self.get_active_pane();
if let Some(active) = active_terminal {
let updated_active_terminal = if let Some(active) = active_terminal {
let terminals = self.get_selectable_panes();
let next_index = terminals
.enumerate()
@ -1875,15 +1898,14 @@ impl Tab {
let next_active_pane = self.panes.get_mut(&p).unwrap();
next_active_pane.set_should_render(true);
self.active_terminal = Some(p);
}
None => {
self.active_terminal = Some(active.pid());
Some(p)
}
None => Some(active.pid()),
}
} else {
self.active_terminal = Some(active_terminal.unwrap().pid());
}
Some(active_terminal.unwrap().pid())
};
self.active_terminal = updated_active_terminal;
self.render();
}
pub fn move_focus_up(&mut self) {
@ -1894,7 +1916,7 @@ impl Tab {
return;
}
let active_terminal = self.get_active_pane();
if let Some(active) = active_terminal {
let updated_active_terminal = if let Some(active) = active_terminal {
let terminals = self.get_selectable_panes();
let next_index = terminals
.enumerate()
@ -1913,15 +1935,14 @@ impl Tab {
let next_active_pane = self.panes.get_mut(&p).unwrap();
next_active_pane.set_should_render(true);
self.active_terminal = Some(p);
}
None => {
self.active_terminal = Some(active.pid());
Some(p)
}
None => Some(active.pid()),
}
} else {
self.active_terminal = Some(active_terminal.unwrap().pid());
}
Some(active_terminal.unwrap().pid())
};
self.active_terminal = updated_active_terminal;
self.render();
}
// returns a boolean that indicates whether the focus moved
@ -1933,7 +1954,7 @@ impl Tab {
return false;
}
let active_terminal = self.get_active_pane();
if let Some(active) = active_terminal {
let updated_active_terminal = if let Some(active) = active_terminal {
let terminals = self.get_selectable_panes();
let next_index = terminals
.enumerate()
@ -1956,13 +1977,12 @@ impl Tab {
self.render();
return true;
}
None => {
self.active_terminal = Some(active.pid());
}
None => Some(active.pid()),
}
} else {
self.active_terminal = Some(active_terminal.unwrap().pid());
}
Some(active_terminal.unwrap().pid())
};
self.active_terminal = updated_active_terminal;
false
}
fn horizontal_borders(&self, terminals: &[PaneId]) -> HashSet<usize> {
@ -1981,6 +2001,7 @@ impl Tab {
borders
})
}
fn panes_to_the_left_between_aligning_borders(&self, id: PaneId) -> Option<Vec<PaneId>> {
if let Some(terminal) = self.panes.get(&id) {
let upper_close_border = terminal.y();
@ -2107,7 +2128,7 @@ impl Tab {
if let Some(pane) = self.panes.get_mut(&id) {
pane.set_selectable(selectable);
if self.get_active_pane_id() == Some(id) && !selectable {
self.active_terminal = self.next_active_pane(&self.get_pane_ids())
self.active_terminal = self.next_active_pane(&self.get_pane_ids());
}
}
self.render();
@ -2345,10 +2366,9 @@ impl Tab {
.unwrap();
}
fn is_inside_viewport(&self, pane_id: &PaneId) -> bool {
let pane_position_and_size = self.panes.get(pane_id).unwrap().current_geom();
pane_position_and_size.y >= self.viewport.y
&& pane_position_and_size.y + pane_position_and_size.rows.as_usize()
<= self.viewport.y + self.viewport.rows
// this is mostly separated to an outside function in order to allow us to pass a clone to
// it sometimes when we need to get around the borrow checker
is_inside_viewport(&self.viewport, self.panes.get(pane_id).unwrap())
}
fn offset_viewport(&mut self, position_and_size: &Viewport) {
if position_and_size.x == self.viewport.x
@ -2376,6 +2396,29 @@ impl Tab {
}
}
}
pub fn visible(&self, visible: bool) {
let pids_in_this_tab = self.panes.keys().filter_map(|p| match p {
PaneId::Plugin(pid) => Some(pid),
_ => None,
});
for pid in pids_in_this_tab {
self.senders
.send_to_plugin(PluginInstruction::Update(
Some(*pid),
Event::Visible(visible),
))
.unwrap();
}
}
}
#[allow(clippy::borrowed_box)]
fn is_inside_viewport(viewport: &Viewport, pane: &Box<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)]

View file

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

View file

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

View file

@ -1,11 +1,12 @@
use super::Tab;
use crate::zellij_tile::data::{ModeInfo, Palette};
use crate::{
os_input_output::{AsyncReader, Pid, ServerOsApi},
os_input_output::{AsyncReader, ChildId, Pid, ServerOsApi},
panes::PaneId,
thread_bus::ThreadSenders,
SessionState,
};
use std::path::PathBuf;
use std::sync::{Arc, RwLock};
use zellij_utils::input::layout::LayoutTemplate;
use zellij_utils::pane_size::Size;
@ -27,7 +28,7 @@ impl ServerOsApi for FakeInputOutput {
fn set_terminal_size_using_fd(&self, _fd: RawFd, _cols: u16, _rows: u16) {
// noop
}
fn spawn_terminal(&self, _file_to_open: Option<TerminalAction>) -> (RawFd, Pid) {
fn spawn_terminal(&self, _file_to_open: TerminalAction) -> (RawFd, ChildId) {
unimplemented!()
}
fn read_from_tty_stdout(&self, _fd: RawFd, _buf: &mut [u8]) -> Result<usize, nix::Error> {
@ -72,6 +73,9 @@ impl ServerOsApi for FakeInputOutput {
fn load_palette(&self) -> Palette {
unimplemented!()
}
fn get_cwd(&self, _pid: Pid) -> Option<PathBuf> {
unimplemented!()
}
}
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::fs;
use std::path::PathBuf;
@ -28,8 +28,8 @@ use zellij_utils::{input::command::TerminalAction, serde, zellij_tile};
#[derive(Clone, Debug)]
pub(crate) enum PluginInstruction {
Load(Sender<u32>, PathBuf, usize), // tx_pid, path_of_plugin , tab_index
Update(Option<u32>, Event), // Focused plugin / broadcast, event data
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
Render(Sender<String>, u32, usize, usize), // String buffer, plugin id, rows, cols
Unload(u32),
Exit,
@ -54,6 +54,9 @@ pub(crate) struct PluginEnv {
pub senders: ThreadSenders,
pub wasi_env: WasiEnv,
pub subscriptions: Arc<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 --------------------------------------------------------------------------------------------------------
@ -61,12 +64,15 @@ pub(crate) fn wasm_thread_main(bus: Bus<PluginInstruction>, store: Store, data_d
info!("Wasm main thread starts");
let mut plugin_id = 0;
let mut plugin_map = HashMap::new();
let plugin_dir = data_dir.join("plugins/");
let plugin_global_data_dir = plugin_dir.join("data");
fs::create_dir_all(plugin_global_data_dir.as_path()).unwrap();
loop {
let (event, mut err_ctx) = bus.recv().expect("failed to receive event on channel");
err_ctx.add_call(ContextType::Plugin((&event).into()));
match event {
PluginInstruction::Load(pid_tx, path, tab_index) => {
let plugin_dir = data_dir.join("plugins/");
PluginInstruction::Load(pid_tx, path, tab_index, _allow_exec_host_cmd) => {
let wasm_bytes = fs::read(&path)
.or_else(|_| fs::read(&path.with_extension("wasm")))
.or_else(|_| fs::read(&plugin_dir.join(&path).with_extension("wasm")))
@ -81,15 +87,15 @@ pub(crate) fn wasm_thread_main(bus: Bus<PluginInstruction>, store: Store, data_d
path.as_path().file_name().unwrap().to_str().unwrap(),
plugin_id,
);
let plugin_name = path.as_path().file_stem().unwrap();
let plugin_own_data_dir = plugin_global_data_dir.join(plugin_name);
let mut wasi_env = WasiState::new("Zellij")
.env("CLICOLOR_FORCE", "1")
.preopen(|p| {
p.directory(".") // FIXME: Change this to a more meaningful dir
.alias(".")
.read(true)
.write(true)
.create(true)
})
.map_dir("/host", ".")
.unwrap()
.map_dir("/data", plugin_own_data_dir.as_path())
.unwrap()
.stdin(Box::new(input))
.stdout(Box::new(output))
@ -99,12 +105,18 @@ pub(crate) fn wasm_thread_main(bus: Bus<PluginInstruction>, store: Store, data_d
let wasi = wasi_env.import_object(&module).unwrap();
if _allow_exec_host_cmd {
info!("Plugin({:?}) is able to run any host command, this may lead to some security issues!", path);
}
let plugin_env = PluginEnv {
plugin_id,
tab_index,
senders: bus.senders.clone(),
wasi_env,
subscriptions: Arc::new(Mutex::new(HashSet::new())),
_allow_exec_host_cmd,
plugin_own_data_dir,
};
let zellij = zellij_exports(&store, &plugin_env);
@ -147,10 +159,16 @@ pub(crate) fn wasm_thread_main(bus: Bus<PluginInstruction>, store: Store, data_d
buf_tx.send(wasi_read_string(&plugin_env.wasi_env)).unwrap();
}
}
PluginInstruction::Unload(pid) => drop(plugin_map.remove(&pid)),
PluginInstruction::Unload(pid) => {
info!("Bye from plugin {}", &pid);
// TODO: remove plugin's own data directory
drop(plugin_map.remove(&pid));
}
PluginInstruction::Exit => break,
}
}
info!("wasm main thread exits");
fs::remove_dir_all(plugin_global_data_dir.as_path()).unwrap();
}
// Plugin API ---------------------------------------------------------------------------------------------------------
@ -174,6 +192,7 @@ pub(crate) fn zellij_exports(store: &Store, plugin_env: &PluginEnv) -> ImportObj
host_get_plugin_ids,
host_open_file,
host_set_timeout,
host_exec_cmd,
}
}
@ -248,6 +267,24 @@ fn host_set_timeout(plugin_env: &PluginEnv, secs: f64) {
});
}
fn host_exec_cmd(plugin_env: &PluginEnv) {
let mut cmdline: Vec<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 ---------------------------------------------------------------------------------------------------
pub fn wasi_read_string(wasi_env: &WasiEnv) -> String {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -62,6 +62,13 @@ pub enum Command {
Sessions(Sessions),
}
#[derive(Debug, StructOpt, Clone, Serialize, Deserialize)]
pub enum SessionCommand {
/// Change the behaviour of zellij
#[structopt(name = "options")]
Options(Options),
}
#[derive(Debug, StructOpt, Clone, Serialize, Deserialize)]
pub enum Sessions {
/// List active sessions
@ -78,5 +85,13 @@ pub enum Sessions {
/// zellij client (if any) and attach to this.
#[structopt(long, short)]
force: bool,
/// Create a session if one does not exist.
#[structopt(short, long)]
create: bool,
/// Change the behaviour of zellij
#[structopt(subcommand, name = "options")]
options: Option<SessionCommand>,
},
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -45,7 +45,10 @@ fn default_layout_merged_correctly() {
borderless: true,
parts: vec![],
split_size: Some(SplitSize::Fixed(1)),
run: Some(Run::Plugin(Some("tab-bar".into()))),
run: Some(Run::Plugin(Some(RunPlugin {
path: "tab-bar".into(),
..Default::default()
}))),
},
Layout {
direction: Direction::Vertical,
@ -59,7 +62,10 @@ fn default_layout_merged_correctly() {
borderless: true,
parts: vec![],
split_size: Some(SplitSize::Fixed(2)),
run: Some(Run::Plugin(Some("status-bar".into()))),
run: Some(Run::Plugin(Some(RunPlugin {
path: "status-bar".into(),
..Default::default()
}))),
},
],
split_size: None,
@ -83,7 +89,10 @@ fn default_layout_new_tab_correct() {
borderless: true,
parts: vec![],
split_size: Some(SplitSize::Fixed(1)),
run: Some(Run::Plugin(Some("tab-bar".into()))),
run: Some(Run::Plugin(Some(RunPlugin {
path: "tab-bar".into(),
..Default::default()
}))),
},
Layout {
direction: Direction::Horizontal,
@ -97,7 +106,10 @@ fn default_layout_new_tab_correct() {
borderless: true,
parts: vec![],
split_size: Some(SplitSize::Fixed(2)),
run: Some(Run::Plugin(Some("status-bar".into()))),
run: Some(Run::Plugin(Some(RunPlugin {
path: "status-bar".into(),
..Default::default()
}))),
},
],
split_size: None,
@ -253,7 +265,10 @@ fn three_panes_with_tab_and_default_plugins_merged_correctly() {
borderless: false,
parts: vec![],
split_size: Some(SplitSize::Fixed(1)),
run: Some(Run::Plugin(Some("tab-bar".into()))),
run: Some(Run::Plugin(Some(RunPlugin {
path: "tab-bar".into(),
..Default::default()
}))),
},
Layout {
direction: Direction::Vertical,
@ -297,7 +312,10 @@ fn three_panes_with_tab_and_default_plugins_merged_correctly() {
borderless: false,
parts: vec![],
split_size: Some(SplitSize::Fixed(2)),
run: Some(Run::Plugin(Some("status-bar".into()))),
run: Some(Run::Plugin(Some(RunPlugin {
path: "status-bar".into(),
..Default::default()
}))),
},
],
split_size: None,
@ -321,7 +339,10 @@ fn three_panes_with_tab_and_default_plugins_new_tab_is_correct() {
borderless: false,
parts: vec![],
split_size: Some(SplitSize::Fixed(1)),
run: Some(Run::Plugin(Some("tab-bar".into()))),
run: Some(Run::Plugin(Some(RunPlugin {
path: "tab-bar".into(),
..Default::default()
}))),
},
Layout {
direction: Direction::Horizontal,
@ -335,7 +356,10 @@ fn three_panes_with_tab_and_default_plugins_new_tab_is_correct() {
borderless: false,
parts: vec![],
split_size: Some(SplitSize::Fixed(2)),
run: Some(Run::Plugin(Some("status-bar".into()))),
run: Some(Run::Plugin(Some(RunPlugin {
path: "status-bar".into(),
..Default::default()
}))),
},
],
split_size: None,

View file

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