zellij/zellij-server/src/lib.rs
Brooks Rady 76a5bc8a05
feat(ui): overhauled resize and layout systems
* refactor(panes): move to parametric pane sizes

* Fixed the simpler errors by casting to usize

* The least I can do is pass the formatting check...

* Move to stable toolchain

* Well, it compiles?

* And now it doesn't! ;)

* Baseline functionality with the new Dimension type

* Working POC for percent-based resizing

* REVERT THIS COMMIT – DELETES TESTS

* Perfected the discrete resize algorithm

* Fixed fixed-size panes

* Basic bidirectional resize

* feat(resize): finalised parametric resize algorithm

* Reduce the logging level a bit

* Fixed nested layouts using percents

* Bug squishing for implicit sizing

* Here is a funky (read: rubbish) rounding approach

* And now it's gone again!

* Improve discretisation algorithm to fix rounding errors

* Fix the last layout bug (maybe?)

* Mixed explicit and implied percents work now

* Let's pretend that didn't happen...

* Make things a bit less crashy

* Crash slightly more for now (to find bugs)

* Manaually splitting of panes works now

* Start moving to percent-based resizes

* Everything but fullscreen seems to be working

* Fix compilatation errors

* Culled a massive amount of border code

* Why not pause to please rustfmt?

* Turns out I was still missing a few tests...

* Bringing back even more tests!

* Fix tests and pane boarders

* Fix the resize system without gaps

* Fix content offset

* Fixed a bug with pane closing

* Add a hack to fix setting of the viewport

* Fix toggling between shared borders and frames

* fix(tests): make e2e properly use PaneGeom

* style(fmt): make rustfmt happy

* Revert unintentional rounding of borders

* Purge some old borderless stuff

* Fix busted tab-bar shrinking

* Update E2E tests

* Finish implementing fullscreen!

* Don't crash anymore?

* Fix (almost) all tests

* Fix a lack of tab-stops

* All tests passing

* I really can't be bothered to debug a CI issue

* Tie up loose ends

* Knock out some lingering FIXMEs

* Continue to clean things up

* Change some naming and address FIXMEs

* Cull more code + FIXMEs

* Refactor of the resize system + polish

* Only draw frames when absolutely necessary

* Fix the tab-bar crash

* Fix rendering of boarders on reattach

* Fix resizing at small pane sizes

* Deduplicate code in the layout system

* Update tab-bar WASM

* Fixed the pinching of panes during resize

* Unexpose needlessly public type

* Add back a lost test

* Re-add tab tests and get them to compile

* All tabs need layouts

* Start fixing tests + bug in main

* Stabilize the resize algorithm rounding

* All tests from main are now passing

* Cull more dead code
2021-08-28 17:46:24 +01:00

418 lines
15 KiB
Rust

pub mod os_input_output;
pub mod panes;
pub mod tab;
mod logging_pipe;
mod pty;
mod route;
mod screen;
mod thread_bus;
mod ui;
mod wasm_vm;
use log::info;
use std::{
path::PathBuf,
sync::{Arc, Mutex, RwLock},
thread,
};
use zellij_utils::zellij_tile;
use wasmer::Store;
use zellij_tile::data::{Event, Palette, PluginCapabilities};
use crate::{
os_input_output::ServerOsApi,
pty::{pty_thread_main, Pty, PtyInstruction},
screen::{screen_thread_main, ScreenInstruction},
thread_bus::{Bus, ThreadSenders},
wasm_vm::{wasm_thread_main, PluginInstruction},
};
use route::route_thread_main;
use zellij_utils::{
channels::{self, ChannelWithContext, SenderWithContext},
cli::CliArgs,
errors::{ContextType, ErrorInstruction, ServerContext},
input::{
command::{RunCommand, TerminalAction},
get_mode_info,
layout::LayoutFromYaml,
options::Options,
},
ipc::{ClientAttributes, ClientToServerMsg, ExitReason, ServerToClientMsg},
setup::get_default_data_dir,
};
/// Instructions related to server-side application
#[derive(Debug, Clone)]
pub(crate) enum ServerInstruction {
NewClient(ClientAttributes, Box<CliArgs>, Box<Options>, LayoutFromYaml),
Render(Option<String>),
UnblockInputThread,
ClientExit,
Error(String),
DetachSession,
AttachClient(ClientAttributes, bool, Options),
}
impl From<ClientToServerMsg> for ServerInstruction {
fn from(instruction: ClientToServerMsg) -> Self {
match instruction {
ClientToServerMsg::NewClient(attrs, opts, options, layout) => {
ServerInstruction::NewClient(attrs, opts, options, layout)
}
ClientToServerMsg::AttachClient(attrs, force, options) => {
ServerInstruction::AttachClient(attrs, force, options)
}
_ => unreachable!(),
}
}
}
impl From<&ServerInstruction> for ServerContext {
fn from(server_instruction: &ServerInstruction) -> Self {
match *server_instruction {
ServerInstruction::NewClient(..) => ServerContext::NewClient,
ServerInstruction::Render(_) => ServerContext::Render,
ServerInstruction::UnblockInputThread => ServerContext::UnblockInputThread,
ServerInstruction::ClientExit => ServerContext::ClientExit,
ServerInstruction::Error(_) => ServerContext::Error,
ServerInstruction::DetachSession => ServerContext::DetachSession,
ServerInstruction::AttachClient(..) => ServerContext::AttachClient,
}
}
}
impl ErrorInstruction for ServerInstruction {
fn error(err: String) -> Self {
ServerInstruction::Error(err)
}
}
pub(crate) struct SessionMetaData {
pub senders: ThreadSenders,
pub capabilities: PluginCapabilities,
pub palette: Palette,
pub default_shell: Option<TerminalAction>,
screen_thread: Option<thread::JoinHandle<()>>,
pty_thread: Option<thread::JoinHandle<()>>,
wasm_thread: Option<thread::JoinHandle<()>>,
}
impl Drop for SessionMetaData {
fn drop(&mut self) {
let _ = self.senders.send_to_pty(PtyInstruction::Exit);
let _ = self.senders.send_to_screen(ScreenInstruction::Exit);
let _ = self.senders.send_to_plugin(PluginInstruction::Exit);
let _ = self.screen_thread.take().unwrap().join();
let _ = self.pty_thread.take().unwrap().join();
let _ = self.wasm_thread.take().unwrap().join();
}
}
#[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) {
info!("Starting Zellij server!");
daemonize::Daemonize::new()
.working_directory(std::env::current_dir().unwrap())
.umask(0o077)
.start()
.expect("could not daemonize the server process");
std::env::set_var(&"ZELLIJ", "0");
let (to_server, server_receiver): ChannelWithContext<ServerInstruction> = channels::bounded(50);
let to_server = SenderWithContext::new(to_server);
let session_data: Arc<RwLock<Option<SessionMetaData>>> = Arc::new(RwLock::new(None));
let session_state = Arc::new(RwLock::new(SessionState::Uninitialized));
std::panic::set_hook({
use zellij_utils::errors::handle_panic;
let to_server = to_server.clone();
Box::new(move |info| {
handle_panic(info, &to_server);
})
});
let thread_handles = Arc::new(Mutex::new(Vec::new()));
let _ = thread::Builder::new()
.name("server_listener".to_string())
.spawn({
use zellij_utils::{
interprocess::local_socket::LocalSocketListener, shared::set_permissions,
};
let os_input = os_input.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();
set_permissions(&socket_path).unwrap();
for stream in listener.incoming() {
match stream {
Ok(stream) => {
let mut os_input = os_input.clone();
os_input.update_receiver(stream);
let session_data = session_data.clone();
let session_state = session_state.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(
session_data,
session_state,
os_input,
to_server,
)
}
})
.unwrap(),
);
}
Err(err) => {
panic!("err {:?}", err);
}
}
}
}
});
loop {
let (instruction, mut err_ctx) = server_receiver.recv().unwrap();
err_ctx.add_call(ContextType::IPCServer((&instruction).into()));
match instruction {
ServerInstruction::NewClient(client_attributes, opts, config_options, layout) => {
let session = init_session(
os_input.clone(),
opts,
config_options.clone(),
to_server.clone(),
client_attributes,
session_state.clone(),
layout.clone(),
);
*session_data.write().unwrap() = Some(session);
*session_state.write().unwrap() = SessionState::Attached;
let default_shell = config_options.default_shell.map(|shell| {
TerminalAction::RunCommand(RunCommand {
command: shell,
..Default::default()
})
});
let spawn_tabs = |tab_layout| {
session_data
.read()
.unwrap()
.as_ref()
.unwrap()
.senders
.send_to_pty(PtyInstruction::NewTab(default_shell.clone(), tab_layout))
.unwrap()
};
if !&layout.tabs.is_empty() {
for tab_layout in layout.tabs {
spawn_tabs(Some(tab_layout.clone()));
}
} else {
spawn_tabs(None);
}
}
ServerInstruction::AttachClient(attrs, _, options) => {
*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.size))
.unwrap();
let default_mode = options.default_mode.unwrap_or_default();
let mode_info =
get_mode_info(default_mode, 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 => {
if *session_state.read().unwrap() == SessionState::Attached {
os_input.send_to_client(ServerToClientMsg::UnblockInputThread);
}
}
ServerInstruction::ClientExit => {
*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) => {
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) => {
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()));
drop(std::fs::remove_file(&socket_path));
}
fn init_session(
os_input: Box<dyn ServerOsApi>,
opts: Box<CliArgs>,
config_options: Box<Options>,
to_server: SenderWithContext<ServerInstruction>,
client_attributes: ClientAttributes,
session_state: Arc<RwLock<SessionState>>,
layout: LayoutFromYaml,
) -> SessionMetaData {
let (to_screen, screen_receiver): ChannelWithContext<ScreenInstruction> = channels::unbounded();
let to_screen = SenderWithContext::new(to_screen);
let (to_screen_bounded, bounded_screen_receiver): ChannelWithContext<ScreenInstruction> =
channels::bounded(50);
let to_screen_bounded = SenderWithContext::new(to_screen_bounded);
let (to_plugin, plugin_receiver): ChannelWithContext<PluginInstruction> = channels::unbounded();
let to_plugin = SenderWithContext::new(to_plugin);
let (to_pty, pty_receiver): ChannelWithContext<PtyInstruction> = channels::unbounded();
let to_pty = SenderWithContext::new(to_pty);
// Determine and initialize the data directory
let data_dir = opts.data_dir.unwrap_or_else(get_default_data_dir);
let capabilities = PluginCapabilities {
arrow_fonts: config_options.simplified_ui,
};
let default_shell = config_options.default_shell.clone().map(|command| {
TerminalAction::RunCommand(RunCommand {
command,
..Default::default()
})
});
let pty_thread = thread::Builder::new()
.name("pty".to_string())
.spawn({
let pty = Pty::new(
Bus::new(
vec![pty_receiver],
Some(&to_screen_bounded),
None,
Some(&to_plugin),
Some(&to_server),
Some(os_input.clone()),
),
opts.debug,
);
move || pty_thread_main(pty, layout)
})
.unwrap();
let screen_thread = thread::Builder::new()
.name("screen".to_string())
.spawn({
let screen_bus = Bus::new(
vec![screen_receiver, bounded_screen_receiver],
None,
Some(&to_pty),
Some(&to_plugin),
Some(&to_server),
Some(os_input.clone()),
);
let max_panes = opts.max_panes;
move || {
screen_thread_main(
screen_bus,
max_panes,
client_attributes,
config_options,
session_state,
);
}
})
.unwrap();
let wasm_thread = thread::Builder::new()
.name("wasm".to_string())
.spawn({
let plugin_bus = Bus::new(
vec![plugin_receiver],
Some(&to_screen),
Some(&to_pty),
Some(&to_plugin),
None,
None,
);
let store = Store::default();
move || wasm_thread_main(plugin_bus, store, data_dir)
})
.unwrap();
SessionMetaData {
senders: ThreadSenders {
to_screen: Some(to_screen),
to_pty: Some(to_pty),
to_plugin: Some(to_plugin),
to_server: None,
should_silently_fail: false,
},
capabilities,
default_shell,
palette: client_attributes.palette,
screen_thread: Some(screen_thread),
pty_thread: Some(pty_thread),
wasm_thread: Some(wasm_thread),
}
}