feat(collaboration): implement multiple users (#957)

* work

* feat(collaboration): implement multiple users

* style(cleanup): some leftovers
This commit is contained in:
Aram Drevekenin 2021-12-20 17:31:07 +01:00 committed by GitHub
parent 2c1d3a9817
commit ca8438b0aa
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 858 additions and 173 deletions

BIN
assets/plugins/status-bar.wasm Normal file → Executable file

Binary file not shown.

BIN
assets/plugins/strider.wasm Normal file → Executable file

Binary file not shown.

BIN
assets/plugins/tab-bar.wasm Normal file → Executable file

Binary file not shown.

View file

@ -85,6 +85,7 @@ impl ZellijPlugin for State {
t.is_sync_panes_active, t.is_sync_panes_active,
self.mode_info.palette, self.mode_info.palette,
self.mode_info.capabilities, self.mode_info.capabilities,
t.other_focused_clients.as_slice(),
); );
all_tabs.push(tab); all_tabs.push(tab);
} }

View file

@ -1,33 +1,62 @@
use crate::{line::tab_separator, LinePart}; use crate::{line::tab_separator, LinePart};
use ansi_term::ANSIStrings; use ansi_term::{ANSIString, ANSIStrings};
use unicode_width::UnicodeWidthStr; use unicode_width::UnicodeWidthStr;
use zellij_tile::prelude::*; use zellij_tile::prelude::*;
use zellij_tile_utils::style; use zellij_tile_utils::style;
pub fn active_tab(text: String, palette: Palette, separator: &str) -> LinePart { fn cursors(focused_clients: &[ClientId], palette: Palette) -> (Vec<ANSIString>, usize) {
let left_separator = style!(palette.gray, palette.green).paint(separator); // cursor section, text length
let tab_text_len = text.width() + 2 + separator.width() * 2; // 2 for left and right separators, 2 for the text padding let mut len = 0;
let tab_styled_text = style!(palette.black, palette.green) let mut cursors = vec![];
.bold() for client_id in focused_clients.iter() {
.paint(format!(" {} ", text)); if let Some(color) = client_id_to_colors(*client_id, palette) {
let right_separator = style!(palette.green, palette.gray).paint(separator); cursors.push(style!(color.1, color.0).paint(" "));
let tab_styled_text = len += 1;
ANSIStrings(&[left_separator, tab_styled_text, right_separator]).to_string(); }
LinePart {
part: tab_styled_text,
len: tab_text_len,
} }
(cursors, len)
} }
pub fn non_active_tab(text: String, palette: Palette, separator: &str) -> LinePart { pub fn render_tab(
let left_separator = style!(palette.gray, palette.fg).paint(separator); text: String,
let tab_text_len = text.width() + 2 + separator.width() * 2; // 2 for left and right separators, 2 for the text padding palette: Palette,
let tab_styled_text = style!(palette.black, palette.fg) 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() .bold()
.paint(format!(" {} ", text)); .paint(format!(" {} ", text));
let right_separator = style!(palette.fg, palette.gray).paint(separator);
let tab_styled_text = let right_separator = style!(background_color, palette.gray).paint(separator);
ANSIStrings(&[left_separator, tab_styled_text, right_separator]).to_string(); 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 { LinePart {
part: tab_styled_text, part: tab_styled_text,
len: tab_text_len, len: tab_text_len,
@ -40,15 +69,12 @@ pub fn tab_style(
is_sync_panes_active: bool, is_sync_panes_active: bool,
palette: Palette, palette: Palette,
capabilities: PluginCapabilities, capabilities: PluginCapabilities,
focused_clients: &[ClientId],
) -> LinePart { ) -> LinePart {
let separator = tab_separator(capabilities); let separator = tab_separator(capabilities);
let mut tab_text = text; let mut tab_text = text;
if is_sync_panes_active { if is_sync_panes_active {
tab_text.push_str(" (Sync)"); tab_text.push_str(" (Sync)");
} }
if is_active_tab { render_tab(tab_text, palette, separator, focused_clients, is_active_tab)
active_tab(tab_text, palette, separator)
} else {
non_active_tab(tab_text, palette, separator)
}
} }

View file

@ -178,7 +178,6 @@ pub fn cannot_split_terminals_vertically_when_active_terminal_is_too_small() {
name: "Make sure only one pane appears", name: "Make sure only one pane appears",
instruction: |remote_terminal: RemoteTerminal| -> bool { instruction: |remote_terminal: RemoteTerminal| -> bool {
let mut step_is_complete = false; 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) { if remote_terminal.cursor_position_is(3, 2) {
// ... is the truncated tip line // ... is the truncated tip line
step_is_complete = true; step_is_complete = true;
@ -928,7 +927,7 @@ pub fn detach_and_attach_session() {
let last_snapshot = loop { let last_snapshot = loop {
RemoteRunner::kill_running_sessions(fake_win_size); RemoteRunner::kill_running_sessions(fake_win_size);
drop(()); drop(());
let mut runner = RemoteRunner::new(fake_win_size) let mut runner = RemoteRunner::new_mirrored_session(fake_win_size)
.add_step(Step { .add_step(Step {
name: "Split pane to the right", name: "Split pane to the right",
instruction: |mut remote_terminal: RemoteTerminal| -> bool { 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 // then make sure they were also reflected (mirrored) in the first runner afterwards
RemoteRunner::kill_running_sessions(fake_win_size); RemoteRunner::kill_running_sessions(fake_win_size);
drop(()); drop(());
let mut first_runner = RemoteRunner::new_with_session_name(fake_win_size, session_name) let mut first_runner =
.dont_panic() RemoteRunner::new_with_session_name(fake_win_size, session_name, true)
.add_step(Step { .dont_panic()
name: "Wait for app to load", .add_step(Step {
instruction: |mut remote_terminal: RemoteTerminal| -> bool { name: "Wait for app to load",
let mut step_is_complete = false; instruction: |mut remote_terminal: RemoteTerminal| -> bool {
if remote_terminal.status_bar_appears() let mut step_is_complete = false;
&& remote_terminal.cursor_position_is(3, 2) if remote_terminal.status_bar_appears()
{ && remote_terminal.cursor_position_is(3, 2)
step_is_complete = true; {
} step_is_complete = true;
step_is_complete }
}, step_is_complete
}); },
});
first_runner.run_all_steps(); first_runner.run_all_steps();
let mut second_runner = RemoteRunner::new_existing_session(fake_win_size, session_name) let mut second_runner = RemoteRunner::new_existing_session(fake_win_size, session_name)
@ -1396,6 +1396,286 @@ pub fn mirrored_sessions() {
assert_snapshot!(second_runner_snapshot); assert_snapshot!(second_runner_snapshot);
} }
#[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] #[test]
#[ignore] #[ignore]
pub fn bracketed_paste() { pub fn bracketed_paste() {

View file

@ -67,13 +67,27 @@ fn start_zellij(channel: &mut ssh2::Channel) {
channel.flush().unwrap(); 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); stop_zellij(channel);
channel channel
.write_all( .write_all(
format!( format!(
"{} --session {}\n", "{} --session {} options --mirror-session true\n",
ZELLIJ_EXECUTABLE_LOCATION, session_name 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(), .as_bytes(),
) )
@ -333,13 +347,48 @@ impl RemoteRunner {
reader_thread, 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,
};
setup_remote_environment(&mut channel, win_size);
start_zellij_mirrored_session(&mut channel);
let channel = Arc::new(Mutex::new(channel));
let last_snapshot = Arc::new(Mutex::new(String::new()));
let cursor_coordinates = Arc::new(Mutex::new((0, 0)));
sess.set_blocking(false);
let reader_thread =
read_from_channel(&channel, &last_snapshot, &cursor_coordinates, &pane_geom);
RemoteRunner {
steps: vec![],
channel,
currently_running_step: None,
current_step_index: 0,
retries_left: RETRIES,
retry_pause_ms: 100,
test_timed_out: false,
panic_on_no_retries_left: true,
last_snapshot,
cursor_coordinates,
reader_thread,
}
}
pub fn kill_running_sessions(win_size: Size) { pub fn kill_running_sessions(win_size: Size) {
let sess = ssh_connect(); let sess = ssh_connect();
let mut channel = sess.channel_session().unwrap(); let mut channel = sess.channel_session().unwrap();
setup_remote_environment(&mut channel, win_size); setup_remote_environment(&mut channel, win_size);
start_zellij(&mut channel); start_zellij(&mut channel);
} }
pub fn new_with_session_name(win_size: Size, session_name: &str) -> Self { pub fn new_with_session_name(win_size: Size, session_name: &str, mirrored: bool) -> Self {
// notice that this method does not have a timeout, so use with caution! // notice that this method does not have a timeout, so use with caution!
let sess = ssh_connect_without_timeout(); let sess = ssh_connect_without_timeout();
let mut channel = sess.channel_session().unwrap(); let mut channel = sess.channel_session().unwrap();
@ -354,7 +403,7 @@ impl RemoteRunner {
cols, cols,
}; };
setup_remote_environment(&mut channel, win_size); setup_remote_environment(&mut channel, win_size);
start_zellij_in_session(&mut channel, session_name); start_zellij_in_session(&mut channel, session_name, mirrored);
let channel = Arc::new(Mutex::new(channel)); let channel = Arc::new(Mutex::new(channel));
let last_snapshot = Arc::new(Mutex::new(String::new())); let last_snapshot = Arc::new(Mutex::new(String::new()));
let cursor_coordinates = Arc::new(Mutex::new((0, 0))); let cursor_coordinates = Arc::new(Mutex::new((0, 0)));

View file

@ -0,0 +1,29 @@
---
source: src/tests/e2e/cases.rs
expression: second_runner_snapshot
---
Zellij (multiple_users_in_same_pane_and_tab)  Tab #1 [ ]
┌ Pane #1 ───────────┤ FOCUSED USER: ├───────────────────┐┌ Pane #2 ──────────────┤ MY FOCUS ├───────────────────────┐
│$ ││$ █ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
└──────────────────────────────────────────────────────────┘└──────────────────────────────────────────────────────────┘
Ctrl + <g> LOCK  <p> PANE  <t> TAB  <n> RESIZE  <h> MOVE  <s> SCROLL  <o> SESSION  <q> QUIT 
Tip: Alt + <n> => new pane. Alt + <[] or hjkl> => navigate. Alt + <+-> => resize pane.

View file

@ -0,0 +1,29 @@
---
source: src/tests/e2e/cases.rs
expression: first_runner_snapshot
---
Zellij (multiple_users_in_same_pane_and_tab)  Tab #1 [ ]
┌ Pane #1 ──────────────┤ MY FOCUS ├───────────────────────┐┌ Pane #2 ───────────┤ FOCUSED USER: ├───────────────────┐
│$ █ ││$ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
└──────────────────────────────────────────────────────────┘└──────────────────────────────────────────────────────────┘
Ctrl + <g> LOCK  <p> PANE  <t> TAB  <n> RESIZE  <h> MOVE  <s> SCROLL  <o> SESSION  <q> QUIT 
Tip: Alt + <n> => new pane. Alt + <[] or hjkl> => navigate. Alt + <+-> => resize pane.

View file

@ -0,0 +1,29 @@
---
source: src/tests/e2e/cases.rs
expression: second_runner_snapshot
---
Zellij (multiple_users_in_different_tabs)  Tab #1 [ ] Tab #2 
┌ Pane #1 ────────────────────────────────────────────┤ MY FOCUS ├─────────────────────────────────────────────────────┐
│$ █ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
Ctrl + <g> LOCK  <p> PANE  <t> TAB  <n> RESIZE  <h> MOVE  <s> SCROLL  <o> SESSION  <q> QUIT 
Tip: Alt + <n> => new pane. Alt + <[] or hjkl> => navigate. Alt + <+-> => resize pane.

View file

@ -0,0 +1,29 @@
---
source: src/tests/e2e/cases.rs
expression: first_runner_snapshot
---
Zellij (multiple_users_in_different_tabs)  Tab #1  Tab #2 [ ]
┌ Pane #1 ────────────────────────────────────────────┤ MY FOCUS ├─────────────────────────────────────────────────────┐
│$ █ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
Ctrl + <g> LOCK  <p> PANE  <t> TAB  <n> RESIZE  <h> MOVE  <s> SCROLL  <o> SESSION  <q> QUIT 
Tip: Alt + <n> => new pane. Alt + <[] or hjkl> => navigate. Alt + <+-> => resize pane.

View file

@ -0,0 +1,29 @@
---
source: src/tests/e2e/cases.rs
expression: second_runner_snapshot
---
Zellij (multiple_users_in_same_pane_and_tab)  Tab #1 [ ]
┌ Pane #1 ─────────────────────────────────────────┤ MY FOCUS AND: ├─────────────────────────────────────────────────┐
│$ █ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
Ctrl + <g> LOCK  <p> PANE  <t> TAB  <n> RESIZE  <h> MOVE  <s> SCROLL  <o> SESSION  <q> QUIT 
Tip: Alt + <n> => new pane. Alt + <[] or hjkl> => navigate. Alt + <+-> => resize pane.

View file

@ -0,0 +1,29 @@
---
source: src/tests/e2e/cases.rs
expression: first_runner_snapshot
---
Zellij (multiple_users_in_same_pane_and_tab)  Tab #1 [ ]
┌ Pane #1 ─────────────────────────────────────────┤ MY FOCUS AND: ├─────────────────────────────────────────────────┐
│$ █ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
Ctrl + <g> LOCK  <p> PANE  <t> TAB  <n> RESIZE  <h> MOVE  <s> SCROLL  <o> SESSION  <q> QUIT 
Tip: Alt + <n> => new pane. Alt + <[] or hjkl> => navigate. Alt + <+-> => resize pane.

View file

@ -1,7 +1,9 @@
//! Things related to [`Screen`]s. //! Things related to [`Screen`]s.
use std::cell::RefCell;
use std::collections::{BTreeMap, HashSet}; use std::collections::{BTreeMap, HashSet};
use std::os::unix::io::RawFd; use std::os::unix::io::RawFd;
use std::rc::Rc;
use std::str; use std::str;
use zellij_utils::pane_size::Size; use zellij_utils::pane_size::Size;
@ -182,6 +184,7 @@ pub(crate) struct Screen {
size: Size, size: Size,
/// The overlay that is drawn on top of [`Pane`]'s', [`Tab`]'s and the [`Screen`] /// The overlay that is drawn on top of [`Pane`]'s', [`Tab`]'s and the [`Screen`]
overlay: OverlayWindow, overlay: OverlayWindow,
connected_clients: Rc<RefCell<HashSet<ClientId>>>,
/// The indices of this [`Screen`]'s active [`Tab`]s. /// The indices of this [`Screen`]'s active [`Tab`]s.
active_tab_indices: BTreeMap<ClientId, usize>, active_tab_indices: BTreeMap<ClientId, usize>,
tab_history: BTreeMap<ClientId, Vec<usize>>, tab_history: BTreeMap<ClientId, Vec<usize>>,
@ -189,6 +192,7 @@ pub(crate) struct Screen {
default_mode_info: ModeInfo, // TODO: restructure ModeInfo to prevent this duplication default_mode_info: ModeInfo, // TODO: restructure ModeInfo to prevent this duplication
colors: Palette, colors: Palette,
draw_pane_frames: bool, draw_pane_frames: bool,
session_is_mirrored: bool,
} }
impl Screen { impl Screen {
@ -199,12 +203,14 @@ impl Screen {
max_panes: Option<usize>, max_panes: Option<usize>,
mode_info: ModeInfo, mode_info: ModeInfo,
draw_pane_frames: bool, draw_pane_frames: bool,
session_is_mirrored: bool,
) -> Self { ) -> Self {
Screen { Screen {
bus, bus,
max_panes, max_panes,
size: client_attributes.size, size: client_attributes.size,
colors: client_attributes.palette, colors: client_attributes.palette,
connected_clients: Rc::new(RefCell::new(HashSet::new())),
active_tab_indices: BTreeMap::new(), active_tab_indices: BTreeMap::new(),
tabs: BTreeMap::new(), tabs: BTreeMap::new(),
overlay: OverlayWindow::default(), overlay: OverlayWindow::default(),
@ -212,6 +218,7 @@ impl Screen {
mode_info: BTreeMap::new(), mode_info: BTreeMap::new(),
default_mode_info: mode_info, default_mode_info: mode_info,
draw_pane_frames, draw_pane_frames,
session_is_mirrored,
} }
} }
@ -226,34 +233,50 @@ impl Screen {
} }
} }
fn move_clients_from_closed_tab(&mut self, previous_tab_index: usize) { fn move_clients_from_closed_tab(
let client_ids_in_closed_tab: Vec<ClientId> = self &mut self,
.active_tab_indices client_ids_and_mode_infos: Vec<(ClientId, ModeInfo)>,
.iter() ) {
.filter(|(_c_id, t_index)| **t_index == previous_tab_index) for (client_id, client_mode_info) in client_ids_and_mode_infos {
.map(|(c_id, _t_index)| c_id)
.copied()
.collect();
for client_id in client_ids_in_closed_tab {
let client_previous_tab = self.tab_history.get_mut(&client_id).unwrap().pop().unwrap(); let client_previous_tab = self.tab_history.get_mut(&client_id).unwrap().pop().unwrap();
self.active_tab_indices self.active_tab_indices
.insert(client_id, client_previous_tab); .insert(client_id, client_previous_tab);
self.tabs self.tabs
.get_mut(&client_previous_tab) .get_mut(&client_previous_tab)
.unwrap() .unwrap()
.add_client(client_id); .add_client(client_id, Some(client_mode_info));
} }
} }
fn move_clients(&mut self, source_index: usize, destination_index: usize) { fn move_clients_between_tabs(
let (connected_clients_in_source_tab, client_mode_infos_in_source_tab) = { &mut self,
let source_tab = self.tabs.get_mut(&source_index).unwrap(); source_tab_index: usize,
source_tab.drain_connected_clients() destination_tab_index: usize,
}; clients_to_move: Option<Vec<ClientId>>,
let destination_tab = self.tabs.get_mut(&destination_index).unwrap(); ) {
destination_tab.add_multiple_clients( // None ==> move all clients
connected_clients_in_source_tab, let drained_clients = self
client_mode_infos_in_source_tab, .get_indexed_tab_mut(source_tab_index)
); .map(|t| t.drain_connected_clients(clients_to_move));
if let Some(client_mode_info_in_source_tab) = drained_clients {
let destination_tab = self.get_indexed_tab_mut(destination_tab_index).unwrap();
destination_tab.add_multiple_clients(client_mode_info_in_source_tab);
destination_tab.update_input_modes();
destination_tab.set_force_render();
destination_tab.visible(true);
}
}
fn update_client_tab_focus(&mut self, client_id: ClientId, new_tab_index: usize) {
match self.active_tab_indices.remove(&client_id) {
Some(old_active_index) => {
self.active_tab_indices.insert(client_id, new_tab_index);
let client_tab_history = self.tab_history.entry(client_id).or_insert_with(Vec::new);
client_tab_history.retain(|&e| e != new_tab_index);
client_tab_history.push(old_active_index);
}
None => {
self.active_tab_indices.insert(client_id, new_tab_index);
}
}
} }
/// A helper function to switch to a new tab at specified position. /// A helper function to switch to a new tab at specified position.
fn switch_active_tab(&mut self, new_tab_pos: usize, client_id: ClientId) { fn switch_active_tab(&mut self, new_tab_pos: usize, client_id: ClientId) {
@ -265,24 +288,29 @@ impl Screen {
return; return;
} }
current_tab.visible(false);
let current_tab_index = current_tab.index; let current_tab_index = current_tab.index;
let new_tab_index = new_tab.index; let new_tab_index = new_tab.index;
let new_tab = self.get_indexed_tab_mut(new_tab_index).unwrap(); if self.session_is_mirrored {
new_tab.set_force_render(); self.move_clients_between_tabs(current_tab_index, new_tab_index, None);
new_tab.visible(true); let all_connected_clients: Vec<ClientId> =
self.connected_clients.borrow().iter().copied().collect();
// currently all clients are just mirrors, so we perform the action for every entry in for client_id in all_connected_clients {
// tab_history self.update_client_tab_focus(client_id, new_tab_index);
// TODO: receive a client_id and only do it for the client }
for (client_id, tab_history) in &mut self.tab_history { } else {
let old_active_index = self.active_tab_indices.remove(client_id).unwrap(); self.move_clients_between_tabs(
self.active_tab_indices.insert(*client_id, new_tab_index); current_tab_index,
tab_history.retain(|&e| e != new_tab_pos); new_tab_index,
tab_history.push(old_active_index); Some(vec![client_id]),
);
self.update_client_tab_focus(client_id, new_tab_index);
} }
self.move_clients(current_tab_index, new_tab_index); if let Some(current_tab) = self.get_indexed_tab_mut(current_tab_index) {
if current_tab.has_no_connected_clients() {
current_tab.visible(false);
}
}
self.update_tabs(); self.update_tabs();
self.render(); self.render();
@ -314,7 +342,7 @@ impl Screen {
} }
fn close_tab_at_index(&mut self, tab_index: usize) { fn close_tab_at_index(&mut self, tab_index: usize) {
let tab_to_close = self.tabs.remove(&tab_index).unwrap(); let mut tab_to_close = self.tabs.remove(&tab_index).unwrap();
let pane_ids = tab_to_close.get_pane_ids(); let pane_ids = tab_to_close.get_pane_ids();
// below we don't check the result of sending the CloseTab instruction to the pty thread // below we don't check the result of sending the CloseTab instruction to the pty thread
// because this might be happening when the app is closing, at which point the pty thread // because this might be happening when the app is closing, at which point the pty thread
@ -330,7 +358,8 @@ impl Screen {
.send_to_server(ServerInstruction::Render(None)) .send_to_server(ServerInstruction::Render(None))
.unwrap(); .unwrap();
} else { } else {
self.move_clients_from_closed_tab(tab_index); let client_mode_infos_in_closed_tab = tab_to_close.drain_connected_clients(None);
self.move_clients_from_closed_tab(client_mode_infos_in_closed_tab);
let visible_tab_indices: HashSet<usize> = let visible_tab_indices: HashSet<usize> =
self.active_tab_indices.values().copied().collect(); self.active_tab_indices.values().copied().collect();
for t in self.tabs.values_mut() { for t in self.tabs.values_mut() {
@ -446,28 +475,38 @@ impl Screen {
client_mode_info, client_mode_info,
self.colors, self.colors,
self.draw_pane_frames, self.draw_pane_frames,
self.connected_clients.clone(),
self.session_is_mirrored,
client_id, client_id,
); );
tab.apply_layout(layout, new_pids, tab_index, client_id); tab.apply_layout(layout, new_pids, tab_index, client_id);
if let Some(active_tab) = self.get_active_tab_mut(client_id) { if self.session_is_mirrored {
active_tab.visible(false); if let Some(active_tab) = self.get_active_tab_mut(client_id) {
let (connected_clients_in_source_tab, client_mode_infos_in_source_tab) = let client_mode_infos_in_source_tab = active_tab.drain_connected_clients(None);
active_tab.drain_connected_clients(); tab.add_multiple_clients(client_mode_infos_in_source_tab);
tab.add_multiple_clients( if active_tab.has_no_connected_clients() {
connected_clients_in_source_tab, active_tab.visible(false);
client_mode_infos_in_source_tab, }
); }
} let all_connected_clients: Vec<ClientId> =
for (client_id, tab_history) in &mut self.tab_history { self.connected_clients.borrow().iter().copied().collect();
let old_active_index = self.active_tab_indices.remove(client_id).unwrap(); for client_id in all_connected_clients {
self.active_tab_indices.insert(*client_id, tab_index); self.update_client_tab_focus(client_id, tab_index);
tab_history.retain(|&e| e != tab_index); }
tab_history.push(old_active_index); } else if let Some(active_tab) = self.get_active_tab_mut(client_id) {
let client_mode_info_in_source_tab =
active_tab.drain_connected_clients(Some(vec![client_id]));
tab.add_multiple_clients(client_mode_info_in_source_tab);
if active_tab.has_no_connected_clients() {
active_tab.visible(false);
}
self.update_client_tab_focus(client_id, tab_index);
} }
tab.update_input_modes(); tab.update_input_modes();
tab.visible(true); tab.visible(true);
self.tabs.insert(tab_index, tab); self.tabs.insert(tab_index, tab);
if !self.active_tab_indices.contains_key(&client_id) { if !self.active_tab_indices.contains_key(&client_id) {
// this means this is a new client and we need to add it to our state properly
self.add_client(client_id); self.add_client(client_id);
} }
self.update_tabs(); self.update_tabs();
@ -486,12 +525,19 @@ impl Screen {
tab_history = first_tab_history.clone(); tab_history = first_tab_history.clone();
} }
self.active_tab_indices.insert(client_id, tab_index); self.active_tab_indices.insert(client_id, tab_index);
self.connected_clients.borrow_mut().insert(client_id);
self.tab_history.insert(client_id, tab_history); self.tab_history.insert(client_id, tab_history);
self.tabs.get_mut(&tab_index).unwrap().add_client(client_id); self.tabs
.get_mut(&tab_index)
.unwrap()
.add_client(client_id, None);
} }
pub fn remove_client(&mut self, client_id: ClientId) { pub fn remove_client(&mut self, client_id: ClientId) {
if let Some(client_tab) = self.get_active_tab_mut(client_id) { if let Some(client_tab) = self.get_active_tab_mut(client_id) {
client_tab.remove_client(client_id); client_tab.remove_client(client_id);
if client_tab.has_no_connected_clients() {
client_tab.visible(false);
}
} }
if self.active_tab_indices.contains_key(&client_id) { if self.active_tab_indices.contains_key(&client_id) {
self.active_tab_indices.remove(&client_id); self.active_tab_indices.remove(&client_id);
@ -499,30 +545,41 @@ impl Screen {
if self.tab_history.contains_key(&client_id) { if self.tab_history.contains_key(&client_id) {
self.tab_history.remove(&client_id); self.tab_history.remove(&client_id);
} }
self.connected_clients.borrow_mut().remove(&client_id);
self.update_tabs();
} }
pub fn update_tabs(&self) { pub fn update_tabs(&self) {
let mut tab_data = vec![]; for (client_id, active_tab_index) in self.active_tab_indices.iter() {
// TODO: right now all clients are synced, so we just take the first active_tab which is let mut tab_data = vec![];
// the same for everyone - when this is no longer the case, we need to update the TabInfo
// to account for this (or send multiple TabInfos)
if let Some((_first_client, first_active_tab_index)) = self.active_tab_indices.iter().next()
{
for tab in self.tabs.values() { for tab in self.tabs.values() {
let other_focused_clients: Vec<ClientId> = if self.session_is_mirrored {
vec![]
} else {
self.active_tab_indices
.iter()
.filter(|(c_id, tab_position)| {
**tab_position == tab.index && *c_id != client_id
})
.map(|(c_id, _)| c_id)
.copied()
.collect()
};
tab_data.push(TabInfo { tab_data.push(TabInfo {
position: tab.position, position: tab.position,
name: tab.name.clone(), name: tab.name.clone(),
active: *first_active_tab_index == tab.index, active: *active_tab_index == tab.index,
panes_to_hide: tab.panes_to_hide.len(), panes_to_hide: tab.panes_to_hide.len(),
is_fullscreen_active: tab.is_fullscreen_active(), is_fullscreen_active: tab.is_fullscreen_active(),
is_sync_panes_active: tab.is_sync_panes_active(), is_sync_panes_active: tab.is_sync_panes_active(),
other_focused_clients,
}); });
} }
self.bus self.bus
.senders .senders
.send_to_plugin(PluginInstruction::Update( .send_to_plugin(PluginInstruction::Update(
None, None,
None, Some(*client_id),
Event::TabUpdate(tab_data), Event::TabUpdate(tab_data),
)) ))
.unwrap(); .unwrap();
@ -607,6 +664,7 @@ pub(crate) fn screen_thread_main(
) { ) {
let capabilities = config_options.simplified_ui; let capabilities = config_options.simplified_ui;
let draw_pane_frames = config_options.pane_frames.unwrap_or(true); let draw_pane_frames = config_options.pane_frames.unwrap_or(true);
let session_is_mirrored = config_options.mirror_session.unwrap_or(false);
let mut screen = Screen::new( let mut screen = Screen::new(
bus, bus,
@ -620,6 +678,7 @@ pub(crate) fn screen_thread_main(
}, },
), ),
draw_pane_frames, draw_pane_frames,
session_is_mirrored,
); );
loop { loop {
let (event, mut err_ctx) = screen let (event, mut err_ctx) = screen

View file

@ -17,7 +17,9 @@ use crate::{
ClientId, ServerInstruction, ClientId, ServerInstruction,
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::cell::RefCell;
use std::os::unix::io::RawFd; use std::os::unix::io::RawFd;
use std::rc::Rc;
use std::sync::mpsc::channel; use std::sync::mpsc::channel;
use std::time::Instant; use std::time::Instant;
use std::{ use std::{
@ -111,9 +113,16 @@ impl Output {
.insert(*client_id, String::new()); .insert(*client_id, String::new());
} }
} }
pub fn push_str_to_all_clients(&mut self, to_push: &str) { pub fn push_str_to_multiple_clients(
for render_instruction in self.client_render_instructions.values_mut() { &mut self,
render_instruction.push_str(to_push) to_push: &str,
client_ids: impl Iterator<Item = ClientId>,
) {
for client_id in client_ids {
self.client_render_instructions
.get_mut(&client_id)
.unwrap()
.push_str(to_push)
} }
} }
pub fn push_to_client(&mut self, client_id: ClientId, to_push: &str) { pub fn push_to_client(&mut self, client_id: ClientId, to_push: &str) {
@ -141,6 +150,7 @@ pub(crate) struct Tab {
mode_info: HashMap<ClientId, ModeInfo>, mode_info: HashMap<ClientId, ModeInfo>,
default_mode_info: ModeInfo, default_mode_info: ModeInfo,
pub colors: Palette, pub colors: Palette,
connected_clients_in_app: Rc<RefCell<HashSet<ClientId>>>, // TODO: combine this and connected_clients
connected_clients: HashSet<ClientId>, connected_clients: HashSet<ClientId>,
draw_pane_frames: bool, draw_pane_frames: bool,
session_is_mirrored: bool, session_is_mirrored: bool,
@ -322,6 +332,8 @@ impl Tab {
mode_info: ModeInfo, mode_info: ModeInfo,
colors: Palette, colors: Palette,
draw_pane_frames: bool, draw_pane_frames: bool,
connected_clients_in_app: Rc<RefCell<HashSet<ClientId>>>,
session_is_mirrored: bool,
client_id: ClientId, client_id: ClientId,
) -> Self { ) -> Self {
let panes = BTreeMap::new(); let panes = BTreeMap::new();
@ -354,10 +366,9 @@ impl Tab {
default_mode_info: mode_info, default_mode_info: mode_info,
colors, colors,
draw_pane_frames, draw_pane_frames,
// at the moment this is hard-coded while the feature is being developed session_is_mirrored,
// the only effect this has is to make sure the UI is drawn without additional information about other connected clients
session_is_mirrored: true,
pending_vte_events: HashMap::new(), pending_vte_events: HashMap::new(),
connected_clients_in_app,
connected_clients, connected_clients,
} }
} }
@ -491,14 +502,16 @@ impl Tab {
.unwrap(); .unwrap();
} }
} }
pub fn add_client(&mut self, client_id: ClientId) { pub fn add_client(&mut self, client_id: ClientId, mode_info: Option<ModeInfo>) {
match self.connected_clients.iter().next() { match self.connected_clients.iter().next() {
Some(first_client_id) => { Some(first_client_id) => {
let first_active_pane_id = *self.active_panes.get(first_client_id).unwrap(); let first_active_pane_id = *self.active_panes.get(first_client_id).unwrap();
self.connected_clients.insert(client_id); self.connected_clients.insert(client_id);
self.active_panes.insert(client_id, first_active_pane_id); self.active_panes.insert(client_id, first_active_pane_id);
self.mode_info self.mode_info.insert(
.insert(client_id, self.default_mode_info.clone()); client_id,
mode_info.unwrap_or_else(|| self.default_mode_info.clone()),
);
} }
None => { None => {
let mut pane_ids: Vec<PaneId> = self.panes.keys().copied().collect(); let mut pane_ids: Vec<PaneId> = self.panes.keys().copied().collect();
@ -511,40 +524,53 @@ impl Tab {
let first_pane_id = pane_ids.get(0).unwrap(); let first_pane_id = pane_ids.get(0).unwrap();
self.connected_clients.insert(client_id); self.connected_clients.insert(client_id);
self.active_panes.insert(client_id, *first_pane_id); self.active_panes.insert(client_id, *first_pane_id);
self.mode_info self.mode_info.insert(
.insert(client_id, self.default_mode_info.clone()); client_id,
mode_info.unwrap_or_else(|| self.default_mode_info.clone()),
);
} }
} }
// TODO: we might be able to avoid this, we do this so that newly connected clients will // TODO: we might be able to avoid this, we do this so that newly connected clients will
// necessarily get a full render // necessarily get a full render
self.set_force_render(); self.set_force_render();
self.update_input_modes();
} }
pub fn change_mode_info(&mut self, mode_info: ModeInfo, client_id: ClientId) { pub fn change_mode_info(&mut self, mode_info: ModeInfo, client_id: ClientId) {
self.mode_info.insert(client_id, mode_info); self.mode_info.insert(client_id, mode_info);
} }
pub fn add_multiple_clients( pub fn add_multiple_clients(&mut self, client_ids_to_mode_infos: Vec<(ClientId, ModeInfo)>) {
&mut self, for (client_id, client_mode_info) in client_ids_to_mode_infos {
client_ids: Vec<ClientId>, self.add_client(client_id, None);
client_mode_infos: Vec<(ClientId, ModeInfo)>,
) {
for client_id in client_ids {
self.add_client(client_id);
}
for (client_id, client_mode_info) in client_mode_infos {
self.mode_info.insert(client_id, client_mode_info); self.mode_info.insert(client_id, client_mode_info);
} }
} }
pub fn remove_client(&mut self, client_id: ClientId) { pub fn remove_client(&mut self, client_id: ClientId) {
self.connected_clients.remove(&client_id); self.connected_clients.remove(&client_id);
self.active_panes.remove(&client_id);
self.set_force_render(); self.set_force_render();
} }
pub fn drain_connected_clients(&mut self) -> (Vec<ClientId>, Vec<(ClientId, ModeInfo)>) { pub fn drain_connected_clients(
let client_mode_info = self.mode_info.drain(); &mut self,
( clients_to_drain: Option<Vec<ClientId>>,
self.connected_clients.drain().collect(), ) -> Vec<(ClientId, ModeInfo)> {
client_mode_info.collect(), // None => all clients
) let mut client_ids_to_mode_infos = vec![];
let clients_to_drain =
clients_to_drain.unwrap_or_else(|| self.connected_clients.drain().collect());
for client_id in clients_to_drain {
client_ids_to_mode_infos.push(self.drain_single_client(client_id));
}
client_ids_to_mode_infos
}
pub fn drain_single_client(&mut self, client_id: ClientId) -> (ClientId, ModeInfo) {
let client_mode_info = self
.mode_info
.remove(&client_id)
.unwrap_or_else(|| self.default_mode_info.clone());
self.connected_clients.remove(&client_id);
(client_id, client_mode_info)
}
pub fn has_no_connected_clients(&self) -> bool {
self.connected_clients.is_empty()
} }
pub fn new_pane(&mut self, pid: PaneId, client_id: Option<ClientId>) { pub fn new_pane(&mut self, pid: PaneId, client_id: Option<ClientId>) {
self.close_down_to_max_terminals(); self.close_down_to_max_terminals();
@ -786,7 +812,8 @@ impl Tab {
}); });
} }
pub fn write_to_active_terminal(&mut self, input_bytes: Vec<u8>, client_id: ClientId) { pub fn write_to_active_terminal(&mut self, input_bytes: Vec<u8>, client_id: ClientId) {
self.write_to_pane_id(input_bytes, self.get_active_pane_id(client_id).unwrap()); let pane_id = self.get_active_pane_id(client_id).unwrap();
self.write_to_pane_id(input_bytes, pane_id);
} }
pub fn write_to_pane_id(&mut self, input_bytes: Vec<u8>, pane_id: PaneId) { pub fn write_to_pane_id(&mut self, input_bytes: Vec<u8>, pane_id: PaneId) {
match pane_id { match pane_id {
@ -981,10 +1008,21 @@ impl Tab {
// render panes and their frames // render panes and their frames
for (kind, pane) in self.panes.iter_mut() { for (kind, pane) in self.panes.iter_mut() {
if !self.panes_to_hide.contains(&pane.pid()) { if !self.panes_to_hide.contains(&pane.pid()) {
let mut pane_contents_and_ui = let mut active_panes = self.active_panes.clone();
PaneContentsAndUi::new(pane, output, self.colors, &self.active_panes); let multiple_users_exist_in_session =
{ self.connected_clients_in_app.borrow().len() > 1 };
active_panes.retain(|c_id, _| self.connected_clients.contains(c_id));
let mut pane_contents_and_ui = PaneContentsAndUi::new(
pane,
output,
self.colors,
&active_panes,
multiple_users_exist_in_session,
);
if let PaneId::Terminal(..) = kind { if let PaneId::Terminal(..) = kind {
pane_contents_and_ui.render_pane_contents_for_all_clients(); pane_contents_and_ui.render_pane_contents_to_multiple_clients(
self.connected_clients.iter().copied(),
);
} }
for &client_id in &self.connected_clients { for &client_id in &self.connected_clients {
let client_mode = self let client_mode = self
@ -1024,16 +1062,21 @@ impl Tab {
} }
// FIXME: Once clients can be distinguished // FIXME: Once clients can be distinguished
if let Some(overlay_vte) = &overlay { if let Some(overlay_vte) = &overlay {
output.push_str_to_all_clients(overlay_vte); // output.push_str_to_all_clients(overlay_vte);
output
.push_str_to_multiple_clients(overlay_vte, self.connected_clients.iter().copied());
} }
self.render_cursor(output); self.render_cursor(output);
} }
fn hide_cursor_and_clear_display_as_needed(&mut self, output: &mut Output) { fn hide_cursor_and_clear_display_as_needed(&mut self, output: &mut Output) {
let hide_cursor = "\u{1b}[?25l"; let hide_cursor = "\u{1b}[?25l";
output.push_str_to_all_clients(hide_cursor); output.push_str_to_multiple_clients(hide_cursor, self.connected_clients.iter().copied());
if self.should_clear_display_before_rendering { if self.should_clear_display_before_rendering {
let clear_display = "\u{1b}[2J"; let clear_display = "\u{1b}[2J";
output.push_str_to_all_clients(clear_display); output.push_str_to_multiple_clients(
clear_display,
self.connected_clients.iter().copied(),
);
self.should_clear_display_before_rendering = false; self.should_clear_display_before_rendering = false;
} }
} }
@ -3400,10 +3443,10 @@ impl Tab {
fn write_selection_to_clipboard(&self, selection: &str) { fn write_selection_to_clipboard(&self, selection: &str) {
let mut output = Output::default(); let mut output = Output::default();
output.add_clients(&self.connected_clients); output.add_clients(&self.connected_clients);
output.push_str_to_all_clients(&format!( output.push_str_to_multiple_clients(
"\u{1b}]52;c;{}\u{1b}\\", &format!("\u{1b}]52;c;{}\u{1b}\\", base64::encode(selection)),
base64::encode(selection) self.connected_clients.iter().copied(),
)); );
// TODO: ideally we should be sending the Render instruction from the screen // TODO: ideally we should be sending the Render instruction from the screen
self.senders self.senders

View file

@ -3,7 +3,7 @@ use crate::ClientId;
use ansi_term::Colour::{Fixed, RGB}; use ansi_term::Colour::{Fixed, RGB};
use ansi_term::Style; use ansi_term::Style;
use zellij_utils::pane_size::Viewport; use zellij_utils::pane_size::Viewport;
use zellij_utils::zellij_tile::prelude::{Palette, PaletteColor}; use zellij_utils::zellij_tile::prelude::{client_id_to_colors, Palette, PaletteColor};
use unicode_width::{UnicodeWidthChar, UnicodeWidthStr}; use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
@ -29,22 +29,6 @@ fn background_color(character: &str, color: Option<PaletteColor>) -> String {
} }
} }
// TODO: move elsewhere
pub(crate) fn client_id_to_colors(
client_id: ClientId,
colors: Palette,
) -> Option<(PaletteColor, PaletteColor)> {
// (primary color, secondary color)
match client_id {
1 => Some((colors.green, colors.black)),
2 => Some((colors.blue, colors.black)),
3 => Some((colors.cyan, colors.black)),
4 => Some((colors.magenta, colors.black)),
5 => Some((colors.yellow, colors.black)),
_ => None,
}
}
pub struct FrameParams { pub struct FrameParams {
pub focused_client: Option<ClientId>, pub focused_client: Option<ClientId>,
pub is_main_client: bool, pub is_main_client: bool,

View file

@ -1,11 +1,12 @@
use crate::panes::PaneId; use crate::panes::PaneId;
use crate::tab::{Output, Pane}; use crate::tab::{Output, Pane};
use crate::ui::boundaries::Boundaries; use crate::ui::boundaries::Boundaries;
use crate::ui::pane_boundaries_frame::client_id_to_colors;
use crate::ui::pane_boundaries_frame::FrameParams; use crate::ui::pane_boundaries_frame::FrameParams;
use crate::ClientId; use crate::ClientId;
use std::collections::HashMap; use std::collections::HashMap;
use zellij_tile::data::{InputMode, Palette, PaletteColor}; use zellij_tile::data::{
client_id_to_colors, single_client_color, InputMode, Palette, PaletteColor,
};
pub struct PaneContentsAndUi<'a> { pub struct PaneContentsAndUi<'a> {
pane: &'a mut Box<dyn Pane>, pane: &'a mut Box<dyn Pane>,
@ -21,13 +22,13 @@ impl<'a> PaneContentsAndUi<'a> {
output: &'a mut Output, output: &'a mut Output,
colors: Palette, colors: Palette,
active_panes: &HashMap<ClientId, PaneId>, active_panes: &HashMap<ClientId, PaneId>,
multiple_users_exist_in_session: bool,
) -> Self { ) -> Self {
let focused_clients: Vec<ClientId> = active_panes let focused_clients: Vec<ClientId> = active_panes
.iter() .iter()
.filter(|(_c_id, p_id)| **p_id == pane.pid()) .filter(|(_c_id, p_id)| **p_id == pane.pid())
.map(|(c_id, _p_id)| *c_id) .map(|(c_id, _p_id)| *c_id)
.collect(); .collect();
let multiple_users_exist_in_session = active_panes.len() > 1;
PaneContentsAndUi { PaneContentsAndUi {
pane, pane,
output, output,
@ -36,15 +37,21 @@ impl<'a> PaneContentsAndUi<'a> {
multiple_users_exist_in_session, multiple_users_exist_in_session,
} }
} }
pub fn render_pane_contents_for_all_clients(&mut self) { pub fn render_pane_contents_to_multiple_clients(
&mut self,
clients: impl Iterator<Item = ClientId>,
) {
if let Some(vte_output) = self.pane.render(None) { if let Some(vte_output) = self.pane.render(None) {
// FIXME: Use Termion for cursor and style clearing? // FIXME: Use Termion for cursor and style clearing?
self.output.push_str_to_all_clients(&format!( self.output.push_str_to_multiple_clients(
"\u{1b}[{};{}H\u{1b}[m{}", &format!(
self.pane.y() + 1, "\u{1b}[{};{}H\u{1b}[m{}",
self.pane.x() + 1, self.pane.y() + 1,
vte_output self.pane.x() + 1,
)); vte_output
),
clients,
);
} }
} }
pub fn render_pane_contents_for_client(&mut self, client_id: ClientId) { pub fn render_pane_contents_for_client(&mut self, client_id: ClientId) {
@ -165,9 +172,9 @@ impl<'a> PaneContentsAndUi<'a> {
if pane_focused_for_client_id { if pane_focused_for_client_id {
match mode { match mode {
InputMode::Normal | InputMode::Locked => { InputMode::Normal | InputMode::Locked => {
if session_is_mirrored { if session_is_mirrored || !self.multiple_users_exist_in_session {
let colors = client_id_to_colors(1, self.colors); // mirrored sessions only have one focused color let colors = single_client_color(self.colors); // mirrored sessions only have one focused color
colors.map(|colors| colors.0) Some(colors.0)
} else { } else {
let colors = client_id_to_colors(client_id, self.colors); let colors = client_id_to_colors(client_id, self.colors);
colors.map(|colors| colors.0) colors.map(|colors| colors.0)

View file

@ -90,12 +90,14 @@ fn create_new_screen(size: Size) -> Screen {
let max_panes = None; let max_panes = None;
let mode_info = ModeInfo::default(); let mode_info = ModeInfo::default();
let draw_pane_frames = false; let draw_pane_frames = false;
let session_is_mirrored = true;
Screen::new( Screen::new(
bus, bus,
&client_attributes, &client_attributes,
max_panes, max_panes,
mode_info, mode_info,
draw_pane_frames, draw_pane_frames,
session_is_mirrored,
) )
} }

View file

@ -12,7 +12,10 @@ use zellij_utils::input::layout::LayoutTemplate;
use zellij_utils::ipc::IpcReceiverWithContext; use zellij_utils::ipc::IpcReceiverWithContext;
use zellij_utils::pane_size::Size; use zellij_utils::pane_size::Size;
use std::cell::{RefCell, RefMut};
use std::collections::HashSet;
use std::os::unix::io::RawFd; use std::os::unix::io::RawFd;
use std::rc::Rc;
use zellij_utils::nix; use zellij_utils::nix;
@ -89,6 +92,10 @@ fn create_new_tab(size: Size) -> Tab {
let colors = Palette::default(); let colors = Palette::default();
let draw_pane_frames = true; let draw_pane_frames = true;
let client_id = 1; let client_id = 1;
let session_is_mirrored = true;
let mut connected_clients = HashSet::new();
connected_clients.insert(client_id);
let connected_clients = Rc::new(RefCell::new(connected_clients));
let mut tab = Tab::new( let mut tab = Tab::new(
index, index,
position, position,
@ -100,6 +107,8 @@ fn create_new_tab(size: Size) -> Tab {
mode_info, mode_info,
colors, colors,
draw_pane_frames, draw_pane_frames,
connected_clients,
session_is_mirrored,
client_id, client_id,
); );
tab.apply_layout( tab.apply_layout(

View file

@ -3,6 +3,32 @@ use std::fmt;
use std::str::FromStr; use std::str::FromStr;
use strum_macros::{EnumDiscriminants, EnumIter, EnumString, ToString}; use strum_macros::{EnumDiscriminants, EnumIter, EnumString, ToString};
pub type ClientId = u16; // TODO: merge with crate type?
pub fn client_id_to_colors(
client_id: ClientId,
colors: Palette,
) -> Option<(PaletteColor, PaletteColor)> {
// (primary color, secondary color)
match client_id {
1 => Some((colors.magenta, colors.black)),
2 => Some((colors.blue, colors.black)),
3 => Some((colors.purple, colors.black)),
4 => Some((colors.yellow, colors.black)),
5 => Some((colors.cyan, colors.black)),
6 => Some((colors.gold, colors.black)),
7 => Some((colors.red, colors.black)),
8 => Some((colors.silver, colors.black)),
9 => Some((colors.pink, colors.black)),
10 => Some((colors.brown, colors.black)),
_ => None,
}
}
pub fn single_client_color(colors: Palette) -> (PaletteColor, PaletteColor) {
(colors.green, colors.black)
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum Key { pub enum Key {
Backspace, Backspace,
@ -169,6 +195,11 @@ pub struct Palette {
pub white: PaletteColor, pub white: PaletteColor,
pub orange: PaletteColor, pub orange: PaletteColor,
pub gray: PaletteColor, pub gray: PaletteColor,
pub purple: PaletteColor,
pub gold: PaletteColor,
pub silver: PaletteColor,
pub pink: PaletteColor,
pub brown: PaletteColor,
} }
/// Represents the contents of the help message that is printed in the status bar, /// Represents the contents of the help message that is printed in the status bar,
@ -193,6 +224,7 @@ pub struct TabInfo {
pub panes_to_hide: usize, pub panes_to_hide: usize,
pub is_fullscreen_active: bool, pub is_fullscreen_active: bool,
pub is_sync_panes_active: bool, pub is_sync_panes_active: bool,
pub other_focused_clients: Vec<ClientId>,
} }
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)] #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)]

View file

@ -65,6 +65,10 @@ pub struct Options {
#[serde(default)] #[serde(default)]
/// Set display of the pane frames (true or false) /// Set display of the pane frames (true or false)
pub pane_frames: Option<bool>, pub pane_frames: Option<bool>,
#[structopt(long)]
#[serde(default)]
/// Mirror session when multiple users are connected (true or false)
pub mirror_session: Option<bool>,
/// Set behaviour on force close (quit or detach) /// Set behaviour on force close (quit or detach)
#[structopt(long)] #[structopt(long)]
pub on_force_close: Option<OnForceClose>, pub on_force_close: Option<OnForceClose>,
@ -85,6 +89,7 @@ impl Options {
pub fn merge(&self, other: Options) -> Options { pub fn merge(&self, other: Options) -> Options {
let mouse_mode = other.mouse_mode.or(self.mouse_mode); let mouse_mode = other.mouse_mode.or(self.mouse_mode);
let pane_frames = other.pane_frames.or(self.pane_frames); let pane_frames = other.pane_frames.or(self.pane_frames);
let mirror_session = other.mirror_session.or(self.mirror_session);
let simplified_ui = other.simplified_ui.or(self.simplified_ui); let simplified_ui = other.simplified_ui.or(self.simplified_ui);
let default_mode = other.default_mode.or(self.default_mode); let default_mode = other.default_mode.or(self.default_mode);
let default_shell = other.default_shell.or_else(|| self.default_shell.clone()); let default_shell = other.default_shell.or_else(|| self.default_shell.clone());
@ -100,6 +105,7 @@ impl Options {
layout_dir, layout_dir,
mouse_mode, mouse_mode,
pane_frames, pane_frames,
mirror_session,
on_force_close, on_force_close,
} }
} }
@ -122,6 +128,7 @@ impl Options {
let simplified_ui = merge_bool(other.simplified_ui, self.simplified_ui); let simplified_ui = merge_bool(other.simplified_ui, self.simplified_ui);
let mouse_mode = merge_bool(other.mouse_mode, self.mouse_mode); let mouse_mode = merge_bool(other.mouse_mode, self.mouse_mode);
let pane_frames = merge_bool(other.pane_frames, self.pane_frames); let pane_frames = merge_bool(other.pane_frames, self.pane_frames);
let mirror_session = merge_bool(other.mirror_session, self.mirror_session);
let default_mode = other.default_mode.or(self.default_mode); let default_mode = other.default_mode.or(self.default_mode);
let default_shell = other.default_shell.or_else(|| self.default_shell.clone()); let default_shell = other.default_shell.or_else(|| self.default_shell.clone());
@ -137,6 +144,7 @@ impl Options {
layout_dir, layout_dir,
mouse_mode, mouse_mode,
pane_frames, pane_frames,
mirror_session,
on_force_close, on_force_close,
} }
} }
@ -183,6 +191,7 @@ impl From<CliOptions> for Options {
layout_dir: opts.layout_dir, layout_dir: opts.layout_dir,
mouse_mode: opts.mouse_mode, mouse_mode: opts.mouse_mode,
pane_frames: opts.pane_frames, pane_frames: opts.pane_frames,
mirror_session: opts.mirror_session,
on_force_close: opts.on_force_close, on_force_close: opts.on_force_close,
} }
} }

View file

@ -53,6 +53,11 @@ pub mod colors {
pub const CYAN: u8 = 51; pub const CYAN: u8 = 51;
pub const YELLOW: u8 = 226; pub const YELLOW: u8 = 226;
pub const BLUE: u8 = 45; pub const BLUE: u8 = 45;
pub const PURPLE: u8 = 99;
pub const GOLD: u8 = 136;
pub const SILVER: u8 = 245;
pub const PINK: u8 = 207;
pub const BROWN: u8 = 215;
} }
pub fn _hex_to_rgb(hex: &str) -> (u8, u8, u8) { pub fn _hex_to_rgb(hex: &str) -> (u8, u8, u8) {
@ -77,6 +82,11 @@ pub fn default_palette() -> Palette {
white: PaletteColor::EightBit(colors::WHITE), white: PaletteColor::EightBit(colors::WHITE),
orange: PaletteColor::EightBit(colors::ORANGE), orange: PaletteColor::EightBit(colors::ORANGE),
gray: PaletteColor::EightBit(colors::GRAY), gray: PaletteColor::EightBit(colors::GRAY),
purple: PaletteColor::EightBit(colors::PURPLE),
gold: PaletteColor::EightBit(colors::GOLD),
silver: PaletteColor::EightBit(colors::SILVER),
pink: PaletteColor::EightBit(colors::PINK),
brown: PaletteColor::EightBit(colors::BROWN),
} }
} }