diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index a54585ae..9b2ee756 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -37,5 +37,5 @@ The Boundaries refer to those lines that are drawn between terminal panes. A few * The Rect trait is here so that different panes can implement it, giving boundaries a generic way to calculate the size of the pane and draw boundaries around it. * Here we use the [unicode box drawing characters](https://en.wikipedia.org/wiki/Box-drawing_character) in order to draw the borders. There's some logic here about combining them together for all possible combinations of pane locations. -## PTY Bus (src/pty_bus.rs) +## PTY Bus (src/pty.rs) The PtyBus keeps track of several asynchronous streams that read from pty sockets (eg. /dev/pts/999), parse those bytes into ANSI/VT events and send them on to the Screen so that they can be received in the relevant TerminalPane. diff --git a/src/client/mod.rs b/src/client/mod.rs index 8a5834f0..ba27bc3e 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -16,7 +16,7 @@ use crate::common::{ input::config::Config, input::handler::input_loop, os_input_output::ClientOsApi, - SenderType, SenderWithContext, SyncChannelWithContext, + thread_bus::{SenderType, SenderWithContext, SyncChannelWithContext}, }; use crate::server::ServerInstruction; diff --git a/src/client/panes/plugin_pane.rs b/src/client/panes/plugin_pane.rs index c3b633ec..cd22e934 100644 --- a/src/client/panes/plugin_pane.rs +++ b/src/client/panes/plugin_pane.rs @@ -1,4 +1,6 @@ -use crate::{common::SenderWithContext, pty_bus::VteBytes, tab::Pane, wasm_vm::PluginInstruction}; +use crate::{ + common::thread_bus::SenderWithContext, pty::VteBytes, tab::Pane, wasm_vm::PluginInstruction, +}; use crate::panes::{PaneId, PositionAndSize}; diff --git a/src/client/panes/terminal_pane.rs b/src/client/panes/terminal_pane.rs index f58c3a27..e0ce8ffe 100644 --- a/src/client/panes/terminal_pane.rs +++ b/src/client/panes/terminal_pane.rs @@ -9,7 +9,7 @@ use crate::panes::grid::Grid; use crate::panes::terminal_character::{ CharacterStyles, TerminalCharacter, EMPTY_TERMINAL_CHARACTER, }; -use crate::pty_bus::VteBytes; +use crate::pty::VteBytes; #[derive(PartialEq, Eq, Ord, PartialOrd, Hash, Clone, Copy, Debug, Serialize, Deserialize)] pub enum PaneId { diff --git a/src/client/tab.rs b/src/client/tab.rs index c4dc3474..bbd68a4d 100644 --- a/src/client/tab.rs +++ b/src/client/tab.rs @@ -2,11 +2,12 @@ //! as well as how they should be resized use crate::client::pane_resizer::PaneResizer; -use crate::common::{input::handler::parse_keys, SenderWithContext}; +use crate::common::input::handler::parse_keys; +use crate::common::thread_bus::ThreadSenders; use crate::layout::Layout; use crate::os_input_output::ServerOsApi; use crate::panes::{PaneId, PositionAndSize, TerminalPane}; -use crate::pty_bus::{PtyInstruction, VteBytes}; +use crate::pty::{PtyInstruction, VteBytes}; use crate::server::ServerInstruction; use crate::utils::shared::adjust_to_size; use crate::wasm_vm::PluginInstruction; @@ -69,9 +70,7 @@ pub struct Tab { full_screen_ws: PositionAndSize, fullscreen_is_active: bool, os_api: Box, - send_plugin_instructions: SenderWithContext, - send_pty_instructions: SenderWithContext, - send_server_instructions: SenderWithContext, + pub senders: ThreadSenders, synchronize_is_active: bool, should_clear_display_before_rendering: bool, pub mode_info: ModeInfo, @@ -219,7 +218,7 @@ pub trait Pane { } impl Tab { - // FIXME: Too many arguments here! Maybe bundle all of the senders for the whole program in a struct? + // FIXME: Still too many arguments for clippy to be happy... #[allow(clippy::too_many_arguments)] pub fn new( index: usize, @@ -227,9 +226,7 @@ impl Tab { name: String, full_screen_ws: &PositionAndSize, mut os_api: Box, - send_plugin_instructions: SenderWithContext, - send_pty_instructions: SenderWithContext, - send_server_instructions: SenderWithContext, + senders: ThreadSenders, max_panes: Option, pane_id: Option, mode_info: ModeInfo, @@ -261,9 +258,7 @@ impl Tab { fullscreen_is_active: false, synchronize_is_active: false, os_api, - send_plugin_instructions, - send_pty_instructions, - send_server_instructions, + senders, should_clear_display_before_rendering: false, mode_info, input_mode, @@ -315,14 +310,14 @@ impl Tab { // Just a regular terminal if let Some(plugin) = &layout.plugin { let (pid_tx, pid_rx) = channel(); - self.send_plugin_instructions - .send(PluginInstruction::Load(pid_tx, plugin.clone())) + self.senders + .send_to_plugin(PluginInstruction::Load(pid_tx, plugin.clone())) .unwrap(); let pid = pid_rx.recv().unwrap(); let mut new_plugin = PluginPane::new( pid, *position_and_size, - self.send_plugin_instructions.clone(), + self.senders.to_plugin.as_ref().unwrap().clone(), ); if let Some(max_rows) = position_and_size.max_rows { new_plugin.set_max_height(max_rows); @@ -332,8 +327,8 @@ impl Tab { } self.panes.insert(PaneId::Plugin(pid), Box::new(new_plugin)); // Send an initial mode update to the newly loaded plugin only! - self.send_plugin_instructions - .send(PluginInstruction::Update( + self.senders + .send_to_plugin(PluginInstruction::Update( Some(pid), Event::ModeUpdate(self.mode_info.clone()), )) @@ -355,8 +350,8 @@ impl Tab { // this is a bit of a hack and happens because we don't have any central location that // can query the screen as to how many panes it needs to create a layout // fixing this will require a bit of an architecture change - self.send_pty_instructions - .send(PtyInstruction::ClosePane(PaneId::Terminal(*unused_pid))) + self.senders + .send_to_pty(PtyInstruction::ClosePane(PaneId::Terminal(*unused_pid))) .unwrap(); } self.active_terminal = self.panes.iter().map(|(id, _)| id.to_owned()).next(); @@ -400,9 +395,9 @@ impl Tab { }, ); if terminal_id_to_split.is_none() { - self.send_pty_instructions - .send(PtyInstruction::ClosePane(pid)) - .unwrap(); // we can't open this pane, close the pty + self.senders + .send_to_pty(PtyInstruction::ClosePane(pid)) // we can't open this pane, close the pty + .unwrap(); return; // likely no terminal large enough to split } let terminal_id_to_split = terminal_id_to_split.unwrap(); @@ -481,9 +476,9 @@ impl Tab { let active_pane_id = &self.get_active_pane_id().unwrap(); let active_pane = self.panes.get_mut(active_pane_id).unwrap(); if active_pane.rows() < MIN_TERMINAL_HEIGHT * 2 + 1 { - self.send_pty_instructions - .send(PtyInstruction::ClosePane(pid)) - .unwrap(); // we can't open this pane, close the pty + self.senders + .send_to_pty(PtyInstruction::ClosePane(pid)) // we can't open this pane, close the pty + .unwrap(); return; } let terminal_ws = PositionAndSize { @@ -538,9 +533,9 @@ impl Tab { let active_pane_id = &self.get_active_pane_id().unwrap(); let active_pane = self.panes.get_mut(active_pane_id).unwrap(); if active_pane.columns() < MIN_TERMINAL_WIDTH * 2 + 1 { - self.send_pty_instructions - .send(PtyInstruction::ClosePane(pid)) - .unwrap(); // we can't open this pane, close the pty + self.senders + .send_to_pty(PtyInstruction::ClosePane(pid)) // we can't open this pane, close the pty + .unwrap(); return; } let terminal_ws = PositionAndSize { @@ -597,7 +592,7 @@ impl Tab { } pub fn handle_pty_bytes(&mut self, pid: RawFd, bytes: VteBytes) { // if we don't have the terminal in self.terminals it's probably because - // of a race condition where the terminal was created in pty_bus but has not + // of a race condition where the terminal was created in pty but has not // yet been created in Screen. These events are currently not buffered, so // if you're debugging seemingly randomly missing stdout data, this is // the reason @@ -628,8 +623,8 @@ impl Tab { } PaneId::Plugin(pid) => { for key in parse_keys(&input_bytes) { - self.send_plugin_instructions - .send(PluginInstruction::Update(Some(pid), Event::KeyPress(key))) + self.senders + .send_to_plugin(PluginInstruction::Update(Some(pid), Event::KeyPress(key))) .unwrap() } } @@ -706,7 +701,7 @@ impl Tab { pub fn is_sync_panes_active(&self) -> bool { self.synchronize_is_active } - pub fn toggle_sync_tab_is_active(&mut self) { + pub fn toggle_sync_panes_is_active(&mut self) { self.synchronize_is_active = !self.synchronize_is_active; } pub fn panes_contain_widechar(&self) -> bool { @@ -781,8 +776,8 @@ impl Tab { } } - self.send_server_instructions - .send(ServerInstruction::Render(Some(output))) + self.senders + .send_to_server(ServerInstruction::Render(Some(output))) .unwrap(); } fn get_panes(&self) -> impl Iterator)> { @@ -2086,8 +2081,8 @@ impl Tab { if let Some(max_panes) = self.max_panes { let terminals = self.get_pane_ids(); for &pid in terminals.iter().skip(max_panes - 1) { - self.send_pty_instructions - .send(PtyInstruction::ClosePane(pid)) + self.senders + .send_to_pty(PtyInstruction::ClosePane(pid)) .unwrap(); self.close_pane_without_rerender(pid); } @@ -2198,8 +2193,8 @@ impl Tab { pub fn close_focused_pane(&mut self) { if let Some(active_pane_id) = self.get_active_pane_id() { self.close_pane(active_pane_id); - self.send_pty_instructions - .send(PtyInstruction::ClosePane(active_pane_id)) + self.senders + .send_to_pty(PtyInstruction::ClosePane(active_pane_id)) .unwrap(); } } diff --git a/src/common/errors.rs b/src/common/errors.rs index d8327462..46121312 100644 --- a/src/common/errors.rs +++ b/src/common/errors.rs @@ -1,9 +1,9 @@ //! Error context system based on a thread-local representation of the call stack, itself based on //! the instructions that are sent between threads. -use super::{ServerInstruction, ASYNCOPENCALLS, OPENCALLS}; +use super::{thread_bus::ASYNCOPENCALLS, thread_bus::OPENCALLS, ServerInstruction}; use crate::client::ClientInstruction; -use crate::pty_bus::PtyInstruction; +use crate::pty::PtyInstruction; use crate::screen::ScreenInstruction; use serde::{Deserialize, Serialize}; @@ -14,7 +14,7 @@ use std::fmt::{Display, Error, Formatter}; const MAX_THREAD_CALL_STACK: usize = 6; #[cfg(not(test))] -use super::SenderWithContext; +use super::thread_bus::SenderWithContext; #[cfg(not(test))] use std::panic::PanicInfo; /// Custom panic handler/hook. Prints the [`ErrorContext`]. @@ -200,7 +200,7 @@ pub enum ScreenContext { PageScrollDown, ClearScroll, CloseFocusedPane, - ToggleActiveSyncTab, + ToggleActiveSyncPanes, ToggleActiveTerminalFullscreen, SetSelectable, SetInvisibleBorders, @@ -261,7 +261,7 @@ impl From<&ScreenInstruction> for ScreenContext { ScreenInstruction::UpdateTabName(_) => ScreenContext::UpdateTabName, ScreenInstruction::TerminalResize(_) => ScreenContext::TerminalResize, ScreenInstruction::ChangeMode(_) => ScreenContext::ChangeMode, - ScreenInstruction::ToggleActiveSyncTab => ScreenContext::ToggleActiveSyncTab, + ScreenInstruction::ToggleActiveSyncPanes => ScreenContext::ToggleActiveSyncPanes, } } } diff --git a/src/common/input/actions.rs b/src/common/input/actions.rs index 54cbbcdc..f07d7513 100644 --- a/src/common/input/actions.rs +++ b/src/common/input/actions.rs @@ -40,7 +40,7 @@ pub enum Action { /// Toggle between fullscreen focus pane and normal layout. ToggleFocusFullscreen, /// Toggle between sending text commands to all panes on the current tab and normal mode. - ToggleActiveSyncTab, + ToggleActiveSyncPanes, /// Open a new pane in the specified direction (relative to focus). /// If no direction is specified, will try to use the biggest available space. NewPane(Option), diff --git a/src/common/input/handler.rs b/src/common/input/handler.rs index a4293220..78635237 100644 --- a/src/common/input/handler.rs +++ b/src/common/input/handler.rs @@ -4,7 +4,7 @@ use super::actions::Action; use super::keybinds::Keybinds; use crate::client::ClientInstruction; use crate::common::input::config::Config; -use crate::common::{SenderWithContext, OPENCALLS}; +use crate::common::thread_bus::{SenderWithContext, OPENCALLS}; use crate::errors::ContextType; use crate::os_input_output::ClientOsApi; use crate::server::ServerInstruction; diff --git a/src/common/mod.rs b/src/common/mod.rs index 6061f1a6..c91e1e05 100644 --- a/src/common/mod.rs +++ b/src/common/mod.rs @@ -3,73 +3,12 @@ pub mod errors; pub mod input; pub mod ipc; pub mod os_input_output; -pub mod pty_bus; +pub mod pty; pub mod screen; pub mod setup; +pub mod thread_bus; pub mod utils; pub mod wasm_vm; use crate::panes::PaneId; use crate::server::ServerInstruction; -use async_std::task_local; -use errors::{get_current_ctx, ErrorContext}; -use std::cell::RefCell; -use std::sync::mpsc; - -/// An [MPSC](mpsc) asynchronous channel with added error context. -pub type ChannelWithContext = ( - mpsc::Sender<(T, ErrorContext)>, - mpsc::Receiver<(T, ErrorContext)>, -); -/// An [MPSC](mpsc) synchronous channel with added error context. -pub type SyncChannelWithContext = ( - mpsc::SyncSender<(T, ErrorContext)>, - mpsc::Receiver<(T, ErrorContext)>, -); - -/// Wrappers around the two standard [MPSC](mpsc) sender types, [`mpsc::Sender`] and [`mpsc::SyncSender`], with an additional [`ErrorContext`]. -#[derive(Clone)] -pub enum SenderType { - /// A wrapper around an [`mpsc::Sender`], adding an [`ErrorContext`]. - Sender(mpsc::Sender<(T, ErrorContext)>), - /// A wrapper around an [`mpsc::SyncSender`], adding an [`ErrorContext`]. - SyncSender(mpsc::SyncSender<(T, ErrorContext)>), -} - -/// Sends messages on an [MPSC](std::sync::mpsc) channel, along with an [`ErrorContext`], -/// synchronously or asynchronously depending on the underlying [`SenderType`]. -#[derive(Clone)] -pub struct SenderWithContext { - sender: SenderType, -} - -impl SenderWithContext { - pub fn new(sender: SenderType) -> Self { - Self { sender } - } - - /// Sends an event, along with the current [`ErrorContext`], on this - /// [`SenderWithContext`]'s channel. - pub fn send(&self, event: T) -> Result<(), mpsc::SendError<(T, ErrorContext)>> { - let err_ctx = get_current_ctx(); - match self.sender { - SenderType::Sender(ref s) => s.send((event, err_ctx)), - SenderType::SyncSender(ref s) => s.send((event, err_ctx)), - } - } -} - -unsafe impl Send for SenderWithContext {} -unsafe impl Sync for SenderWithContext {} - -thread_local!( - /// A key to some thread local storage (TLS) that holds a representation of the thread's call - /// stack in the form of an [`ErrorContext`]. - pub static OPENCALLS: RefCell = RefCell::default() -); - -task_local! { - /// A key to some task local storage that holds a representation of the task's call - /// stack in the form of an [`ErrorContext`]. - static ASYNCOPENCALLS: RefCell = RefCell::default() -} diff --git a/src/common/pty_bus.rs b/src/common/pty.rs similarity index 65% rename from src/common/pty_bus.rs rename to src/common/pty.rs index 31696186..483aa51e 100644 --- a/src/common/pty_bus.rs +++ b/src/common/pty.rs @@ -4,20 +4,19 @@ use ::async_std::task::*; use ::std::collections::HashMap; use ::std::os::unix::io::RawFd; use ::std::pin::*; -use ::std::sync::mpsc::Receiver; use ::std::time::{Duration, Instant}; use std::path::PathBuf; -use super::SenderWithContext; -use crate::layout::Layout; +use super::{screen::ScreenInstruction, thread_bus::SenderWithContext}; use crate::os_input_output::ServerOsApi; use crate::utils::logging::debug_to_file; +use crate::wasm_vm::PluginInstruction; use crate::{ - errors::{get_current_ctx, ContextType, ErrorContext}, + common::thread_bus::Bus, + errors::{get_current_ctx, ContextType, PtyContext}, panes::PaneId, - screen::ScreenInstruction, - wasm_vm::PluginInstruction, }; +use crate::{layout::Layout, server::ServerInstruction}; pub struct ReadFromPid { pid: RawFd, @@ -79,16 +78,69 @@ pub enum PtyInstruction { Exit, } -pub struct PtyBus { - pub receive_pty_instructions: Receiver<(PtyInstruction, ErrorContext)>, +pub struct Pty { + pub bus: Bus, pub id_to_child_pid: HashMap, - pub send_screen_instructions: SenderWithContext, - send_plugin_instructions: SenderWithContext, - os_input: Box, debug_to_file: bool, task_handles: HashMap>, } +pub fn pty_thread_main(mut pty: Pty, maybe_layout: Option) { + loop { + let (event, mut err_ctx) = pty.bus.recv().expect("failed to receive event on channel"); + err_ctx.add_call(ContextType::Pty(PtyContext::from(&event))); + match event { + PtyInstruction::SpawnTerminal(file_to_open) => { + let pid = pty.spawn_terminal(file_to_open); + pty.bus + .senders + .send_to_screen(ScreenInstruction::NewPane(PaneId::Terminal(pid))) + .unwrap(); + } + PtyInstruction::SpawnTerminalVertically(file_to_open) => { + let pid = pty.spawn_terminal(file_to_open); + pty.bus + .senders + .send_to_screen(ScreenInstruction::VerticalSplit(PaneId::Terminal(pid))) + .unwrap(); + } + PtyInstruction::SpawnTerminalHorizontally(file_to_open) => { + let pid = pty.spawn_terminal(file_to_open); + pty.bus + .senders + .send_to_screen(ScreenInstruction::HorizontalSplit(PaneId::Terminal(pid))) + .unwrap(); + } + PtyInstruction::NewTab => { + if let Some(layout) = maybe_layout.clone() { + pty.spawn_terminals_for_layout(layout); + } else { + let pid = pty.spawn_terminal(None); + pty.bus + .senders + .send_to_screen(ScreenInstruction::NewTab(pid)) + .unwrap(); + } + } + PtyInstruction::ClosePane(id) => { + pty.close_pane(id); + pty.bus + .senders + .send_to_server(ServerInstruction::UnblockInputThread) + .unwrap(); + } + PtyInstruction::CloseTab(ids) => { + pty.close_tab(ids); + pty.bus + .senders + .send_to_server(ServerInstruction::UnblockInputThread) + .unwrap(); + } + PtyInstruction::Exit => break, + } + } +} + fn stream_terminal_bytes( pid: RawFd, send_screen_instructions: SenderWithContext, @@ -124,9 +176,7 @@ fn stream_terminal_bytes( Some(receive_time) => { if receive_time.elapsed() > max_render_pause { pending_render = false; - send_screen_instructions - .send(ScreenInstruction::Render) - .unwrap(); + let _ = send_screen_instructions.send(ScreenInstruction::Render); last_byte_receive_time = Some(Instant::now()); } else { pending_render = true; @@ -140,9 +190,7 @@ fn stream_terminal_bytes( } else { if pending_render { pending_render = false; - send_screen_instructions - .send(ScreenInstruction::Render) - .unwrap(); + let _ = send_screen_instructions.send(ScreenInstruction::Render); } last_byte_receive_time = None; task::sleep(::std::time::Duration::from_millis(10)).await; @@ -162,31 +210,26 @@ fn stream_terminal_bytes( }) } -impl PtyBus { - pub fn new( - receive_pty_instructions: Receiver<(PtyInstruction, ErrorContext)>, - send_screen_instructions: SenderWithContext, - send_plugin_instructions: SenderWithContext, - os_input: Box, - debug_to_file: bool, - ) -> Self { - PtyBus { - receive_pty_instructions, - os_input, +impl Pty { + pub fn new(bus: Bus, debug_to_file: bool) -> Self { + Pty { + bus, id_to_child_pid: HashMap::new(), - send_screen_instructions, - send_plugin_instructions, debug_to_file, task_handles: HashMap::new(), } } pub fn spawn_terminal(&mut self, file_to_open: Option) -> RawFd { - let (pid_primary, pid_secondary): (RawFd, RawFd) = - self.os_input.spawn_terminal(file_to_open); + let (pid_primary, pid_secondary): (RawFd, RawFd) = self + .bus + .os_input + .as_mut() + .unwrap() + .spawn_terminal(file_to_open); let task_handle = stream_terminal_bytes( pid_primary, - self.send_screen_instructions.clone(), - self.os_input.clone(), + self.bus.senders.to_screen.as_ref().unwrap().clone(), + self.bus.os_input.as_ref().unwrap().clone(), self.debug_to_file, ); self.task_handles.insert(pid_primary, task_handle); @@ -197,12 +240,14 @@ impl PtyBus { let total_panes = layout.total_terminal_panes(); let mut new_pane_pids = vec![]; for _ in 0..total_panes { - let (pid_primary, pid_secondary): (RawFd, RawFd) = self.os_input.spawn_terminal(None); + let (pid_primary, pid_secondary): (RawFd, RawFd) = + self.bus.os_input.as_mut().unwrap().spawn_terminal(None); self.id_to_child_pid.insert(pid_primary, pid_secondary); new_pane_pids.push(pid_primary); } - self.send_screen_instructions - .send(ScreenInstruction::ApplyLayout( + self.bus + .senders + .send_to_screen(ScreenInstruction::ApplyLayout( layout, new_pane_pids.clone(), )) @@ -210,8 +255,8 @@ impl PtyBus { for id in new_pane_pids { let task_handle = stream_terminal_bytes( id, - self.send_screen_instructions.clone(), - self.os_input.clone(), + self.bus.senders.to_screen.as_ref().unwrap().clone(), + self.bus.os_input.as_ref().unwrap().clone(), self.debug_to_file, ); self.task_handles.insert(id, task_handle); @@ -222,15 +267,16 @@ impl PtyBus { PaneId::Terminal(id) => { let child_pid = self.id_to_child_pid.remove(&id).unwrap(); let handle = self.task_handles.remove(&id).unwrap(); - self.os_input.kill(child_pid).unwrap(); + self.bus.os_input.as_mut().unwrap().kill(child_pid).unwrap(); task::block_on(async { handle.cancel().await; }); } - PaneId::Plugin(pid) => self - .send_plugin_instructions - .send(PluginInstruction::Unload(pid)) - .unwrap(), + PaneId::Plugin(pid) => drop( + self.bus + .senders + .send_to_plugin(PluginInstruction::Unload(pid)), + ), } } pub fn close_tab(&mut self, ids: Vec) { @@ -240,7 +286,7 @@ impl PtyBus { } } -impl Drop for PtyBus { +impl Drop for Pty { fn drop(&mut self) { let child_ids: Vec = self.id_to_child_pid.keys().copied().collect(); for id in child_ids { diff --git a/src/common/screen.rs b/src/common/screen.rs index 62d87b51..131b874d 100644 --- a/src/common/screen.rs +++ b/src/common/screen.rs @@ -3,15 +3,14 @@ use std::collections::BTreeMap; use std::os::unix::io::RawFd; use std::str; -use std::sync::mpsc::Receiver; -use crate::common::SenderWithContext; -use crate::os_input_output::ServerOsApi; +use crate::common::pty::{PtyInstruction, VteBytes}; +use crate::common::thread_bus::Bus; +use crate::errors::{ContextType, ScreenContext}; use crate::panes::PositionAndSize; -use crate::pty_bus::{PtyInstruction, VteBytes}; use crate::server::ServerInstruction; use crate::tab::Tab; -use crate::{errors::ErrorContext, wasm_vm::PluginInstruction}; +use crate::wasm_vm::PluginInstruction; use crate::{layout::Layout, panes::PaneId}; use zellij_tile::data::{Event, InputMode, ModeInfo, Palette, TabInfo}; @@ -52,7 +51,7 @@ pub enum ScreenInstruction { NewTab(RawFd), SwitchTabNext, SwitchTabPrev, - ToggleActiveSyncTab, + ToggleActiveSyncPanes, CloseTab, GoToTab(u32), UpdateTabName(Vec), @@ -63,55 +62,37 @@ pub enum ScreenInstruction { /// A [`Screen`] holds multiple [`Tab`]s, each one holding multiple [`panes`](crate::client::panes). /// It only directly controls which tab is active, delegating the rest to the individual `Tab`. pub struct Screen { - /// A [`ScreenInstruction`] and [`ErrorContext`] receiver. - pub receiver: Receiver<(ScreenInstruction, ErrorContext)>, + /// A Bus for sending and receiving messages with the other threads. + pub bus: Bus, /// An optional maximal amount of panes allowed per [`Tab`] in this [`Screen`] instance. max_panes: Option, /// A map between this [`Screen`]'s tabs and their ID/key. tabs: BTreeMap, - /// A [`PluginInstruction`] and [`ErrorContext`] sender. - pub send_plugin_instructions: SenderWithContext, - /// An [`PtyInstruction`] and [`ErrorContext`] sender. - pub send_pty_instructions: SenderWithContext, - /// An [`ServerInstruction`] and [`ErrorContext`] sender. - pub send_server_instructions: SenderWithContext, /// The full size of this [`Screen`]. full_screen_ws: PositionAndSize, /// The index of this [`Screen`]'s active [`Tab`]. active_tab_index: Option, - /// The [`ServerOsApi`] this [`Screen`] uses. - os_api: Box, mode_info: ModeInfo, input_mode: InputMode, colors: Palette, } impl Screen { - // FIXME: This lint needs actual fixing! Maybe by bundling the Senders /// Creates and returns a new [`Screen`]. - #[allow(clippy::too_many_arguments)] pub fn new( - receive_screen_instructions: Receiver<(ScreenInstruction, ErrorContext)>, - send_plugin_instructions: SenderWithContext, - send_pty_instructions: SenderWithContext, - send_server_instructions: SenderWithContext, + bus: Bus, full_screen_ws: &PositionAndSize, - os_api: Box, max_panes: Option, mode_info: ModeInfo, input_mode: InputMode, colors: Palette, ) -> Self { Screen { - receiver: receive_screen_instructions, + bus, max_panes, - send_plugin_instructions, - send_pty_instructions, - send_server_instructions, full_screen_ws: *full_screen_ws, active_tab_index: None, tabs: BTreeMap::new(), - os_api, mode_info, input_mode, colors, @@ -128,10 +109,8 @@ impl Screen { position, String::new(), &self.full_screen_ws, - self.os_api.clone(), - self.send_plugin_instructions.clone(), - self.send_pty_instructions.clone(), - self.send_server_instructions.clone(), + self.bus.os_input.as_ref().unwrap().clone(), + self.bus.senders.clone(), self.max_panes, Some(PaneId::Terminal(pane_id)), self.mode_info.clone(), @@ -215,13 +194,15 @@ impl Screen { // below we don't check the result of sending the CloseTab instruction to the pty thread // because this might be happening when the app is closing, at which point the pty thread // has already closed and this would result in an error - self.send_pty_instructions - .send(PtyInstruction::CloseTab(pane_ids)) + self.bus + .senders + .send_to_pty(PtyInstruction::CloseTab(pane_ids)) .unwrap(); if self.tabs.is_empty() { self.active_tab_index = None; - self.send_server_instructions - .send(ServerInstruction::Render(None)) + self.bus + .senders + .send_to_server(ServerInstruction::Render(None)) .unwrap(); } else { for t in self.tabs.values_mut() { @@ -284,10 +265,8 @@ impl Screen { position, String::new(), &self.full_screen_ws, - self.os_api.clone(), - self.send_plugin_instructions.clone(), - self.send_pty_instructions.clone(), - self.send_server_instructions.clone(), + self.bus.os_input.as_ref().unwrap().clone(), + self.bus.senders.clone(), self.max_panes, None, self.mode_info.clone(), @@ -311,8 +290,9 @@ impl Screen { is_sync_panes_active: tab.is_sync_panes_active(), }); } - self.send_plugin_instructions - .send(PluginInstruction::Update(None, Event::TabUpdate(tab_data))) + self.bus + .senders + .send_to_plugin(PluginInstruction::Update(None, Event::TabUpdate(tab_data))) .unwrap(); } @@ -340,3 +320,246 @@ impl Screen { } } } + +pub fn screen_thread_main( + bus: Bus, + max_panes: Option, + full_screen_ws: PositionAndSize, +) { + let colors = bus.os_input.as_ref().unwrap().load_palette(); + let mut screen = Screen::new( + bus, + &full_screen_ws, + max_panes, + ModeInfo { + palette: colors, + ..ModeInfo::default() + }, + InputMode::Normal, + colors, + ); + loop { + let (event, mut err_ctx) = screen + .bus + .recv() + .expect("failed to receive event on channel"); + err_ctx.add_call(ContextType::Screen(ScreenContext::from(&event))); + match event { + ScreenInstruction::PtyBytes(pid, vte_bytes) => { + let active_tab = screen.get_active_tab_mut().unwrap(); + if active_tab.has_terminal_pid(pid) { + // it's most likely that this event is directed at the active tab + // look there first + active_tab.handle_pty_bytes(pid, vte_bytes); + } else { + // if this event wasn't directed at the active tab, start looking + // in other tabs + let all_tabs = screen.get_tabs_mut(); + for tab in all_tabs.values_mut() { + if tab.has_terminal_pid(pid) { + tab.handle_pty_bytes(pid, vte_bytes); + break; + } + } + } + } + ScreenInstruction::Render => { + screen.render(); + } + ScreenInstruction::NewPane(pid) => { + screen.get_active_tab_mut().unwrap().new_pane(pid); + screen + .bus + .senders + .send_to_server(ServerInstruction::UnblockInputThread) + .unwrap(); + } + ScreenInstruction::HorizontalSplit(pid) => { + screen.get_active_tab_mut().unwrap().horizontal_split(pid); + screen + .bus + .senders + .send_to_server(ServerInstruction::UnblockInputThread) + .unwrap(); + } + ScreenInstruction::VerticalSplit(pid) => { + screen.get_active_tab_mut().unwrap().vertical_split(pid); + screen + .bus + .senders + .send_to_server(ServerInstruction::UnblockInputThread) + .unwrap(); + } + ScreenInstruction::WriteCharacter(bytes) => { + let active_tab = screen.get_active_tab_mut().unwrap(); + match active_tab.is_sync_panes_active() { + true => active_tab.write_to_terminals_on_current_tab(bytes), + false => active_tab.write_to_active_terminal(bytes), + } + } + ScreenInstruction::ResizeLeft => { + screen.get_active_tab_mut().unwrap().resize_left(); + } + ScreenInstruction::ResizeRight => { + screen.get_active_tab_mut().unwrap().resize_right(); + } + ScreenInstruction::ResizeDown => { + screen.get_active_tab_mut().unwrap().resize_down(); + } + ScreenInstruction::ResizeUp => { + screen.get_active_tab_mut().unwrap().resize_up(); + } + ScreenInstruction::SwitchFocus => { + screen.get_active_tab_mut().unwrap().move_focus(); + } + ScreenInstruction::FocusNextPane => { + screen.get_active_tab_mut().unwrap().focus_next_pane(); + } + ScreenInstruction::FocusPreviousPane => { + screen.get_active_tab_mut().unwrap().focus_previous_pane(); + } + ScreenInstruction::MoveFocusLeft => { + screen.get_active_tab_mut().unwrap().move_focus_left(); + } + ScreenInstruction::MoveFocusDown => { + screen.get_active_tab_mut().unwrap().move_focus_down(); + } + ScreenInstruction::MoveFocusRight => { + screen.get_active_tab_mut().unwrap().move_focus_right(); + } + ScreenInstruction::MoveFocusUp => { + screen.get_active_tab_mut().unwrap().move_focus_up(); + } + ScreenInstruction::ScrollUp => { + screen + .get_active_tab_mut() + .unwrap() + .scroll_active_terminal_up(); + } + ScreenInstruction::ScrollDown => { + screen + .get_active_tab_mut() + .unwrap() + .scroll_active_terminal_down(); + } + ScreenInstruction::PageScrollUp => { + screen + .get_active_tab_mut() + .unwrap() + .scroll_active_terminal_up_page(); + } + ScreenInstruction::PageScrollDown => { + screen + .get_active_tab_mut() + .unwrap() + .scroll_active_terminal_down_page(); + } + ScreenInstruction::ClearScroll => { + screen + .get_active_tab_mut() + .unwrap() + .clear_active_terminal_scroll(); + } + ScreenInstruction::CloseFocusedPane => { + screen.get_active_tab_mut().unwrap().close_focused_pane(); + screen.render(); + } + ScreenInstruction::SetSelectable(id, selectable) => { + screen + .get_active_tab_mut() + .unwrap() + .set_pane_selectable(id, selectable); + } + ScreenInstruction::SetMaxHeight(id, max_height) => { + screen + .get_active_tab_mut() + .unwrap() + .set_pane_max_height(id, max_height); + } + ScreenInstruction::SetInvisibleBorders(id, invisible_borders) => { + screen + .get_active_tab_mut() + .unwrap() + .set_pane_invisible_borders(id, invisible_borders); + screen.render(); + } + ScreenInstruction::ClosePane(id) => { + screen.get_active_tab_mut().unwrap().close_pane(id); + screen.render(); + } + ScreenInstruction::ToggleActiveTerminalFullscreen => { + screen + .get_active_tab_mut() + .unwrap() + .toggle_active_pane_fullscreen(); + } + ScreenInstruction::NewTab(pane_id) => { + screen.new_tab(pane_id); + screen + .bus + .senders + .send_to_server(ServerInstruction::UnblockInputThread) + .unwrap(); + } + ScreenInstruction::SwitchTabNext => { + screen.switch_tab_next(); + screen + .bus + .senders + .send_to_server(ServerInstruction::UnblockInputThread) + .unwrap(); + } + ScreenInstruction::SwitchTabPrev => { + screen.switch_tab_prev(); + screen + .bus + .senders + .send_to_server(ServerInstruction::UnblockInputThread) + .unwrap(); + } + ScreenInstruction::CloseTab => { + screen.close_tab(); + screen + .bus + .senders + .send_to_server(ServerInstruction::UnblockInputThread) + .unwrap(); + } + ScreenInstruction::ApplyLayout(layout, new_pane_pids) => { + screen.apply_layout(layout, new_pane_pids); + screen + .bus + .senders + .send_to_server(ServerInstruction::UnblockInputThread) + .unwrap(); + } + ScreenInstruction::GoToTab(tab_index) => { + screen.go_to_tab(tab_index as usize); + screen + .bus + .senders + .send_to_server(ServerInstruction::UnblockInputThread) + .unwrap(); + } + ScreenInstruction::UpdateTabName(c) => { + screen.update_active_tab_name(c); + } + ScreenInstruction::TerminalResize(new_size) => { + screen.resize_to_screen(new_size); + } + ScreenInstruction::ChangeMode(mode_info) => { + screen.change_mode(mode_info); + } + ScreenInstruction::ToggleActiveSyncPanes => { + screen + .get_active_tab_mut() + .unwrap() + .toggle_sync_panes_is_active(); + screen.update_tabs(); + } + ScreenInstruction::Exit => { + break; + } + } + } +} diff --git a/src/common/thread_bus.rs b/src/common/thread_bus.rs new file mode 100644 index 00000000..8827d068 --- /dev/null +++ b/src/common/thread_bus.rs @@ -0,0 +1,142 @@ +//! Definitions and helpers for sending and receiving messages between threads. + +use async_std::task_local; +use std::cell::RefCell; +use std::sync::mpsc; + +use crate::common::pty::PtyInstruction; +use crate::common::ServerInstruction; +use crate::errors::{get_current_ctx, ErrorContext}; +use crate::os_input_output::ServerOsApi; +use crate::screen::ScreenInstruction; +use crate::wasm_vm::PluginInstruction; + +/// An [MPSC](mpsc) asynchronous channel with added error context. +pub type ChannelWithContext = ( + mpsc::Sender<(T, ErrorContext)>, + mpsc::Receiver<(T, ErrorContext)>, +); +/// An [MPSC](mpsc) synchronous channel with added error context. +pub type SyncChannelWithContext = ( + mpsc::SyncSender<(T, ErrorContext)>, + mpsc::Receiver<(T, ErrorContext)>, +); + +/// Wrappers around the two standard [MPSC](mpsc) sender types, [`mpsc::Sender`] and [`mpsc::SyncSender`], with an additional [`ErrorContext`]. +#[derive(Clone)] +pub enum SenderType { + /// A wrapper around an [`mpsc::Sender`], adding an [`ErrorContext`]. + Sender(mpsc::Sender<(T, ErrorContext)>), + /// A wrapper around an [`mpsc::SyncSender`], adding an [`ErrorContext`]. + SyncSender(mpsc::SyncSender<(T, ErrorContext)>), +} + +/// Sends messages on an [MPSC](std::sync::mpsc) channel, along with an [`ErrorContext`], +/// synchronously or asynchronously depending on the underlying [`SenderType`]. +#[derive(Clone)] +pub struct SenderWithContext { + sender: SenderType, +} + +impl SenderWithContext { + pub fn new(sender: SenderType) -> Self { + Self { sender } + } + + /// Sends an event, along with the current [`ErrorContext`], on this + /// [`SenderWithContext`]'s channel. + pub fn send(&self, event: T) -> Result<(), mpsc::SendError<(T, ErrorContext)>> { + let err_ctx = get_current_ctx(); + match self.sender { + SenderType::Sender(ref s) => s.send((event, err_ctx)), + SenderType::SyncSender(ref s) => s.send((event, err_ctx)), + } + } +} + +unsafe impl Send for SenderWithContext {} +unsafe impl Sync for SenderWithContext {} + +thread_local!( + /// A key to some thread local storage (TLS) that holds a representation of the thread's call + /// stack in the form of an [`ErrorContext`]. + pub static OPENCALLS: RefCell = RefCell::default() +); + +task_local! { + /// A key to some task local storage that holds a representation of the task's call + /// stack in the form of an [`ErrorContext`]. + pub static ASYNCOPENCALLS: RefCell = RefCell::default() +} + +/// A container for senders to the different threads in zellij on the server side +#[derive(Clone)] +pub struct ThreadSenders { + pub to_screen: Option>, + pub to_pty: Option>, + pub to_plugin: Option>, + pub to_server: Option>, +} + +impl ThreadSenders { + pub fn send_to_screen( + &self, + instruction: ScreenInstruction, + ) -> Result<(), mpsc::SendError<(ScreenInstruction, ErrorContext)>> { + self.to_screen.as_ref().unwrap().send(instruction) + } + + pub fn send_to_pty( + &self, + instruction: PtyInstruction, + ) -> Result<(), mpsc::SendError<(PtyInstruction, ErrorContext)>> { + self.to_pty.as_ref().unwrap().send(instruction) + } + + pub fn send_to_plugin( + &self, + instruction: PluginInstruction, + ) -> Result<(), mpsc::SendError<(PluginInstruction, ErrorContext)>> { + self.to_plugin.as_ref().unwrap().send(instruction) + } + + pub fn send_to_server( + &self, + instruction: ServerInstruction, + ) -> Result<(), mpsc::SendError<(ServerInstruction, ErrorContext)>> { + self.to_server.as_ref().unwrap().send(instruction) + } +} + +/// A container for a receiver, OS input and the senders to a given thread +pub struct Bus { + pub receiver: mpsc::Receiver<(T, ErrorContext)>, + pub senders: ThreadSenders, + pub os_input: Option>, +} + +impl Bus { + pub fn new( + receiver: mpsc::Receiver<(T, ErrorContext)>, + to_screen: Option<&SenderWithContext>, + to_pty: Option<&SenderWithContext>, + to_plugin: Option<&SenderWithContext>, + to_server: Option<&SenderWithContext>, + os_input: Option>, + ) -> Self { + Bus { + receiver, + senders: ThreadSenders { + to_screen: to_screen.cloned(), + to_pty: to_pty.cloned(), + to_plugin: to_plugin.cloned(), + to_server: to_server.cloned(), + }, + os_input: os_input.clone(), + } + } + + pub fn recv(&self) -> Result<(T, ErrorContext), mpsc::RecvError> { + self.receiver.recv() + } +} diff --git a/src/common/wasm_vm.rs b/src/common/wasm_vm.rs index db52c2bb..8d29b6a9 100644 --- a/src/common/wasm_vm.rs +++ b/src/common/wasm_vm.rs @@ -1,17 +1,28 @@ use serde::{de::DeserializeOwned, Serialize}; use std::{ - collections::HashSet, + collections::{HashMap, HashSet}, + fs, path::PathBuf, process, + str::FromStr, sync::{mpsc::Sender, Arc, Mutex}, thread, time::{Duration, Instant}, }; -use wasmer::{imports, Function, ImportObject, Store, WasmerEnv}; -use wasmer_wasi::WasiEnv; +use wasmer::{ + imports, ChainableNamedResolver, Function, ImportObject, Instance, Module, Store, Value, + WasmerEnv, +}; +use wasmer_wasi::{Pipe, WasiEnv, WasiState}; use zellij_tile::data::{Event, EventType, PluginIds}; -use super::{pty_bus::PtyInstruction, screen::ScreenInstruction, PaneId, SenderWithContext}; +use super::{ + errors::{ContextType, PluginContext}, + pty::PtyInstruction, + screen::ScreenInstruction, + thread_bus::{Bus, ThreadSenders}, + PaneId, +}; #[derive(Clone, Debug)] pub enum PluginInstruction { @@ -25,14 +36,97 @@ pub enum PluginInstruction { #[derive(WasmerEnv, Clone)] pub struct PluginEnv { pub plugin_id: u32, - // FIXME: This should be a big bundle of all of the channels - pub send_screen_instructions: SenderWithContext, - pub send_pty_instructions: SenderWithContext, - pub send_plugin_instructions: SenderWithContext, + pub senders: ThreadSenders, pub wasi_env: WasiEnv, pub subscriptions: Arc>>, } +// Thread main -------------------------------------------------------------------------------------------------------- +pub fn wasm_thread_main(bus: Bus, store: Store, data_dir: PathBuf) { + let mut plugin_id = 0; + let mut plugin_map = HashMap::new(); + loop { + let (event, mut err_ctx) = bus.recv().expect("failed to receive event on channel"); + err_ctx.add_call(ContextType::Plugin(PluginContext::from(&event))); + match event { + PluginInstruction::Load(pid_tx, path) => { + let plugin_dir = data_dir.join("plugins/"); + let wasm_bytes = fs::read(&path) + .or_else(|_| fs::read(&path.with_extension("wasm"))) + .or_else(|_| fs::read(&plugin_dir.join(&path).with_extension("wasm"))) + .unwrap_or_else(|_| panic!("cannot find plugin {}", &path.display())); + + // FIXME: Cache this compiled module on disk. I could use `(de)serialize_to_file()` for that + let module = Module::new(&store, &wasm_bytes).unwrap(); + + let output = Pipe::new(); + let input = Pipe::new(); + let mut wasi_env = WasiState::new("Zellij") + .env("CLICOLOR_FORCE", "1") + .preopen(|p| { + p.directory(".") // FIXME: Change this to a more meaningful dir + .alias(".") + .read(true) + .write(true) + .create(true) + }) + .unwrap() + .stdin(Box::new(input)) + .stdout(Box::new(output)) + .finalize() + .unwrap(); + + let wasi = wasi_env.import_object(&module).unwrap(); + + let plugin_env = PluginEnv { + plugin_id, + senders: bus.senders.clone(), + wasi_env, + subscriptions: Arc::new(Mutex::new(HashSet::new())), + }; + + let zellij = zellij_exports(&store, &plugin_env); + let instance = Instance::new(&module, &zellij.chain_back(wasi)).unwrap(); + + let start = instance.exports.get_function("_start").unwrap(); + + // This eventually calls the `.load()` method + start.call(&[]).unwrap(); + + plugin_map.insert(plugin_id, (instance, plugin_env)); + pid_tx.send(plugin_id).unwrap(); + plugin_id += 1; + } + PluginInstruction::Update(pid, event) => { + for (&i, (instance, plugin_env)) in &plugin_map { + let subs = plugin_env.subscriptions.lock().unwrap(); + // FIXME: This is very janky... Maybe I should write my own macro for Event -> EventType? + let event_type = EventType::from_str(&event.to_string()).unwrap(); + if (pid.is_none() || pid == Some(i)) && subs.contains(&event_type) { + let update = instance.exports.get_function("update").unwrap(); + wasi_write_object(&plugin_env.wasi_env, &event); + update.call(&[]).unwrap(); + } + } + drop(bus.senders.send_to_screen(ScreenInstruction::Render)); + } + PluginInstruction::Render(buf_tx, pid, rows, cols) => { + let (instance, plugin_env) = plugin_map.get(&pid).unwrap(); + + let render = instance.exports.get_function("render").unwrap(); + + render + .call(&[Value::I32(rows as i32), Value::I32(cols as i32)]) + .unwrap(); + + buf_tx.send(wasi_read_string(&plugin_env.wasi_env)).unwrap(); + } + PluginInstruction::Unload(pid) => drop(plugin_map.remove(&pid)), + PluginInstruction::Exit => break, + } + } +} + // Plugin API --------------------------------------------------------------------------------------------------------- pub fn zellij_exports(store: &Store, plugin_env: &PluginEnv) -> ImportObject { @@ -74,8 +168,8 @@ fn host_unsubscribe(plugin_env: &PluginEnv) { fn host_set_selectable(plugin_env: &PluginEnv, selectable: i32) { let selectable = selectable != 0; plugin_env - .send_screen_instructions - .send(ScreenInstruction::SetSelectable( + .senders + .send_to_screen(ScreenInstruction::SetSelectable( PaneId::Plugin(plugin_env.plugin_id), selectable, )) @@ -85,8 +179,8 @@ fn host_set_selectable(plugin_env: &PluginEnv, selectable: i32) { fn host_set_max_height(plugin_env: &PluginEnv, max_height: i32) { let max_height = max_height as usize; plugin_env - .send_screen_instructions - .send(ScreenInstruction::SetMaxHeight( + .senders + .send_to_screen(ScreenInstruction::SetMaxHeight( PaneId::Plugin(plugin_env.plugin_id), max_height, )) @@ -96,8 +190,8 @@ fn host_set_max_height(plugin_env: &PluginEnv, max_height: i32) { fn host_set_invisible_borders(plugin_env: &PluginEnv, invisible_borders: i32) { let invisible_borders = invisible_borders != 0; plugin_env - .send_screen_instructions - .send(ScreenInstruction::SetInvisibleBorders( + .senders + .send_to_screen(ScreenInstruction::SetInvisibleBorders( PaneId::Plugin(plugin_env.plugin_id), invisible_borders, )) @@ -115,8 +209,8 @@ fn host_get_plugin_ids(plugin_env: &PluginEnv) { fn host_open_file(plugin_env: &PluginEnv) { let path: PathBuf = wasi_read_object(&plugin_env.wasi_env); plugin_env - .send_pty_instructions - .send(PtyInstruction::SpawnTerminal(Some(path))) + .senders + .send_to_pty(PtyInstruction::SpawnTerminal(Some(path))) .unwrap(); } @@ -130,7 +224,7 @@ fn host_set_timeout(plugin_env: &PluginEnv, secs: f64) { // timers as we'd like. // // But that's a lot of code, and this is a few lines: - let send_plugin_instructions = plugin_env.send_plugin_instructions.clone(); + let send_plugin_instructions = plugin_env.senders.to_plugin.clone(); let update_target = Some(plugin_env.plugin_id); thread::spawn(move || { let start_time = Instant::now(); @@ -140,6 +234,7 @@ fn host_set_timeout(plugin_env: &PluginEnv, secs: f64) { let elapsed_time = Instant::now().duration_since(start_time).as_secs_f64(); send_plugin_instructions + .unwrap() .send(PluginInstruction::Update( update_target, Event::Timer(elapsed_time), diff --git a/src/main.rs b/src/main.rs index 44b98549..12998d4d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,9 +6,7 @@ mod server; mod tests; use client::{boundaries, layout, panes, start_client, tab}; -use common::{ - command_is_executing, errors, os_input_output, pty_bus, screen, setup, utils, wasm_vm, -}; +use common::{command_is_executing, errors, os_input_output, pty, screen, setup, utils, wasm_vm}; use server::start_server; use structopt::StructOpt; diff --git a/src/server/mod.rs b/src/server/mod.rs index a90831d9..6c067e90 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -1,35 +1,29 @@ +pub mod route; + use interprocess::local_socket::LocalSocketListener; use serde::{Deserialize, Serialize}; -use std::path::PathBuf; -use std::sync::mpsc::channel; +use std::sync::{Arc, RwLock}; use std::thread; -use std::{collections::HashMap, fs}; -use std::{ - collections::HashSet, - str::FromStr, - sync::{Arc, Mutex, RwLock}, -}; -use wasmer::{ChainableNamedResolver, Instance, Module, Store, Value}; -use wasmer_wasi::{Pipe, WasiState}; -use zellij_tile::data::{Event, EventType, InputMode, ModeInfo}; +use std::{path::PathBuf, sync::mpsc::channel}; +use wasmer::Store; use crate::cli::CliArgs; use crate::client::ClientInstruction; +use crate::common::thread_bus::{Bus, ThreadSenders}; use crate::common::{ - errors::{ContextType, PluginContext, PtyContext, ScreenContext, ServerContext}, - input::actions::{Action, Direction}, - input::handler::get_mode_info, + errors::{ContextType, ServerContext}, + input::actions::Action, os_input_output::{set_permissions, ServerOsApi}, - pty_bus::{PtyBus, PtyInstruction}, - screen::{Screen, ScreenInstruction}, + pty::{pty_thread_main, Pty, PtyInstruction}, + screen::{screen_thread_main, ScreenInstruction}, setup::install::populate_data_dir, + thread_bus::{ChannelWithContext, SenderType, SenderWithContext}, utils::consts::{ZELLIJ_IPC_PIPE, ZELLIJ_PROJ_DIR}, - wasm_vm::{wasi_read_string, wasi_write_object, zellij_exports, PluginEnv, PluginInstruction}, - ChannelWithContext, SenderType, SenderWithContext, + wasm_vm::{wasm_thread_main, PluginInstruction}, }; use crate::layout::Layout; -use crate::panes::PaneId; use crate::panes::PositionAndSize; +use route::route_thread_main; /// Instructions related to server-side application including the /// ones sent by client to server @@ -43,10 +37,8 @@ pub enum ServerInstruction { ClientExit, } -struct SessionMetaData { - pub send_pty_instructions: SenderWithContext, - pub send_screen_instructions: SenderWithContext, - pub send_plugin_instructions: SenderWithContext, +pub struct SessionMetaData { + pub senders: ThreadSenders, screen_thread: Option>, pty_thread: Option>, wasm_thread: Option>, @@ -54,9 +46,9 @@ struct SessionMetaData { impl Drop for SessionMetaData { fn drop(&mut self) { - let _ = self.send_pty_instructions.send(PtyInstruction::Exit); - let _ = self.send_screen_instructions.send(ScreenInstruction::Exit); - let _ = self.send_plugin_instructions.send(PluginInstruction::Exit); + 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(); @@ -64,26 +56,27 @@ impl Drop for SessionMetaData { } pub fn start_server(os_input: Box) -> thread::JoinHandle<()> { - let (send_server_instructions, receive_server_instructions): ChannelWithContext< - ServerInstruction, - > = channel(); - let send_server_instructions = - SenderWithContext::new(SenderType::Sender(send_server_instructions)); + let (to_server, server_receiver): ChannelWithContext = channel(); + let to_server = SenderWithContext::new(SenderType::Sender(to_server)); let sessions: Arc>> = Arc::new(RwLock::new(None)); #[cfg(test)] - handle_client( - sessions.clone(), - os_input.clone(), - send_server_instructions.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(); + + move || route_thread_main(sessions, os_input, to_server) + }); #[cfg(not(test))] let _ = thread::Builder::new() .name("server_listener".to_string()) .spawn({ let os_input = os_input.clone(); let sessions = sessions.clone(); - let send_server_instructions = send_server_instructions.clone(); + let to_server = to_server.clone(); move || { drop(std::fs::remove_file(&*ZELLIJ_IPC_PIPE)); let listener = LocalSocketListener::bind(&**ZELLIJ_IPC_PIPE).unwrap(); @@ -94,8 +87,17 @@ pub fn start_server(os_input: Box) -> thread::JoinHandle<()> { let mut os_input = os_input.clone(); os_input.update_receiver(stream); let sessions = sessions.clone(); - let send_server_instructions = send_server_instructions.clone(); - handle_client(sessions, os_input, send_server_instructions); + 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(); + + move || route_thread_main(sessions, os_input, to_server) + }) + .unwrap(); } Err(err) => { panic!("err {:?}", err); @@ -109,24 +111,20 @@ pub fn start_server(os_input: Box) -> thread::JoinHandle<()> { .name("server_thread".to_string()) .spawn({ move || loop { - let (instruction, mut err_ctx) = receive_server_instructions.recv().unwrap(); + let (instruction, mut err_ctx) = server_receiver.recv().unwrap(); err_ctx.add_call(ContextType::IPCServer(ServerContext::from(&instruction))); match instruction { ServerInstruction::NewClient(full_screen_ws, opts) => { - let session_data = init_session( - os_input.clone(), - opts, - send_server_instructions.clone(), - full_screen_ws, - ); + let session_data = + init_session(os_input.clone(), opts, to_server.clone(), full_screen_ws); *sessions.write().unwrap() = Some(session_data); sessions .read() .unwrap() .as_ref() .unwrap() - .send_pty_instructions - .send(PtyInstruction::NewTab) + .senders + .send_to_pty(PtyInstruction::NewTab) .unwrap(); } ServerInstruction::UnblockInputThread => { @@ -148,65 +146,19 @@ pub fn start_server(os_input: Box) -> thread::JoinHandle<()> { .unwrap() } -fn handle_client( - sessions: Arc>>, - mut os_input: Box, - send_server_instructions: SenderWithContext, -) { - thread::Builder::new() - .name("server_router".to_string()) - .spawn(move || loop { - let (instruction, mut err_ctx) = os_input.recv_from_client(); - err_ctx.add_call(ContextType::IPCServer(ServerContext::from(&instruction))); - let rlocked_sessions = sessions.read().unwrap(); - match instruction { - ServerInstruction::ClientExit => { - send_server_instructions.send(instruction).unwrap(); - break; - } - ServerInstruction::Action(action) => { - route_action(action, rlocked_sessions.as_ref().unwrap(), &*os_input); - } - ServerInstruction::TerminalResize(new_size) => { - rlocked_sessions - .as_ref() - .unwrap() - .send_screen_instructions - .send(ScreenInstruction::TerminalResize(new_size)) - .unwrap(); - } - ServerInstruction::NewClient(..) => { - os_input.add_client_sender(); - send_server_instructions.send(instruction).unwrap(); - } - _ => { - send_server_instructions.send(instruction).unwrap(); - } - } - }) - .unwrap(); -} - fn init_session( os_input: Box, opts: CliArgs, - send_server_instructions: SenderWithContext, + to_server: SenderWithContext, full_screen_ws: PositionAndSize, ) -> SessionMetaData { - let (send_screen_instructions, receive_screen_instructions): ChannelWithContext< - ScreenInstruction, - > = channel(); - let send_screen_instructions = - SenderWithContext::new(SenderType::Sender(send_screen_instructions)); + let (to_screen, screen_receiver): ChannelWithContext = channel(); + let to_screen = SenderWithContext::new(SenderType::Sender(to_screen)); - let (send_plugin_instructions, receive_plugin_instructions): ChannelWithContext< - PluginInstruction, - > = channel(); - let send_plugin_instructions = - SenderWithContext::new(SenderType::Sender(send_plugin_instructions)); - let (send_pty_instructions, receive_pty_instructions): ChannelWithContext = - channel(); - let send_pty_instructions = SenderWithContext::new(SenderType::Sender(send_pty_instructions)); + let (to_plugin, plugin_receiver): ChannelWithContext = channel(); + let to_plugin = SenderWithContext::new(SenderType::Sender(to_plugin)); + let (to_pty, pty_receiver): ChannelWithContext = channel(); + let to_pty = SenderWithContext::new(SenderType::Sender(to_pty)); // Determine and initialize the data directory let data_dir = opts @@ -224,322 +176,40 @@ fn init_session( .map(|p| Layout::new(&p, &data_dir)) .or_else(|| default_layout.map(|p| Layout::from_defaults(&p, &data_dir))); - let mut pty_bus = PtyBus::new( - receive_pty_instructions, - send_screen_instructions.clone(), - send_plugin_instructions.clone(), - os_input.clone(), - opts.debug, - ); - let pty_thread = thread::Builder::new() .name("pty".to_string()) .spawn({ - let send_server_instructions = send_server_instructions.clone(); - move || loop { - let (event, mut err_ctx) = pty_bus - .receive_pty_instructions - .recv() - .expect("failed to receive event on channel"); - err_ctx.add_call(ContextType::Pty(PtyContext::from(&event))); - match event { - PtyInstruction::SpawnTerminal(file_to_open) => { - let pid = pty_bus.spawn_terminal(file_to_open); - pty_bus - .send_screen_instructions - .send(ScreenInstruction::NewPane(PaneId::Terminal(pid))) - .unwrap(); - } - PtyInstruction::SpawnTerminalVertically(file_to_open) => { - let pid = pty_bus.spawn_terminal(file_to_open); - pty_bus - .send_screen_instructions - .send(ScreenInstruction::VerticalSplit(PaneId::Terminal(pid))) - .unwrap(); - } - PtyInstruction::SpawnTerminalHorizontally(file_to_open) => { - let pid = pty_bus.spawn_terminal(file_to_open); - pty_bus - .send_screen_instructions - .send(ScreenInstruction::HorizontalSplit(PaneId::Terminal(pid))) - .unwrap(); - } - PtyInstruction::NewTab => { - if let Some(layout) = maybe_layout.clone() { - pty_bus.spawn_terminals_for_layout(layout); - } else { - let pid = pty_bus.spawn_terminal(None); - pty_bus - .send_screen_instructions - .send(ScreenInstruction::NewTab(pid)) - .unwrap(); - } - } - PtyInstruction::ClosePane(id) => { - pty_bus.close_pane(id); - send_server_instructions - .send(ServerInstruction::UnblockInputThread) - .unwrap(); - } - PtyInstruction::CloseTab(ids) => { - pty_bus.close_tab(ids); - send_server_instructions - .send(ServerInstruction::UnblockInputThread) - .unwrap(); - } - PtyInstruction::Exit => { - break; - } - } - } + let pty = Pty::new( + Bus::new( + pty_receiver, + Some(&to_screen), + None, + Some(&to_plugin), + None, + Some(os_input.clone()), + ), + opts.debug, + ); + + move || pty_thread_main(pty, maybe_layout) }) .unwrap(); let screen_thread = thread::Builder::new() .name("screen".to_string()) .spawn({ - let os_input = os_input.clone(); - let send_plugin_instructions = send_plugin_instructions.clone(); - let send_pty_instructions = send_pty_instructions.clone(); - let send_server_instructions = send_server_instructions; + let screen_bus = Bus::new( + screen_receiver, + None, + Some(&to_pty), + Some(&to_plugin), + Some(&to_server), + Some(os_input.clone()), + ); let max_panes = opts.max_panes; - let colors = os_input.load_palette(); move || { - let mut screen = Screen::new( - receive_screen_instructions, - send_plugin_instructions, - send_pty_instructions, - send_server_instructions, - &full_screen_ws, - os_input, - max_panes, - ModeInfo { - palette: colors, - ..ModeInfo::default() - }, - InputMode::Normal, - colors, - ); - loop { - let (event, mut err_ctx) = screen - .receiver - .recv() - .expect("failed to receive event on channel"); - err_ctx.add_call(ContextType::Screen(ScreenContext::from(&event))); - match event { - ScreenInstruction::PtyBytes(pid, vte_bytes) => { - let active_tab = screen.get_active_tab_mut().unwrap(); - if active_tab.has_terminal_pid(pid) { - // it's most likely that this event is directed at the active tab - // look there first - active_tab.handle_pty_bytes(pid, vte_bytes); - } else { - // if this event wasn't directed at the active tab, start looking - // in other tabs - let all_tabs = screen.get_tabs_mut(); - for tab in all_tabs.values_mut() { - if tab.has_terminal_pid(pid) { - tab.handle_pty_bytes(pid, vte_bytes); - break; - } - } - } - } - ScreenInstruction::Render => { - screen.render(); - } - ScreenInstruction::NewPane(pid) => { - screen.get_active_tab_mut().unwrap().new_pane(pid); - screen - .send_server_instructions - .send(ServerInstruction::UnblockInputThread) - .unwrap(); - } - ScreenInstruction::HorizontalSplit(pid) => { - screen.get_active_tab_mut().unwrap().horizontal_split(pid); - screen - .send_server_instructions - .send(ServerInstruction::UnblockInputThread) - .unwrap(); - } - ScreenInstruction::VerticalSplit(pid) => { - screen.get_active_tab_mut().unwrap().vertical_split(pid); - screen - .send_server_instructions - .send(ServerInstruction::UnblockInputThread) - .unwrap(); - } - ScreenInstruction::WriteCharacter(bytes) => { - let active_tab = screen.get_active_tab_mut().unwrap(); - match active_tab.is_sync_panes_active() { - true => active_tab.write_to_terminals_on_current_tab(bytes), - false => active_tab.write_to_active_terminal(bytes), - } - } - ScreenInstruction::ResizeLeft => { - screen.get_active_tab_mut().unwrap().resize_left(); - } - ScreenInstruction::ResizeRight => { - screen.get_active_tab_mut().unwrap().resize_right(); - } - ScreenInstruction::ResizeDown => { - screen.get_active_tab_mut().unwrap().resize_down(); - } - ScreenInstruction::ResizeUp => { - screen.get_active_tab_mut().unwrap().resize_up(); - } - ScreenInstruction::SwitchFocus => { - screen.get_active_tab_mut().unwrap().move_focus(); - } - ScreenInstruction::FocusNextPane => { - screen.get_active_tab_mut().unwrap().focus_next_pane(); - } - ScreenInstruction::FocusPreviousPane => { - screen.get_active_tab_mut().unwrap().focus_previous_pane(); - } - ScreenInstruction::MoveFocusLeft => { - screen.get_active_tab_mut().unwrap().move_focus_left(); - } - ScreenInstruction::MoveFocusDown => { - screen.get_active_tab_mut().unwrap().move_focus_down(); - } - ScreenInstruction::MoveFocusRight => { - screen.get_active_tab_mut().unwrap().move_focus_right(); - } - ScreenInstruction::MoveFocusUp => { - screen.get_active_tab_mut().unwrap().move_focus_up(); - } - ScreenInstruction::ScrollUp => { - screen - .get_active_tab_mut() - .unwrap() - .scroll_active_terminal_up(); - } - ScreenInstruction::ScrollDown => { - screen - .get_active_tab_mut() - .unwrap() - .scroll_active_terminal_down(); - } - ScreenInstruction::PageScrollUp => { - screen - .get_active_tab_mut() - .unwrap() - .scroll_active_terminal_up_page(); - } - ScreenInstruction::PageScrollDown => { - screen - .get_active_tab_mut() - .unwrap() - .scroll_active_terminal_down_page(); - } - ScreenInstruction::ClearScroll => { - screen - .get_active_tab_mut() - .unwrap() - .clear_active_terminal_scroll(); - } - ScreenInstruction::CloseFocusedPane => { - screen.get_active_tab_mut().unwrap().close_focused_pane(); - screen.render(); - } - ScreenInstruction::SetSelectable(id, selectable) => { - screen - .get_active_tab_mut() - .unwrap() - .set_pane_selectable(id, selectable); - } - ScreenInstruction::SetMaxHeight(id, max_height) => { - screen - .get_active_tab_mut() - .unwrap() - .set_pane_max_height(id, max_height); - } - ScreenInstruction::SetInvisibleBorders(id, invisible_borders) => { - screen - .get_active_tab_mut() - .unwrap() - .set_pane_invisible_borders(id, invisible_borders); - screen.render(); - } - ScreenInstruction::ClosePane(id) => { - screen.get_active_tab_mut().unwrap().close_pane(id); - screen.render(); - } - ScreenInstruction::ToggleActiveTerminalFullscreen => { - screen - .get_active_tab_mut() - .unwrap() - .toggle_active_pane_fullscreen(); - } - ScreenInstruction::NewTab(pane_id) => { - screen.new_tab(pane_id); - screen - .send_server_instructions - .send(ServerInstruction::UnblockInputThread) - .unwrap(); - } - ScreenInstruction::SwitchTabNext => { - screen.switch_tab_next(); - screen - .send_server_instructions - .send(ServerInstruction::UnblockInputThread) - .unwrap(); - } - ScreenInstruction::SwitchTabPrev => { - screen.switch_tab_prev(); - screen - .send_server_instructions - .send(ServerInstruction::UnblockInputThread) - .unwrap(); - } - ScreenInstruction::CloseTab => { - screen.close_tab(); - screen - .send_server_instructions - .send(ServerInstruction::UnblockInputThread) - .unwrap(); - } - ScreenInstruction::ApplyLayout(layout, new_pane_pids) => { - screen.apply_layout(layout, new_pane_pids); - screen - .send_server_instructions - .send(ServerInstruction::UnblockInputThread) - .unwrap(); - } - ScreenInstruction::GoToTab(tab_index) => { - screen.go_to_tab(tab_index as usize); - screen - .send_server_instructions - .send(ServerInstruction::UnblockInputThread) - .unwrap(); - } - ScreenInstruction::UpdateTabName(c) => { - screen.update_active_tab_name(c); - screen - .send_server_instructions - .send(ServerInstruction::UnblockInputThread) - .unwrap(); - } - ScreenInstruction::TerminalResize(new_size) => { - screen.resize_to_screen(new_size); - } - ScreenInstruction::ChangeMode(mode_info) => { - screen.change_mode(mode_info); - } - ScreenInstruction::ToggleActiveSyncTab => { - screen - .get_active_tab_mut() - .unwrap() - .toggle_sync_tab_is_active(); - screen.update_tabs(); - } - ScreenInstruction::Exit => { - break; - } - } - } + screen_thread_main(screen_bus, max_panes, full_screen_ws); } }) .unwrap(); @@ -547,265 +217,28 @@ fn init_session( let wasm_thread = thread::Builder::new() .name("wasm".to_string()) .spawn({ - let send_screen_instructions = send_screen_instructions.clone(); - let send_pty_instructions = send_pty_instructions.clone(); - let send_plugin_instructions = send_plugin_instructions.clone(); - + let plugin_bus = Bus::new( + plugin_receiver, + Some(&to_screen), + Some(&to_pty), + Some(&to_plugin), + Some(&to_server), + None, + ); let store = Store::default(); - let mut plugin_id = 0; - let mut plugin_map = HashMap::new(); - move || loop { - let (event, mut err_ctx) = receive_plugin_instructions - .recv() - .expect("failed to receive event on channel"); - err_ctx.add_call(ContextType::Plugin(PluginContext::from(&event))); - match event { - PluginInstruction::Load(pid_tx, path) => { - let plugin_dir = data_dir.join("plugins/"); - let wasm_bytes = fs::read(&path) - .or_else(|_| fs::read(&path.with_extension("wasm"))) - .or_else(|_| fs::read(&plugin_dir.join(&path).with_extension("wasm"))) - .unwrap_or_else(|_| panic!("cannot find plugin {}", &path.display())); - // FIXME: Cache this compiled module on disk. I could use `(de)serialize_to_file()` for that - let module = Module::new(&store, &wasm_bytes).unwrap(); - - let output = Pipe::new(); - let input = Pipe::new(); - let mut wasi_env = WasiState::new("Zellij") - .env("CLICOLOR_FORCE", "1") - .preopen(|p| { - p.directory(".") // FIXME: Change this to a more meaningful dir - .alias(".") - .read(true) - .write(true) - .create(true) - }) - .unwrap() - .stdin(Box::new(input)) - .stdout(Box::new(output)) - .finalize() - .unwrap(); - - let wasi = wasi_env.import_object(&module).unwrap(); - - let plugin_env = PluginEnv { - plugin_id, - send_screen_instructions: send_screen_instructions.clone(), - send_pty_instructions: send_pty_instructions.clone(), - send_plugin_instructions: send_plugin_instructions.clone(), - wasi_env, - subscriptions: Arc::new(Mutex::new(HashSet::new())), - }; - - let zellij = zellij_exports(&store, &plugin_env); - let instance = Instance::new(&module, &zellij.chain_back(wasi)).unwrap(); - - let start = instance.exports.get_function("_start").unwrap(); - - // This eventually calls the `.init()` method - start.call(&[]).unwrap(); - - plugin_map.insert(plugin_id, (instance, plugin_env)); - pid_tx.send(plugin_id).unwrap(); - plugin_id += 1; - } - PluginInstruction::Update(pid, event) => { - for (&i, (instance, plugin_env)) in &plugin_map { - let subs = plugin_env.subscriptions.lock().unwrap(); - // FIXME: This is very janky... Maybe I should write my own macro for Event -> EventType? - let event_type = EventType::from_str(&event.to_string()).unwrap(); - if (pid.is_none() || pid == Some(i)) && subs.contains(&event_type) { - let update = instance.exports.get_function("update").unwrap(); - wasi_write_object(&plugin_env.wasi_env, &event); - update.call(&[]).unwrap(); - } - } - drop(send_screen_instructions.send(ScreenInstruction::Render)); - } - PluginInstruction::Render(buf_tx, pid, rows, cols) => { - let (instance, plugin_env) = plugin_map.get(&pid).unwrap(); - - let render = instance.exports.get_function("render").unwrap(); - - render - .call(&[Value::I32(rows as i32), Value::I32(cols as i32)]) - .unwrap(); - - buf_tx.send(wasi_read_string(&plugin_env.wasi_env)).unwrap(); - } - PluginInstruction::Unload(pid) => drop(plugin_map.remove(&pid)), - PluginInstruction::Exit => break, - } - } + move || wasm_thread_main(plugin_bus, store, data_dir) }) .unwrap(); SessionMetaData { - send_plugin_instructions, - send_screen_instructions, - send_pty_instructions, + senders: ThreadSenders { + to_screen: Some(to_screen), + to_pty: Some(to_pty), + to_plugin: Some(to_plugin), + to_server: None, + }, screen_thread: Some(screen_thread), pty_thread: Some(pty_thread), wasm_thread: Some(wasm_thread), } } - -fn route_action(action: Action, session: &SessionMetaData, os_input: &dyn ServerOsApi) { - match action { - Action::Write(val) => { - session - .send_screen_instructions - .send(ScreenInstruction::ClearScroll) - .unwrap(); - session - .send_screen_instructions - .send(ScreenInstruction::WriteCharacter(val)) - .unwrap(); - } - Action::SwitchToMode(mode) => { - let palette = os_input.load_palette(); - session - .send_plugin_instructions - .send(PluginInstruction::Update( - None, - Event::ModeUpdate(get_mode_info(mode, palette)), - )) - .unwrap(); - session - .send_screen_instructions - .send(ScreenInstruction::ChangeMode(get_mode_info(mode, palette))) - .unwrap(); - session - .send_screen_instructions - .send(ScreenInstruction::Render) - .unwrap(); - } - Action::Resize(direction) => { - let screen_instr = match direction { - Direction::Left => ScreenInstruction::ResizeLeft, - Direction::Right => ScreenInstruction::ResizeRight, - Direction::Up => ScreenInstruction::ResizeUp, - Direction::Down => ScreenInstruction::ResizeDown, - }; - session.send_screen_instructions.send(screen_instr).unwrap(); - } - Action::SwitchFocus => { - session - .send_screen_instructions - .send(ScreenInstruction::SwitchFocus) - .unwrap(); - } - Action::FocusNextPane => { - session - .send_screen_instructions - .send(ScreenInstruction::FocusNextPane) - .unwrap(); - } - Action::FocusPreviousPane => { - session - .send_screen_instructions - .send(ScreenInstruction::FocusPreviousPane) - .unwrap(); - } - Action::MoveFocus(direction) => { - let screen_instr = match direction { - Direction::Left => ScreenInstruction::MoveFocusLeft, - Direction::Right => ScreenInstruction::MoveFocusRight, - Direction::Up => ScreenInstruction::MoveFocusUp, - Direction::Down => ScreenInstruction::MoveFocusDown, - }; - session.send_screen_instructions.send(screen_instr).unwrap(); - } - Action::ScrollUp => { - session - .send_screen_instructions - .send(ScreenInstruction::ScrollUp) - .unwrap(); - } - Action::ScrollDown => { - session - .send_screen_instructions - .send(ScreenInstruction::ScrollDown) - .unwrap(); - } - Action::PageScrollUp => { - session - .send_screen_instructions - .send(ScreenInstruction::PageScrollUp) - .unwrap(); - } - Action::PageScrollDown => { - session - .send_screen_instructions - .send(ScreenInstruction::PageScrollDown) - .unwrap(); - } - Action::ToggleFocusFullscreen => { - session - .send_screen_instructions - .send(ScreenInstruction::ToggleActiveTerminalFullscreen) - .unwrap(); - } - Action::NewPane(direction) => { - let pty_instr = match direction { - Some(Direction::Left) => PtyInstruction::SpawnTerminalVertically(None), - Some(Direction::Right) => PtyInstruction::SpawnTerminalVertically(None), - Some(Direction::Up) => PtyInstruction::SpawnTerminalHorizontally(None), - Some(Direction::Down) => PtyInstruction::SpawnTerminalHorizontally(None), - // No direction specified - try to put it in the biggest available spot - None => PtyInstruction::SpawnTerminal(None), - }; - session.send_pty_instructions.send(pty_instr).unwrap(); - } - Action::CloseFocus => { - session - .send_screen_instructions - .send(ScreenInstruction::CloseFocusedPane) - .unwrap(); - } - Action::NewTab => { - session - .send_pty_instructions - .send(PtyInstruction::NewTab) - .unwrap(); - } - Action::GoToNextTab => { - session - .send_screen_instructions - .send(ScreenInstruction::SwitchTabNext) - .unwrap(); - } - Action::GoToPreviousTab => { - session - .send_screen_instructions - .send(ScreenInstruction::SwitchTabPrev) - .unwrap(); - } - Action::ToggleActiveSyncTab => { - session - .send_screen_instructions - .send(ScreenInstruction::ToggleActiveSyncTab) - .unwrap(); - } - Action::CloseTab => { - session - .send_screen_instructions - .send(ScreenInstruction::CloseTab) - .unwrap(); - } - Action::GoToTab(i) => { - session - .send_screen_instructions - .send(ScreenInstruction::GoToTab(i)) - .unwrap(); - } - Action::TabNameInput(c) => { - session - .send_screen_instructions - .send(ScreenInstruction::UpdateTabName(c)) - .unwrap(); - } - Action::NoOp => {} - Action::Quit => panic!("Received unexpected action"), - } -} diff --git a/src/server/route.rs b/src/server/route.rs new file mode 100644 index 00000000..264f62bb --- /dev/null +++ b/src/server/route.rs @@ -0,0 +1,205 @@ +use std::sync::{Arc, RwLock}; + +use zellij_tile::data::Event; + +use crate::common::errors::{ContextType, ServerContext}; +use crate::common::input::actions::Action; +use crate::common::input::{actions::Direction, handler::get_mode_info}; +use crate::common::os_input_output::ServerOsApi; +use crate::common::pty::PtyInstruction; +use crate::common::screen::ScreenInstruction; +use crate::common::thread_bus::SenderWithContext; +use crate::common::wasm_vm::PluginInstruction; +use crate::server::{ServerInstruction, SessionMetaData}; + +fn route_action(action: Action, session: &SessionMetaData, os_input: &dyn ServerOsApi) { + match action { + Action::Write(val) => { + session + .senders + .send_to_screen(ScreenInstruction::ClearScroll) + .unwrap(); + session + .senders + .send_to_screen(ScreenInstruction::WriteCharacter(val)) + .unwrap(); + } + Action::SwitchToMode(mode) => { + let palette = os_input.load_palette(); + session + .senders + .send_to_plugin(PluginInstruction::Update( + None, + Event::ModeUpdate(get_mode_info(mode, palette)), + )) + .unwrap(); + session + .senders + .send_to_screen(ScreenInstruction::ChangeMode(get_mode_info(mode, palette))) + .unwrap(); + session + .senders + .send_to_screen(ScreenInstruction::Render) + .unwrap(); + } + Action::Resize(direction) => { + let screen_instr = match direction { + Direction::Left => ScreenInstruction::ResizeLeft, + Direction::Right => ScreenInstruction::ResizeRight, + Direction::Up => ScreenInstruction::ResizeUp, + Direction::Down => ScreenInstruction::ResizeDown, + }; + session.senders.send_to_screen(screen_instr).unwrap(); + } + Action::SwitchFocus => { + session + .senders + .send_to_screen(ScreenInstruction::SwitchFocus) + .unwrap(); + } + Action::FocusNextPane => { + session + .senders + .send_to_screen(ScreenInstruction::FocusNextPane) + .unwrap(); + } + Action::FocusPreviousPane => { + session + .senders + .send_to_screen(ScreenInstruction::FocusPreviousPane) + .unwrap(); + } + Action::MoveFocus(direction) => { + let screen_instr = match direction { + Direction::Left => ScreenInstruction::MoveFocusLeft, + Direction::Right => ScreenInstruction::MoveFocusRight, + Direction::Up => ScreenInstruction::MoveFocusUp, + Direction::Down => ScreenInstruction::MoveFocusDown, + }; + session.senders.send_to_screen(screen_instr).unwrap(); + } + Action::ScrollUp => { + session + .senders + .send_to_screen(ScreenInstruction::ScrollUp) + .unwrap(); + } + Action::ScrollDown => { + session + .senders + .send_to_screen(ScreenInstruction::ScrollDown) + .unwrap(); + } + Action::PageScrollUp => { + session + .senders + .send_to_screen(ScreenInstruction::PageScrollUp) + .unwrap(); + } + Action::PageScrollDown => { + session + .senders + .send_to_screen(ScreenInstruction::PageScrollDown) + .unwrap(); + } + Action::ToggleFocusFullscreen => { + session + .senders + .send_to_screen(ScreenInstruction::ToggleActiveTerminalFullscreen) + .unwrap(); + } + Action::NewPane(direction) => { + let pty_instr = match direction { + Some(Direction::Left) => PtyInstruction::SpawnTerminalVertically(None), + Some(Direction::Right) => PtyInstruction::SpawnTerminalVertically(None), + Some(Direction::Up) => PtyInstruction::SpawnTerminalHorizontally(None), + Some(Direction::Down) => PtyInstruction::SpawnTerminalHorizontally(None), + // No direction specified - try to put it in the biggest available spot + None => PtyInstruction::SpawnTerminal(None), + }; + session.senders.send_to_pty(pty_instr).unwrap(); + } + Action::CloseFocus => { + session + .senders + .send_to_screen(ScreenInstruction::CloseFocusedPane) + .unwrap(); + } + Action::NewTab => { + session.senders.send_to_pty(PtyInstruction::NewTab).unwrap(); + } + Action::GoToNextTab => { + session + .senders + .send_to_screen(ScreenInstruction::SwitchTabNext) + .unwrap(); + } + Action::GoToPreviousTab => { + session + .senders + .send_to_screen(ScreenInstruction::SwitchTabPrev) + .unwrap(); + } + Action::ToggleActiveSyncPanes => { + session + .senders + .send_to_screen(ScreenInstruction::ToggleActiveSyncPanes) + .unwrap(); + } + Action::CloseTab => { + session + .senders + .send_to_screen(ScreenInstruction::CloseTab) + .unwrap(); + } + Action::GoToTab(i) => { + session + .senders + .send_to_screen(ScreenInstruction::GoToTab(i)) + .unwrap(); + } + Action::TabNameInput(c) => { + session + .senders + .send_to_screen(ScreenInstruction::UpdateTabName(c)) + .unwrap(); + } + Action::NoOp => {} + Action::Quit => panic!("Received unexpected action"), + } +} +pub fn route_thread_main( + sessions: Arc>>, + mut os_input: Box, + to_server: SenderWithContext, +) { + loop { + let (instruction, mut err_ctx) = os_input.recv_from_client(); + err_ctx.add_call(ContextType::IPCServer(ServerContext::from(&instruction))); + let rlocked_sessions = sessions.read().unwrap(); + match instruction { + ServerInstruction::ClientExit => { + to_server.send(instruction).unwrap(); + break; + } + ServerInstruction::Action(action) => { + route_action(action, rlocked_sessions.as_ref().unwrap(), &*os_input); + } + ServerInstruction::TerminalResize(new_size) => { + rlocked_sessions + .as_ref() + .unwrap() + .senders + .send_to_screen(ScreenInstruction::TerminalResize(new_size)) + .unwrap(); + } + ServerInstruction::NewClient(..) => { + os_input.add_client_sender(); + to_server.send(instruction).unwrap(); + } + _ => { + to_server.send(instruction).unwrap(); + } + } + } +} diff --git a/src/tests/fakes.rs b/src/tests/fakes.rs index 4c780410..eb0d14e8 100644 --- a/src/tests/fakes.rs +++ b/src/tests/fakes.rs @@ -8,7 +8,7 @@ use std::sync::{mpsc, Arc, Condvar, Mutex}; use std::time::{Duration, Instant}; use crate::client::ClientInstruction; -use crate::common::{ChannelWithContext, SenderType, SenderWithContext}; +use crate::common::thread_bus::{ChannelWithContext, SenderType, SenderWithContext}; use crate::errors::ErrorContext; use crate::os_input_output::{ClientOsApi, ServerOsApi}; use crate::server::ServerInstruction;