feat(sessions): mirrored sessions (#740)
* feat(sessions): mirrored sessions * fix(tests): input units * style(fmt): make rustfmt happy * fix(tests): make mirrored sessions e2e test more robust * refactor(sessions): remove force attach * style(fmt): rustfmtify * docs(changelog): update change * fix(e2e): retry on all errors
This commit is contained in:
parent
c93a4f1f67
commit
5c54bf18c2
22 changed files with 730 additions and 324 deletions
|
|
@ -12,6 +12,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
|
|||
* Feature: Add ability to solely specify the tab name in the `tabs` section (https://github.com/zellij-org/zellij/pull/722)
|
||||
* Feature: Plugins can be configured and the groundwork for "Headless" plugins has been laid (https://github.com/zellij-org/zellij/pull/660)
|
||||
* Automatically update `example/default.yaml` on release (https://github.com/zellij-org/zellij/pull/736)
|
||||
* Feature: allow mirroring sessions in multiple terminal windows (https://github.com/zellij-org/zellij/pull/740)
|
||||
|
||||
## [0.17.0] - 2021-09-15
|
||||
* New panes/tabs now open in CWD of focused pane (https://github.com/zellij-org/zellij/pull/691)
|
||||
|
|
|
|||
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -2736,6 +2736,7 @@ dependencies = [
|
|||
"log",
|
||||
"mio",
|
||||
"termbg",
|
||||
"zellij-tile",
|
||||
"zellij-utils",
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -142,7 +142,7 @@ args = ["build", "--verbose", "--release", "--target", "${CARGO_MAKE_TASK_ARGS}"
|
|||
workspace = false
|
||||
dependencies = ["build-plugins", "build-dev-data-dir"]
|
||||
command = "cargo"
|
||||
args = ["build", "--verbose", "--target", "x86_64-unknown-linux-musl"]
|
||||
args = ["build", "--verbose", "--release", "--target", "x86_64-unknown-linux-musl"]
|
||||
|
||||
# Run e2e tests - we mark the e2e tests as "ignored" so they will not be run with the normal ones
|
||||
[tasks.e2e-test]
|
||||
|
|
|
|||
15
src/main.rs
15
src/main.rs
|
|
@ -56,7 +56,6 @@ pub fn main() {
|
|||
};
|
||||
if let Some(Command::Sessions(Sessions::Attach {
|
||||
session_name,
|
||||
force,
|
||||
create,
|
||||
options,
|
||||
})) = opts.command.clone()
|
||||
|
|
@ -73,22 +72,14 @@ pub fn main() {
|
|||
(ClientInfo::New(session_name.unwrap()), layout)
|
||||
} else {
|
||||
(
|
||||
ClientInfo::Attach(
|
||||
session_name.unwrap(),
|
||||
force,
|
||||
config_options.clone(),
|
||||
),
|
||||
ClientInfo::Attach(session_name.unwrap(), config_options.clone()),
|
||||
None,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
assert_session(session);
|
||||
(
|
||||
ClientInfo::Attach(
|
||||
session_name.unwrap(),
|
||||
force,
|
||||
config_options.clone(),
|
||||
),
|
||||
ClientInfo::Attach(session_name.unwrap(), config_options.clone()),
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
|
@ -106,7 +97,7 @@ pub fn main() {
|
|||
}
|
||||
}
|
||||
ActiveSession::One(session_name) => (
|
||||
ClientInfo::Attach(session_name, force, config_options.clone()),
|
||||
ClientInfo::Attach(session_name, config_options.clone()),
|
||||
None,
|
||||
),
|
||||
ActiveSession::Many => {
|
||||
|
|
|
|||
|
|
@ -153,20 +153,12 @@ pub fn cannot_split_terminals_vertically_when_active_terminal_is_too_small() {
|
|||
},
|
||||
})
|
||||
.add_step(Step {
|
||||
name: "Send text to terminal",
|
||||
instruction: |mut remote_terminal: RemoteTerminal| -> bool {
|
||||
// this is just normal input that should be sent into the one terminal so that we can make
|
||||
// sure we silently failed to split in the previous step
|
||||
remote_terminal.send_key("Hi!".as_bytes());
|
||||
true
|
||||
},
|
||||
})
|
||||
.add_step(Step {
|
||||
name: "Wait for text to appear",
|
||||
name: "Make sure only one pane appears",
|
||||
instruction: |remote_terminal: RemoteTerminal| -> bool {
|
||||
let mut step_is_complete = false;
|
||||
if remote_terminal.cursor_position_is(6, 2) && remote_terminal.snapshot_contains("Hi!")
|
||||
if remote_terminal.cursor_position_is(3, 2) && remote_terminal.snapshot_contains("...")
|
||||
{
|
||||
// ... is the truncated tip line
|
||||
step_is_complete = true;
|
||||
}
|
||||
step_is_complete
|
||||
|
|
@ -917,3 +909,85 @@ pub fn start_without_pane_frames() {
|
|||
.run_all_steps();
|
||||
assert_snapshot!(last_snapshot);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
pub fn mirrored_sessions() {
|
||||
let fake_win_size = Size {
|
||||
cols: 120,
|
||||
rows: 24,
|
||||
};
|
||||
let mut test_attempts = 10;
|
||||
let session_name = "mirrored_sessions";
|
||||
let mut last_snapshot = None;
|
||||
loop {
|
||||
// we run this test in a loop because there are some edge cases (especially in the CI)
|
||||
// where the second runner times out and then we also need to restart the first runner
|
||||
// if no test timed out, we break the loop and assert the snapshot
|
||||
let mut first_runner =
|
||||
RemoteRunner::new_with_session_name("mirrored_sessions", fake_win_size, session_name)
|
||||
.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
|
||||
},
|
||||
})
|
||||
.add_step(Step {
|
||||
name: "Wait for new pane to open",
|
||||
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
|
||||
},
|
||||
});
|
||||
first_runner.run_all_steps();
|
||||
|
||||
let mut second_runner =
|
||||
RemoteRunner::new_existing_session("mirrored_sessions", fake_win_size, session_name)
|
||||
.add_step(Step {
|
||||
name: "Make sure session appears correctly",
|
||||
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 last_test_snapshot = second_runner.run_all_steps();
|
||||
|
||||
if (first_runner.test_timed_out || second_runner.test_timed_out) && test_attempts >= 0 {
|
||||
test_attempts -= 1;
|
||||
continue;
|
||||
} else {
|
||||
last_snapshot = Some(last_test_snapshot);
|
||||
break;
|
||||
}
|
||||
}
|
||||
match last_snapshot {
|
||||
Some(last_snapshot) => {
|
||||
assert_snapshot!(last_snapshot);
|
||||
}
|
||||
None => {
|
||||
panic!("test timed out before completing");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ use std::net::TcpStream;
|
|||
|
||||
use std::path::Path;
|
||||
|
||||
const ZELLIJ_EXECUTABLE_LOCATION: &str = "/usr/src/zellij/x86_64-unknown-linux-musl/debug/zellij";
|
||||
const ZELLIJ_EXECUTABLE_LOCATION: &str = "/usr/src/zellij/x86_64-unknown-linux-musl/release/zellij";
|
||||
const ZELLIJ_LAYOUT_PATH: &str = "/usr/src/zellij/fixtures/layouts";
|
||||
const CONNECTION_STRING: &str = "127.0.0.1:2222";
|
||||
const CONNECTION_USERNAME: &str = "test";
|
||||
|
|
@ -24,7 +24,7 @@ fn ssh_connect() -> ssh2::Session {
|
|||
sess.handshake().unwrap();
|
||||
sess.userauth_password(CONNECTION_USERNAME, CONNECTION_PASSWORD)
|
||||
.unwrap();
|
||||
sess.set_timeout(20000);
|
||||
sess.set_timeout(3000);
|
||||
sess
|
||||
}
|
||||
|
||||
|
|
@ -58,6 +58,27 @@ fn start_zellij(channel: &mut ssh2::Channel) {
|
|||
channel.flush().unwrap();
|
||||
}
|
||||
|
||||
fn start_zellij_in_session(channel: &mut ssh2::Channel, session_name: &str) {
|
||||
stop_zellij(channel);
|
||||
channel
|
||||
.write_all(
|
||||
format!(
|
||||
"{} --session {}\n",
|
||||
ZELLIJ_EXECUTABLE_LOCATION, session_name
|
||||
)
|
||||
.as_bytes(),
|
||||
)
|
||||
.unwrap();
|
||||
channel.flush().unwrap();
|
||||
}
|
||||
|
||||
fn attach_to_existing_session(channel: &mut ssh2::Channel, session_name: &str) {
|
||||
channel
|
||||
.write_all(format!("{} attach {}\n", ZELLIJ_EXECUTABLE_LOCATION, session_name).as_bytes())
|
||||
.unwrap();
|
||||
channel.flush().unwrap();
|
||||
}
|
||||
|
||||
fn start_zellij_without_frames(channel: &mut ssh2::Channel) {
|
||||
stop_zellij(channel);
|
||||
channel
|
||||
|
|
@ -188,6 +209,9 @@ pub struct RemoteRunner {
|
|||
win_size: Size,
|
||||
layout_file_name: Option<&'static str>,
|
||||
without_frames: bool,
|
||||
session_name: Option<String>,
|
||||
attach_to_existing: bool,
|
||||
pub test_timed_out: bool,
|
||||
}
|
||||
|
||||
impl RemoteRunner {
|
||||
|
|
@ -216,10 +240,89 @@ impl RemoteRunner {
|
|||
test_name,
|
||||
currently_running_step: None,
|
||||
current_step_index: 0,
|
||||
retries_left: 3,
|
||||
retries_left: 10,
|
||||
win_size,
|
||||
layout_file_name: None,
|
||||
without_frames: false,
|
||||
session_name: None,
|
||||
attach_to_existing: false,
|
||||
test_timed_out: false,
|
||||
}
|
||||
}
|
||||
pub fn new_with_session_name(
|
||||
test_name: &'static str,
|
||||
win_size: Size,
|
||||
session_name: &str,
|
||||
) -> Self {
|
||||
let sess = ssh_connect();
|
||||
let mut channel = sess.channel_session().unwrap();
|
||||
let vte_parser = vte::Parser::new();
|
||||
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,
|
||||
};
|
||||
let terminal_output = TerminalPane::new(0, pane_geom, Palette::default(), 0); // 0 is the pane index
|
||||
setup_remote_environment(&mut channel, win_size);
|
||||
start_zellij_in_session(&mut channel, &session_name);
|
||||
RemoteRunner {
|
||||
steps: vec![],
|
||||
channel,
|
||||
terminal_output,
|
||||
vte_parser,
|
||||
test_name,
|
||||
currently_running_step: None,
|
||||
current_step_index: 0,
|
||||
retries_left: 10,
|
||||
win_size,
|
||||
layout_file_name: None,
|
||||
without_frames: false,
|
||||
session_name: Some(String::from(session_name)),
|
||||
attach_to_existing: false,
|
||||
test_timed_out: false,
|
||||
}
|
||||
}
|
||||
pub fn new_existing_session(
|
||||
test_name: &'static str,
|
||||
win_size: Size,
|
||||
session_name: &str,
|
||||
) -> Self {
|
||||
let sess = ssh_connect();
|
||||
let mut channel = sess.channel_session().unwrap();
|
||||
let vte_parser = vte::Parser::new();
|
||||
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,
|
||||
};
|
||||
let terminal_output = TerminalPane::new(0, pane_geom, Palette::default(), 0); // 0 is the pane index
|
||||
setup_remote_environment(&mut channel, win_size);
|
||||
attach_to_existing_session(&mut channel, &session_name);
|
||||
RemoteRunner {
|
||||
steps: vec![],
|
||||
channel,
|
||||
terminal_output,
|
||||
vte_parser,
|
||||
test_name,
|
||||
currently_running_step: None,
|
||||
current_step_index: 0,
|
||||
retries_left: 10,
|
||||
win_size,
|
||||
layout_file_name: None,
|
||||
without_frames: false,
|
||||
session_name: Some(String::from(session_name)),
|
||||
attach_to_existing: true,
|
||||
test_timed_out: false,
|
||||
}
|
||||
}
|
||||
pub fn new_without_frames(test_name: &'static str, win_size: Size) -> Self {
|
||||
|
|
@ -247,10 +350,13 @@ impl RemoteRunner {
|
|||
test_name,
|
||||
currently_running_step: None,
|
||||
current_step_index: 0,
|
||||
retries_left: 3,
|
||||
retries_left: 10,
|
||||
win_size,
|
||||
layout_file_name: None,
|
||||
without_frames: true,
|
||||
session_name: None,
|
||||
attach_to_existing: false,
|
||||
test_timed_out: false,
|
||||
}
|
||||
}
|
||||
pub fn new_with_layout(
|
||||
|
|
@ -283,10 +389,13 @@ impl RemoteRunner {
|
|||
test_name,
|
||||
currently_running_step: None,
|
||||
current_step_index: 0,
|
||||
retries_left: 3,
|
||||
retries_left: 10,
|
||||
win_size,
|
||||
layout_file_name: Some(layout_file_name),
|
||||
without_frames: false,
|
||||
session_name: None,
|
||||
attach_to_existing: false,
|
||||
test_timed_out: false,
|
||||
}
|
||||
}
|
||||
pub fn add_step(mut self, step: Step) -> Self {
|
||||
|
|
@ -296,16 +405,6 @@ impl RemoteRunner {
|
|||
pub fn replace_steps(&mut self, steps: Vec<Step>) {
|
||||
self.steps = steps;
|
||||
}
|
||||
fn current_remote_terminal_state(&mut self) -> RemoteTerminal {
|
||||
let current_snapshot = self.get_current_snapshot();
|
||||
let (cursor_x, cursor_y) = self.terminal_output.cursor_coordinates().unwrap_or((0, 0));
|
||||
RemoteTerminal {
|
||||
cursor_x,
|
||||
cursor_y,
|
||||
current_snapshot,
|
||||
channel: &mut self.channel,
|
||||
}
|
||||
}
|
||||
pub fn run_next_step(&mut self) {
|
||||
if let Some(next_step) = self.steps.get(self.current_step_index) {
|
||||
let current_snapshot = take_snapshot(&mut self.terminal_output);
|
||||
|
|
@ -341,6 +440,24 @@ impl RemoteRunner {
|
|||
new_runner.replace_steps(self.steps.clone());
|
||||
drop(std::mem::replace(self, new_runner));
|
||||
self.run_all_steps()
|
||||
} else if self.session_name.is_some() {
|
||||
let mut new_runner = if self.attach_to_existing {
|
||||
RemoteRunner::new_existing_session(
|
||||
self.test_name,
|
||||
self.win_size,
|
||||
&self.session_name.as_ref().unwrap(),
|
||||
)
|
||||
} else {
|
||||
RemoteRunner::new_with_session_name(
|
||||
self.test_name,
|
||||
self.win_size,
|
||||
&self.session_name.as_ref().unwrap(),
|
||||
)
|
||||
};
|
||||
new_runner.retries_left = self.retries_left - 1;
|
||||
new_runner.replace_steps(self.steps.clone());
|
||||
drop(std::mem::replace(self, new_runner));
|
||||
self.run_all_steps()
|
||||
} else {
|
||||
let mut new_runner = RemoteRunner::new(self.test_name, self.win_size);
|
||||
new_runner.retries_left = self.retries_left - 1;
|
||||
|
|
@ -349,22 +466,6 @@ impl RemoteRunner {
|
|||
self.run_all_steps()
|
||||
}
|
||||
}
|
||||
fn display_informative_error(&mut self) {
|
||||
let test_name = self.test_name;
|
||||
let current_step_name = self.currently_running_step.as_ref().cloned();
|
||||
match current_step_name {
|
||||
Some(current_step) => {
|
||||
let remote_terminal = self.current_remote_terminal_state();
|
||||
eprintln!("Timed out waiting for data on the SSH channel for test {}. Was waiting for step: {}", test_name, current_step);
|
||||
eprintln!("{:?}", remote_terminal);
|
||||
}
|
||||
None => {
|
||||
let remote_terminal = self.current_remote_terminal_state();
|
||||
eprintln!("Timed out waiting for data on the SSH channel for test {}. Haven't begun running steps yet.", test_name);
|
||||
eprintln!("{:?}", remote_terminal);
|
||||
}
|
||||
}
|
||||
}
|
||||
pub fn run_all_steps(&mut self) -> String {
|
||||
// returns the last snapshot
|
||||
loop {
|
||||
|
|
@ -381,23 +482,16 @@ impl RemoteRunner {
|
|||
break;
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
if e.kind() == std::io::ErrorKind::TimedOut {
|
||||
Err(_e) => {
|
||||
if self.retries_left > 0 {
|
||||
return self.restart_test();
|
||||
}
|
||||
self.display_informative_error();
|
||||
panic!("Timed out waiting for test");
|
||||
}
|
||||
panic!("Error while reading remote session: {}", e);
|
||||
self.test_timed_out = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
take_snapshot(&mut self.terminal_output)
|
||||
}
|
||||
pub fn get_current_snapshot(&mut self) -> String {
|
||||
take_snapshot(&mut self.terminal_output)
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for RemoteRunner {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ expression: last_snapshot
|
|||
---
|
||||
Zellij
|
||||
┌──────┐
|
||||
│$ Hi!█│
|
||||
│$ █ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
|
|
@ -22,4 +22,4 @@ expression: last_snapshot
|
|||
│ │
|
||||
└──────┘
|
||||
Ctrl +
|
||||
|
||||
...
|
||||
|
|
|
|||
|
|
@ -0,0 +1,29 @@
|
|||
---
|
||||
source: src/tests/e2e/cases.rs
|
||||
expression: last_snapshot
|
||||
|
||||
---
|
||||
Zellij (mirrored_sessions) Tab #1
|
||||
┌ Pane #1 ─────────────────────────────────────────────────┐┌ Pane #2 ─────────────────────────────────────────────────┐
|
||||
│$ ││$ █ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
└──────────────────────────────────────────────────────────┘└──────────────────────────────────────────────────────────┘
|
||||
Ctrl + <g> LOCK <p> PANE <t> TAB <n> RESIZE <s> SCROLL <o> SESSION <q> QUIT
|
||||
Tip: Alt + n => open new pane. Alt + [] or hjkl => navigate between panes.
|
||||
|
|
@ -12,6 +12,7 @@ license = "MIT"
|
|||
mio = "0.7.11"
|
||||
termbg = "0.2.3"
|
||||
zellij-utils = { path = "../zellij-utils/", version = "0.18.0" }
|
||||
zellij-tile = { path = "../zellij-tile/", version = "0.18.0" }
|
||||
log = "0.4.14"
|
||||
|
||||
[dev-dependencies]
|
||||
|
|
|
|||
|
|
@ -8,15 +8,16 @@ use zellij_utils::{
|
|||
termion, zellij_tile,
|
||||
};
|
||||
|
||||
use crate::{os_input_output::ClientOsApi, ClientInstruction, CommandIsExecuting};
|
||||
use crate::{
|
||||
os_input_output::ClientOsApi, ClientInstruction, CommandIsExecuting, InputInstruction,
|
||||
};
|
||||
use zellij_utils::{
|
||||
channels::{SenderWithContext, OPENCALLS},
|
||||
errors::ContextType,
|
||||
channels::{Receiver, SenderWithContext, OPENCALLS},
|
||||
errors::{ContextType, ErrorContext},
|
||||
input::{actions::Action, cast_termion_key, config::Config, keybinds::Keybinds},
|
||||
ipc::{ClientToServerMsg, ExitReason},
|
||||
};
|
||||
|
||||
use termion::input::TermReadEventsAndRaw;
|
||||
use zellij_tile::data::{InputMode, Key};
|
||||
|
||||
/// Handles the dispatching of [`Action`]s according to the current
|
||||
|
|
@ -31,6 +32,7 @@ struct InputHandler {
|
|||
send_client_instructions: SenderWithContext<ClientInstruction>,
|
||||
should_exit: bool,
|
||||
pasting: bool,
|
||||
receive_input_instructions: Receiver<(InputInstruction, ErrorContext)>,
|
||||
}
|
||||
|
||||
impl InputHandler {
|
||||
|
|
@ -42,6 +44,7 @@ impl InputHandler {
|
|||
options: Options,
|
||||
send_client_instructions: SenderWithContext<ClientInstruction>,
|
||||
mode: InputMode,
|
||||
receive_input_instructions: Receiver<(InputInstruction, ErrorContext)>,
|
||||
) -> Self {
|
||||
InputHandler {
|
||||
mode,
|
||||
|
|
@ -52,6 +55,7 @@ impl InputHandler {
|
|||
send_client_instructions,
|
||||
should_exit: false,
|
||||
pasting: false,
|
||||
receive_input_instructions,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -71,10 +75,9 @@ impl InputHandler {
|
|||
if self.should_exit {
|
||||
break;
|
||||
}
|
||||
let stdin_buffer = self.os_input.read_from_stdin();
|
||||
for key_result in stdin_buffer.events_and_raw() {
|
||||
match key_result {
|
||||
Ok((event, raw_bytes)) => match event {
|
||||
match self.receive_input_instructions.recv() {
|
||||
Ok((InputInstruction::KeyEvent(event, raw_bytes), _error_context)) => {
|
||||
match event {
|
||||
termion::event::Event::Key(key) => {
|
||||
let key = cast_termion_key(key);
|
||||
self.handle_key(&key, raw_bytes);
|
||||
|
|
@ -101,10 +104,13 @@ impl InputHandler {
|
|||
self.handle_unknown_key(raw_bytes);
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(err) => panic!("Encountered read error: {:?}", err),
|
||||
}
|
||||
}
|
||||
Ok((InputInstruction::SwitchToMode(input_mode), _error_context)) => {
|
||||
self.mode = input_mode;
|
||||
}
|
||||
Err(err) => panic!("Encountered read error: {:?}", err),
|
||||
}
|
||||
}
|
||||
}
|
||||
fn handle_unknown_key(&mut self, raw_bytes: Vec<u8>) {
|
||||
|
|
@ -179,6 +185,8 @@ impl InputHandler {
|
|||
should_break = true;
|
||||
}
|
||||
Action::SwitchToMode(mode) => {
|
||||
// this is an optimistic update, we should get a SwitchMode instruction from the
|
||||
// server later that atomically changes the mode as well
|
||||
self.mode = mode;
|
||||
self.os_input
|
||||
.send_to_server(ClientToServerMsg::Action(action));
|
||||
|
|
@ -224,6 +232,7 @@ pub(crate) fn input_loop(
|
|||
command_is_executing: CommandIsExecuting,
|
||||
send_client_instructions: SenderWithContext<ClientInstruction>,
|
||||
default_mode: InputMode,
|
||||
receive_input_instructions: Receiver<(InputInstruction, ErrorContext)>,
|
||||
) {
|
||||
let _handler = InputHandler::new(
|
||||
os_input,
|
||||
|
|
@ -232,6 +241,7 @@ pub(crate) fn input_loop(
|
|||
options,
|
||||
send_client_instructions,
|
||||
default_mode,
|
||||
receive_input_instructions,
|
||||
)
|
||||
.handle_input();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,12 +14,15 @@ use crate::{
|
|||
command_is_executing::CommandIsExecuting, input_handler::input_loop,
|
||||
os_input_output::ClientOsApi,
|
||||
};
|
||||
use termion::input::TermReadEventsAndRaw;
|
||||
use zellij_tile::data::InputMode;
|
||||
use zellij_utils::{
|
||||
channels::{self, ChannelWithContext, SenderWithContext},
|
||||
consts::{SESSION_NAME, ZELLIJ_IPC_PIPE},
|
||||
errors::{ClientContext, ContextType, ErrorInstruction},
|
||||
input::{actions::Action, config::Config, options::Options},
|
||||
ipc::{ClientAttributes, ClientToServerMsg, ExitReason, ServerToClientMsg},
|
||||
termion,
|
||||
};
|
||||
use zellij_utils::{cli::CliArgs, input::layout::LayoutFromYaml};
|
||||
|
||||
|
|
@ -30,6 +33,7 @@ pub(crate) enum ClientInstruction {
|
|||
Render(String),
|
||||
UnblockInputThread,
|
||||
Exit(ExitReason),
|
||||
SwitchToMode(InputMode),
|
||||
}
|
||||
|
||||
impl From<ServerToClientMsg> for ClientInstruction {
|
||||
|
|
@ -38,6 +42,9 @@ impl From<ServerToClientMsg> for ClientInstruction {
|
|||
ServerToClientMsg::Exit(e) => ClientInstruction::Exit(e),
|
||||
ServerToClientMsg::Render(buffer) => ClientInstruction::Render(buffer),
|
||||
ServerToClientMsg::UnblockInputThread => ClientInstruction::UnblockInputThread,
|
||||
ServerToClientMsg::SwitchToMode(input_mode) => {
|
||||
ClientInstruction::SwitchToMode(input_mode)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -49,6 +56,7 @@ impl From<&ClientInstruction> for ClientContext {
|
|||
ClientInstruction::Error(_) => ClientContext::Error,
|
||||
ClientInstruction::Render(_) => ClientContext::Render,
|
||||
ClientInstruction::UnblockInputThread => ClientContext::UnblockInputThread,
|
||||
ClientInstruction::SwitchToMode(_) => ClientContext::SwitchToMode,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -78,10 +86,16 @@ fn spawn_server(socket_path: &Path) -> io::Result<()> {
|
|||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ClientInfo {
|
||||
Attach(String, bool, Options),
|
||||
Attach(String, Options),
|
||||
New(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum InputInstruction {
|
||||
KeyEvent(termion::event::Event, Vec<u8>),
|
||||
SwitchToMode(InputMode),
|
||||
}
|
||||
|
||||
pub fn start_client(
|
||||
mut os_input: Box<dyn ClientOsApi>,
|
||||
opts: CliArgs,
|
||||
|
|
@ -121,11 +135,11 @@ pub fn start_client(
|
|||
};
|
||||
|
||||
let first_msg = match info {
|
||||
ClientInfo::Attach(name, force, config_options) => {
|
||||
ClientInfo::Attach(name, config_options) => {
|
||||
SESSION_NAME.set(name).unwrap();
|
||||
std::env::set_var(&"ZELLIJ_SESSION_NAME", SESSION_NAME.get().unwrap());
|
||||
|
||||
ClientToServerMsg::AttachClient(client_attributes, force, config_options)
|
||||
ClientToServerMsg::AttachClient(client_attributes, config_options)
|
||||
}
|
||||
ClientInfo::New(name) => {
|
||||
SESSION_NAME.set(name).unwrap();
|
||||
|
|
@ -159,6 +173,11 @@ pub fn start_client(
|
|||
> = channels::bounded(50);
|
||||
let send_client_instructions = SenderWithContext::new(send_client_instructions);
|
||||
|
||||
let (send_input_instructions, receive_input_instructions): ChannelWithContext<
|
||||
InputInstruction,
|
||||
> = channels::bounded(50);
|
||||
let send_input_instructions = SenderWithContext::new(send_input_instructions);
|
||||
|
||||
std::panic::set_hook({
|
||||
use zellij_utils::errors::handle_panic;
|
||||
let send_client_instructions = send_client_instructions.clone();
|
||||
|
|
@ -171,6 +190,22 @@ pub fn start_client(
|
|||
|
||||
let _stdin_thread = thread::Builder::new()
|
||||
.name("stdin_handler".to_string())
|
||||
.spawn({
|
||||
let os_input = os_input.clone();
|
||||
let send_input_instructions = send_input_instructions.clone();
|
||||
move || loop {
|
||||
let stdin_buffer = os_input.read_from_stdin();
|
||||
for key_result in stdin_buffer.events_and_raw() {
|
||||
let (key_event, raw_bytes) = key_result.unwrap();
|
||||
send_input_instructions
|
||||
.send(InputInstruction::KeyEvent(key_event, raw_bytes))
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let _input_thread = thread::Builder::new()
|
||||
.name("input_handler".to_string())
|
||||
.spawn({
|
||||
let send_client_instructions = send_client_instructions.clone();
|
||||
let command_is_executing = command_is_executing.clone();
|
||||
|
|
@ -184,6 +219,7 @@ pub fn start_client(
|
|||
command_is_executing,
|
||||
send_client_instructions,
|
||||
default_mode,
|
||||
receive_input_instructions,
|
||||
)
|
||||
}
|
||||
});
|
||||
|
|
@ -239,12 +275,13 @@ pub fn start_client(
|
|||
os_input.disable_mouse();
|
||||
let error = format!(
|
||||
"{}\n{}{}",
|
||||
goto_start_of_last_line, restore_snapshot, backtrace
|
||||
restore_snapshot, goto_start_of_last_line, backtrace
|
||||
);
|
||||
let _ = os_input
|
||||
.get_stdout_writer()
|
||||
.write(error.as_bytes())
|
||||
.unwrap();
|
||||
let _ = os_input.get_stdout_writer().flush().unwrap();
|
||||
std::process::exit(1);
|
||||
};
|
||||
|
||||
|
|
@ -280,6 +317,11 @@ pub fn start_client(
|
|||
ClientInstruction::UnblockInputThread => {
|
||||
command_is_executing.unblock_input_thread();
|
||||
}
|
||||
ClientInstruction::SwitchToMode(input_mode) => {
|
||||
send_input_instructions
|
||||
.send(InputInstruction::SwitchToMode(input_mode))
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,8 +3,11 @@ use zellij_utils::input::actions::{Action, Direction};
|
|||
use zellij_utils::input::config::Config;
|
||||
use zellij_utils::input::options::Options;
|
||||
use zellij_utils::pane_size::Size;
|
||||
use zellij_utils::termion::event::Event;
|
||||
use zellij_utils::termion::event::Key;
|
||||
use zellij_utils::zellij_tile::data::Palette;
|
||||
|
||||
use crate::InputInstruction;
|
||||
use crate::{os_input_output::ClientOsApi, ClientInstruction, CommandIsExecuting};
|
||||
|
||||
use std::path::Path;
|
||||
|
|
@ -67,14 +70,12 @@ pub mod commands {
|
|||
}
|
||||
|
||||
struct FakeClientOsApi {
|
||||
stdin_events: Arc<Mutex<Vec<Vec<u8>>>>,
|
||||
events_sent_to_server: Arc<Mutex<Vec<ClientToServerMsg>>>,
|
||||
command_is_executing: Arc<Mutex<CommandIsExecuting>>,
|
||||
}
|
||||
|
||||
impl FakeClientOsApi {
|
||||
pub fn new(
|
||||
mut stdin_events: Vec<Vec<u8>>,
|
||||
events_sent_to_server: Arc<Mutex<Vec<ClientToServerMsg>>>,
|
||||
command_is_executing: CommandIsExecuting,
|
||||
) -> Self {
|
||||
|
|
@ -82,10 +83,7 @@ impl FakeClientOsApi {
|
|||
// Arc<Mutex> here because we need interior mutability, otherwise we'll have to change the
|
||||
// ClientOsApi trait, and that will cause a lot of havoc
|
||||
let command_is_executing = Arc::new(Mutex::new(command_is_executing));
|
||||
stdin_events.push(commands::QUIT.to_vec());
|
||||
let stdin_events = Arc::new(Mutex::new(stdin_events)); // this is also done for interior mutability
|
||||
FakeClientOsApi {
|
||||
stdin_events,
|
||||
events_sent_to_server,
|
||||
command_is_executing,
|
||||
}
|
||||
|
|
@ -106,11 +104,7 @@ impl ClientOsApi for FakeClientOsApi {
|
|||
unimplemented!()
|
||||
}
|
||||
fn read_from_stdin(&self) -> Vec<u8> {
|
||||
let mut stdin_events = self.stdin_events.lock().unwrap();
|
||||
if stdin_events.is_empty() {
|
||||
panic!("ran out of stdin events!");
|
||||
}
|
||||
stdin_events.remove(0)
|
||||
unimplemented!()
|
||||
}
|
||||
fn box_clone(&self) -> Box<dyn ClientOsApi> {
|
||||
unimplemented!()
|
||||
|
|
@ -156,11 +150,10 @@ fn extract_actions_sent_to_server(
|
|||
|
||||
#[test]
|
||||
pub fn quit_breaks_input_loop() {
|
||||
let stdin_events = vec![];
|
||||
let stdin_events = vec![(commands::QUIT.to_vec(), Event::Key(Key::Ctrl('q')))];
|
||||
let events_sent_to_server = Arc::new(Mutex::new(vec![]));
|
||||
let command_is_executing = CommandIsExecuting::new();
|
||||
let client_os_api = Box::new(FakeClientOsApi::new(
|
||||
stdin_events,
|
||||
events_sent_to_server.clone(),
|
||||
command_is_executing.clone(),
|
||||
));
|
||||
|
|
@ -172,6 +165,16 @@ pub fn quit_breaks_input_loop() {
|
|||
> = channels::bounded(50);
|
||||
let send_client_instructions = SenderWithContext::new(send_client_instructions);
|
||||
|
||||
let (send_input_instructions, receive_input_instructions): ChannelWithContext<
|
||||
InputInstruction,
|
||||
> = channels::bounded(50);
|
||||
let send_input_instructions = SenderWithContext::new(send_input_instructions);
|
||||
for event in stdin_events {
|
||||
send_input_instructions
|
||||
.send(InputInstruction::KeyEvent(event.1, event.0))
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
let default_mode = InputMode::Normal;
|
||||
input_loop(
|
||||
client_os_api,
|
||||
|
|
@ -180,6 +183,7 @@ pub fn quit_breaks_input_loop() {
|
|||
command_is_executing,
|
||||
send_client_instructions,
|
||||
default_mode,
|
||||
receive_input_instructions,
|
||||
);
|
||||
let expected_actions_sent_to_server = vec![Action::Quit];
|
||||
let received_actions = extract_actions_sent_to_server(events_sent_to_server);
|
||||
|
|
@ -190,12 +194,18 @@ pub fn quit_breaks_input_loop() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
pub fn move_focus_left_in_pane_mode() {
|
||||
let stdin_events = vec![commands::MOVE_FOCUS_LEFT_IN_NORMAL_MODE.to_vec()];
|
||||
pub fn move_focus_left_in_normal_mode() {
|
||||
let stdin_events = vec![
|
||||
(
|
||||
commands::MOVE_FOCUS_LEFT_IN_NORMAL_MODE.to_vec(),
|
||||
Event::Key(Key::Alt('h')),
|
||||
),
|
||||
(commands::QUIT.to_vec(), Event::Key(Key::Ctrl('q'))),
|
||||
];
|
||||
|
||||
let events_sent_to_server = Arc::new(Mutex::new(vec![]));
|
||||
let command_is_executing = CommandIsExecuting::new();
|
||||
let client_os_api = Box::new(FakeClientOsApi::new(
|
||||
stdin_events,
|
||||
events_sent_to_server.clone(),
|
||||
command_is_executing.clone(),
|
||||
));
|
||||
|
|
@ -207,6 +217,16 @@ pub fn move_focus_left_in_pane_mode() {
|
|||
> = channels::bounded(50);
|
||||
let send_client_instructions = SenderWithContext::new(send_client_instructions);
|
||||
|
||||
let (send_input_instructions, receive_input_instructions): ChannelWithContext<
|
||||
InputInstruction,
|
||||
> = channels::bounded(50);
|
||||
let send_input_instructions = SenderWithContext::new(send_input_instructions);
|
||||
for event in stdin_events {
|
||||
send_input_instructions
|
||||
.send(InputInstruction::KeyEvent(event.1, event.0))
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
let default_mode = InputMode::Normal;
|
||||
input_loop(
|
||||
client_os_api,
|
||||
|
|
@ -215,6 +235,7 @@ pub fn move_focus_left_in_pane_mode() {
|
|||
command_is_executing,
|
||||
send_client_instructions,
|
||||
default_mode,
|
||||
receive_input_instructions,
|
||||
);
|
||||
let expected_actions_sent_to_server =
|
||||
vec![Action::MoveFocusOrTab(Direction::Left), Action::Quit];
|
||||
|
|
@ -228,14 +249,23 @@ pub fn move_focus_left_in_pane_mode() {
|
|||
#[test]
|
||||
pub fn bracketed_paste() {
|
||||
let stdin_events = vec![
|
||||
(
|
||||
commands::BRACKETED_PASTE_START.to_vec(),
|
||||
Event::Unsupported(commands::BRACKETED_PASTE_START.to_vec()),
|
||||
),
|
||||
(
|
||||
commands::MOVE_FOCUS_LEFT_IN_NORMAL_MODE.to_vec(),
|
||||
Event::Key(Key::Alt('h')),
|
||||
),
|
||||
(
|
||||
commands::BRACKETED_PASTE_END.to_vec(),
|
||||
Event::Unsupported(commands::BRACKETED_PASTE_END.to_vec()),
|
||||
),
|
||||
(commands::QUIT.to_vec(), Event::Key(Key::Ctrl('q'))),
|
||||
];
|
||||
let events_sent_to_server = Arc::new(Mutex::new(vec![]));
|
||||
let command_is_executing = CommandIsExecuting::new();
|
||||
let client_os_api = Box::new(FakeClientOsApi::new(
|
||||
stdin_events,
|
||||
events_sent_to_server.clone(),
|
||||
command_is_executing.clone(),
|
||||
));
|
||||
|
|
@ -247,6 +277,16 @@ pub fn bracketed_paste() {
|
|||
> = channels::bounded(50);
|
||||
let send_client_instructions = SenderWithContext::new(send_client_instructions);
|
||||
|
||||
let (send_input_instructions, receive_input_instructions): ChannelWithContext<
|
||||
InputInstruction,
|
||||
> = channels::bounded(50);
|
||||
let send_input_instructions = SenderWithContext::new(send_input_instructions);
|
||||
for event in stdin_events {
|
||||
send_input_instructions
|
||||
.send(InputInstruction::KeyEvent(event.1, event.0))
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
let default_mode = InputMode::Normal;
|
||||
input_loop(
|
||||
client_os_api,
|
||||
|
|
@ -255,6 +295,7 @@ pub fn bracketed_paste() {
|
|||
command_is_executing,
|
||||
send_client_instructions,
|
||||
default_mode,
|
||||
receive_input_instructions,
|
||||
);
|
||||
let expected_actions_sent_to_server = vec![
|
||||
Action::Write(commands::BRACKETED_PASTE_START.to_vec()),
|
||||
|
|
|
|||
|
|
@ -11,11 +11,13 @@ mod ui;
|
|||
mod wasm_vm;
|
||||
|
||||
use log::info;
|
||||
use std::collections::HashMap;
|
||||
use std::{
|
||||
path::PathBuf,
|
||||
sync::{Arc, Mutex, RwLock},
|
||||
thread,
|
||||
};
|
||||
use zellij_utils::pane_size::Size;
|
||||
use zellij_utils::zellij_tile;
|
||||
|
||||
use wasmer::Store;
|
||||
|
|
@ -40,10 +42,12 @@ use zellij_utils::{
|
|||
options::Options,
|
||||
plugins::PluginsConfig,
|
||||
},
|
||||
ipc::{ClientAttributes, ClientToServerMsg, ExitReason, ServerToClientMsg},
|
||||
ipc::{ClientAttributes, ExitReason, ServerToClientMsg},
|
||||
setup::get_default_data_dir,
|
||||
};
|
||||
|
||||
pub(crate) type ClientId = u16;
|
||||
|
||||
/// Instructions related to server-side application
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) enum ServerInstruction {
|
||||
|
|
@ -52,28 +56,16 @@ pub(crate) enum ServerInstruction {
|
|||
Box<CliArgs>,
|
||||
Box<Options>,
|
||||
LayoutFromYaml,
|
||||
ClientId,
|
||||
Option<PluginsConfig>,
|
||||
),
|
||||
Render(Option<String>),
|
||||
UnblockInputThread,
|
||||
ClientExit,
|
||||
ClientExit(ClientId),
|
||||
RemoveClient(ClientId),
|
||||
Error(String),
|
||||
DetachSession,
|
||||
AttachClient(ClientAttributes, bool, Options),
|
||||
}
|
||||
|
||||
impl From<ClientToServerMsg> for ServerInstruction {
|
||||
fn from(instruction: ClientToServerMsg) -> Self {
|
||||
match instruction {
|
||||
ClientToServerMsg::NewClient(attrs, opts, options, layout, plugins) => {
|
||||
ServerInstruction::NewClient(attrs, opts, options, layout, plugins)
|
||||
}
|
||||
ClientToServerMsg::AttachClient(attrs, force, options) => {
|
||||
ServerInstruction::AttachClient(attrs, force, options)
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
DetachSession(ClientId),
|
||||
AttachClient(ClientAttributes, Options, ClientId),
|
||||
}
|
||||
|
||||
impl From<&ServerInstruction> for ServerContext {
|
||||
|
|
@ -82,9 +74,10 @@ impl From<&ServerInstruction> for ServerContext {
|
|||
ServerInstruction::NewClient(..) => ServerContext::NewClient,
|
||||
ServerInstruction::Render(_) => ServerContext::Render,
|
||||
ServerInstruction::UnblockInputThread => ServerContext::UnblockInputThread,
|
||||
ServerInstruction::ClientExit => ServerContext::ClientExit,
|
||||
ServerInstruction::ClientExit(..) => ServerContext::ClientExit,
|
||||
ServerInstruction::RemoveClient(..) => ServerContext::RemoveClient,
|
||||
ServerInstruction::Error(_) => ServerContext::Error,
|
||||
ServerInstruction::DetachSession => ServerContext::DetachSession,
|
||||
ServerInstruction::DetachSession(..) => ServerContext::DetachSession,
|
||||
ServerInstruction::AttachClient(..) => ServerContext::AttachClient,
|
||||
}
|
||||
}
|
||||
|
|
@ -117,14 +110,67 @@ impl Drop for SessionMetaData {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||
pub(crate) enum SessionState {
|
||||
Attached,
|
||||
Detached,
|
||||
Uninitialized,
|
||||
macro_rules! remove_client {
|
||||
($client_id:expr, $os_input:expr, $session_state:expr) => {
|
||||
$os_input.remove_client($client_id);
|
||||
$session_state.write().unwrap().remove_client($client_id);
|
||||
};
|
||||
}
|
||||
|
||||
pub fn start_server(os_input: Box<dyn ServerOsApi>, socket_path: PathBuf) {
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub(crate) struct SessionState {
|
||||
clients: HashMap<ClientId, Option<Size>>,
|
||||
}
|
||||
|
||||
impl SessionState {
|
||||
pub fn new() -> Self {
|
||||
SessionState {
|
||||
clients: HashMap::new(),
|
||||
}
|
||||
}
|
||||
pub fn new_client(&mut self) -> ClientId {
|
||||
let mut clients: Vec<ClientId> = self.clients.keys().copied().collect();
|
||||
clients.sort_unstable();
|
||||
let next_client_id = clients.last().unwrap_or(&0) + 1;
|
||||
self.clients.insert(next_client_id, None);
|
||||
next_client_id
|
||||
}
|
||||
pub fn remove_client(&mut self, client_id: ClientId) {
|
||||
self.clients.remove(&client_id);
|
||||
}
|
||||
pub fn set_client_size(&mut self, client_id: ClientId, size: Size) {
|
||||
self.clients.insert(client_id, Some(size));
|
||||
}
|
||||
pub fn min_client_terminal_size(&self) -> Option<Size> {
|
||||
// None if there are no client sizes
|
||||
let mut rows: Vec<usize> = self
|
||||
.clients
|
||||
.values()
|
||||
.filter_map(|size| size.map(|size| size.rows))
|
||||
.collect();
|
||||
rows.sort_unstable();
|
||||
let mut cols: Vec<usize> = self
|
||||
.clients
|
||||
.values()
|
||||
.filter_map(|size| size.map(|size| size.cols))
|
||||
.collect();
|
||||
cols.sort_unstable();
|
||||
let min_rows = rows.first();
|
||||
let min_cols = cols.first();
|
||||
match (min_rows, min_cols) {
|
||||
(Some(min_rows), Some(min_cols)) => Some(Size {
|
||||
rows: *min_rows,
|
||||
cols: *min_cols,
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
pub fn client_ids(&self) -> Vec<ClientId> {
|
||||
self.clients.keys().copied().collect()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn start_server(mut os_input: Box<dyn ServerOsApi>, socket_path: PathBuf) {
|
||||
info!("Starting Zellij server!");
|
||||
daemonize::Daemonize::new()
|
||||
.working_directory(std::env::current_dir().unwrap())
|
||||
|
|
@ -137,7 +183,7 @@ pub fn start_server(os_input: Box<dyn ServerOsApi>, socket_path: PathBuf) {
|
|||
let (to_server, server_receiver): ChannelWithContext<ServerInstruction> = channels::bounded(50);
|
||||
let to_server = SenderWithContext::new(to_server);
|
||||
let session_data: Arc<RwLock<Option<SessionMetaData>>> = Arc::new(RwLock::new(None));
|
||||
let session_state = Arc::new(RwLock::new(SessionState::Uninitialized));
|
||||
let session_state = Arc::new(RwLock::new(SessionState::new()));
|
||||
|
||||
std::panic::set_hook({
|
||||
use zellij_utils::errors::handle_panic;
|
||||
|
|
@ -170,7 +216,8 @@ pub fn start_server(os_input: Box<dyn ServerOsApi>, socket_path: PathBuf) {
|
|||
match stream {
|
||||
Ok(stream) => {
|
||||
let mut os_input = os_input.clone();
|
||||
os_input.update_receiver(stream);
|
||||
let client_id = session_state.write().unwrap().new_client();
|
||||
let receiver = os_input.new_client(client_id, stream);
|
||||
let session_data = session_data.clone();
|
||||
let session_state = session_state.clone();
|
||||
let to_server = to_server.clone();
|
||||
|
|
@ -183,6 +230,8 @@ pub fn start_server(os_input: Box<dyn ServerOsApi>, socket_path: PathBuf) {
|
|||
session_state,
|
||||
os_input,
|
||||
to_server,
|
||||
receiver,
|
||||
client_id,
|
||||
)
|
||||
})
|
||||
.unwrap(),
|
||||
|
|
@ -205,6 +254,7 @@ pub fn start_server(os_input: Box<dyn ServerOsApi>, socket_path: PathBuf) {
|
|||
opts,
|
||||
config_options,
|
||||
layout,
|
||||
client_id,
|
||||
plugins,
|
||||
) => {
|
||||
let session = init_session(
|
||||
|
|
@ -220,7 +270,10 @@ pub fn start_server(os_input: Box<dyn ServerOsApi>, socket_path: PathBuf) {
|
|||
},
|
||||
);
|
||||
*session_data.write().unwrap() = Some(session);
|
||||
*session_state.write().unwrap() = SessionState::Attached;
|
||||
session_state
|
||||
.write()
|
||||
.unwrap()
|
||||
.set_client_size(client_id, client_attributes.size);
|
||||
|
||||
let default_shell = config_options.default_shell.map(|shell| {
|
||||
TerminalAction::RunCommand(RunCommand {
|
||||
|
|
@ -248,17 +301,26 @@ pub fn start_server(os_input: Box<dyn ServerOsApi>, socket_path: PathBuf) {
|
|||
spawn_tabs(None);
|
||||
}
|
||||
}
|
||||
ServerInstruction::AttachClient(attrs, _, options) => {
|
||||
*session_state.write().unwrap() = SessionState::Attached;
|
||||
ServerInstruction::AttachClient(attrs, options, client_id) => {
|
||||
let rlock = session_data.read().unwrap();
|
||||
let session_data = rlock.as_ref().unwrap();
|
||||
session_state
|
||||
.write()
|
||||
.unwrap()
|
||||
.set_client_size(client_id, attrs.size);
|
||||
let min_size = session_state
|
||||
.read()
|
||||
.unwrap()
|
||||
.min_client_terminal_size()
|
||||
.unwrap();
|
||||
session_data
|
||||
.senders
|
||||
.send_to_screen(ScreenInstruction::TerminalResize(attrs.size))
|
||||
.send_to_screen(ScreenInstruction::TerminalResize(min_size))
|
||||
.unwrap();
|
||||
let default_mode = options.default_mode.unwrap_or_default();
|
||||
let mode_info =
|
||||
get_mode_info(default_mode, attrs.palette, session_data.capabilities);
|
||||
let mode = mode_info.mode;
|
||||
session_data
|
||||
.senders
|
||||
.send_to_screen(ScreenInstruction::ChangeMode(mode_info.clone()))
|
||||
|
|
@ -270,37 +332,86 @@ pub fn start_server(os_input: Box<dyn ServerOsApi>, socket_path: PathBuf) {
|
|||
Event::ModeUpdate(mode_info),
|
||||
))
|
||||
.unwrap();
|
||||
for client_id in session_state.read().unwrap().clients.keys() {
|
||||
os_input.send_to_client(*client_id, ServerToClientMsg::SwitchToMode(mode));
|
||||
}
|
||||
}
|
||||
ServerInstruction::UnblockInputThread => {
|
||||
if *session_state.read().unwrap() == SessionState::Attached {
|
||||
os_input.send_to_client(ServerToClientMsg::UnblockInputThread);
|
||||
for client_id in session_state.read().unwrap().clients.keys() {
|
||||
os_input.send_to_client(*client_id, ServerToClientMsg::UnblockInputThread);
|
||||
}
|
||||
}
|
||||
ServerInstruction::ClientExit => {
|
||||
os_input.send_to_client(ServerToClientMsg::Exit(ExitReason::Normal));
|
||||
ServerInstruction::ClientExit(client_id) => {
|
||||
os_input.send_to_client(client_id, ServerToClientMsg::Exit(ExitReason::Normal));
|
||||
remove_client!(client_id, os_input, session_state);
|
||||
if let Some(min_size) = session_state.read().unwrap().min_client_terminal_size() {
|
||||
session_data
|
||||
.write()
|
||||
.unwrap()
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.senders
|
||||
.send_to_screen(ScreenInstruction::TerminalResize(min_size))
|
||||
.unwrap();
|
||||
}
|
||||
if session_state.read().unwrap().clients.is_empty() {
|
||||
*session_data.write().unwrap() = None;
|
||||
break;
|
||||
}
|
||||
ServerInstruction::DetachSession => {
|
||||
*session_state.write().unwrap() = SessionState::Detached;
|
||||
os_input.send_to_client(ServerToClientMsg::Exit(ExitReason::Normal));
|
||||
os_input.remove_client_sender();
|
||||
}
|
||||
ServerInstruction::Render(output) => {
|
||||
if *session_state.read().unwrap() == SessionState::Attached {
|
||||
// Here output is of the type Option<String> sent by screen thread.
|
||||
// If `Some(_)`- unwrap it and forward it to the client to render.
|
||||
// If `None`- Send an exit instruction. This is the case when the user closes last Tab/Pane.
|
||||
if let Some(op) = output {
|
||||
os_input.send_to_client(ServerToClientMsg::Render(op));
|
||||
ServerInstruction::RemoveClient(client_id) => {
|
||||
remove_client!(client_id, os_input, session_state);
|
||||
if let Some(min_size) = session_state.read().unwrap().min_client_terminal_size() {
|
||||
session_data
|
||||
.write()
|
||||
.unwrap()
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.senders
|
||||
.send_to_screen(ScreenInstruction::TerminalResize(min_size))
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
ServerInstruction::DetachSession(client_id) => {
|
||||
os_input.send_to_client(client_id, ServerToClientMsg::Exit(ExitReason::Normal));
|
||||
remove_client!(client_id, os_input, session_state);
|
||||
if let Some(min_size) = session_state.read().unwrap().min_client_terminal_size() {
|
||||
session_data
|
||||
.write()
|
||||
.unwrap()
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.senders
|
||||
.send_to_screen(ScreenInstruction::TerminalResize(min_size))
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
ServerInstruction::Render(mut output) => {
|
||||
let client_ids = session_state.read().unwrap().client_ids();
|
||||
// Here the output is of the type Option<String> sent by screen thread.
|
||||
// If `Some(_)`- unwrap it and forward it to the clients to render.
|
||||
// If `None`- Send an exit instruction. This is the case when a user closes the last Tab/Pane.
|
||||
if let Some(op) = output.as_mut() {
|
||||
for client_id in client_ids {
|
||||
os_input.send_to_client(client_id, ServerToClientMsg::Render(op.clone()));
|
||||
}
|
||||
} else {
|
||||
os_input.send_to_client(ServerToClientMsg::Exit(ExitReason::Normal));
|
||||
break;
|
||||
for client_id in client_ids {
|
||||
os_input
|
||||
.send_to_client(client_id, ServerToClientMsg::Exit(ExitReason::Normal));
|
||||
remove_client!(client_id, os_input, session_state);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
ServerInstruction::Error(backtrace) => {
|
||||
if *session_state.read().unwrap() == SessionState::Attached {
|
||||
os_input.send_to_client(ServerToClientMsg::Exit(ExitReason::Error(backtrace)));
|
||||
let client_ids = session_state.read().unwrap().client_ids();
|
||||
for client_id in client_ids {
|
||||
os_input.send_to_client(
|
||||
client_id,
|
||||
ServerToClientMsg::Exit(ExitReason::Error(backtrace.clone())),
|
||||
);
|
||||
remove_client!(client_id, os_input, session_state);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
use darwin_libproc;
|
||||
|
||||
|
|
@ -22,12 +24,8 @@ use nix::unistd::{self, ForkResult};
|
|||
use signal_hook::consts::*;
|
||||
use zellij_tile::data::Palette;
|
||||
use zellij_utils::{
|
||||
errors::ErrorContext,
|
||||
input::command::{RunCommand, TerminalAction},
|
||||
ipc::{
|
||||
ClientToServerMsg, ExitReason, IpcReceiverWithContext, IpcSenderWithContext,
|
||||
ServerToClientMsg,
|
||||
},
|
||||
ipc::{ClientToServerMsg, IpcReceiverWithContext, IpcSenderWithContext, ServerToClientMsg},
|
||||
shared::default_palette,
|
||||
};
|
||||
|
||||
|
|
@ -37,6 +35,8 @@ use byteorder::{BigEndian, ByteOrder};
|
|||
|
||||
pub use nix::unistd::Pid;
|
||||
|
||||
use crate::ClientId;
|
||||
|
||||
pub(crate) fn set_terminal_size_using_fd(fd: RawFd, columns: u16, rows: u16) {
|
||||
// TODO: do this with the nix ioctl
|
||||
use libc::ioctl;
|
||||
|
|
@ -230,8 +230,7 @@ pub fn spawn_terminal(
|
|||
#[derive(Clone)]
|
||||
pub struct ServerOsInputOutput {
|
||||
orig_termios: Arc<Mutex<termios::Termios>>,
|
||||
receive_instructions_from_client: Option<Arc<Mutex<IpcReceiverWithContext<ClientToServerMsg>>>>,
|
||||
send_instructions_to_client: Arc<Mutex<Option<IpcSenderWithContext<ServerToClientMsg>>>>,
|
||||
client_senders: Arc<Mutex<HashMap<ClientId, IpcSenderWithContext<ServerToClientMsg>>>>,
|
||||
}
|
||||
|
||||
// async fn in traits is not supported by rust, so dtolnay's excellent async_trait macro is being
|
||||
|
|
@ -285,22 +284,13 @@ pub trait ServerOsApi: Send + Sync {
|
|||
fn force_kill(&self, pid: Pid) -> Result<(), nix::Error>;
|
||||
/// Returns a [`Box`] pointer to this [`ServerOsApi`] struct.
|
||||
fn box_clone(&self) -> Box<dyn ServerOsApi>;
|
||||
/// Receives a message on server-side IPC channel
|
||||
fn recv_from_client(&self) -> (ClientToServerMsg, ErrorContext);
|
||||
/// Sends a message to client
|
||||
fn send_to_client(&self, msg: ServerToClientMsg);
|
||||
/// Adds a sender to client
|
||||
fn add_client_sender(&self);
|
||||
/// Send to the temporary client
|
||||
// A temporary client is the one that hasn't been registered as a client yet.
|
||||
// Only the corresponding router thread has access to send messages to it.
|
||||
// This can be the case when the client cannot attach to the session,
|
||||
// so it tries to connect and then exits, hence temporary.
|
||||
fn send_to_temp_client(&self, msg: ServerToClientMsg);
|
||||
/// Removes the sender to client
|
||||
fn remove_client_sender(&self);
|
||||
/// Update the receiver socket for the client
|
||||
fn update_receiver(&mut self, stream: LocalSocketStream);
|
||||
fn send_to_client(&self, client_id: ClientId, msg: ServerToClientMsg);
|
||||
fn new_client(
|
||||
&mut self,
|
||||
client_id: ClientId,
|
||||
stream: LocalSocketStream,
|
||||
) -> IpcReceiverWithContext<ClientToServerMsg>;
|
||||
fn remove_client(&mut self, client_id: ClientId);
|
||||
fn load_palette(&self) -> Palette;
|
||||
/// Returns the current working directory for a given pid
|
||||
fn get_cwd(&self, pid: Pid) -> Option<PathBuf>;
|
||||
|
|
@ -340,55 +330,29 @@ impl ServerOsApi for ServerOsInputOutput {
|
|||
let _ = kill(pid, Some(Signal::SIGKILL));
|
||||
Ok(())
|
||||
}
|
||||
fn recv_from_client(&self) -> (ClientToServerMsg, ErrorContext) {
|
||||
self.receive_instructions_from_client
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
fn send_to_client(&self, client_id: ClientId, msg: ServerToClientMsg) {
|
||||
if let Some(sender) = self.client_senders.lock().unwrap().get_mut(&client_id) {
|
||||
sender.send(msg);
|
||||
}
|
||||
}
|
||||
fn new_client(
|
||||
&mut self,
|
||||
client_id: ClientId,
|
||||
stream: LocalSocketStream,
|
||||
) -> IpcReceiverWithContext<ClientToServerMsg> {
|
||||
let receiver = IpcReceiverWithContext::new(stream);
|
||||
let sender = receiver.get_sender();
|
||||
self.client_senders
|
||||
.lock()
|
||||
.unwrap()
|
||||
.recv()
|
||||
.insert(client_id, sender);
|
||||
receiver
|
||||
}
|
||||
fn send_to_client(&self, msg: ServerToClientMsg) {
|
||||
self.send_instructions_to_client
|
||||
.lock()
|
||||
.unwrap()
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.send(msg);
|
||||
fn remove_client(&mut self, client_id: ClientId) {
|
||||
let mut client_senders = self.client_senders.lock().unwrap();
|
||||
if client_senders.contains_key(&client_id) {
|
||||
client_senders.remove(&client_id);
|
||||
}
|
||||
fn add_client_sender(&self) {
|
||||
let sender = self
|
||||
.receive_instructions_from_client
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.lock()
|
||||
.unwrap()
|
||||
.get_sender();
|
||||
let old_sender = self
|
||||
.send_instructions_to_client
|
||||
.lock()
|
||||
.unwrap()
|
||||
.replace(sender);
|
||||
if let Some(mut sender) = old_sender {
|
||||
sender.send(ServerToClientMsg::Exit(ExitReason::ForceDetached));
|
||||
}
|
||||
}
|
||||
fn send_to_temp_client(&self, msg: ServerToClientMsg) {
|
||||
self.receive_instructions_from_client
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.lock()
|
||||
.unwrap()
|
||||
.get_sender()
|
||||
.send(msg);
|
||||
}
|
||||
fn remove_client_sender(&self) {
|
||||
assert!(self.send_instructions_to_client.lock().unwrap().is_some());
|
||||
*self.send_instructions_to_client.lock().unwrap() = None;
|
||||
}
|
||||
fn update_receiver(&mut self, stream: LocalSocketStream) {
|
||||
self.receive_instructions_from_client =
|
||||
Some(Arc::new(Mutex::new(IpcReceiverWithContext::new(stream))));
|
||||
}
|
||||
fn load_palette(&self) -> Palette {
|
||||
default_palette()
|
||||
|
|
@ -418,8 +382,7 @@ pub fn get_server_os_input() -> Result<ServerOsInputOutput, nix::Error> {
|
|||
let orig_termios = Arc::new(Mutex::new(current_termios));
|
||||
Ok(ServerOsInputOutput {
|
||||
orig_termios,
|
||||
receive_instructions_from_client: None,
|
||||
send_instructions_to_client: Arc::new(Mutex::new(None)),
|
||||
client_senders: Arc::new(Mutex::new(HashMap::new())),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,14 +13,17 @@ use zellij_utils::{
|
|||
command::TerminalAction,
|
||||
get_mode_info,
|
||||
},
|
||||
ipc::{ClientToServerMsg, ExitReason, ServerToClientMsg},
|
||||
ipc::{ClientToServerMsg, IpcReceiverWithContext, ServerToClientMsg},
|
||||
};
|
||||
|
||||
use crate::ClientId;
|
||||
|
||||
fn route_action(
|
||||
action: Action,
|
||||
session: &SessionMetaData,
|
||||
_os_input: &dyn ServerOsApi,
|
||||
to_server: &SenderWithContext<ServerInstruction>,
|
||||
client_id: ClientId,
|
||||
) -> bool {
|
||||
let mut should_break = false;
|
||||
session
|
||||
|
|
@ -241,11 +244,15 @@ fn route_action(
|
|||
.unwrap();
|
||||
}
|
||||
Action::Quit => {
|
||||
to_server.send(ServerInstruction::ClientExit).unwrap();
|
||||
to_server
|
||||
.send(ServerInstruction::ClientExit(client_id))
|
||||
.unwrap();
|
||||
should_break = true;
|
||||
}
|
||||
Action::Detach => {
|
||||
to_server.send(ServerInstruction::DetachSession).unwrap();
|
||||
to_server
|
||||
.send(ServerInstruction::DetachSession(client_id))
|
||||
.unwrap();
|
||||
should_break = true;
|
||||
}
|
||||
Action::LeftClick(point) => {
|
||||
|
|
@ -282,47 +289,75 @@ pub(crate) fn route_thread_main(
|
|||
session_state: Arc<RwLock<SessionState>>,
|
||||
os_input: Box<dyn ServerOsApi>,
|
||||
to_server: SenderWithContext<ServerInstruction>,
|
||||
mut receiver: IpcReceiverWithContext<ClientToServerMsg>,
|
||||
client_id: ClientId,
|
||||
) {
|
||||
loop {
|
||||
let (instruction, err_ctx) = os_input.recv_from_client();
|
||||
let (instruction, err_ctx) = receiver.recv();
|
||||
err_ctx.update_thread_ctx();
|
||||
let rlocked_sessions = session_data.read().unwrap();
|
||||
|
||||
match instruction {
|
||||
ClientToServerMsg::Action(action) => {
|
||||
if let Some(rlocked_sessions) = rlocked_sessions.as_ref() {
|
||||
if route_action(action, rlocked_sessions, &*os_input, &to_server) {
|
||||
if let Action::SwitchToMode(input_mode) = action {
|
||||
for client_id in session_state.read().unwrap().clients.keys() {
|
||||
os_input.send_to_client(
|
||||
*client_id,
|
||||
ServerToClientMsg::SwitchToMode(input_mode),
|
||||
);
|
||||
}
|
||||
}
|
||||
if route_action(action, rlocked_sessions, &*os_input, &to_server, client_id) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
ClientToServerMsg::TerminalResize(new_size) => {
|
||||
session_state
|
||||
.write()
|
||||
.unwrap()
|
||||
.set_client_size(client_id, new_size);
|
||||
let min_size = session_state
|
||||
.read()
|
||||
.unwrap()
|
||||
.min_client_terminal_size()
|
||||
.unwrap();
|
||||
rlocked_sessions
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.senders
|
||||
.send_to_screen(ScreenInstruction::TerminalResize(new_size))
|
||||
.send_to_screen(ScreenInstruction::TerminalResize(min_size))
|
||||
.unwrap();
|
||||
}
|
||||
ClientToServerMsg::NewClient(..) => {
|
||||
if *session_state.read().unwrap() != SessionState::Uninitialized {
|
||||
os_input.send_to_temp_client(ServerToClientMsg::Exit(ExitReason::Error(
|
||||
"Cannot add new client".into(),
|
||||
)));
|
||||
} else {
|
||||
os_input.add_client_sender();
|
||||
to_server.send(instruction.into()).unwrap();
|
||||
ClientToServerMsg::NewClient(
|
||||
client_attributes,
|
||||
cli_args,
|
||||
opts,
|
||||
layout,
|
||||
plugin_config,
|
||||
) => {
|
||||
let new_client_instruction = ServerInstruction::NewClient(
|
||||
client_attributes,
|
||||
cli_args,
|
||||
opts,
|
||||
layout,
|
||||
client_id,
|
||||
plugin_config,
|
||||
);
|
||||
to_server.send(new_client_instruction).unwrap();
|
||||
}
|
||||
ClientToServerMsg::AttachClient(client_attributes, opts) => {
|
||||
let attach_client_instruction =
|
||||
ServerInstruction::AttachClient(client_attributes, opts, client_id);
|
||||
to_server.send(attach_client_instruction).unwrap();
|
||||
}
|
||||
ClientToServerMsg::ClientExited => {
|
||||
// we don't unwrap this because we don't really care if there's an error here (eg.
|
||||
// if the main server thread exited before this router thread did)
|
||||
let _ = to_server.send(ServerInstruction::RemoveClient(client_id));
|
||||
break;
|
||||
}
|
||||
ClientToServerMsg::AttachClient(_, force, _) => {
|
||||
if *session_state.read().unwrap() == SessionState::Attached && !force {
|
||||
os_input.send_to_temp_client(ServerToClientMsg::Exit(ExitReason::CannotAttach));
|
||||
} else {
|
||||
os_input.add_client_sender();
|
||||
to_server.send(instruction.into()).unwrap();
|
||||
}
|
||||
}
|
||||
ClientToServerMsg::ClientExited => break,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -252,12 +252,10 @@ impl Screen {
|
|||
.unwrap();
|
||||
if self.tabs.is_empty() {
|
||||
self.active_tab_index = None;
|
||||
if *self.session_state.read().unwrap() == SessionState::Attached {
|
||||
self.bus
|
||||
.senders
|
||||
.send_to_server(ServerInstruction::Render(None))
|
||||
.unwrap();
|
||||
}
|
||||
} else {
|
||||
if let Some(tab) = self.get_active_tab() {
|
||||
tab.visible(false);
|
||||
|
|
@ -288,9 +286,6 @@ impl Screen {
|
|||
|
||||
/// Renders this [`Screen`], which amounts to rendering its active [`Tab`].
|
||||
pub fn render(&mut self) {
|
||||
if *self.session_state.read().unwrap() != SessionState::Attached {
|
||||
return;
|
||||
}
|
||||
if let Some(active_tab) = self.get_active_tab_mut() {
|
||||
if active_tab.get_active_pane().is_some() {
|
||||
active_tab.render();
|
||||
|
|
|
|||
|
|
@ -725,12 +725,10 @@ impl Tab {
|
|||
}
|
||||
}
|
||||
pub fn render(&mut self) {
|
||||
if self.active_terminal.is_none()
|
||||
|| *self.session_state.read().unwrap() != SessionState::Attached
|
||||
{
|
||||
if self.active_terminal.is_none() || self.session_state.read().unwrap().clients.is_empty() {
|
||||
// we might not have an active terminal if we closed the last pane
|
||||
// in that case, we should not render as the app is exiting
|
||||
// or if this session is not attached to a client, we do not have to render
|
||||
// or if there are no attached clients to this session
|
||||
return;
|
||||
}
|
||||
self.senders
|
||||
|
|
|
|||
|
|
@ -3,13 +3,14 @@ use crate::zellij_tile::data::{ModeInfo, Palette};
|
|||
use crate::{
|
||||
os_input_output::{AsyncReader, ChildId, Pid, ServerOsApi},
|
||||
thread_bus::Bus,
|
||||
SessionState,
|
||||
ClientId, SessionState,
|
||||
};
|
||||
use std::convert::TryInto;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::{Arc, RwLock};
|
||||
use zellij_utils::input::command::TerminalAction;
|
||||
use zellij_utils::input::layout::LayoutTemplate;
|
||||
use zellij_utils::ipc::IpcReceiverWithContext;
|
||||
use zellij_utils::pane_size::Size;
|
||||
|
||||
use std::os::unix::io::RawFd;
|
||||
|
|
@ -18,7 +19,6 @@ use zellij_utils::ipc::ClientAttributes;
|
|||
use zellij_utils::nix;
|
||||
|
||||
use zellij_utils::{
|
||||
errors::ErrorContext,
|
||||
interprocess::local_socket::LocalSocketStream,
|
||||
ipc::{ClientToServerMsg, ServerToClientMsg},
|
||||
};
|
||||
|
|
@ -27,49 +27,44 @@ use zellij_utils::{
|
|||
struct FakeInputOutput {}
|
||||
|
||||
impl ServerOsApi for FakeInputOutput {
|
||||
fn set_terminal_size_using_fd(&self, _fd: RawFd, _cols: u16, _rows: u16) {
|
||||
fn set_terminal_size_using_fd(&self, fd: RawFd, cols: u16, rows: u16) {
|
||||
// noop
|
||||
}
|
||||
fn spawn_terminal(&self, _file_to_open: TerminalAction) -> (RawFd, ChildId) {
|
||||
unimplemented!()
|
||||
}
|
||||
fn read_from_tty_stdout(&self, _fd: RawFd, _buf: &mut [u8]) -> Result<usize, nix::Error> {
|
||||
fn read_from_tty_stdout(&self, fd: RawFd, buf: &mut [u8]) -> Result<usize, nix::Error> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn async_file_reader(&self, _fd: RawFd) -> Box<dyn AsyncReader> {
|
||||
fn async_file_reader(&self, fd: RawFd) -> Box<dyn AsyncReader> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn write_to_tty_stdin(&self, _fd: RawFd, _buf: &[u8]) -> Result<usize, nix::Error> {
|
||||
fn write_to_tty_stdin(&self, fd: RawFd, buf: &[u8]) -> Result<usize, nix::Error> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn tcdrain(&self, _fd: RawFd) -> Result<(), nix::Error> {
|
||||
fn tcdrain(&self, fd: RawFd) -> Result<(), nix::Error> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn kill(&self, pid: Pid) -> Result<(), nix::Error> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn force_kill(&self, pid: Pid) -> Result<(), nix::Error> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn box_clone(&self) -> Box<dyn ServerOsApi> {
|
||||
Box::new((*self).clone())
|
||||
}
|
||||
fn force_kill(&self, _pid: Pid) -> Result<(), nix::Error> {
|
||||
fn send_to_client(&self, client_id: ClientId, msg: ServerToClientMsg) {
|
||||
unimplemented!()
|
||||
}
|
||||
fn kill(&self, _pid: Pid) -> Result<(), nix::Error> {
|
||||
fn new_client(
|
||||
&mut self,
|
||||
client_id: ClientId,
|
||||
stream: LocalSocketStream,
|
||||
) -> IpcReceiverWithContext<ClientToServerMsg> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn recv_from_client(&self) -> (ClientToServerMsg, ErrorContext) {
|
||||
unimplemented!()
|
||||
}
|
||||
fn send_to_client(&self, _msg: ServerToClientMsg) {
|
||||
unimplemented!()
|
||||
}
|
||||
fn add_client_sender(&self) {
|
||||
unimplemented!()
|
||||
}
|
||||
fn send_to_temp_client(&self, _msg: ServerToClientMsg) {
|
||||
unimplemented!()
|
||||
}
|
||||
fn remove_client_sender(&self) {
|
||||
unimplemented!()
|
||||
}
|
||||
fn update_receiver(&mut self, _stream: LocalSocketStream) {
|
||||
fn remove_client(&mut self, client_id: ClientId) {
|
||||
unimplemented!()
|
||||
}
|
||||
fn load_palette(&self) -> Palette {
|
||||
|
|
@ -90,7 +85,7 @@ fn create_new_screen(size: Size) -> Screen {
|
|||
};
|
||||
let max_panes = None;
|
||||
let mode_info = ModeInfo::default();
|
||||
let session_state = Arc::new(RwLock::new(SessionState::Attached));
|
||||
let session_state = Arc::new(RwLock::new(SessionState::new()));
|
||||
Screen::new(
|
||||
bus,
|
||||
&client_attributes,
|
||||
|
|
|
|||
|
|
@ -4,12 +4,13 @@ use crate::{
|
|||
os_input_output::{AsyncReader, ChildId, Pid, ServerOsApi},
|
||||
panes::PaneId,
|
||||
thread_bus::ThreadSenders,
|
||||
SessionState,
|
||||
ClientId, SessionState,
|
||||
};
|
||||
use std::convert::TryInto;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::{Arc, RwLock};
|
||||
use zellij_utils::input::layout::LayoutTemplate;
|
||||
use zellij_utils::ipc::IpcReceiverWithContext;
|
||||
use zellij_utils::pane_size::Size;
|
||||
|
||||
use std::os::unix::io::RawFd;
|
||||
|
|
@ -17,58 +18,53 @@ use std::os::unix::io::RawFd;
|
|||
use zellij_utils::nix;
|
||||
|
||||
use zellij_utils::{
|
||||
errors::ErrorContext,
|
||||
input::command::TerminalAction,
|
||||
interprocess::local_socket::LocalSocketStream,
|
||||
ipc::{ClientToServerMsg, ServerToClientMsg},
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
struct FakeInputOutput {}
|
||||
|
||||
impl ServerOsApi for FakeInputOutput {
|
||||
fn set_terminal_size_using_fd(&self, _fd: RawFd, _cols: u16, _rows: u16) {
|
||||
fn set_terminal_size_using_fd(&self, fd: RawFd, cols: u16, rows: u16) {
|
||||
// noop
|
||||
}
|
||||
fn spawn_terminal(&self, _file_to_open: TerminalAction) -> (RawFd, ChildId) {
|
||||
unimplemented!()
|
||||
}
|
||||
fn read_from_tty_stdout(&self, _fd: RawFd, _buf: &mut [u8]) -> Result<usize, nix::Error> {
|
||||
fn read_from_tty_stdout(&self, fd: RawFd, buf: &mut [u8]) -> Result<usize, nix::Error> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn async_file_reader(&self, _fd: RawFd) -> Box<dyn AsyncReader> {
|
||||
fn async_file_reader(&self, fd: RawFd) -> Box<dyn AsyncReader> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn write_to_tty_stdin(&self, _fd: RawFd, _buf: &[u8]) -> Result<usize, nix::Error> {
|
||||
fn write_to_tty_stdin(&self, fd: RawFd, buf: &[u8]) -> Result<usize, nix::Error> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn tcdrain(&self, _fd: RawFd) -> Result<(), nix::Error> {
|
||||
fn tcdrain(&self, fd: RawFd) -> Result<(), nix::Error> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn kill(&self, pid: Pid) -> Result<(), nix::Error> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn force_kill(&self, pid: Pid) -> Result<(), nix::Error> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn box_clone(&self) -> Box<dyn ServerOsApi> {
|
||||
Box::new((*self).clone())
|
||||
}
|
||||
fn send_to_client(&self, client_id: ClientId, msg: ServerToClientMsg) {
|
||||
unimplemented!()
|
||||
}
|
||||
fn force_kill(&self, _pid: Pid) -> Result<(), nix::Error> {
|
||||
fn new_client(
|
||||
&mut self,
|
||||
client_id: ClientId,
|
||||
stream: LocalSocketStream,
|
||||
) -> IpcReceiverWithContext<ClientToServerMsg> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn kill(&self, _pid: Pid) -> Result<(), nix::Error> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn recv_from_client(&self) -> (ClientToServerMsg, ErrorContext) {
|
||||
unimplemented!()
|
||||
}
|
||||
fn send_to_client(&self, _msg: ServerToClientMsg) {
|
||||
unimplemented!()
|
||||
}
|
||||
fn add_client_sender(&self) {
|
||||
unimplemented!()
|
||||
}
|
||||
fn send_to_temp_client(&self, _msg: ServerToClientMsg) {
|
||||
unimplemented!()
|
||||
}
|
||||
fn remove_client_sender(&self) {
|
||||
unimplemented!()
|
||||
}
|
||||
fn update_receiver(&mut self, _stream: LocalSocketStream) {
|
||||
fn remove_client(&mut self, client_id: ClientId) {
|
||||
unimplemented!()
|
||||
}
|
||||
fn load_palette(&self) -> Palette {
|
||||
|
|
@ -88,7 +84,7 @@ fn create_new_tab(size: Size) -> Tab {
|
|||
let max_panes = None;
|
||||
let mode_info = ModeInfo::default();
|
||||
let colors = Palette::default();
|
||||
let session_state = Arc::new(RwLock::new(SessionState::Attached));
|
||||
let session_state = Arc::new(RwLock::new(SessionState::new()));
|
||||
let mut tab = Tab::new(
|
||||
index,
|
||||
position,
|
||||
|
|
|
|||
|
|
@ -81,11 +81,6 @@ pub enum Sessions {
|
|||
/// Name of the session to attach to.
|
||||
session_name: Option<String>,
|
||||
|
||||
/// Force attach- session will detach from the other
|
||||
/// zellij client (if any) and attach to this.
|
||||
#[structopt(long, short)]
|
||||
force: bool,
|
||||
|
||||
/// Create a session if one does not exist.
|
||||
#[structopt(short, long)]
|
||||
create: bool,
|
||||
|
|
|
|||
|
|
@ -60,8 +60,37 @@ where
|
|||
),
|
||||
};
|
||||
|
||||
let one_line_backtrace = match (info.location(), msg) {
|
||||
(Some(location), Some(msg)) => format!(
|
||||
"{}\n\u{1b}[0;0mError: \u{1b}[0;31mthread '{}' panicked at '{}': {}:{}\n\u{1b}[0;0m",
|
||||
err_ctx,
|
||||
thread,
|
||||
msg,
|
||||
location.file(),
|
||||
location.line(),
|
||||
),
|
||||
(Some(location), None) => format!(
|
||||
"{}\n\u{1b}[0;0mError: \u{1b}[0;31mthread '{}' panicked: {}:{}\n\u{1b}[0;0m",
|
||||
err_ctx,
|
||||
thread,
|
||||
location.file(),
|
||||
location.line(),
|
||||
),
|
||||
(None, Some(msg)) => format!(
|
||||
"{}\n\u{1b}[0;0mError: \u{1b}[0;31mthread '{}' panicked at '{}'\n\u{1b}[0;0m",
|
||||
err_ctx, thread, msg
|
||||
),
|
||||
(None, None) => format!(
|
||||
"{}\n\u{1b}[0;0mError: \u{1b}[0;31mthread '{}' panicked\n\u{1b}[0;0m",
|
||||
err_ctx, thread
|
||||
),
|
||||
};
|
||||
|
||||
if thread == "main" {
|
||||
println!("{}", backtrace);
|
||||
// here we only show the first line because the backtrace is not readable otherwise
|
||||
// a better solution would be to escape raw mode before we do this, but it's not trivial
|
||||
// to get os_input here
|
||||
println!("\u{1b}[2J{}", one_line_backtrace);
|
||||
process::exit(1);
|
||||
} else {
|
||||
let _ = sender.send(T::error(backtrace));
|
||||
|
|
@ -262,6 +291,7 @@ pub enum ClientContext {
|
|||
UnblockInputThread,
|
||||
Render,
|
||||
ServerError,
|
||||
SwitchToMode,
|
||||
}
|
||||
|
||||
/// Stack call representations corresponding to the different types of [`ServerInstruction`]s.
|
||||
|
|
@ -271,6 +301,7 @@ pub enum ServerContext {
|
|||
Render,
|
||||
UnblockInputThread,
|
||||
ClientExit,
|
||||
RemoveClient,
|
||||
Error,
|
||||
DetachSession,
|
||||
AttachClient,
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ use std::{
|
|||
os::unix::io::{AsRawFd, FromRawFd},
|
||||
};
|
||||
|
||||
use zellij_tile::data::Palette;
|
||||
use zellij_tile::data::{InputMode, Palette};
|
||||
|
||||
type SessionId = u64;
|
||||
|
||||
|
|
@ -65,7 +65,7 @@ pub enum ClientToServerMsg {
|
|||
LayoutFromYaml,
|
||||
Option<PluginsConfig>,
|
||||
),
|
||||
AttachClient(ClientAttributes, bool, Options),
|
||||
AttachClient(ClientAttributes, Options),
|
||||
Action(Action),
|
||||
ClientExited,
|
||||
}
|
||||
|
|
@ -80,6 +80,7 @@ pub enum ServerToClientMsg {
|
|||
Render(String),
|
||||
UnblockInputThread,
|
||||
Exit(ExitReason),
|
||||
SwitchToMode(InputMode),
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
|
|
@ -126,7 +127,9 @@ impl<T: Serialize> IpcSenderWithContext<T> {
|
|||
pub fn send(&mut self, msg: T) {
|
||||
let err_ctx = get_current_ctx();
|
||||
bincode::serialize_into(&mut self.sender, &(msg, err_ctx)).unwrap();
|
||||
self.sender.flush().unwrap();
|
||||
// TODO: unwrapping here can cause issues when the server disconnects which we don't mind
|
||||
// do we need to handle errors here in other cases?
|
||||
let _ = self.sender.flush();
|
||||
}
|
||||
|
||||
/// Returns an [`IpcReceiverWithContext`] with the same socket as this sender.
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue