aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorKunal Mohan <44079328+kunalmohan@users.noreply.github.com>2021-05-25 17:30:40 +0530
committerGitHub <noreply@github.com>2021-05-25 17:30:40 +0530
commit2bca7e007aad9a74e6c48191b8a7a3edf85808d4 (patch)
tree61366b1e29601ad003fc53a715f4b925d8ef5871
parenthotfix(build): Fix cargo make install (diff)
parentcheck for residual socket files and clean them (diff)
downloadsl0ppij-2bca7e007aad9a74e6c48191b8a7a3edf85808d4.tar.gz
sl0ppij-2bca7e007aad9a74e6c48191b8a7a3edf85808d4.tar.xz
sl0ppij-2bca7e007aad9a74e6c48191b8a7a3edf85808d4.zip
Merge pull request #531 from zellij-org/detach-sessions
Feature: Detachable/Persistent sessions
-rw-r--r--Cargo.lock3
-rw-r--r--Cargo.toml1
-rw-r--r--assets/config/default.yaml27
-rw-r--r--default-plugins/status-bar/src/first_line.rs68
-rw-r--r--src/main.rs45
-rw-r--r--src/sessions.rs109
-rw-r--r--src/tests/fakes.rs2
-rw-r--r--src/tests/mod.rs4
-rw-r--r--zellij-client/src/input_handler.rs8
-rw-r--r--zellij-client/src/lib.rs111
-rw-r--r--zellij-server/src/lib.rs160
-rw-r--r--zellij-server/src/os_input_output.rs36
-rw-r--r--zellij-server/src/route.rs55
-rw-r--r--zellij-server/src/screen.rs24
-rw-r--r--zellij-server/src/tab.rs12
-rw-r--r--zellij-tile/src/data.rs3
-rw-r--r--zellij-utils/Cargo.toml2
-rw-r--r--zellij-utils/src/cli.rs33
-rw-r--r--zellij-utils/src/consts.rs15
-rw-r--r--zellij-utils/src/errors.rs2
-rw-r--r--zellij-utils/src/input/actions.rs2
-rw-r--r--zellij-utils/src/input/config.rs6
-rw-r--r--zellij-utils/src/input/mod.rs3
-rw-r--r--zellij-utils/src/input/options.rs6
-rw-r--r--zellij-utils/src/ipc.rs36
25 files changed, 592 insertions, 181 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 0dda8373..596062e1 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2301,6 +2301,7 @@ name = "zellij"
version = "0.12.0"
dependencies = [
"insta",
+ "names",
"zellij-client",
"zellij-server",
"zellij-utils",
@@ -2358,8 +2359,8 @@ dependencies = [
"interprocess",
"lazy_static",
"libc",
- "names",
"nix",
+ "once_cell",
"serde",
"serde_yaml",
"signal-hook 0.3.8",
diff --git a/Cargo.toml b/Cargo.toml
index 9e7e31ab..5f7afaaa 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -13,6 +13,7 @@ resolver = "2"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
+names = "0.11.0"
zellij-client = { path = "zellij-client/", version = "0.12.0" }
zellij-server = { path = "zellij-server/", version = "0.12.0" }
zellij-utils = { path = "zellij-utils/", version = "0.12.0" }
diff --git a/assets/config/default.yaml b/assets/config/default.yaml
index 4582e79f..42bc1688 100644
--- a/assets/config/default.yaml
+++ b/assets/config/default.yaml
@@ -12,6 +12,8 @@ keybinds:
key: [Ctrl: 't',]
- action: [SwitchToMode: Scroll,]
key: [Ctrl: 's',]
+ - action: [SwitchToMode: Session,]
+ key: [Ctrl: 'o',]
- action: [Quit,]
key: [Ctrl: 'q',]
- action: [NewPane: ]
@@ -42,6 +44,8 @@ keybinds:
key: [Ctrl: 'r', Char: "\n", Char: ' ',]
- action: [SwitchToMode: Scroll,]
key: [Ctrl: 's']
+ - action: [SwitchToMode: Session,]
+ key: [Ctrl: 'o',]
- action: [Quit]
key: [Ctrl: 'q']
- action: [Resize: Left,]
@@ -77,6 +81,8 @@ keybinds:
key: [Ctrl: 'p', Char: "\n", Char: ' ',]
- action: [SwitchToMode: Scroll,]
key: [Ctrl: 's']
+ - action: [SwitchToMode: Session,]
+ key: [Ctrl: 'o',]
- action: [Quit,]
key: [Ctrl: 'q',]
- action: [MoveFocus: Left,]
@@ -114,6 +120,8 @@ keybinds:
key: [Ctrl: 't', Char: "\n", Char: ' ',]
- action: [SwitchToMode: Scroll,]
key: [Ctrl: 's']
+ - action: [SwitchToMode: Session,]
+ key: [Ctrl: 'o',]
- action: [SwitchToMode: RenameTab, TabNameInput: [0],]
key: [Char: 'r']
- action: [Quit,]
@@ -168,6 +176,8 @@ keybinds:
key: [Ctrl: 'g',]
- action: [SwitchToMode: Pane,]
key: [Ctrl: 'p',]
+ - action: [SwitchToMode: Session,]
+ key: [Ctrl: 'o',]
- action: [Quit,]
key: [Ctrl: 'q',]
- action: [ScrollDown,]
@@ -213,3 +223,20 @@ keybinds:
key: [ Alt: '[',]
- action: [FocusNextPane,]
key: [ Alt: ']',]
+ session:
+ - action: [SwitchToMode: Locked,]
+ key: [Ctrl: 'g']
+ - action: [SwitchToMode: Resize,]
+ key: [Ctrl: 'r',]
+ - action: [SwitchToMode: Pane,]
+ key: [Ctrl: 'p',]
+ - action: [SwitchToMode: Tab,]
+ key: [Ctrl: 't',]
+ - action: [SwitchToMode: Normal,]
+ key: [Ctrl: 'o', Char: "\n", Char: ' ',]
+ - action: [SwitchToMode: Scroll,]
+ key: [Ctrl: 's']
+ - action: [Quit,]
+ key: [Ctrl: 'q',]
+ - action: [Detach,]
+ key: [Char: 'd',]
diff --git a/default-plugins/status-bar/src/first_line.rs b/default-plugins/status-bar/src/first_line.rs
index 37175e3f..7a678cdc 100644
--- a/default-plugins/status-bar/src/first_line.rs
+++ b/default-plugins/status-bar/src/first_line.rs
@@ -22,6 +22,7 @@ enum CtrlKeyAction {
Resize,
Scroll,
Quit,
+ Session,
}
enum CtrlKeyMode {
@@ -39,16 +40,7 @@ impl CtrlKeyShortcut {
CtrlKeyAction::Resize => String::from("RESIZE"),
CtrlKeyAction::Scroll => String::from("SCROLL"),
CtrlKeyAction::Quit => String::from("QUIT"),
- }
- }
- pub fn shortened_text(&self) -> String {
- match self.action {
- CtrlKeyAction::Lock => String::from("LOCK"),
- CtrlKeyAction::Pane => String::from("ane"),
- CtrlKeyAction::Tab => String::from("ab"),
- CtrlKeyAction::Resize => String::from("esize"),
- CtrlKeyAction::Scroll => String::from("croll"),
- CtrlKeyAction::Quit => String::from("uit"),
+ CtrlKeyAction::Session => String::from("SESSION"),
}
}
pub fn letter_shortcut(&self) -> char {
@@ -59,6 +51,7 @@ impl CtrlKeyShortcut {
CtrlKeyAction::Resize => 'r',
CtrlKeyAction::Scroll => 's',
CtrlKeyAction::Quit => 'q',
+ CtrlKeyAction::Session => 'o',
}
}
}
@@ -193,32 +186,6 @@ fn full_ctrl_key(key: &CtrlKeyShortcut, palette: ColoredElements, separator: &st
}
}
-fn shortened_ctrl_key(
- key: &CtrlKeyShortcut,
- palette: ColoredElements,
- separator: &str,
-) -> LinePart {
- let shortened_text = key.shortened_text();
- let letter_shortcut = key.letter_shortcut();
- let shortened_text = match key.action {
- CtrlKeyAction::Lock => format!(" {}", shortened_text),
- _ => shortened_text,
- };
- match key.mode {
- CtrlKeyMode::Unselected => {
- unselected_mode_shortcut(letter_shortcut, &shortened_text, palette, separator)
- }
- CtrlKeyMode::Selected => {
- selected_mode_shortcut(letter_shortcut, &shortened_text, palette, separator)
- }
- CtrlKeyMode::Disabled => disabled_mode_shortcut(
- &format!(" <{}>{}", letter_shortcut, shortened_text),
- palette,
- separator,
- ),
- }
-}
-
fn single_letter_ctrl_key(
key: &CtrlKeyShortcut,
palette: ColoredElements,
@@ -255,15 +222,6 @@ fn key_indicators(
}
line_part = LinePart::default();
for ctrl_key in keys {
- let key = shortened_ctrl_key(ctrl_key, palette, separator);
- line_part.part = format!("{}{}", line_part.part, key.part);
- line_part.len += key.len;
- }
- if line_part.len < max_len {
- return line_part;
- }
- line_part = LinePart::default();
- for ctrl_key in keys {
let key = single_letter_ctrl_key(ctrl_key, palette, separator);
line_part.part = format!("{}{}", line_part.part, key.part);
line_part.len += key.len;
@@ -296,6 +254,7 @@ pub fn ctrl_keys(help: &ModeInfo, max_len: usize, separator: &str) -> LinePart {
CtrlKeyShortcut::new(CtrlKeyMode::Disabled, CtrlKeyAction::Tab),
CtrlKeyShortcut::new(CtrlKeyMode::Disabled, CtrlKeyAction::Resize),
CtrlKeyShortcut::new(CtrlKeyMode::Disabled, CtrlKeyAction::Scroll),
+ CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Session),
CtrlKeyShortcut::new(CtrlKeyMode::Disabled, CtrlKeyAction::Quit),
],
colored_elements,
@@ -309,6 +268,7 @@ pub fn ctrl_keys(help: &ModeInfo, max_len: usize, separator: &str) -> LinePart {
CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Tab),
CtrlKeyShortcut::new(CtrlKeyMode::Selected, CtrlKeyAction::Resize),
CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Scroll),
+ CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Session),
CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Quit),
],
colored_elements,
@@ -322,6 +282,7 @@ pub fn ctrl_keys(help: &ModeInfo, max_len: usize, separator: &str) -> LinePart {
CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Tab),
CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Resize),
CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Scroll),
+ CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Session),
CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Quit),
],
colored_elements,
@@ -335,6 +296,7 @@ pub fn ctrl_keys(help: &ModeInfo, max_len: usize, separator: &str) -> LinePart {
CtrlKeyShortcut::new(CtrlKeyMode::Selected, CtrlKeyAction::Tab),
CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Resize),
CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Scroll),
+ CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Session),
CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Quit),
],
colored_elements,
@@ -348,6 +310,7 @@ pub fn ctrl_keys(help: &ModeInfo, max_len: usize, separator: &str) -> LinePart {
CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Tab),
CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Resize),
CtrlKeyShortcut::new(CtrlKeyMode::Selected, CtrlKeyAction::Scroll),
+ CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Session),
CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Quit),
],
colored_elements,
@@ -361,6 +324,21 @@ pub fn ctrl_keys(help: &ModeInfo, max_len: usize, separator: &str) -> LinePart {
CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Tab),
CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Resize),
CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Scroll),
+ CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Session),
+ CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Quit),
+ ],
+ colored_elements,
+ separator,
+ ),
+ InputMode::Session => key_indicators(
+ max_len,
+ &[
+ CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Lock),
+ CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Pane),
+ CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Tab),
+ CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Resize),
+ CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Scroll),
+ CtrlKeyShortcut::new(CtrlKeyMode::Selected, CtrlKeyAction::Session),
CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Quit),
],
colored_elements,
diff --git a/src/main.rs b/src/main.rs
index a970ce44..67b83398 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,11 +1,14 @@
+mod sessions;
#[cfg(test)]
mod tests;
+use sessions::{assert_session, assert_session_ne, list_sessions};
use std::convert::TryFrom;
-use zellij_client::{os_input_output::get_client_os_input, start_client};
+use std::process;
+use zellij_client::{os_input_output::get_client_os_input, start_client, ClientInfo};
use zellij_server::{os_input_output::get_server_os_input, start_server};
use zellij_utils::{
- cli::{CliArgs, ConfigCli},
+ cli::{CliArgs, Command, Sessions},
consts::{ZELLIJ_TMP_DIR, ZELLIJ_TMP_LOG_DIR},
input::config::Config,
logging::*,
@@ -16,15 +19,17 @@ use zellij_utils::{
pub fn main() {
let opts = CliArgs::from_args();
- if let Some(ConfigCli::Setup(setup)) = opts.option.clone() {
- Setup::from_cli(&setup, &opts).expect("Failed to print to stdout");
+ if let Some(Command::Sessions(Sessions::ListSessions)) = opts.command {
+ list_sessions();
+ } else if let Some(Command::Setup(ref setup)) = opts.command {
+ Setup::from_cli(setup, &opts).expect("Failed to print to stdout");
}
let config = match Config::try_from(&opts) {
Ok(config) => config,
Err(e) => {
eprintln!("There was an error in the config file:\n{}", e);
- std::process::exit(1);
+ process::exit(1);
}
};
atomic_create_dir(&*ZELLIJ_TMP_DIR).unwrap();
@@ -34,7 +39,7 @@ pub fn main() {
Ok(server_os_input) => server_os_input,
Err(e) => {
eprintln!("failed to open terminal:\n{}", e);
- std::process::exit(1);
+ process::exit(1);
}
};
start_server(Box::new(os_input), path);
@@ -43,9 +48,33 @@ pub fn main() {
Ok(os_input) => os_input,
Err(e) => {
eprintln!("failed to open terminal:\n{}", e);
- std::process::exit(1);
+ process::exit(1);
}
};
- start_client(Box::new(os_input), opts, config);
+ if let Some(Command::Sessions(Sessions::Attach {
+ session_name,
+ force,
+ })) = opts.command.clone()
+ {
+ assert_session(&session_name);
+ start_client(
+ Box::new(os_input),
+ opts,
+ config,
+ ClientInfo::Attach(session_name, force),
+ );
+ } else {
+ let session_name = opts
+ .session
+ .clone()
+ .unwrap_or_else(|| names::Generator::default().next().unwrap());
+ assert_session_ne(&session_name);
+ start_client(
+ Box::new(os_input),
+ opts,
+ config,
+ ClientInfo::New(session_name),
+ );
+ }
}
}
diff --git a/src/sessions.rs b/src/sessions.rs
new file mode 100644
index 00000000..fd834e40
--- /dev/null
+++ b/src/sessions.rs
@@ -0,0 +1,109 @@
+use std::os::unix::fs::FileTypeExt;
+use std::{fs, io, process};
+use zellij_utils::{
+ consts::ZELLIJ_SOCK_DIR,
+ interprocess::local_socket::LocalSocketStream,
+ ipc::{ClientToServerMsg, IpcSenderWithContext},
+};
+
+fn get_sessions() -> Result<Vec<String>, io::ErrorKind> {
+ match fs::read_dir(&*ZELLIJ_SOCK_DIR) {
+ Ok(files) => {
+ let mut sessions = Vec::new();
+ files.for_each(|file| {
+ let file = file.unwrap();
+ let file_name = file.file_name().into_string().unwrap();
+ if file.file_type().unwrap().is_socket() && assert_socket(&file_name) {
+ sessions.push(file_name);
+ }
+ });
+ Ok(sessions)
+ }
+ Err(err) => {
+ if let io::ErrorKind::NotFound = err.kind() {
+ Ok(Vec::with_capacity(0))
+ } else {
+ Err(err.kind())
+ }
+ }
+ }
+}
+
+pub(crate) fn list_sessions() {
+ let exit_code = match get_sessions() {
+ Ok(sessions) => {
+ if sessions.is_empty() {
+ println!("No active zellij sessions found.");
+ } else {
+ let curr_session =
+ std::env::var("ZELLIJ_SESSION_NAME").unwrap_or_else(|_| "".into());
+ sessions.iter().for_each(|session| {
+ let suffix = if curr_session == *session {
+ " (current)"
+ } else {
+ ""
+ };
+ println!("{}{}", session, suffix);
+ })
+ }
+ 0
+ }
+ Err(e) => {
+ eprintln!("Error occured: {:?}", e);
+ 1
+ }
+ };
+ process::exit(exit_code);
+}
+
+pub(crate) fn assert_session(name: &str) {
+ let exit_code = match get_sessions() {
+ Ok(sessions) => {
+ if sessions.iter().any(|s| s == name) {
+ return;
+ }
+ println!("No session named {:?} found.", name);
+ 0
+ }
+ Err(e) => {
+ eprintln!("Error occured: {:?}", e);
+ 1
+ }
+ };
+ process::exit(exit_code);
+}
+
+pub(crate) fn assert_session_ne(name: &str) {
+ let exit_code = match get_sessions() {
+ Ok(sessions) => {
+ if sessions.iter().all(|s| s != name) {
+ return;
+ }
+ println!("Session with name {:?} aleady exists. Use attach command to connect to it or specify a different name.", name);
+ 0
+ }
+ Err(e) => {
+ eprintln!("Error occured: {:?}", e);
+ 1
+ }
+ };
+ process::exit(exit_code);
+}
+
+fn assert_socket(name: &str) -> bool {
+ let path = &*ZELLIJ_SOCK_DIR.join(name);
+ match LocalSocketStream::connect(path) {
+ Ok(stream) => {
+ IpcSenderWithContext::new(stream).send(ClientToServerMsg::ClientExited);
+ true
+ }
+ Err(e) => {
+ if e.kind() == io::ErrorKind::ConnectionRefused {
+ drop(fs::remove_file(path));
+ false
+ } else {
+ true
+ }
+ }
+ }
+}
diff --git a/src/tests/fakes.rs b/src/tests/fakes.rs
index a2f338ba..23f9f182 100644
--- a/src/tests/fakes.rs
+++ b/src/tests/fakes.rs
@@ -333,6 +333,8 @@ impl ServerOsApi for FakeInputOutput {
self.send_instructions_to_client.send(msg).unwrap();
}
fn add_client_sender(&self) {}
+ fn remove_client_sender(&self) {}
+ fn send_to_temp_client(&self, _msg: ServerToClientMsg) {}
fn update_receiver(&mut self, _stream: LocalSocketStream) {}
fn load_palette(&self) -> Palette {
default_palette()
diff --git a/src/tests/mod.rs b/src/tests/mod.rs
index 3e548db7..e075ed49 100644
--- a/src/tests/mod.rs
+++ b/src/tests/mod.rs
@@ -5,7 +5,7 @@ pub mod tty_inputs;
pub mod utils;
use std::path::PathBuf;
-use zellij_client::{os_input_output::ClientOsApi, start_client};
+use zellij_client::{os_input_output::ClientOsApi, start_client, ClientInfo};
use zellij_server::{os_input_output::ServerOsApi, start_server};
use zellij_utils::{cli::CliArgs, input::config::Config};
@@ -21,6 +21,6 @@ pub fn start(
start_server(server_os_input, PathBuf::from(""));
})
.unwrap();
- start_client(client_os_input, opts, config);
+ start_client(client_os_input, opts, config, ClientInfo::New("".into()));
let _ = server_thread.join();
}
diff --git a/zellij-client/src/input_handler.rs b/zellij-client/src/input_handler.rs
index 4e2cff94..70f6e982 100644
--- a/zellij-client/src/input_handler.rs
+++ b/zellij-client/src/input_handler.rs
@@ -7,7 +7,7 @@ use zellij_utils::{
channels::{SenderWithContext, OPENCALLS},
errors::ContextType,
input::{actions::Action, cast_termion_key, config::Config, keybinds::Keybinds},
- ipc::ClientToServerMsg,
+ ipc::{ClientToServerMsg, ExitReason},
};
use termion::input::TermReadEventsAndRaw;
@@ -132,7 +132,9 @@ impl InputHandler {
let mut should_break = false;
match action {
- Action::Quit => {
+ Action::Quit | Action::Detach => {
+ self.os_input
+ .send_to_server(ClientToServerMsg::Action(action));
self.exit();
should_break = true;
}
@@ -167,7 +169,7 @@ impl InputHandler {
/// same as quitting Zellij).
fn exit(&mut self) {
self.send_client_instructions
- .send(ClientInstruction::Exit)
+ .send(ClientInstruction::Exit(ExitReason::Normal))
.unwrap();
}
}
diff --git a/zellij-client/src/lib.rs b/zellij-client/src/lib.rs
index 9deb415e..f3b0f11d 100644
--- a/zellij-client/src/lib.rs
+++ b/zellij-client/src/lib.rs
@@ -17,30 +17,27 @@ use crate::{
use zellij_utils::cli::CliArgs;
use zellij_utils::{
channels::{SenderType, SenderWithContext, SyncChannelWithContext},
- consts::ZELLIJ_IPC_PIPE,
+ consts::{SESSION_NAME, ZELLIJ_IPC_PIPE},
errors::{ClientContext, ContextType, ErrorInstruction},
- input::config::Config,
- input::options::Options,
- ipc::{ClientAttributes, ClientToServerMsg, ServerToClientMsg},
+ input::{actions::Action, config::Config, options::Options},
+ ipc::{ClientAttributes, ClientToServerMsg, ExitReason, ServerToClientMsg},
};
/// Instructions related to the client-side application
#[derive(Debug, Clone)]
pub(crate) enum ClientInstruction {
Error(String),
- Render(Option<String>),
+ Render(String),
UnblockInputThread,
- Exit,
- ServerError(String),
+ Exit(ExitReason),
}
impl From<ServerToClientMsg> for ClientInstruction {
fn from(instruction: ServerToClientMsg) -> Self {
match instruction {
- ServerToClientMsg::Exit => ClientInstruction::Exit,
+ ServerToClientMsg::Exit(e) => ClientInstruction::Exit(e),
ServerToClientMsg::Render(buffer) => ClientInstruction::Render(buffer),
ServerToClientMsg::UnblockInputThread => ClientInstruction::UnblockInputThread,
- ServerToClientMsg::ServerError(backtrace) => ClientInstruction::ServerError(backtrace),
}
}
}
@@ -48,9 +45,8 @@ impl From<ServerToClientMsg> for ClientInstruction {
impl From<&ClientInstruction> for ClientContext {
fn from(client_instruction: &ClientInstruction) -> Self {
match *client_instruction {
- ClientInstruction::Exit => ClientContext::Exit,
+ ClientInstruction::Exit(_) => ClientContext::Exit,
ClientInstruction::Error(_) => ClientContext::Error,
- ClientInstruction::ServerError(_) => ClientContext::ServerError,
ClientInstruction::Render(_) => ClientContext::Render,
ClientInstruction::UnblockInputThread => ClientContext::UnblockInputThread,
}
@@ -80,7 +76,18 @@ fn spawn_server(socket_path: &Path) -> io::Result<()> {
}
}
-pub fn start_client(mut os_input: Box<dyn ClientOsApi>, opts: CliArgs, config: Config) {
+#[derive(Debug, Clone)]
+pub enum ClientInfo {
+ Attach(String, bool),
+ New(String),
+}
+
+pub fn start_client(
+ mut os_input: Box<dyn ClientOsApi>,
+ opts: CliArgs,
+ config: Config,
+ info: ClientInfo,
+) {
let clear_client_terminal_attributes = "\u{1b}[?1l\u{1b}=\u{1b}[r\u{1b}12l\u{1b}[?1000l\u{1b}[?1002l\u{1b}[?1003l\u{1b}[?1005l\u{1b}[?1006l\u{1b}[?12l";
let take_snapshot = "\u{1b}[?1049h";
let bracketed_paste = "\u{1b}[?2004h";
@@ -96,24 +103,46 @@ pub fn start_client(mut os_input: Box<dyn ClientOsApi>, opts: CliArgs, config: C
.unwrap();
std::env::set_var(&"ZELLIJ", "0");
- #[cfg(not(any(feature = "test", test)))]
- spawn_server(&*ZELLIJ_IPC_PIPE).unwrap();
-
- let mut command_is_executing = CommandIsExecuting::new();
-
- let config_options = Options::from_cli(&config.options, opts.option.clone());
+ let config_options = Options::from_cli(&config.options, opts.command.clone());
let full_screen_ws = os_input.get_terminal_size_using_fd(0);
let client_attributes = ClientAttributes {
position_and_size: full_screen_ws,
palette,
};
+
+ #[cfg(not(any(feature = "test", test)))]
+ let first_msg = match info {
+ ClientInfo::Attach(name, force) => {
+ SESSION_NAME.set(name).unwrap();
+ std::env::set_var(&"ZELLIJ_SESSION_NAME", SESSION_NAME.get().unwrap());
+
+ ClientToServerMsg::AttachClient(client_attributes, force)
+ }
+ ClientInfo::New(name) => {
+ SESSION_NAME.set(name).unwrap();
+ std::env::set_var(&"ZELLIJ_SESSION_NAME", SESSION_NAME.get().unwrap());
+
+ spawn_server(&*ZELLIJ_IPC_PIPE).unwrap();
+
+ ClientToServerMsg::NewClient(
+ client_attributes,
+ Box::new(opts),
+ Box::new(config_options),
+ )
+ }
+ };
+ #[cfg(any(feature = "test", test))]
+ let first_msg = {
+ let _ = SESSION_NAME.set("".into());
+ ClientToServerMsg::NewClient(client_attributes, Box::new(opts), Box::new(config_options))
+ };
+
os_input.connect_to_server(&*ZELLIJ_IPC_PIPE);
- os_input.send_to_server(ClientToServerMsg::NewClient(
- client_attributes,
- Box::new(opts),
- Box::new(config_options),
- ));
+ os_input.send_to_server(first_msg);
+
+ let mut command_is_executing = CommandIsExecuting::new();
+
os_input.set_raw_mode(0);
let _ = os_input
.get_stdout_writer()
@@ -170,7 +199,7 @@ pub fn start_client(mut os_input: Box<dyn ClientOsApi>, opts: CliArgs, config: C
let send_client_instructions = send_client_instructions.clone();
move || {
send_client_instructions
- .send(ClientInstruction::Exit)
+ .send(ClientInstruction::Exit(ExitReason::Normal))
.unwrap()
}
}),
@@ -187,11 +216,8 @@ pub fn start_client(mut os_input: Box<dyn ClientOsApi>, opts: CliArgs, config: C
move || loop {
let (instruction, err_ctx) = os_input.recv_from_server();
err_ctx.update_thread_ctx();
- match instruction {
- ServerToClientMsg::Exit | ServerToClientMsg::ServerError(_) => {
- should_break = true;
- }
- _ => {}
+ if let ServerToClientMsg::Exit(_) = instruction {
+ should_break = true;
}
send_client_instructions.send(instruction.into()).unwrap();
if should_break {
@@ -216,6 +242,8 @@ pub fn start_client(mut os_input: Box<dyn ClientOsApi>, opts: CliArgs, config: C
std::process::exit(1);
};
+ let exit_msg: String;
+
loop {
let (client_instruction, mut err_ctx) = receive_client_instructions
.recv()
@@ -223,21 +251,23 @@ pub fn start_client(mut os_input: Box<dyn ClientOsApi>, opts: CliArgs, config: C
err_ctx.add_call(ContextType::Client((&client_instruction).into()));
match client_instruction {
- ClientInstruction::Exit => break,
- ClientInstruction::Error(backtrace) => {
- let _ = os_input.send_to_server(ClientToServerMsg::ClientExit);
- handle_error(backtrace);
+ ClientInstruction::Exit(reason) => {
+ os_input.send_to_server(ClientToServerMsg::ClientExited);
+
+ if let ExitReason::Error(_) = reason {
+ handle_error(format!("{}", reason));
+ }
+ exit_msg = format!("{}", reason);
+ break;
}
- ClientInstruction::ServerError(backtrace) => {
+ ClientInstruction::Error(backtrace) => {
+ let _ = os_input.send_to_server(ClientToServerMsg::Action(Action::Quit));
handle_error(backtrace);
}
ClientInstruction::Render(output) => {
- if output.is_none() {
- break;
- }
let mut stdout = os_input.get_stdout_writer();
stdout
- .write_all(&output.unwrap().as_bytes())
+ .write_all(&output.as_bytes())
.expect("cannot write to stdout");
stdout.flush().expect("could not flush");
}
@@ -247,7 +277,6 @@ pub fn start_client(mut os_input: Box<dyn ClientOsApi>, opts: CliArgs, config: C
}
}
- let _ = os_input.send_to_server(ClientToServerMsg::ClientExit);
router_thread.join().unwrap();
// cleanup();
@@ -256,8 +285,8 @@ pub fn start_client(mut os_input: Box<dyn ClientOsApi>, opts: CliArgs, config: C
let restore_snapshot = "\u{1b}[?1049l";
let goto_start_of_last_line = format!("\u{1b}[{};{}H", full_screen_ws.rows, 1);
let goodbye_message = format!(
- "{}\n{}{}{}Bye from Zellij!\n",
- goto_start_of_last_line, restore_snapshot, reset_style, show_cursor
+ "{}\n{}{}{}{}\n",
+ goto_start_of_last_line, restore_snapshot, reset_style, show_cursor, exit_msg
);
os_input.unset_raw_mode(0);
diff --git a/zellij-server/src/lib.rs b/zellij-server/src/lib.rs
index df0eea65..40fb87f7 100644
--- a/zellij-server/src/lib.rs
+++ b/zellij-server/src/lib.rs
@@ -11,11 +11,11 @@ mod wasm_vm;
use zellij_utils::zellij_tile;
-use std::sync::{Arc, RwLock};
+use std::sync::{Arc, Mutex, RwLock};
use std::thread;
use std::{path::PathBuf, sync::mpsc};
use wasmer::Store;
-use zellij_tile::data::PluginCapabilities;
+use zellij_tile::data::{Event, InputMode, PluginCapabilities};
use crate::{
os_input_output::ServerOsApi,
@@ -30,8 +30,8 @@ use zellij_utils::{
channels::{ChannelWithContext, SenderType, SenderWithContext, SyncChannelWithContext},
cli::CliArgs,
errors::{ContextType, ErrorInstruction, ServerContext},
- input::options::Options,
- ipc::{ClientAttributes, ClientToServerMsg, ServerToClientMsg},
+ input::{get_mode_info, options::Options},
+ ipc::{ClientAttributes, ClientToServerMsg, ExitReason, ServerToClientMsg},
setup::{get_default_data_dir, install::populate_data_dir},
};
@@ -43,14 +43,18 @@ pub(crate) enum ServerInstruction {
UnblockInputThread,
ClientExit,
Error(String),
+ DetachSession,
+ AttachClient(ClientAttributes, bool),
}
impl From<ClientToServerMsg> for ServerInstruction {
fn from(instruction: ClientToServerMsg) -> Self {
match instruction {
- ClientToServerMsg::ClientExit => ServerInstruction::ClientExit,
- ClientToServerMsg::NewClient(pos, opts, options) => {
- ServerInstruction::NewClient(pos, opts, options)
+ ClientToServerMsg::NewClient(attrs, opts, options) => {
+ ServerInstruction::NewClient(attrs, opts, options)
+ }
+ ClientToServerMsg::AttachClient(attrs, force) => {
+ ServerInstruction::AttachClient(attrs, force)
}
_ => unreachable!(),
}
@@ -65,6 +69,8 @@ impl From<&ServerInstruction> for ServerContext {
ServerInstruction::UnblockInputThread => ServerContext::UnblockInputThread,
ServerInstruction::ClientExit => ServerContext::ClientExit,
ServerInstruction::Error(_) => ServerContext::Error,
+ ServerInstruction::DetachSession => ServerContext::DetachSession,
+ ServerInstruction::AttachClient(..) => ServerContext::AttachClient,
}
}
}
@@ -94,6 +100,13 @@ impl Drop for SessionMetaData {
}
}
+#[derive(Copy, Clone, Debug, Eq, PartialEq)]
+pub(crate) enum SessionState {
+ Attached,
+ Detached,
+ Uninitialized,
+}
+
pub fn start_server(os_input: Box<dyn ServerOsApi>, socket_path: PathBuf) {
#[cfg(not(any(feature = "test", test)))]
daemonize::Daemonize::new()
@@ -107,7 +120,8 @@ pub fn start_server(os_input: Box<dyn ServerOsApi>, socket_path: PathBuf) {
let (to_server, server_receiver): SyncChannelWithContext<ServerInstruction> =
mpsc::sync_channel(50);
let to_server = SenderWithContext::new(SenderType::SyncSender(to_server));
- let sessions: Arc<RwLock<Option<SessionMetaData>>> = Arc::new(RwLock::new(None));
+ let session_data: Arc<RwLock<Option<SessionMetaData>>> = Arc::new(RwLock::new(None));
+ let session_state = Arc::new(RwLock::new(SessionState::Uninitialized));
#[cfg(not(any(feature = "test", test)))]
std::panic::set_hook({
@@ -118,17 +132,22 @@ pub fn start_server(os_input: Box<dyn ServerOsApi>, socket_path: PathBuf) {
})
});
+ let thread_handles = Arc::new(Mutex::new(Vec::new()));
+
#[cfg(any(feature = "test", test))]
- thread::Builder::new()
- .name("server_router".to_string())
- .spawn({
- let sessions = sessions.clone();
- let os_input = os_input.clone();
- let to_server = to_server.clone();
+ thread_handles.lock().unwrap().push(
+ thread::Builder::new()
+ .name("server_router".to_string())
+ .spawn({
+ let session_data = session_data.clone();
+ let os_input = os_input.clone();
+ let to_server = to_server.clone();
+ let session_state = session_state.clone();
- move || route_thread_main(sessions, os_input, to_server)
- })
- .unwrap();
+ move || route_thread_main(session_data, session_state, os_input, to_server)
+ })
+ .unwrap(),
+ );
#[cfg(not(any(feature = "test", test)))]
let _ = thread::Builder::new()
.name("server_listener".to_string())
@@ -138,9 +157,11 @@ pub fn start_server(os_input: Box<dyn ServerOsApi>, socket_path: PathBuf) {
};
let os_input = os_input.clone();
- let sessions = sessions.clone();
+ let session_data = session_data.clone();
+ let session_state = session_state.clone();
let to_server = to_server.clone();
let socket_path = socket_path.clone();
+ let thread_handles = thread_handles.clone();
move || {
drop(std::fs::remove_file(&socket_path));
let listener = LocalSocketListener::bind(&*socket_path).unwrap();
@@ -150,18 +171,28 @@ pub fn start_server(os_input: Box<dyn ServerOsApi>, socket_path: PathBuf) {
Ok(stream) => {
let mut os_input = os_input.clone();
os_input.update_receiver(stream);
- let sessions = sessions.clone();
+ let session_data = session_data.clone();
+ let session_state = session_state.clone();
let to_server = to_server.clone();
- thread::Builder::new()
- .name("server_router".to_string())
- .spawn({
- let sessions = sessions.clone();
- let os_input = os_input.clone();
- let to_server = to_server.clone();
+ thread_handles.lock().unwrap().push(
+ thread::Builder::new()
+ .name("server_router".to_string())
+ .spawn({
+ let session_data = session_data.clone();
+ let os_input = os_input.clone();
+ let to_server = to_server.clone();
- move || route_thread_main(sessions, os_input, to_server)
- })
- .unwrap();
+ move || {
+ route_thread_main(
+ session_data,
+ session_state,
+ os_input,
+ to_server,
+ )
+ }
+ })
+ .unwrap(),
+ );
}
Err(err) => {
panic!("err {:?}", err);
@@ -176,15 +207,17 @@ pub fn start_server(os_input: Box<dyn ServerOsApi>, socket_path: PathBuf) {
err_ctx.add_call(ContextType::IPCServer((&instruction).into()));
match instruction {
ServerInstruction::NewClient(client_attributes, opts, config_options) => {
- let session_data = init_session(
+ let session = init_session(
os_input.clone(),
opts,
config_options,
to_server.clone(),
client_attributes,
+ session_state.clone(),
);
- *sessions.write().unwrap() = Some(session_data);
- sessions
+ *session_data.write().unwrap() = Some(session);
+ *session_state.write().unwrap() = SessionState::Attached;
+ session_data
.read()
.unwrap()
.as_ref()
@@ -193,23 +226,69 @@ pub fn start_server(os_input: Box<dyn ServerOsApi>, socket_path: PathBuf) {
.send_to_pty(PtyInstruction::NewTab)
.unwrap();
}
+ ServerInstruction::AttachClient(attrs, _) => {
+ *session_state.write().unwrap() = SessionState::Attached;
+ let rlock = session_data.read().unwrap();
+ let session_data = rlock.as_ref().unwrap();
+ session_data
+ .senders
+ .send_to_screen(ScreenInstruction::TerminalResize(attrs.position_and_size))
+ .unwrap();
+ let mode_info =
+ get_mode_info(InputMode::Normal, attrs.palette, session_data.capabilities);
+ session_data
+ .senders
+ .send_to_screen(ScreenInstruction::ChangeMode(mode_info.clone()))
+ .unwrap();
+ session_data
+ .senders
+ .send_to_plugin(PluginInstruction::Update(
+ None,
+ Event::ModeUpdate(mode_info),
+ ))
+ .unwrap();
+ }
ServerInstruction::UnblockInputThread => {
- os_input.send_to_client(ServerToClientMsg::UnblockInputThread);
+ if *session_state.read().unwrap() == SessionState::Attached {
+ os_input.send_to_client(ServerToClientMsg::UnblockInputThread);
+ }
}
ServerInstruction::ClientExit => {
- *sessions.write().unwrap() = None;
- os_input.send_to_client(ServerToClientMsg::Exit);
+ *session_data.write().unwrap() = None;
+ os_input.send_to_client(ServerToClientMsg::Exit(ExitReason::Normal));
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) => {
- os_input.send_to_client(ServerToClientMsg::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));
+ } else {
+ os_input.send_to_client(ServerToClientMsg::Exit(ExitReason::Normal));
+ break;
+ }
+ }
}
ServerInstruction::Error(backtrace) => {
- os_input.send_to_client(ServerToClientMsg::ServerError(backtrace));
+ if *session_state.read().unwrap() == SessionState::Attached {
+ os_input.send_to_client(ServerToClientMsg::Exit(ExitReason::Error(backtrace)));
+ }
break;
}
}
}
+ thread_handles
+ .lock()
+ .unwrap()
+ .drain(..)
+ .for_each(|h| drop(h.join()));
#[cfg(not(any(feature = "test", test)))]
drop(std::fs::remove_file(&socket_path));
}
@@ -220,6 +299,7 @@ fn init_session(
config_options: Box<Options>,
to_server: SenderWithContext<ServerInstruction>,
client_attributes: ClientAttributes,
+ session_state: Arc<RwLock<SessionState>>,
) -> SessionMetaData {
let (to_screen, screen_receiver): ChannelWithContext<ScreenInstruction> = mpsc::channel();
let to_screen = SenderWithContext::new(SenderType::Sender(to_screen));
@@ -285,7 +365,13 @@ fn init_session(
let max_panes = opts.max_panes;
move || {
- screen_thread_main(screen_bus, max_panes, client_attributes, config_options);
+ screen_thread_main(
+ screen_bus,
+ max_panes,
+ client_attributes,
+ config_options,
+ session_state,
+ );
}
})
.unwrap();
diff --git a/zellij-server/src/os_input_output.rs b/zellij-server/src/os_input_output.rs
index 2ac35b69..4cda73ed 100644
--- a/zellij-server/src/os_input_output.rs
+++ b/zellij-server/src/os_input_output.rs
@@ -18,7 +18,10 @@ use signal_hook::consts::*;
use zellij_tile::data::Palette;
use zellij_utils::{
errors::ErrorContext,
- ipc::{ClientToServerMsg, IpcReceiverWithContext, IpcSenderWithContext, ServerToClientMsg},
+ ipc::{
+ ClientToServerMsg, ExitReason, IpcReceiverWithContext, IpcSenderWithContext,
+ ServerToClientMsg,
+ },
shared::default_palette,
};
@@ -189,6 +192,14 @@ pub trait ServerOsApi: Send + Sync {
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 load_palette(&self) -> Palette;
@@ -245,7 +256,6 @@ impl ServerOsApi for ServerOsInputOutput {
.send(msg);
}
fn add_client_sender(&self) {
- assert!(self.send_instructions_to_client.lock().unwrap().is_none());
let sender = self
.receive_instructions_from_client
.as_ref()
@@ -253,7 +263,27 @@ impl ServerOsApi for ServerOsInputOutput {
.lock()
.unwrap()
.get_sender();
- *self.send_instructions_to_client.lock().unwrap() = Some(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 =
diff --git a/zellij-server/src/route.rs b/zellij-server/src/route.rs
index 0187d953..8dcd98ff 100644
--- a/zellij-server/src/route.rs
+++ b/zellij-server/src/route.rs
@@ -4,7 +4,7 @@ use zellij_utils::zellij_tile::data::Event;
use crate::{
os_input_output::ServerOsApi, pty::PtyInstruction, screen::ScreenInstruction,
- wasm_vm::PluginInstruction, ServerInstruction, SessionMetaData,
+ wasm_vm::PluginInstruction, ServerInstruction, SessionMetaData, SessionState,
};
use zellij_utils::{
channels::SenderWithContext,
@@ -12,10 +12,16 @@ use zellij_utils::{
actions::{Action, Direction},
get_mode_info,
},
- ipc::ClientToServerMsg,
+ ipc::{ClientToServerMsg, ExitReason, ServerToClientMsg},
};
-fn route_action(action: Action, session: &SessionMetaData, os_input: &dyn ServerOsApi) {
+fn route_action(
+ action: Action,
+ session: &SessionMetaData,
+ os_input: &dyn ServerOsApi,
+ to_server: &SenderWithContext<ServerInstruction>,
+) -> bool {
+ let mut should_break = false;
match action {
Action::Write(val) => {
session
@@ -182,28 +188,36 @@ fn route_action(action: Action, session: &SessionMetaData, os_input: &dyn Server
.send_to_screen(ScreenInstruction::UpdateTabName(c))
.unwrap();
}
+ Action::Quit => {
+ to_server.send(ServerInstruction::ClientExit).unwrap();
+ should_break = true;
+ }
+ Action::Detach => {
+ to_server.send(ServerInstruction::DetachSession).unwrap();
+ should_break = true;
+ }
Action::NoOp => {}
- Action::Quit => panic!("Received unexpected action"),
}
+ should_break
}
pub(crate) fn route_thread_main(
- sessions: Arc<RwLock<Option<SessionMetaData>>>,
+ session_data: Arc<RwLock<Option<SessionMetaData>>>,
+ session_state: Arc<RwLock<SessionState>>,
os_input: Box<dyn ServerOsApi>,
to_server: SenderWithContext<ServerInstruction>,
) {
loop {
let (instruction, err_ctx) = os_input.recv_from_client();
err_ctx.update_thread_ctx();
- let rlocked_sessions = sessions.read().unwrap();
+ let rlocked_sessions = session_data.read().unwrap();
+
match instruction {
- ClientToServerMsg::ClientExit => {
- to_server.send(instruction.into()).unwrap();
- break;
- }
ClientToServerMsg::Action(action) => {
if let Some(rlocked_sessions) = rlocked_sessions.as_ref() {
- route_action(action, rlocked_sessions, &*os_input);
+ if route_action(action, rlocked_sessions, &*os_input, &to_server) {
+ break;
+ }
}
}
ClientToServerMsg::TerminalResize(new_size) => {
@@ -215,9 +229,24 @@ pub(crate) fn route_thread_main(
.unwrap();
}
ClientToServerMsg::NewClient(..) => {
- os_input.add_client_sender();
- to_server.send(instruction.into()).unwrap();
+ 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::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,
}
}
}
diff --git a/zellij-server/src/screen.rs b/zellij-server/src/screen.rs
index 830c4251..068037ee 100644
--- a/zellij-server/src/screen.rs
+++ b/zellij-server/src/screen.rs
@@ -3,6 +3,7 @@
use std::collections::BTreeMap;
use std::os::unix::io::RawFd;
use std::str;
+use std::sync::{Arc, RwLock};
use zellij_utils::zellij_tile;
@@ -13,7 +14,7 @@ use crate::{
thread_bus::Bus,
ui::layout::Layout,
wasm_vm::PluginInstruction,
- ServerInstruction,
+ ServerInstruction, SessionState,
};
use zellij_tile::data::{Event, InputMode, ModeInfo, Palette, PluginCapabilities, TabInfo};
use zellij_utils::{
@@ -137,6 +138,7 @@ pub(crate) struct Screen {
mode_info: ModeInfo,
input_mode: InputMode,
colors: Palette,
+ session_state: Arc<RwLock<SessionState>>,
}
impl Screen {
@@ -147,6 +149,7 @@ impl Screen {
max_panes: Option<usize>,
mode_info: ModeInfo,
input_mode: InputMode,
+ session_state: Arc<RwLock<SessionState>>,
) -> Self {
Screen {
bus,
@@ -157,6 +160,7 @@ impl Screen {
tabs: BTreeMap::new(),
mode_info,
input_mode,
+ session_state,
}
}
@@ -177,6 +181,7 @@ impl Screen {
self.mode_info.clone(),
self.input_mode,
self.colors,
+ self.session_state.clone(),
);
self.active_tab_index = Some(tab_index);
self.tabs.insert(tab_index, tab);
@@ -261,10 +266,12 @@ impl Screen {
.unwrap();
if self.tabs.is_empty() {
self.active_tab_index = None;
- self.bus
- .senders
- .send_to_server(ServerInstruction::Render(None))
- .unwrap();
+ if *self.session_state.read().unwrap() == SessionState::Attached {
+ self.bus
+ .senders
+ .send_to_server(ServerInstruction::Render(None))
+ .unwrap();
+ }
} else {
for t in self.tabs.values_mut() {
if t.position > active_tab.position {
@@ -286,6 +293,9 @@ 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();
@@ -333,6 +343,7 @@ impl Screen {
self.mode_info.clone(),
self.input_mode,
self.colors,
+ self.session_state.clone(),
);
tab.apply_layout(layout, new_pids);
self.active_tab_index = Some(tab_index);
@@ -375,6 +386,7 @@ impl Screen {
self.update_tabs();
}
pub fn change_mode(&mut self, mode_info: ModeInfo) {
+ self.colors = mode_info.palette;
self.mode_info = mode_info;
for tab in self.tabs.values_mut() {
tab.mode_info = self.mode_info.clone();
@@ -390,6 +402,7 @@ pub(crate) fn screen_thread_main(
max_panes: Option<usize>,
client_attributes: ClientAttributes,
config_options: Box<Options>,
+ session_state: Arc<RwLock<SessionState>>,
) {
let capabilities = config_options.simplified_ui;
@@ -405,6 +418,7 @@ pub(crate) fn screen_thread_main(
..ModeInfo::default()
},
InputMode::Normal,
+ session_state,
);
loop {
let (event, mut err_ctx) = screen
diff --git a/zellij-server/src/tab.rs b/zellij-server/src/tab.rs
index e6953bfa..4a0db153 100644
--- a/zellij-server/src/tab.rs
+++ b/zellij-server/src/tab.rs
@@ -10,11 +10,11 @@ use crate::{
thread_bus::ThreadSenders,
ui::{boundaries::Boundaries, layout::Layout, pane_resizer::PaneResizer},
wasm_vm::PluginInstruction,
- ServerInstruction,
+ ServerInstruction, SessionState,
};
use serde::{Deserialize, Serialize};
use std::os::unix::io::RawFd;
-use std::sync::mpsc::channel;
+use std::sync::{mpsc::channel, Arc, RwLock};
use std::time::Instant;
use std::{
cmp::Reverse,
@@ -74,6 +74,7 @@ pub(crate) struct Tab {
pub senders: ThreadSenders,
synchronize_is_active: bool,
should_clear_display_before_rendering: bool,
+ session_state: Arc<RwLock<SessionState>>,
pub mode_info: ModeInfo,
pub input_mode: InputMode,
pub colors: Palette,
@@ -242,6 +243,7 @@ impl Tab {
mode_info: ModeInfo,
input_mode: InputMode,
colors: Palette,
+ session_state: Arc<RwLock<SessionState>>,
) -> Self {
let panes = if let Some(PaneId::Terminal(pid)) = pane_id {
let new_terminal = TerminalPane::new(pid, *full_screen_ws, colors);
@@ -273,6 +275,7 @@ impl Tab {
mode_info,
input_mode,
colors,
+ session_state,
}
}
@@ -722,9 +725,12 @@ impl Tab {
self.panes.iter().any(|(_, p)| p.contains_widechar())
}
pub fn render(&mut self) {
- if self.active_terminal.is_none() {
+ if self.active_terminal.is_none()
+ || *self.session_state.read().unwrap() != SessionState::Attached
+ {
// 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
return;
}
// if any pane contain widechar, all pane in the same row will messup. We should render them every time
diff --git a/zellij-tile/src/data.rs b/zellij-tile/src/data.rs
index c1b9c30e..cb4b2aa0 100644
--- a/zellij-tile/src/data.rs
+++ b/zellij-tile/src/data.rs
@@ -59,6 +59,9 @@ pub enum InputMode {
Scroll,
#[serde(alias = "renametab")]
RenameTab,
+ /// `Session` mode allows detaching sessions
+ #[serde(alias = "session")]
+ Session,
}
impl Default for InputMode {
diff --git a/zellij-utils/Cargo.toml b/zellij-utils/Cargo.toml
index b1f48d10..2fcccd7d 100644
--- a/zellij-utils/Cargo.toml
+++ b/zellij-utils/Cargo.toml
@@ -16,8 +16,8 @@ directories-next = "2.0"
interprocess = "1.1.1"
lazy_static = "1.4.0"
libc = "0.2"
-names = "0.11.0"
nix = "0.19.1"
+once_cell = "1.7.2"
serde = { version = "1.0", features = ["derive"] }
serde_yaml = "0.8"
signal-hook = "0.3"
diff --git a/zellij-utils/src/cli.rs b/zellij-utils/src/cli.rs
index 4b2fdbbf..bf7851c9 100644
--- a/zellij-utils/src/cli.rs
+++ b/zellij-utils/src/cli.rs
@@ -17,9 +17,13 @@ pub struct CliArgs {
pub data_dir: Option<PathBuf>,
/// Run server listening at the specified socket path
- #[structopt(long, parse(from_os_str))]
+ #[structopt(long, parse(from_os_str), hidden = true)]
pub server: Option<PathBuf>,
+ /// Specify name of a new session
+ #[structopt(long, short)]
+ pub session: Option<String>,
+
/// Name of a layout file in the layout directory
#[structopt(short, long, parse(from_os_str))]
pub layout: Option<PathBuf>,
@@ -37,14 +41,14 @@ pub struct CliArgs {
pub config_dir: Option<PathBuf>,
#[structopt(subcommand)]
- pub option: Option<ConfigCli>,
+ pub command: Option<Command>,
#[structopt(short, long)]
pub debug: bool,
}
#[derive(Debug, StructOpt, Clone, Serialize, Deserialize)]
-pub enum ConfigCli {
+pub enum Command {
/// Change the behaviour of zellij
#[structopt(name = "options")]
Options(Options),
@@ -52,4 +56,27 @@ pub enum ConfigCli {
/// Setup zellij and check its configuration
#[structopt(name = "setup")]
Setup(Setup),
+
+ /// Explore existing zellij sessions
+ #[structopt(flatten)]
+ Sessions(Sessions),
+}
+
+#[derive(Debug, StructOpt, Clone, Serialize, Deserialize)]
+pub enum Sessions {
+ /// List active sessions
+ #[structopt(alias = "ls")]
+ ListSessions,
+
+ /// Attach to session
+ #[structopt(alias = "a")]
+ Attach {
+ /// Name of the session to attach to.
+ session_name: String,
+
+ /// Force attach- session will detach from the other
+ /// zellij client (if any) and attach to this.
+ #[structopt(long, short)]
+ force: bool,
+ },
}
diff --git a/zellij-utils/src/consts.rs b/zellij-utils/src/consts.rs
index 799e2913..f22dd489 100644
--- a/zellij-utils/src/consts.rs
+++ b/zellij-utils/src/consts.rs
@@ -4,6 +4,7 @@ use crate::shared::set_permissions;
use directories_next::ProjectDirs;
use lazy_static::lazy_static;
use nix::unistd::Uid;
+use once_cell::sync::OnceCell;
use std::path::PathBuf;
use std::{env, fs};
@@ -24,10 +25,10 @@ const fn system_default_data_dir() -> &'static str {
lazy_static! {
static ref UID: Uid = Uid::current();
- pub static ref SESSION_NAME: String = names::Generator::default().next().unwrap();
+ pub static ref SESSION_NAME: OnceCell<String> = OnceCell::new();
pub static ref ZELLIJ_PROJ_DIR: ProjectDirs =
ProjectDirs::from("org", "Zellij Contributors", "Zellij").unwrap();
- pub static ref ZELLIJ_IPC_PIPE: PathBuf = {
+ pub static ref ZELLIJ_SOCK_DIR: PathBuf = {
let mut ipc_dir = env::var("ZELLIJ_SOCKET_DIR").map_or_else(
|_| {
ZELLIJ_PROJ_DIR
@@ -37,11 +38,15 @@ lazy_static! {
PathBuf::from,
);
ipc_dir.push(VERSION);
- fs::create_dir_all(&ipc_dir).unwrap();
- set_permissions(&ipc_dir).unwrap();
- ipc_dir.push(&*SESSION_NAME);
ipc_dir
};
+ pub static ref ZELLIJ_IPC_PIPE: PathBuf = {
+ let mut sock_dir = ZELLIJ_SOCK_DIR.clone();
+ fs::create_dir_all(&sock_dir).unwrap();
+ set_permissions(&sock_dir).unwrap();
+ sock_dir.push(SESSION_NAME.get().unwrap());
+ sock_dir
+ };
pub static ref ZELLIJ_TMP_DIR: PathBuf =
PathBuf::from("/tmp/zellij-".to_string() + &format!("{}", *UID));
pub static ref ZELLIJ_TMP_LOG_DIR: PathBuf = ZELLIJ_TMP_DIR.join("zellij-log");
diff --git a/zellij-utils/src/errors.rs b/zellij-utils/src/errors.rs
index 94a894c4..b47ddb9a 100644
--- a/zellij-utils/src/errors.rs
+++ b/zellij-utils/src/errors.rs
@@ -260,4 +260,6 @@ pub enum ServerContext {
UnblockInputThread,
ClientExit,
Error,
+ DetachSession,
+ AttachClient,
}
diff --git a/zellij-utils/src/input/actions.rs b/zellij-utils/src/input/actions.rs
index 5ad771da..4e0ce0cb 100644
--- a/zellij-utils/src/input/actions.rs
+++ b/zellij-utils/src/input/actions.rs
@@ -65,4 +65,6 @@ pub enum Action {
CloseTab,
GoToTab(u32),
TabNameInput(Vec<u8>),
+ /// Detach session and exit
+ Detach,
}
diff --git a/zellij-utils/src/input/config.rs b/zellij-utils/src/input/config.rs
index a0341689..c4cf0cd7 100644
--- a/zellij-utils/src/input/config.rs
+++ b/zellij-utils/src/input/config.rs
@@ -7,7 +7,7 @@ use std::path::{Path, PathBuf};
use super::keybinds::{Keybinds, KeybindsFromYaml};
use super::options::Options;
-use crate::cli::{CliArgs, ConfigCli};
+use crate::cli::{CliArgs, Command};
use crate::setup;
use serde::{Deserialize, Serialize};
@@ -60,7 +60,7 @@ impl TryFrom<&CliArgs> for Config {
return Config::new(&path);
}
- if let Some(ConfigCli::Setup(setup)) = opts.option.clone() {
+ if let Some(Command::Setup(ref setup)) = opts.command {
if setup.clean {
return Config::from_default_assets();
}
@@ -179,7 +179,7 @@ mod config_test {
fn try_from_cli_args_with_option_clean() {
use crate::setup::Setup;
let mut opts = CliArgs::default();
- opts.option = Some(ConfigCli::Setup(Setup {
+ opts.command = Some(Command::Setup(Setup {
clean: true,
..Setup::default()
}));
diff --git a/zellij-utils/src/input/mod.rs b/zellij-utils/src/input/mod.rs
index 2747205b..068e22bc 100644
--- a/zellij-utils/src/input/mod.rs
+++ b/zellij-utils/src/input/mod.rs
@@ -45,6 +45,9 @@ pub fn get_mode_info(
InputMode::RenameTab => {
keybinds.push(("Enter".to_string(), "when done".to_string()));
}
+ InputMode::Session => {
+ keybinds.push(("d".to_string(), "Detach".to_string()));
+ }
}
ModeInfo {
mode,
diff --git a/zellij-utils/src/input/options.rs b/zellij-utils/src/input/options.rs
index 33625b65..f9724c20 100644
--- a/zellij-utils/src/input/options.rs
+++ b/zellij-utils/src/input/options.rs
@@ -1,5 +1,5 @@
//! Handles cli and configuration options
-use crate::cli::ConfigCli;
+use crate::cli::Command;
use serde::{Deserialize, Serialize};
use structopt::StructOpt;
@@ -35,8 +35,8 @@ impl Options {
Options { simplified_ui }
}
- pub fn from_cli(&self, other: Option<ConfigCli>) -> Options {
- if let Some(ConfigCli::Options(options)) = other {
+ pub fn from_cli(&self, other: Option<Command>) -> Options {
+ if let Some(Command::Options(options)) = other {
Options::merge(&self, options)
} else {
self.to_owned()
diff --git a/zellij-utils/src/ipc.rs b/zellij-utils/src/ipc.rs
index 725bf16b..a160b782 100644
--- a/zellij-utils/src/ipc.rs
+++ b/zellij-utils/src/ipc.rs
@@ -9,6 +9,7 @@ use crate::{
use interprocess::local_socket::LocalSocketStream;
use nix::unistd::dup;
use serde::{Deserialize, Serialize};
+use std::fmt::{Display, Error, Formatter};
use std::io::{self, Write};
use std::marker::PhantomData;
use std::os::unix::io::{AsRawFd, FromRawFd};
@@ -34,7 +35,7 @@ pub enum ClientType {
Writer,
}
-#[derive(Serialize, Deserialize, Debug, Clone)]
+#[derive(Serialize, Deserialize, Debug, Clone, Copy)]
pub struct ClientAttributes {
pub position_and_size: PositionAndSize,
pub palette: Palette,
@@ -54,10 +55,11 @@ pub enum ClientToServerMsg {
DetachSession(SessionId),
// Disconnect from the session we're connected to
DisconnectFromSession,*/
- ClientExit,
TerminalResize(PositionAndSize),
NewClient(ClientAttributes, Box<CliArgs>, Box<Options>),
+ AttachClient(ClientAttributes, bool),
Action(Action),
+ ClientExited,
}
// Types of messages sent from the server to the client
@@ -67,10 +69,34 @@ pub enum ServerToClientMsg {
SessionInfo(Session),
// A list of sessions
SessionList(HashSet<Session>),*/
- Render(Option<String>),
+ Render(String),
UnblockInputThread,
- Exit,
- ServerError(String),
+ Exit(ExitReason),
+}
+
+#[derive(Serialize, Deserialize, Debug, Clone)]
+pub enum ExitReason {
+ Normal,
+ ForceDetached,
+ CannotAttach,
+ Error(String),
+}
+
+impl Display for ExitReason {
+ fn fmt(&self, f: &mut Formatter) -> Result<(), Error> {
+ match self {
+ Self::Normal => write!(f, "Bye from Zellij!"),
+ Self::ForceDetached => write!(
+ f,
+ "Session was detach from this client (possibly because another client connected)"
+ ),
+ Self::CannotAttach => write!(
+ f,
+ "Session attached to another client. Use --force flag to force connect."
+ ),
+ Self::Error(e) => write!(f, "Error occured in server:\n{}", e),
+ }
+ }
}
/// Sends messages on a stream socket, along with an [`ErrorContext`].