feat(collaboration): implement multiple users (#957)
* work * feat(collaboration): implement multiple users * style(cleanup): some leftovers
This commit is contained in:
parent
2c1d3a9817
commit
ca8438b0aa
22 changed files with 858 additions and 173 deletions
BIN
assets/plugins/status-bar.wasm
Normal file → Executable file
BIN
assets/plugins/status-bar.wasm
Normal file → Executable file
Binary file not shown.
BIN
assets/plugins/strider.wasm
Normal file → Executable file
BIN
assets/plugins/strider.wasm
Normal file → Executable file
Binary file not shown.
BIN
assets/plugins/tab-bar.wasm
Normal file → Executable file
BIN
assets/plugins/tab-bar.wasm
Normal file → Executable file
Binary file not shown.
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -1396,6 +1396,286 @@ pub fn mirrored_sessions() {
|
|||
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]
|
||||
#[ignore]
|
||||
pub fn bracketed_paste() {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
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) {
|
||||
let sess = ssh_connect();
|
||||
let mut channel = sess.channel_session().unwrap();
|
||||
setup_remote_environment(&mut channel, win_size);
|
||||
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!
|
||||
let sess = ssh_connect_without_timeout();
|
||||
let mut channel = sess.channel_session().unwrap();
|
||||
|
|
@ -354,7 +403,7 @@ impl RemoteRunner {
|
|||
cols,
|
||||
};
|
||||
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 last_snapshot = Arc::new(Mutex::new(String::new()));
|
||||
let cursor_coordinates = Arc::new(Mutex::new((0, 0)));
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
@ -1,7 +1,9 @@
|
|||
//! Things related to [`Screen`]s.
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::collections::{BTreeMap, HashSet};
|
||||
use std::os::unix::io::RawFd;
|
||||
use std::rc::Rc;
|
||||
use std::str;
|
||||
|
||||
use zellij_utils::pane_size::Size;
|
||||
|
|
@ -182,6 +184,7 @@ pub(crate) struct Screen {
|
|||
size: Size,
|
||||
/// The overlay that is drawn on top of [`Pane`]'s', [`Tab`]'s and the [`Screen`]
|
||||
overlay: OverlayWindow,
|
||||
connected_clients: Rc<RefCell<HashSet<ClientId>>>,
|
||||
/// The indices of this [`Screen`]'s active [`Tab`]s.
|
||||
active_tab_indices: BTreeMap<ClientId, 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
|
||||
colors: Palette,
|
||||
draw_pane_frames: bool,
|
||||
session_is_mirrored: bool,
|
||||
}
|
||||
|
||||
impl Screen {
|
||||
|
|
@ -199,12 +203,14 @@ impl Screen {
|
|||
max_panes: Option<usize>,
|
||||
mode_info: ModeInfo,
|
||||
draw_pane_frames: bool,
|
||||
session_is_mirrored: bool,
|
||||
) -> Self {
|
||||
Screen {
|
||||
bus,
|
||||
max_panes,
|
||||
size: client_attributes.size,
|
||||
colors: client_attributes.palette,
|
||||
connected_clients: Rc::new(RefCell::new(HashSet::new())),
|
||||
active_tab_indices: BTreeMap::new(),
|
||||
tabs: BTreeMap::new(),
|
||||
overlay: OverlayWindow::default(),
|
||||
|
|
@ -212,6 +218,7 @@ impl Screen {
|
|||
mode_info: BTreeMap::new(),
|
||||
default_mode_info: mode_info,
|
||||
draw_pane_frames,
|
||||
session_is_mirrored,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -226,34 +233,50 @@ impl Screen {
|
|||
}
|
||||
}
|
||||
|
||||
fn move_clients_from_closed_tab(&mut self, previous_tab_index: usize) {
|
||||
let client_ids_in_closed_tab: Vec<ClientId> = self
|
||||
.active_tab_indices
|
||||
.iter()
|
||||
.filter(|(_c_id, t_index)| **t_index == previous_tab_index)
|
||||
.map(|(c_id, _t_index)| c_id)
|
||||
.copied()
|
||||
.collect();
|
||||
for client_id in client_ids_in_closed_tab {
|
||||
fn move_clients_from_closed_tab(
|
||||
&mut self,
|
||||
client_ids_and_mode_infos: Vec<(ClientId, ModeInfo)>,
|
||||
) {
|
||||
for (client_id, client_mode_info) in client_ids_and_mode_infos {
|
||||
let client_previous_tab = self.tab_history.get_mut(&client_id).unwrap().pop().unwrap();
|
||||
self.active_tab_indices
|
||||
.insert(client_id, client_previous_tab);
|
||||
self.tabs
|
||||
.get_mut(&client_previous_tab)
|
||||
.unwrap()
|
||||
.add_client(client_id);
|
||||
.add_client(client_id, Some(client_mode_info));
|
||||
}
|
||||
}
|
||||
fn move_clients(&mut self, source_index: usize, destination_index: usize) {
|
||||
let (connected_clients_in_source_tab, client_mode_infos_in_source_tab) = {
|
||||
let source_tab = self.tabs.get_mut(&source_index).unwrap();
|
||||
source_tab.drain_connected_clients()
|
||||
};
|
||||
let destination_tab = self.tabs.get_mut(&destination_index).unwrap();
|
||||
destination_tab.add_multiple_clients(
|
||||
connected_clients_in_source_tab,
|
||||
client_mode_infos_in_source_tab,
|
||||
);
|
||||
fn move_clients_between_tabs(
|
||||
&mut self,
|
||||
source_tab_index: usize,
|
||||
destination_tab_index: usize,
|
||||
clients_to_move: Option<Vec<ClientId>>,
|
||||
) {
|
||||
// None ==> move all clients
|
||||
let drained_clients = self
|
||||
.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.
|
||||
fn switch_active_tab(&mut self, new_tab_pos: usize, client_id: ClientId) {
|
||||
|
|
@ -265,24 +288,29 @@ impl Screen {
|
|||
return;
|
||||
}
|
||||
|
||||
current_tab.visible(false);
|
||||
let current_tab_index = current_tab.index;
|
||||
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);
|
||||
|
||||
// currently all clients are just mirrors, so we perform the action for every entry in
|
||||
// tab_history
|
||||
// TODO: receive a client_id and only do it for the client
|
||||
for (client_id, tab_history) in &mut self.tab_history {
|
||||
let old_active_index = self.active_tab_indices.remove(client_id).unwrap();
|
||||
self.active_tab_indices.insert(*client_id, new_tab_index);
|
||||
tab_history.retain(|&e| e != new_tab_pos);
|
||||
tab_history.push(old_active_index);
|
||||
if self.session_is_mirrored {
|
||||
self.move_clients_between_tabs(current_tab_index, new_tab_index, None);
|
||||
let all_connected_clients: Vec<ClientId> =
|
||||
self.connected_clients.borrow().iter().copied().collect();
|
||||
for client_id in all_connected_clients {
|
||||
self.update_client_tab_focus(client_id, new_tab_index);
|
||||
}
|
||||
} else {
|
||||
self.move_clients_between_tabs(
|
||||
current_tab_index,
|
||||
new_tab_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.render();
|
||||
|
|
@ -314,7 +342,7 @@ impl Screen {
|
|||
}
|
||||
|
||||
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();
|
||||
// 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
|
||||
|
|
@ -330,7 +358,8 @@ impl Screen {
|
|||
.send_to_server(ServerInstruction::Render(None))
|
||||
.unwrap();
|
||||
} 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> =
|
||||
self.active_tab_indices.values().copied().collect();
|
||||
for t in self.tabs.values_mut() {
|
||||
|
|
@ -446,28 +475,38 @@ impl Screen {
|
|||
client_mode_info,
|
||||
self.colors,
|
||||
self.draw_pane_frames,
|
||||
self.connected_clients.clone(),
|
||||
self.session_is_mirrored,
|
||||
client_id,
|
||||
);
|
||||
tab.apply_layout(layout, new_pids, tab_index, client_id);
|
||||
if let Some(active_tab) = self.get_active_tab_mut(client_id) {
|
||||
active_tab.visible(false);
|
||||
let (connected_clients_in_source_tab, client_mode_infos_in_source_tab) =
|
||||
active_tab.drain_connected_clients();
|
||||
tab.add_multiple_clients(
|
||||
connected_clients_in_source_tab,
|
||||
client_mode_infos_in_source_tab,
|
||||
);
|
||||
}
|
||||
for (client_id, tab_history) in &mut self.tab_history {
|
||||
let old_active_index = self.active_tab_indices.remove(client_id).unwrap();
|
||||
self.active_tab_indices.insert(*client_id, tab_index);
|
||||
tab_history.retain(|&e| e != tab_index);
|
||||
tab_history.push(old_active_index);
|
||||
if self.session_is_mirrored {
|
||||
if let Some(active_tab) = self.get_active_tab_mut(client_id) {
|
||||
let client_mode_infos_in_source_tab = active_tab.drain_connected_clients(None);
|
||||
tab.add_multiple_clients(client_mode_infos_in_source_tab);
|
||||
if active_tab.has_no_connected_clients() {
|
||||
active_tab.visible(false);
|
||||
}
|
||||
}
|
||||
let all_connected_clients: Vec<ClientId> =
|
||||
self.connected_clients.borrow().iter().copied().collect();
|
||||
for client_id in all_connected_clients {
|
||||
self.update_client_tab_focus(client_id, tab_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.visible(true);
|
||||
self.tabs.insert(tab_index, tab);
|
||||
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.update_tabs();
|
||||
|
|
@ -486,12 +525,19 @@ impl Screen {
|
|||
tab_history = first_tab_history.clone();
|
||||
}
|
||||
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.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) {
|
||||
if let Some(client_tab) = self.get_active_tab_mut(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) {
|
||||
self.active_tab_indices.remove(&client_id);
|
||||
|
|
@ -499,30 +545,41 @@ impl Screen {
|
|||
if self.tab_history.contains_key(&client_id) {
|
||||
self.tab_history.remove(&client_id);
|
||||
}
|
||||
self.connected_clients.borrow_mut().remove(&client_id);
|
||||
self.update_tabs();
|
||||
}
|
||||
|
||||
pub fn update_tabs(&self) {
|
||||
let mut tab_data = vec![];
|
||||
// TODO: right now all clients are synced, so we just take the first active_tab which is
|
||||
// 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 (client_id, active_tab_index) in self.active_tab_indices.iter() {
|
||||
let mut tab_data = vec![];
|
||||
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 {
|
||||
position: tab.position,
|
||||
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(),
|
||||
is_fullscreen_active: tab.is_fullscreen_active(),
|
||||
is_sync_panes_active: tab.is_sync_panes_active(),
|
||||
other_focused_clients,
|
||||
});
|
||||
}
|
||||
self.bus
|
||||
.senders
|
||||
.send_to_plugin(PluginInstruction::Update(
|
||||
None,
|
||||
None,
|
||||
Some(*client_id),
|
||||
Event::TabUpdate(tab_data),
|
||||
))
|
||||
.unwrap();
|
||||
|
|
@ -607,6 +664,7 @@ pub(crate) fn screen_thread_main(
|
|||
) {
|
||||
let capabilities = config_options.simplified_ui;
|
||||
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(
|
||||
bus,
|
||||
|
|
@ -620,6 +678,7 @@ pub(crate) fn screen_thread_main(
|
|||
},
|
||||
),
|
||||
draw_pane_frames,
|
||||
session_is_mirrored,
|
||||
);
|
||||
loop {
|
||||
let (event, mut err_ctx) = screen
|
||||
|
|
|
|||
|
|
@ -17,7 +17,9 @@ use crate::{
|
|||
ClientId, ServerInstruction,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::cell::RefCell;
|
||||
use std::os::unix::io::RawFd;
|
||||
use std::rc::Rc;
|
||||
use std::sync::mpsc::channel;
|
||||
use std::time::Instant;
|
||||
use std::{
|
||||
|
|
@ -111,9 +113,16 @@ impl Output {
|
|||
.insert(*client_id, String::new());
|
||||
}
|
||||
}
|
||||
pub fn push_str_to_all_clients(&mut self, to_push: &str) {
|
||||
for render_instruction in self.client_render_instructions.values_mut() {
|
||||
render_instruction.push_str(to_push)
|
||||
pub fn push_str_to_multiple_clients(
|
||||
&mut self,
|
||||
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) {
|
||||
|
|
@ -141,6 +150,7 @@ pub(crate) struct Tab {
|
|||
mode_info: HashMap<ClientId, ModeInfo>,
|
||||
default_mode_info: ModeInfo,
|
||||
pub colors: Palette,
|
||||
connected_clients_in_app: Rc<RefCell<HashSet<ClientId>>>, // TODO: combine this and connected_clients
|
||||
connected_clients: HashSet<ClientId>,
|
||||
draw_pane_frames: bool,
|
||||
session_is_mirrored: bool,
|
||||
|
|
@ -322,6 +332,8 @@ impl Tab {
|
|||
mode_info: ModeInfo,
|
||||
colors: Palette,
|
||||
draw_pane_frames: bool,
|
||||
connected_clients_in_app: Rc<RefCell<HashSet<ClientId>>>,
|
||||
session_is_mirrored: bool,
|
||||
client_id: ClientId,
|
||||
) -> Self {
|
||||
let panes = BTreeMap::new();
|
||||
|
|
@ -354,10 +366,9 @@ impl Tab {
|
|||
default_mode_info: mode_info,
|
||||
colors,
|
||||
draw_pane_frames,
|
||||
// at the moment this is hard-coded while the feature is being developed
|
||||
// the only effect this has is to make sure the UI is drawn without additional information about other connected clients
|
||||
session_is_mirrored: true,
|
||||
session_is_mirrored,
|
||||
pending_vte_events: HashMap::new(),
|
||||
connected_clients_in_app,
|
||||
connected_clients,
|
||||
}
|
||||
}
|
||||
|
|
@ -491,14 +502,16 @@ impl Tab {
|
|||
.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() {
|
||||
Some(first_client_id) => {
|
||||
let first_active_pane_id = *self.active_panes.get(first_client_id).unwrap();
|
||||
self.connected_clients.insert(client_id);
|
||||
self.active_panes.insert(client_id, first_active_pane_id);
|
||||
self.mode_info
|
||||
.insert(client_id, self.default_mode_info.clone());
|
||||
self.mode_info.insert(
|
||||
client_id,
|
||||
mode_info.unwrap_or_else(|| self.default_mode_info.clone()),
|
||||
);
|
||||
}
|
||||
None => {
|
||||
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();
|
||||
self.connected_clients.insert(client_id);
|
||||
self.active_panes.insert(client_id, *first_pane_id);
|
||||
self.mode_info
|
||||
.insert(client_id, self.default_mode_info.clone());
|
||||
self.mode_info.insert(
|
||||
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
|
||||
// necessarily get a full render
|
||||
self.set_force_render();
|
||||
self.update_input_modes();
|
||||
}
|
||||
pub fn change_mode_info(&mut self, mode_info: ModeInfo, client_id: ClientId) {
|
||||
self.mode_info.insert(client_id, mode_info);
|
||||
}
|
||||
pub fn add_multiple_clients(
|
||||
&mut self,
|
||||
client_ids: Vec<ClientId>,
|
||||
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 {
|
||||
pub fn add_multiple_clients(&mut self, client_ids_to_mode_infos: Vec<(ClientId, ModeInfo)>) {
|
||||
for (client_id, client_mode_info) in client_ids_to_mode_infos {
|
||||
self.add_client(client_id, None);
|
||||
self.mode_info.insert(client_id, client_mode_info);
|
||||
}
|
||||
}
|
||||
pub fn remove_client(&mut self, client_id: ClientId) {
|
||||
self.connected_clients.remove(&client_id);
|
||||
self.active_panes.remove(&client_id);
|
||||
self.set_force_render();
|
||||
}
|
||||
pub fn drain_connected_clients(&mut self) -> (Vec<ClientId>, Vec<(ClientId, ModeInfo)>) {
|
||||
let client_mode_info = self.mode_info.drain();
|
||||
(
|
||||
self.connected_clients.drain().collect(),
|
||||
client_mode_info.collect(),
|
||||
)
|
||||
pub fn drain_connected_clients(
|
||||
&mut self,
|
||||
clients_to_drain: Option<Vec<ClientId>>,
|
||||
) -> Vec<(ClientId, ModeInfo)> {
|
||||
// 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>) {
|
||||
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) {
|
||||
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) {
|
||||
match pane_id {
|
||||
|
|
@ -981,10 +1008,21 @@ impl Tab {
|
|||
// render panes and their frames
|
||||
for (kind, pane) in self.panes.iter_mut() {
|
||||
if !self.panes_to_hide.contains(&pane.pid()) {
|
||||
let mut pane_contents_and_ui =
|
||||
PaneContentsAndUi::new(pane, output, self.colors, &self.active_panes);
|
||||
let mut active_panes = self.active_panes.clone();
|
||||
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 {
|
||||
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 {
|
||||
let client_mode = self
|
||||
|
|
@ -1024,16 +1062,21 @@ impl Tab {
|
|||
}
|
||||
// FIXME: Once clients can be distinguished
|
||||
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);
|
||||
}
|
||||
fn hide_cursor_and_clear_display_as_needed(&mut self, output: &mut Output) {
|
||||
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 {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
@ -3400,10 +3443,10 @@ impl Tab {
|
|||
fn write_selection_to_clipboard(&self, selection: &str) {
|
||||
let mut output = Output::default();
|
||||
output.add_clients(&self.connected_clients);
|
||||
output.push_str_to_all_clients(&format!(
|
||||
"\u{1b}]52;c;{}\u{1b}\\",
|
||||
base64::encode(selection)
|
||||
));
|
||||
output.push_str_to_multiple_clients(
|
||||
&format!("\u{1b}]52;c;{}\u{1b}\\", base64::encode(selection)),
|
||||
self.connected_clients.iter().copied(),
|
||||
);
|
||||
|
||||
// TODO: ideally we should be sending the Render instruction from the screen
|
||||
self.senders
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ use crate::ClientId;
|
|||
use ansi_term::Colour::{Fixed, RGB};
|
||||
use ansi_term::Style;
|
||||
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};
|
||||
|
||||
|
|
@ -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 focused_client: Option<ClientId>,
|
||||
pub is_main_client: bool,
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
use crate::panes::PaneId;
|
||||
use crate::tab::{Output, Pane};
|
||||
use crate::ui::boundaries::Boundaries;
|
||||
use crate::ui::pane_boundaries_frame::client_id_to_colors;
|
||||
use crate::ui::pane_boundaries_frame::FrameParams;
|
||||
use crate::ClientId;
|
||||
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> {
|
||||
pane: &'a mut Box<dyn Pane>,
|
||||
|
|
@ -21,13 +22,13 @@ impl<'a> PaneContentsAndUi<'a> {
|
|||
output: &'a mut Output,
|
||||
colors: Palette,
|
||||
active_panes: &HashMap<ClientId, PaneId>,
|
||||
multiple_users_exist_in_session: bool,
|
||||
) -> Self {
|
||||
let focused_clients: Vec<ClientId> = active_panes
|
||||
.iter()
|
||||
.filter(|(_c_id, p_id)| **p_id == pane.pid())
|
||||
.map(|(c_id, _p_id)| *c_id)
|
||||
.collect();
|
||||
let multiple_users_exist_in_session = active_panes.len() > 1;
|
||||
PaneContentsAndUi {
|
||||
pane,
|
||||
output,
|
||||
|
|
@ -36,15 +37,21 @@ impl<'a> PaneContentsAndUi<'a> {
|
|||
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) {
|
||||
// FIXME: Use Termion for cursor and style clearing?
|
||||
self.output.push_str_to_all_clients(&format!(
|
||||
"\u{1b}[{};{}H\u{1b}[m{}",
|
||||
self.pane.y() + 1,
|
||||
self.pane.x() + 1,
|
||||
vte_output
|
||||
));
|
||||
self.output.push_str_to_multiple_clients(
|
||||
&format!(
|
||||
"\u{1b}[{};{}H\u{1b}[m{}",
|
||||
self.pane.y() + 1,
|
||||
self.pane.x() + 1,
|
||||
vte_output
|
||||
),
|
||||
clients,
|
||||
);
|
||||
}
|
||||
}
|
||||
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 {
|
||||
match mode {
|
||||
InputMode::Normal | InputMode::Locked => {
|
||||
if session_is_mirrored {
|
||||
let colors = client_id_to_colors(1, self.colors); // mirrored sessions only have one focused color
|
||||
colors.map(|colors| colors.0)
|
||||
if session_is_mirrored || !self.multiple_users_exist_in_session {
|
||||
let colors = single_client_color(self.colors); // mirrored sessions only have one focused color
|
||||
Some(colors.0)
|
||||
} else {
|
||||
let colors = client_id_to_colors(client_id, self.colors);
|
||||
colors.map(|colors| colors.0)
|
||||
|
|
|
|||
|
|
@ -90,12 +90,14 @@ fn create_new_screen(size: Size) -> Screen {
|
|||
let max_panes = None;
|
||||
let mode_info = ModeInfo::default();
|
||||
let draw_pane_frames = false;
|
||||
let session_is_mirrored = true;
|
||||
Screen::new(
|
||||
bus,
|
||||
&client_attributes,
|
||||
max_panes,
|
||||
mode_info,
|
||||
draw_pane_frames,
|
||||
session_is_mirrored,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,10 @@ use zellij_utils::input::layout::LayoutTemplate;
|
|||
use zellij_utils::ipc::IpcReceiverWithContext;
|
||||
use zellij_utils::pane_size::Size;
|
||||
|
||||
use std::cell::{RefCell, RefMut};
|
||||
use std::collections::HashSet;
|
||||
use std::os::unix::io::RawFd;
|
||||
use std::rc::Rc;
|
||||
|
||||
use zellij_utils::nix;
|
||||
|
||||
|
|
@ -89,6 +92,10 @@ fn create_new_tab(size: Size) -> Tab {
|
|||
let colors = Palette::default();
|
||||
let draw_pane_frames = true;
|
||||
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(
|
||||
index,
|
||||
position,
|
||||
|
|
@ -100,6 +107,8 @@ fn create_new_tab(size: Size) -> Tab {
|
|||
mode_info,
|
||||
colors,
|
||||
draw_pane_frames,
|
||||
connected_clients,
|
||||
session_is_mirrored,
|
||||
client_id,
|
||||
);
|
||||
tab.apply_layout(
|
||||
|
|
|
|||
|
|
@ -3,6 +3,32 @@ use std::fmt;
|
|||
use std::str::FromStr;
|
||||
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)]
|
||||
pub enum Key {
|
||||
Backspace,
|
||||
|
|
@ -169,6 +195,11 @@ pub struct Palette {
|
|||
pub white: PaletteColor,
|
||||
pub orange: 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,
|
||||
|
|
@ -193,6 +224,7 @@ pub struct TabInfo {
|
|||
pub panes_to_hide: usize,
|
||||
pub is_fullscreen_active: bool,
|
||||
pub is_sync_panes_active: bool,
|
||||
pub other_focused_clients: Vec<ClientId>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)]
|
||||
|
|
|
|||
|
|
@ -65,6 +65,10 @@ pub struct Options {
|
|||
#[serde(default)]
|
||||
/// Set display of the pane frames (true or false)
|
||||
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)
|
||||
#[structopt(long)]
|
||||
pub on_force_close: Option<OnForceClose>,
|
||||
|
|
@ -85,6 +89,7 @@ impl Options {
|
|||
pub fn merge(&self, other: Options) -> Options {
|
||||
let mouse_mode = other.mouse_mode.or(self.mouse_mode);
|
||||
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 default_mode = other.default_mode.or(self.default_mode);
|
||||
let default_shell = other.default_shell.or_else(|| self.default_shell.clone());
|
||||
|
|
@ -100,6 +105,7 @@ impl Options {
|
|||
layout_dir,
|
||||
mouse_mode,
|
||||
pane_frames,
|
||||
mirror_session,
|
||||
on_force_close,
|
||||
}
|
||||
}
|
||||
|
|
@ -122,6 +128,7 @@ impl Options {
|
|||
let simplified_ui = merge_bool(other.simplified_ui, self.simplified_ui);
|
||||
let mouse_mode = merge_bool(other.mouse_mode, self.mouse_mode);
|
||||
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_shell = other.default_shell.or_else(|| self.default_shell.clone());
|
||||
|
|
@ -137,6 +144,7 @@ impl Options {
|
|||
layout_dir,
|
||||
mouse_mode,
|
||||
pane_frames,
|
||||
mirror_session,
|
||||
on_force_close,
|
||||
}
|
||||
}
|
||||
|
|
@ -183,6 +191,7 @@ impl From<CliOptions> for Options {
|
|||
layout_dir: opts.layout_dir,
|
||||
mouse_mode: opts.mouse_mode,
|
||||
pane_frames: opts.pane_frames,
|
||||
mirror_session: opts.mirror_session,
|
||||
on_force_close: opts.on_force_close,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,6 +53,11 @@ pub mod colors {
|
|||
pub const CYAN: u8 = 51;
|
||||
pub const YELLOW: u8 = 226;
|
||||
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) {
|
||||
|
|
@ -77,6 +82,11 @@ pub fn default_palette() -> Palette {
|
|||
white: PaletteColor::EightBit(colors::WHITE),
|
||||
orange: PaletteColor::EightBit(colors::ORANGE),
|
||||
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),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue