Merge branch 'main' of github.com:zellij-org/zellij
This commit is contained in:
commit
1a1c10a226
54 changed files with 986 additions and 511 deletions
40
CHANGELOG.md
40
CHANGELOG.md
|
|
@ -5,6 +5,46 @@ All notable changes to this project will be documented in this file.
|
|||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
|
||||
|
||||
## [Unreleased]
|
||||
* Fix: Properly open new pane with CWD also when switching to a new tab (https://github.com/zellij-org/zellij/pull/729)
|
||||
* Feature: Option to create a new session if attach fails (`zellij attach --create`) (https://github.com/zellij-org/zellij/pull/731)
|
||||
* Feature: Added the new `Visible` event, allowing plugins to detect if they are visible in the current tab (https://github.com/zellij-org/zellij/pull/717)
|
||||
* Feature: Plugins now have access to a data directory at `/data` – the working directory is now mounted at `/host` instead of `.` (https://github.com/zellij-org/zellij/pull/723)
|
||||
|
||||
## [0.17.0] - 2021-09-15
|
||||
* New panes/tabs now open in CWD of focused pane (https://github.com/zellij-org/zellij/pull/691)
|
||||
* Fix bug when opening new tab the new pane's viewport would sometimes be calculated incorrectly (https://github.com/zellij-org/zellij/pull/683)
|
||||
* Fix bug when in some cases closing a tab would not clear the previous pane's contents (https://github.com/zellij-org/zellij/pull/684)
|
||||
* Fix bug where tabs would sometimes be created with the wrong index in their name (https://github.com/zellij-org/zellij/pull/686)
|
||||
* Fix bug where wide chars would mess up pane titles (https://github.com/zellij-org/zellij/pull/698)
|
||||
* Fix various borderless-frame in viewport bugs (https://github.com/zellij-org/zellij/pull/697)
|
||||
* Fix example configuration file (https://github.com/zellij-org/zellij/pull/693)
|
||||
* Fix various tab bar responsiveness issues (https://github.com/zellij-org/zellij/pull/703)
|
||||
* Allow plugins to run system commands (https://github.com/zellij-org/zellij/pull/666)
|
||||
* This has also added a temporary new permission flag that needs to be specified in the layout. This is a breaking change:
|
||||
```yaml
|
||||
...
|
||||
plugin: strider
|
||||
...
|
||||
```
|
||||
has become:
|
||||
```yaml
|
||||
plugin:
|
||||
path: strider
|
||||
```
|
||||
A plugin can be given command executing permission with:
|
||||
```yaml
|
||||
plugin:
|
||||
path: strider
|
||||
_allow_exec_host_cmd: true
|
||||
```
|
||||
* Use the unicode width in tab-bar plugin, for tab names (https://github.com/zellij-org/zellij/pull/709)
|
||||
* Fix automated builds that make use of the `setup` subcommand (https://github.com/zellij-org/zellij/pull/711)
|
||||
* Add option to specify a tabs name in the tab `layout` file (https://github.com/zellij-org/zellij/pull/715)
|
||||
* Improve handling of empty valid `yaml` files (https://github.com/zellij-org/zellij/pull/716)
|
||||
* Add options subcommand to attach (https://github.com/zellij-org/zellij/pull/718)
|
||||
* Fix: do not pad empty pane frame title (https://github.com/zellij-org/zellij/pull/724)
|
||||
* Fix: Do not overflow empty lines when resizing panes (https://github.com/zellij-org/zellij/pull/725)
|
||||
|
||||
|
||||
## [0.16.0] - 2021-08-31
|
||||
* Plugins don't crash zellij anymore on receiving mouse events (https://github.com/zellij-org/zellij/pull/620)
|
||||
|
|
|
|||
71
Cargo.lock
generated
71
Cargo.lock
generated
|
|
@ -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",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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.
|
|
@ -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" }
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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',]
|
||||
|
|
|
|||
95
src/main.rs
95
src/main.rs
|
|
@ -4,12 +4,15 @@ mod sessions;
|
|||
mod tests;
|
||||
|
||||
use crate::install::populate_data_dir;
|
||||
use sessions::{assert_session, assert_session_ne, get_active_session, list_sessions};
|
||||
use sessions::{
|
||||
assert_session, assert_session_ne, get_active_session, get_sessions, list_sessions,
|
||||
print_sessions, session_exists, ActiveSession,
|
||||
};
|
||||
use std::process;
|
||||
use zellij_client::{os_input_output::get_client_os_input, start_client, ClientInfo};
|
||||
use zellij_server::{os_input_output::get_server_os_input, start_server};
|
||||
use zellij_utils::{
|
||||
cli::{CliArgs, Command, Sessions},
|
||||
cli::{CliArgs, Command, SessionCommand, Sessions},
|
||||
consts::{ZELLIJ_TMP_DIR, ZELLIJ_TMP_LOG_DIR},
|
||||
logging::*,
|
||||
setup::{get_default_data_dir, Setup},
|
||||
|
|
@ -36,6 +39,14 @@ pub fn main() {
|
|||
};
|
||||
start_server(Box::new(os_input), path);
|
||||
} else {
|
||||
let (config, layout, config_options) = match Setup::from_options(&opts) {
|
||||
Ok(results) => results,
|
||||
Err(e) => {
|
||||
eprintln!("{}", e);
|
||||
process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
let os_input = match get_client_os_input() {
|
||||
Ok(os_input) => os_input,
|
||||
Err(e) => {
|
||||
|
|
@ -44,40 +55,77 @@ pub fn main() {
|
|||
}
|
||||
};
|
||||
if let Some(Command::Sessions(Sessions::Attach {
|
||||
mut session_name,
|
||||
session_name,
|
||||
force,
|
||||
create,
|
||||
options,
|
||||
})) = opts.command.clone()
|
||||
{
|
||||
if let Some(session) = session_name.as_ref() {
|
||||
assert_session(session);
|
||||
} else {
|
||||
session_name = Some(get_active_session());
|
||||
}
|
||||
let config_options = match options {
|
||||
Some(SessionCommand::Options(o)) => config_options.merge(o),
|
||||
None => config_options,
|
||||
};
|
||||
|
||||
let (config, _, config_options) = match Setup::from_options(&opts) {
|
||||
Ok(results) => results,
|
||||
Err(e) => {
|
||||
eprintln!("{}", e);
|
||||
process::exit(1);
|
||||
let (client, attach_layout) = match session_name.as_ref() {
|
||||
Some(session) => {
|
||||
if create {
|
||||
if !session_exists(session).unwrap() {
|
||||
(ClientInfo::New(session_name.unwrap()), layout)
|
||||
} else {
|
||||
(
|
||||
ClientInfo::Attach(
|
||||
session_name.unwrap(),
|
||||
force,
|
||||
config_options.clone(),
|
||||
),
|
||||
None,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
assert_session(session);
|
||||
(
|
||||
ClientInfo::Attach(
|
||||
session_name.unwrap(),
|
||||
force,
|
||||
config_options.clone(),
|
||||
),
|
||||
None,
|
||||
)
|
||||
}
|
||||
}
|
||||
None => match get_active_session() {
|
||||
ActiveSession::None => {
|
||||
if create {
|
||||
(
|
||||
ClientInfo::New(names::Generator::default().next().unwrap()),
|
||||
layout,
|
||||
)
|
||||
} else {
|
||||
println!("No active zellij sessions found.");
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
ActiveSession::One(session_name) => (
|
||||
ClientInfo::Attach(session_name, force, config_options.clone()),
|
||||
None,
|
||||
),
|
||||
ActiveSession::Many => {
|
||||
println!("Please specify the session name to attach to. The following sessions are active:");
|
||||
print_sessions(get_sessions().unwrap());
|
||||
process::exit(1);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
start_client(
|
||||
Box::new(os_input),
|
||||
opts,
|
||||
config,
|
||||
ClientInfo::Attach(session_name.unwrap(), force, config_options),
|
||||
None,
|
||||
config_options,
|
||||
client,
|
||||
attach_layout,
|
||||
);
|
||||
} else {
|
||||
let (config, layout, _) = match Setup::from_options(&opts) {
|
||||
Ok(results) => results,
|
||||
Err(e) => {
|
||||
eprintln!("{}", e);
|
||||
process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
let session_name = opts
|
||||
.session
|
||||
.clone()
|
||||
|
|
@ -93,6 +141,7 @@ pub fn main() {
|
|||
Box::new(os_input),
|
||||
opts,
|
||||
config,
|
||||
config_options,
|
||||
ClientInfo::New(session_name),
|
||||
layout,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
20
test-template.yaml
Normal 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
|
||||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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| {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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>,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)]
|
||||
|
|
|
|||
|
|
@ -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!(
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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,]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ template:
|
|||
split_size:
|
||||
Fixed: 1
|
||||
run:
|
||||
plugin: tab-bar
|
||||
plugin:
|
||||
path: tab-bar
|
||||
- direction: Vertical
|
||||
body: true
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>,
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -237,6 +237,7 @@ pub enum PtyContext {
|
|||
SpawnTerminal,
|
||||
SpawnTerminalVertically,
|
||||
SpawnTerminalHorizontally,
|
||||
UpdateActivePane,
|
||||
NewTab,
|
||||
ClosePane,
|
||||
CloseTab,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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]));
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue