Merge pull request #531 from zellij-org/detach-sessions

Feature: Detachable/Persistent sessions
This commit is contained in:
Kunal Mohan 2021-05-25 17:30:40 +05:30 committed by GitHub
commit 2bca7e007a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 593 additions and 182 deletions

3
Cargo.lock generated
View File

@ -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",

View File

@ -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" }

View File

@ -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',]

View File

@ -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,
@ -254,15 +221,6 @@ fn key_indicators(
return line_part;
}
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);
@ -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,

View File

@ -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),
);
}
}
}

109
src/sessions.rs Normal file
View File

@ -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
}
}
}
}

View File

@ -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()

View File

@ -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();
}

View File

@ -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();
}
}

View File

@ -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);

View File

@ -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) {
})
});
#[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();
let thread_handles = Arc::new(Mutex::new(Vec::new()));
move || route_thread_main(sessions, os_input, to_server)
})
.unwrap();
#[cfg(any(feature = "test", test))]
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(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();

View File

@ -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 =

View File

@ -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,
}
}
}

View File

@ -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

View File

@ -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

View File

@ -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 {

View File

@ -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"

View File

@ -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,
},
}

View File

@ -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");

View File

@ -260,4 +260,6 @@ pub enum ServerContext {
UnblockInputThread,
ClientExit,
Error,
DetachSession,
AttachClient,
}

View File

@ -65,4 +65,6 @@ pub enum Action {
CloseTab,
GoToTab(u32),
TabNameInput(Vec<u8>),
/// Detach session and exit
Detach,
}

View File

@ -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()
}));

View File

@ -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,

View File

@ -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()

View File

@ -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`].