summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAram Drevekenin <aram@poor.dev>2021-12-20 17:31:07 +0100
committerGitHub <noreply@github.com>2021-12-20 17:31:07 +0100
commitca8438b0aa0e5e9aa1c69f0217c275d20f269a8f (patch)
tree97ee4922eb41415d54d3cb183ad9dbef66bd7186
parent2c1d3a9817e70ea3d4b55672166f3417d296cc44 (diff)
feat(collaboration): implement multiple users (#957)
* work * feat(collaboration): implement multiple users * style(cleanup): some leftovers
-rwxr-xr-x[-rw-r--r--]assets/plugins/status-bar.wasmbin536007 -> 540477 bytes
-rwxr-xr-x[-rw-r--r--]assets/plugins/strider.wasmbin574888 -> 581410 bytes
-rwxr-xr-x[-rw-r--r--]assets/plugins/tab-bar.wasmbin436564 -> 445668 bytes
-rw-r--r--default-plugins/tab-bar/src/main.rs1
-rw-r--r--default-plugins/tab-bar/src/tab.rs76
-rw-r--r--src/tests/e2e/cases.rs312
-rw-r--r--src/tests/e2e/remote_runner.rs59
-rw-r--r--src/tests/e2e/snapshots/zellij__tests__e2e__cases__multiple_users_in_different_panes_and_same_tab-2.snap29
-rw-r--r--src/tests/e2e/snapshots/zellij__tests__e2e__cases__multiple_users_in_different_panes_and_same_tab.snap29
-rw-r--r--src/tests/e2e/snapshots/zellij__tests__e2e__cases__multiple_users_in_different_tabs-2.snap29
-rw-r--r--src/tests/e2e/snapshots/zellij__tests__e2e__cases__multiple_users_in_different_tabs.snap29
-rw-r--r--src/tests/e2e/snapshots/zellij__tests__e2e__cases__multiple_users_in_same_pane_and_tab-2.snap29
-rw-r--r--src/tests/e2e/snapshots/zellij__tests__e2e__cases__multiple_users_in_same_pane_and_tab.snap29
-rw-r--r--zellij-server/src/screen.rs177
-rw-r--r--zellij-server/src/tab.rs119
-rw-r--r--zellij-server/src/ui/pane_boundaries_frame.rs18
-rw-r--r--zellij-server/src/ui/pane_contents_and_ui.rs33
-rw-r--r--zellij-server/src/unit/screen_tests.rs2
-rw-r--r--zellij-server/src/unit/tab_tests.rs9
-rw-r--r--zellij-tile/src/data.rs32
-rw-r--r--zellij-utils/src/input/options.rs9
-rw-r--r--zellij-utils/src/shared.rs10
22 files changed, 858 insertions, 173 deletions
diff --git a/assets/plugins/status-bar.wasm b/assets/plugins/status-bar.wasm
index b45a3247b..fdff24cf6 100644..100755
--- a/assets/plugins/status-bar.wasm
+++ b/assets/plugins/status-bar.wasm
Binary files differ
diff --git a/assets/plugins/strider.wasm b/assets/plugins/strider.wasm
index 3b62b85ae..379a32473 100644..100755
--- a/assets/plugins/strider.wasm
+++ b/assets/plugins/strider.wasm
Binary files differ
diff --git a/assets/plugins/tab-bar.wasm b/assets/plugins/tab-bar.wasm
index 4f8107df1..ebcfe2ca7 100644..100755
--- a/assets/plugins/tab-bar.wasm
+++ b/assets/plugins/tab-bar.wasm
Binary files differ
diff --git a/default-plugins/tab-bar/src/main.rs b/default-plugins/tab-bar/src/main.rs
index 31b2f2d37..3db531384 100644
--- a/default-plugins/tab-bar/src/main.rs
+++ b/default-plugins/tab-bar/src/main.rs
@@ -85,6 +85,7 @@ impl ZellijPlugin for State {
t.is_sync_panes_active,
self.mode_info.palette,
self.mode_info.capabilities,
+ t.other_focused_clients.as_slice(),
);
all_tabs.push(tab);
}
diff --git a/default-plugins/tab-bar/src/tab.rs b/default-plugins/tab-bar/src/tab.rs
index db960fd52..4ed3b2076 100644
--- a/default-plugins/tab-bar/src/tab.rs
+++ b/default-plugins/tab-bar/src/tab.rs
@@ -1,33 +1,62 @@
use crate::{line::tab_separator, LinePart};
-use ansi_term::ANSIStrings;
+use ansi_term::{ANSIString, 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.gray, palette.green).paint(separator);
- 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));
- let right_separator = style!(palette.green, palette.gray).paint(separator);
- let tab_styled_text =
- ANSIStrings(&[left_separator, tab_styled_text, right_separator]).to_string();
- LinePart {
- part: tab_styled_text,
- len: tab_text_len,
+fn cursors(focused_clients: &[ClientId], palette: Palette) -> (Vec<ANSIString>, usize) {
+ // cursor section, text length
+ let mut len = 0;
+ let mut cursors = vec![];
+ for client_id in focused_clients.iter() {
+ if let Some(color) = client_id_to_colors(*client_id, palette) {
+ cursors.push(style!(color.1, color.0).paint(" "));
+ len += 1;
+ }
}
+ (cursors, len)
}
-pub fn non_active_tab(text: String, palette: Palette, separator: &str) -> LinePart {
- let left_separator = style!(palette.gray, palette.fg).paint(separator);
- 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)
+pub fn render_tab(
+ text: String,
+ palette: Palette,
+ separator: &str,
+ focused_clients: &[ClientId],
+ active: bool,
+) -> LinePart {
+ let background_color = if active { palette.green } else { palette.fg };
+ let left_separator = style!(palette.gray, background_color).paint(separator);
+ let mut 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, background_color)
.bold()
.paint(format!(" {} ", text));
- let right_separator = style!(palette.fg, palette.gray).paint(separator);
- let tab_styled_text =
- ANSIStrings(&[left_separator, tab_styled_text, right_separator]).to_string();
+
+ let right_separator = style!(background_color, palette.gray).paint(separator);
+ let tab_styled_text = if !focused_clients.is_empty() {
+ let (cursor_section, extra_length) = cursors(focused_clients, palette);
+ tab_text_len += extra_length;
+ let mut s = String::new();
+ let cursor_beginning = style!(palette.black, background_color)
+ .bold()
+ .paint("[")
+ .to_string();
+ let cursor_section = ANSIStrings(&cursor_section).to_string();
+ let cursor_end = style!(palette.black, background_color)
+ .bold()
+ .paint("]")
+ .to_string();
+ s.push_str(&left_separator.to_string());
+ s.push_str(&tab_styled_text.to_string());
+ s.push_str(&cursor_beginning);
+ s.push_str(&cursor_section);
+ s.push_str(&cursor_end);
+ s.push_str(&right_separator.to_string());
+ s
+ } else {
+ ANSIStrings(&[left_separator, tab_styled_text, right_separator]).to_string()
+ };
+
LinePart {
part: tab_styled_text,
len: tab_text_len,
@@ -40,15 +69,12 @@ pub fn tab_style(
is_sync_panes_active: bool,
palette: Palette,
capabilities: PluginCapabilities,
+ focused_clients: &[ClientId],
) -> LinePart {
let separator = tab_separator(capabilities);
let mut tab_text = text;
if is_sync_panes_active {
tab_text.push_str(" (Sync)");
}
- if is_active_tab {
- active_tab(tab_text, palette, separator)
- } else {
- non_active_tab(tab_text, palette, separator)
- }
+ render_tab(tab_text, palette, separator, focused_clients, is_active_tab)
}
diff --git a/src/tests/e2e/cases.rs b/src/tests/e2e/cases.rs
index 0585c373b..1457c0a91 100644
--- a/src/tests/e2e/cases.rs
+++ b/src/tests/e2e/cases.rs
@@ -178,7 +178,6 @@ pub fn cannot_split_terminals_vertically_when_active_terminal_is_too_small() {
name: "Make sure only one pane appears",
instruction: |remote_terminal: RemoteTerminal| -> bool {
let mut step_is_complete = false;
- // if remote_terminal.cursor_position_is(3, 2) && remote_terminal.snapshot_contains("...")
if remote_terminal.cursor_position_is(3, 2) {
// ... is the truncated tip line
step_is_complete = true;
@@ -928,7 +927,7 @@ pub fn detach_and_attach_session() {
let last_snapshot = loop {
RemoteRunner::kill_running_sessions(fake_win_size);
drop(());
- let mut runner = RemoteRunner::new(fake_win_size)
+ let mut runner = RemoteRunner::new_mirrored_session(fake_win_size)
.add_step(Step {
name: "Split pane to the right",
instruction: |mut remote_terminal: RemoteTerminal| -> bool {
@@ -1261,20 +1260,21 @@ pub fn mirrored_sessions() {
// then make sure they were also reflected (mirrored) in the first runner afterwards
RemoteRunner::kill_running_sessions(fake_win_size);
drop(());
- let mut first_runner = RemoteRunner::new_with_session_name(fake_win_size, session_name)
- .dont_panic()
- .add_step(Step {
- name: "Wait for app to load",
- instruction: |mut remote_terminal: RemoteTerminal| -> bool {
- let mut step_is_complete = false;
- if remote_terminal.status_bar_appears()
- && remote_terminal.cursor_position_is(3, 2)
- {
- step_is_complete = true;
- }
- step_is_complete
- },
- });
+ let mut first_runner =
+ RemoteRunner::new_with_session_name(fake_win_size, session_name, true)
+ .dont_panic()
+ .add_step(Step {
+ name: "Wait for app to load",
+ instruction: |mut remote_terminal: RemoteTerminal| -> bool {
+ let mut step_is_complete = false;
+ if remote_terminal.status_bar_appears()
+ && remote_terminal.cursor_position_is(3, 2)
+ {
+ step_is_complete = true;
+ }
+ step_is_complete
+ },
+ });
first_runner.run_all_steps();
let mut second_runner = RemoteRunner::new_existing_session(fake_win_size, session_name)
@@ -1398,6 +1398,286 @@ pub fn mirrored_sessions() {
#[test]
#[ignore]
+pub fn multiple_users_in_same_pane_and_tab() {
+ let fake_win_size = Size {
+ cols: 120,
+ rows: 24,
+ };
+ let mut test_attempts = 10;
+ let session_name = "multiple_users_in_same_pane_and_tab";
+ let (first_runner_snapshot, second_runner_snapshot) = loop {
+ // here we connect with one runner, then connect with another, perform some actions and
+ // then make sure they were also reflected (mirrored) in the first runner afterwards
+ RemoteRunner::kill_running_sessions(fake_win_size);
+ drop(());
+ let mut first_runner =
+ RemoteRunner::new_with_session_name(fake_win_size, session_name, false)
+ .dont_panic()
+ .add_step(Step {
+ name: "Wait for app to load",
+ instruction: |mut remote_terminal: RemoteTerminal| -> bool {
+ let mut step_is_complete = false;
+ if remote_terminal.status_bar_appears()
+ && remote_terminal.cursor_position_is(3, 2)
+ {
+ step_is_complete = true;
+ }
+ step_is_complete
+ },
+ });
+ first_runner.run_all_steps();
+
+ let mut second_runner = RemoteRunner::new_existing_session(fake_win_size, session_name)
+ .dont_panic()
+ .add_step(Step {
+ name: "Wait for app to load",
+ instruction: |mut remote_terminal: RemoteTerminal| -> bool {
+ let mut step_is_complete = false;
+ if remote_terminal.status_bar_appears()
+ && remote_terminal.cursor_position_is(3, 2)
+ {
+ step_is_complete = true;
+ }
+ step_is_complete
+ },
+ });
+ second_runner.run_all_steps();
+
+ if first_runner.test_timed_out || second_runner.test_timed_out {
+ test_attempts -= 1;
+ continue;
+ }
+ let second_runner_snapshot = second_runner.take_snapshot_after(Step {
+ name: "take snapshot after",
+ instruction: |remote_terminal: RemoteTerminal| -> bool {
+ let mut step_is_complete = false;
+ if remote_terminal.cursor_position_is(3, 2)
+ && remote_terminal.snapshot_contains("MY FOCUS")
+ {
+ // cursor is back in the first tab
+ step_is_complete = true;
+ }
+ step_is_complete
+ },
+ });
+ let first_runner_snapshot = first_runner.take_snapshot_after(Step {
+ name: "take snapshot after",
+ instruction: |remote_terminal: RemoteTerminal| -> bool {
+ let mut step_is_complete = false;
+ if remote_terminal.cursor_position_is(3, 2)
+ && remote_terminal.snapshot_contains("MY FOCUS")
+ {
+ // cursor is back in the first tab
+ step_is_complete = true;
+ }
+ step_is_complete
+ },
+ });
+
+ if (first_runner.test_timed_out || second_runner.test_timed_out) && test_attempts >= 0 {
+ test_attempts -= 1;
+ continue;
+ } else {
+ break (first_runner_snapshot, second_runner_snapshot);
+ }
+ };
+ assert_snapshot!(first_runner_snapshot);
+ assert_snapshot!(second_runner_snapshot);
+}
+
+#[test]
+#[ignore]
+pub fn multiple_users_in_different_panes_and_same_tab() {
+ let fake_win_size = Size {
+ cols: 120,
+ rows: 24,
+ };
+ let mut test_attempts = 10;
+ let session_name = "multiple_users_in_same_pane_and_tab";
+ let (first_runner_snapshot, second_runner_snapshot) = loop {
+ // here we connect with one runner, then connect with another, perform some actions and
+ // then make sure they were also reflected (mirrored) in the first runner afterwards
+ RemoteRunner::kill_running_sessions(fake_win_size);
+ drop(());
+ let mut first_runner =
+ RemoteRunner::new_with_session_name(fake_win_size, session_name, false)
+ .dont_panic()
+ .add_step(Step {
+ name: "Wait for app to load",
+ instruction: |mut remote_terminal: RemoteTerminal| -> bool {
+ let mut step_is_complete = false;
+ if remote_terminal.status_bar_appears()
+ && remote_terminal.cursor_position_is(3, 2)
+ {
+ step_is_complete = true;
+ }
+ step_is_complete
+ },
+ });
+ first_runner.run_all_steps();
+
+ let mut second_runner = RemoteRunner::new_existing_session(fake_win_size, session_name)
+ .dont_panic()
+ .add_step(Step {
+ name: "Split pane to the right",
+ instruction: |mut remote_terminal: RemoteTerminal| -> bool {
+ let mut step_is_complete = false;
+ if remote_terminal.status_bar_appears()
+ && remote_terminal.cursor_position_is(3, 2)
+ {
+ remote_terminal.send_key(&PANE_MODE);
+ remote_terminal.send_key(&SPLIT_RIGHT_IN_PANE_MODE);
+ // back to normal mode after split
+ remote_terminal.send_key(&ENTER);
+ step_is_complete = true;
+ }
+ step_is_complete
+ },
+ });
+ second_runner.run_all_steps();
+
+ if first_runner.test_timed_out || second_runner.test_timed_out {
+ test_attempts -= 1;
+ continue;
+ }
+
+ let second_runner_snapshot = second_runner.take_snapshot_after(Step {
+ name: "take snapshot after",
+ instruction: |remote_terminal: RemoteTerminal| -> bool {
+ let mut step_is_complete = false;
+ if remote_terminal.cursor_position_is(63, 2) && remote_terminal.tip_appears() {
+ // cursor is in the newly opened second pane
+ step_is_complete = true;
+ }
+ step_is_complete
+ },
+ });
+
+ let first_runner_snapshot = first_runner.take_snapshot_after(Step {
+ name: "take snapshot after",
+ instruction: |remote_terminal: RemoteTerminal| -> bool {
+ let mut step_is_complete = false;
+ if remote_terminal.cursor_position_is(3, 2)
+ && remote_terminal.snapshot_contains("││$")
+ {
+ // cursor is back in the first tab
+ step_is_complete = true;
+ }
+ step_is_complete
+ },
+ });
+
+ if (first_runner.test_timed_out || second_runner.test_timed_out) && test_attempts >= 0 {
+ test_attempts -= 1;
+ continue;
+ } else {
+ break (first_runner_snapshot, second_runner_snapshot);
+ }
+ };
+ assert_snapshot!(first_runner_snapshot);
+ assert_snapshot!(second_runner_snapshot);
+}
+
+#[test]
+#[ignore]
+pub fn multiple_users_in_different_tabs() {
+ let fake_win_size = Size {
+ cols: 120,
+ rows: 24,
+ };
+ let mut test_attempts = 10;
+ let session_name = "multiple_users_in_different_tabs";
+ let (first_runner_snapshot, second_runner_snapshot) = loop {
+ // here we connect with one runner, then connect with another, perform some actions and
+ // then make sure they were also reflected (mirrored) in the first runner afterwards
+ RemoteRunner::kill_running_sessions(fake_win_size);
+ drop(());
+ let mut first_runner =
+ RemoteRunner::new_with_session_name(fake_win_size, session_name, false)
+ .dont_panic()
+ .add_step(Step {
+ name: "Wait for app to load",
+ instruction: |mut remote_terminal: RemoteTerminal| -> bool {
+ let mut step_is_complete = false;
+ if remote_terminal.status_bar_appears()
+ && remote_terminal.cursor_position_is(3, 2)
+ {
+ step_is_complete = true;
+ }
+ step_is_complete
+ },
+ });
+ first_runner.run_all_steps();
+
+ let mut second_runner = RemoteRunner::new_existing_session(fake_win_size, session_name)
+ .dont_panic()
+ .add_step(Step {
+ name: "Open new tab",
+ instruction: |mut remote_terminal: RemoteTerminal| -> bool {
+ let mut step_is_complete = false;
+ if remote_terminal.cursor_position_is(3, 2) && remote_terminal.tip_appears() {
+ // cursor is in the newly opened second pane
+ remote_terminal.send_key(&TAB_MODE);
+ remote_terminal.send_key(&NEW_TAB_IN_TAB_MODE);
+ // back to normal mode after split
+ remote_terminal.send_key(&ENTER);
+ step_is_complete = true;
+ }
+ step_is_complete
+ },
+ });
+ second_runner.run_all_steps();
+
+ if first_runner.test_timed_out || second_runner.test_timed_out {
+ test_attempts -= 1;
+ continue;
+ }
+
+ let second_runner_snapshot = second_runner.take_snapshot_after(Step {
+ name: "Wait for new tab to open",
+ instruction: |remote_terminal: RemoteTerminal| -> bool {
+ let mut step_is_complete = false;
+ if remote_terminal.cursor_position_is(3, 2)
+ && remote_terminal.tip_appears()
+ && remote_terminal.snapshot_contains("Tab #2")
+ && remote_terminal.status_bar_appears()
+ {
+ // cursor is in the newly opened second tab
+ step_is_complete = true;
+ }
+ step_is_complete
+ },
+ });
+
+ let first_runner_snapshot = first_runner.take_snapshot_after(Step {
+ name: "Wait for new tab to open",
+ instruction: |remote_terminal: RemoteTerminal| -> bool {
+ let mut step_is_complete = false;
+ if remote_terminal.cursor_position_is(3, 2)
+ && remote_terminal.tip_appears()
+ && remote_terminal.snapshot_contains("Tab #2")
+ && remote_terminal.status_bar_appears()
+ {
+ // cursor is in the newly opened second tab
+ step_is_complete = true;
+ }
+ step_is_complete
+ },
+ });
+
+ if (first_runner.test_timed_out || second_runner.test_timed_out) && test_attempts >= 0 {
+ test_attempts -= 1;
+ continue;
+ } else {
+ break (first_runner_snapshot, second_runner_snapshot);
+ }
+ };
+ assert_snapshot!(first_runner_snapshot);
+ assert_snapshot!(second_runner_snapshot);
+}
+
+#[test]
+#[ignore]
pub fn bracketed_paste() {
let fake_win_size = Size {
cols: 120,
diff --git a/src/tests/e2e/remote_runner.rs b/src/tests/e2e/remote_runner.rs
index 205710f1d..feaeecdac 100644
--- a/src/tests/e2e/remote_runner.rs
+++ b/src/tests/e2e/remote_runner.rs
@@ -67,13 +67,27 @@ fn start_zellij(channel: &mut ssh2::Channel) {
channel.flush().unwrap();
}
-fn start_zellij_in_session(channel: &mut ssh2::Channel, session_name: &str) {
+fn start_zellij_mirrored_session(channel: &mut ssh2::Channel) {
stop_zellij(channel);
channel
.write_all(
format!(
- "{} --session {}\n",
- ZELLIJ_EXECUTABLE_LOCATION, session_name
+ "{} --session {} options --mirror-session true\n",
+ ZELLIJ_EXECUTABLE_LOCATION, SESSION_NAME
+ )
+ .as_bytes(),
+ )
+ .unwrap();
+ channel.flush().unwrap();
+}
+
+fn start_zellij_in_session(channel: &mut ssh2::Channel, session_name: &str, mirrored: bool) {
+ stop_zellij(channel);
+ channel
+ .write_all(
+ format!(
+ "{} --session {} options --mirror-session {}\n",
+ ZELLIJ_EXECUTABLE_LOCATION, session_name, mirrored
)
.as_bytes(),
)
@@ -333,13 +347,48 @@ impl RemoteRunner {
reader_thread,
}
}
+ pub fn new_mirrored_session(win_size: Size) -> Self {
+ let sess = ssh_connect();
+ let mut channel = sess.channel_session().unwrap();
+ let mut rows = Dimension::fixed(win_size.rows);
+ let mut cols = Dimension::fixed(win_size.cols);
+ rows.set_inner(win_size.rows);
+ cols.set_inner(win_size.cols);
+ let pane_geom = PaneGeom {
+ x: 0,
+ y: 0,
+ rows,
+ cols,
+ };