diff --git a/Cargo.lock b/Cargo.lock index f869fa22..5e7d733e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -53,7 +53,7 @@ version = "1.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00d68a33ebc8b57800847d00787307f84a562224a14db069b0acefe4c2abbf5d" dependencies = [ - "async-task", + "async-task 3.0.0", "crossbeam-utils 0.7.2", "futures-channel", "futures-core", @@ -77,6 +77,12 @@ version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c17772156ef2829aadc587461c7753af20b7e8db1529bc66855add962a3b35d3" +[[package]] +name = "async-task" +version = "4.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91831deabf0d6d7ec49552e489aed63b7456a7a3c46cff62adad428110b0af0" + [[package]] name = "atomic-waker" version = "1.0.0" @@ -138,12 +144,26 @@ checksum = "d2468ff7bf85066b4a3678fede6fe66db31846d753ff0adfbfab2c6a6e81612b" dependencies = [ "async-channel", "atomic-waker", - "futures-lite", + "futures-lite 0.1.10", "once_cell", - "parking", + "parking 1.0.6", "waker-fn", ] +[[package]] +name = "blocking" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e170dbede1f740736619b776d7251cb1b9095c435c34d8ca9f57fcd2f335e9" +dependencies = [ + "async-channel", + "async-task 4.0.3", + "atomic-waker", + "fastrand", + "futures-lite 1.11.3", + "once_cell", +] + [[package]] name = "bumpalo" version = "3.4.0" @@ -491,9 +511,12 @@ checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" [[package]] name = "fastrand" -version = "1.3.3" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36a9cb09840f81cd211e435d00a4e487edd263dc3c8ff815c32dd76ad668ebed" +checksum = "ca5faf057445ce5c9d4329e382b2ce7ca38550ef3b73a5348362d5f24e0c7fe3" +dependencies = [ + "instant", +] [[package]] name = "fnv" @@ -559,11 +582,26 @@ dependencies = [ "futures-core", "futures-io", "memchr", - "parking", + "parking 1.0.6", "pin-project-lite 0.1.7", "waker-fn", ] +[[package]] +name = "futures-lite" +version = "1.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4481d0cd0de1d204a4fa55e7d45f07b1d958abcb06714b3446438e2eff695fb" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "memchr", + "parking 2.0.0", + "pin-project-lite 0.2.0", + "waker-fn", +] + [[package]] name = "futures-macro" version = "0.3.5" @@ -749,6 +787,38 @@ dependencies = [ "serde_yaml", ] +[[package]] +name = "instant" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61124eeebbd69b8190558df225adf7e4caafce0d743919e5d6b19652314ec5ec" +dependencies = [ + "cfg-if 1.0.0", +] + +[[package]] +name = "interprocess" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "672deb438b19b22433dcc1b2c75f34c9964e31f325155da06a2dc605706fd264" +dependencies = [ + "blocking 1.0.2", + "cfg-if 1.0.0", + "futures", + "intmap", + "lazy_static", + "libc", + "spinning", + "thiserror", + "winapi", +] + +[[package]] +name = "intmap" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e50930385956f6c4a0b99f3dd654adcc40788456c36e17c5b20e1d1ceb523ec6" + [[package]] name = "inventory" version = "0.1.9" @@ -829,6 +899,15 @@ version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8dd5a6d5999d9907cda8ed67bbd137d3af8085216c2ac62de5be860bd41f304a" +[[package]] +name = "lock_api" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd96ffd135b2fd7b973ac026d28085defbe8983df057ced3eb4f2130b0831312" +dependencies = [ + "scopeguard", +] + [[package]] name = "log" version = "0.4.11" @@ -906,6 +985,7 @@ dependencies = [ "directories-next", "futures", "insta", + "interprocess", "libc", "nix", "serde", @@ -973,9 +1053,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.4.0" +version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b631f7e854af39a1739f401cf34a8a013dfe09eac4fa4dba91e9768bd28168d" +checksum = "13bd41f508810a131401606d54ac32a467c97172d74ba7662562ebba5ad07fa0" [[package]] name = "parking" @@ -983,6 +1063,12 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6cb300f271742d4a2a66c01b6b2fa0c83dfebd2e0bf11addb879a3547b4ed87c" +[[package]] +name = "parking" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "427c3892f9e783d91cc128285287e70a59e206ca452770ece88a76f7a3eddd72" + [[package]] name = "pin-project" version = "0.4.23" @@ -1374,8 +1460,8 @@ version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "620cbb3c6e34da57d3a248cda0cd01cd5848164dc062e764e65d06fe3ea7aed5" dependencies = [ - "async-task", - "blocking", + "async-task 3.0.0", + "blocking 0.4.7", "concurrent-queue", "fastrand", "futures-io", @@ -1401,6 +1487,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "spinning" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d4f0e86297cad2658d92a707320d87bf4e6ae1050287f51d19b67ef3f153a7b" +dependencies = [ + "lock_api", +] + [[package]] name = "stable_deref_trait" version = "1.2.0" diff --git a/Cargo.toml b/Cargo.toml index 20d7e441..f3b945fb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,7 @@ unicode-width = "0.1.8" vte = "0.8.0" wasmer = "1.0.0" wasmer-wasi = "1.0.0" +interprocess = "1.0.1" [dependencies.async-std] version = "1.3.0" diff --git a/src/boundaries.rs b/src/client/boundaries.rs similarity index 100% rename from src/boundaries.rs rename to src/client/boundaries.rs diff --git a/src/layout.rs b/src/client/layout.rs similarity index 100% rename from src/layout.rs rename to src/client/layout.rs diff --git a/src/client/mod.rs b/src/client/mod.rs new file mode 100644 index 00000000..c8276253 --- /dev/null +++ b/src/client/mod.rs @@ -0,0 +1,6 @@ +pub mod boundaries; +pub mod layout; +pub mod panes; +pub mod tab; + +pub fn start_client() {} diff --git a/src/panes/grid.rs b/src/client/panes/grid.rs similarity index 100% rename from src/panes/grid.rs rename to src/client/panes/grid.rs diff --git a/src/panes/mod.rs b/src/client/panes/mod.rs similarity index 100% rename from src/panes/mod.rs rename to src/client/panes/mod.rs diff --git a/src/panes/plugin_pane.rs b/src/client/panes/plugin_pane.rs similarity index 98% rename from src/panes/plugin_pane.rs rename to src/client/panes/plugin_pane.rs index aeda170a..ff173d18 100644 --- a/src/panes/plugin_pane.rs +++ b/src/client/panes/plugin_pane.rs @@ -1,6 +1,6 @@ #![allow(clippy::clippy::if_same_then_else)] -use crate::{pty_bus::VteEvent, tab::Pane, wasm_vm::PluginInstruction, SenderWithContext}; +use crate::{common::SenderWithContext, pty_bus::VteEvent, tab::Pane, wasm_vm::PluginInstruction}; use std::{sync::mpsc::channel, unimplemented}; diff --git a/src/panes/terminal_character.rs b/src/client/panes/terminal_character.rs similarity index 100% rename from src/panes/terminal_character.rs rename to src/client/panes/terminal_character.rs diff --git a/src/panes/terminal_pane.rs b/src/client/panes/terminal_pane.rs similarity index 100% rename from src/panes/terminal_pane.rs rename to src/client/panes/terminal_pane.rs diff --git a/src/tab.rs b/src/client/tab.rs similarity index 99% rename from src/tab.rs rename to src/client/tab.rs index 190292a0..3b49a1c4 100644 --- a/src/tab.rs +++ b/src/client/tab.rs @@ -1,9 +1,9 @@ +use crate::common::{AppInstruction, SenderWithContext}; use crate::panes::{PaneId, PositionAndSize, TerminalPane}; use crate::pty_bus::{PtyInstruction, VteEvent}; use crate::{boundaries::Boundaries, panes::PluginPane}; use crate::{layout::Layout, wasm_vm::PluginInstruction}; use crate::{os_input_output::OsApi, utils::shared::pad_to_size}; -use crate::{AppInstruction, SenderWithContext}; use std::os::unix::io::RawFd; use std::{ cmp::Reverse, diff --git a/src/command_is_executing.rs b/src/common/command_is_executing.rs similarity index 100% rename from src/command_is_executing.rs rename to src/common/command_is_executing.rs diff --git a/src/errors.rs b/src/common/errors.rs similarity index 99% rename from src/errors.rs rename to src/common/errors.rs index ad2ae7eb..90ca6f9e 100644 --- a/src/errors.rs +++ b/src/common/errors.rs @@ -1,13 +1,13 @@ +use super::{AppInstruction, OPENCALLS}; use crate::pty_bus::PtyInstruction; use crate::screen::ScreenInstruction; -use crate::{AppInstruction, OPENCALLS}; use std::fmt::{Display, Error, Formatter}; const MAX_THREAD_CALL_STACK: usize = 6; #[cfg(not(test))] -use crate::SenderWithContext; +use super::SenderWithContext; #[cfg(not(test))] use std::panic::PanicInfo; #[cfg(not(test))] diff --git a/src/input.rs b/src/common/input.rs similarity index 99% rename from src/input.rs rename to src/common/input.rs index 3c5518f8..125b3ced 100644 --- a/src/input.rs +++ b/src/common/input.rs @@ -1,9 +1,9 @@ +use super::{os_input_output::OsApi, update_state, AppState}; +use super::{AppInstruction, SenderWithContext, OPENCALLS}; use crate::pty_bus::PtyInstruction; use crate::screen::ScreenInstruction; use crate::CommandIsExecuting; use crate::{errors::ContextType, wasm_vm::PluginInstruction}; -use crate::{os_input_output::OsApi, update_state, AppState}; -use crate::{AppInstruction, SenderWithContext, OPENCALLS}; struct InputHandler { mode: InputMode, diff --git a/src/common/ipc.rs b/src/common/ipc.rs new file mode 100644 index 00000000..425d81d6 --- /dev/null +++ b/src/common/ipc.rs @@ -0,0 +1,46 @@ +// IPC stuff for starting to split things into a client and server model +use serde::{Deserialize, Serialize}; +use std::collections::HashSet; + +type SessionID = u64; + +#[derive(PartialEq, Eq, Serialize, Deserialize, Hash)] +pub struct Session { + // Unique ID for this session + id: SessionID, + // Identifier for the underlying IPC primitive (socket, pipe) + conn_name: String, + // User configured alias for the session + alias: String, +} + +// How do we want to connect to a session? +#[derive(Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum ClientType { + Reader, + Writer, +} + +// Types of messages sent from the client to the server +#[derive(Serialize, Deserialize)] +pub enum ClientToServerMsg { + // List which sessions are available + ListSessions, + // Create a new session + CreateSession, + // Attach to a running session + AttachToSession(SessionID, ClientType), + // Force detach + DetachSession(SessionID), + // Disconnect from the session we're connected to + DisconnectFromSession, +} + +// Types of messages sent from the server to the client +// @@@ Implement Serialize and Deserialize for this... +pub enum ServerToClientMsg { + // Info about a particular session + SessionInfo(Session), + // A list of sessions + SessionList(HashSet), +} diff --git a/src/common/mod.rs b/src/common/mod.rs new file mode 100644 index 00000000..ef9919a6 --- /dev/null +++ b/src/common/mod.rs @@ -0,0 +1,662 @@ +pub mod command_is_executing; +pub mod errors; +pub mod input; +pub mod ipc; +pub mod os_input_output; +pub mod pty_bus; +pub mod screen; +pub mod utils; +pub mod wasm_vm; + +use std::io::Write; +use std::path::{Path, PathBuf}; +use std::sync::mpsc::{channel, sync_channel, Receiver, SendError, Sender, SyncSender}; +use std::thread; +use std::{cell::RefCell, sync::mpsc::TrySendError}; +use std::{collections::HashMap, fs}; + +use crate::panes::PaneId; +use directories_next::ProjectDirs; +use input::InputMode; +use serde::{Deserialize, Serialize}; +use termion::input::TermRead; +use wasm_vm::PluginEnv; +use wasmer::{ChainableNamedResolver, Instance, Module, Store, Value}; +use wasmer_wasi::{Pipe, WasiState}; + +use crate::cli::CliArgs; +use crate::layout::Layout; +use command_is_executing::CommandIsExecuting; +use errors::{AppContext, ContextType, ErrorContext, PluginContext, PtyContext, ScreenContext}; +use input::input_loop; +use os_input_output::OsApi; +use pty_bus::{PtyBus, PtyInstruction}; +use screen::{Screen, ScreenInstruction}; +use utils::consts::{MOSAIC_IPC_PIPE, MOSAIC_ROOT_PLUGIN_DIR}; +use wasm_vm::{mosaic_imports, wasi_stdout, wasi_write_string, PluginInstruction}; + +#[derive(Serialize, Deserialize, Debug)] +pub enum ApiCommand { + OpenFile(PathBuf), + SplitHorizontally, + SplitVertically, + MoveFocus, +} +// FIXME: It would be good to add some more things to this over time +#[derive(Debug, Clone)] +pub struct AppState { + pub input_mode: InputMode, +} + +impl Default for AppState { + fn default() -> Self { + Self { + input_mode: InputMode::Normal, + } + } +} +// FIXME: Make this a method on the big `Communication` struct, so that app_tx can be extracted +// from self instead of being explicitly passed here +pub fn update_state( + app_tx: &SenderWithContext, + update_fn: impl FnOnce(AppState) -> AppState, +) { + let (state_tx, state_rx) = channel(); + + drop(app_tx.send(AppInstruction::GetState(state_tx))); + let state = state_rx.recv().unwrap(); + + drop(app_tx.send(AppInstruction::SetState(update_fn(state)))) +} + +pub type ChannelWithContext = (Sender<(T, ErrorContext)>, Receiver<(T, ErrorContext)>); +pub type SyncChannelWithContext = (SyncSender<(T, ErrorContext)>, Receiver<(T, ErrorContext)>); + +#[derive(Clone)] +enum SenderType { + Sender(Sender<(T, ErrorContext)>), + SyncSender(SyncSender<(T, ErrorContext)>), +} + +#[derive(Clone)] +pub struct SenderWithContext { + err_ctx: ErrorContext, + sender: SenderType, +} + +impl SenderWithContext { + fn new(err_ctx: ErrorContext, sender: SenderType) -> Self { + Self { err_ctx, sender } + } + + pub fn send(&self, event: T) -> Result<(), SendError<(T, ErrorContext)>> { + match self.sender { + SenderType::Sender(ref s) => s.send((event, self.err_ctx)), + SenderType::SyncSender(ref s) => s.send((event, self.err_ctx)), + } + } + + pub fn try_send(&self, event: T) -> Result<(), TrySendError<(T, ErrorContext)>> { + if let SenderType::SyncSender(ref s) = self.sender { + s.try_send((event, self.err_ctx)) + } else { + panic!("try_send can only be called on SyncSenders!") + } + } + + pub fn update(&mut self, new_ctx: ErrorContext) { + self.err_ctx = new_ctx; + } +} + +unsafe impl Send for SenderWithContext {} +unsafe impl Sync for SenderWithContext {} + +thread_local!(static OPENCALLS: RefCell = RefCell::default()); + +#[derive(Clone)] +pub enum AppInstruction { + GetState(Sender), + SetState(AppState), + Exit, + Error(String), +} + +pub fn start(mut os_input: Box, opts: CliArgs) { + let take_snapshot = "\u{1b}[?1049h"; + os_input.unset_raw_mode(0); + let _ = os_input + .get_stdout_writer() + .write(take_snapshot.as_bytes()) + .unwrap(); + let mut app_state = AppState::default(); + + let command_is_executing = CommandIsExecuting::new(); + + let full_screen_ws = os_input.get_terminal_size_using_fd(0); + os_input.set_raw_mode(0); + let (send_screen_instructions, receive_screen_instructions): ChannelWithContext< + ScreenInstruction, + > = channel(); + let err_ctx = OPENCALLS.with(|ctx| *ctx.borrow()); + let mut send_screen_instructions = + SenderWithContext::new(err_ctx, SenderType::Sender(send_screen_instructions)); + + let (send_pty_instructions, receive_pty_instructions): ChannelWithContext = + channel(); + let mut send_pty_instructions = + SenderWithContext::new(err_ctx, SenderType::Sender(send_pty_instructions)); + + let (send_plugin_instructions, receive_plugin_instructions): ChannelWithContext< + PluginInstruction, + > = channel(); + let send_plugin_instructions = + SenderWithContext::new(err_ctx, SenderType::Sender(send_plugin_instructions)); + + let (send_app_instructions, receive_app_instructions): SyncChannelWithContext = + sync_channel(0); + let send_app_instructions = + SenderWithContext::new(err_ctx, SenderType::SyncSender(send_app_instructions)); + + let mut pty_bus = PtyBus::new( + receive_pty_instructions, + send_screen_instructions.clone(), + send_plugin_instructions.clone(), + os_input.clone(), + opts.debug, + ); + + // Don't use default layouts in tests, but do everywhere else + #[cfg(not(test))] + let default_layout = Some(PathBuf::from("default")); + #[cfg(test)] + let default_layout = None; + let maybe_layout = opts.layout.or(default_layout).map(Layout::new); + + #[cfg(not(test))] + std::panic::set_hook({ + use errors::handle_panic; + let send_app_instructions = send_app_instructions.clone(); + Box::new(move |info| { + handle_panic(info, &send_app_instructions); + }) + }); + + let pty_thread = thread::Builder::new() + .name("pty".to_string()) + .spawn({ + let mut command_is_executing = command_is_executing.clone(); + send_pty_instructions.send(PtyInstruction::NewTab).unwrap(); + 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))); + pty_bus.send_screen_instructions.update(err_ctx); + 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); + command_is_executing.done_closing_pane(); + } + PtyInstruction::CloseTab(ids) => { + pty_bus.close_tab(ids); + command_is_executing.done_closing_pane(); + } + PtyInstruction::Quit => { + break; + } + } + } + }) + .unwrap(); + + let screen_thread = thread::Builder::new() + .name("screen".to_string()) + .spawn({ + let mut command_is_executing = command_is_executing.clone(); + let os_input = os_input.clone(); + let send_pty_instructions = send_pty_instructions.clone(); + let send_plugin_instructions = send_plugin_instructions.clone(); + let send_app_instructions = send_app_instructions.clone(); + let max_panes = opts.max_panes; + + move || { + let mut screen = Screen::new( + receive_screen_instructions, + send_pty_instructions, + send_plugin_instructions, + send_app_instructions, + &full_screen_ws, + os_input, + max_panes, + ); + 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))); + screen.send_app_instructions.update(err_ctx); + screen.send_pty_instructions.update(err_ctx); + match event { + ScreenInstruction::Pty(pid, vte_event) => { + screen + .get_active_tab_mut() + .unwrap() + .handle_pty_event(pid, vte_event); + } + ScreenInstruction::Render => { + screen.render(); + } + ScreenInstruction::NewPane(pid) => { + screen.get_active_tab_mut().unwrap().new_pane(pid); + command_is_executing.done_opening_new_pane(); + } + ScreenInstruction::HorizontalSplit(pid) => { + screen.get_active_tab_mut().unwrap().horizontal_split(pid); + command_is_executing.done_opening_new_pane(); + } + ScreenInstruction::VerticalSplit(pid) => { + screen.get_active_tab_mut().unwrap().vertical_split(pid); + command_is_executing.done_opening_new_pane(); + } + ScreenInstruction::WriteCharacter(bytes) => { + screen + .get_active_tab_mut() + .unwrap() + .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::MoveFocus => { + screen.get_active_tab_mut().unwrap().move_focus(); + } + 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::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); + // FIXME: Is this needed? + screen.render(); + } + ScreenInstruction::SetMaxHeight(id, max_height) => { + screen + .get_active_tab_mut() + .unwrap() + .set_pane_max_height(id, max_height); + } + 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); + command_is_executing.done_opening_new_pane(); + } + ScreenInstruction::SwitchTabNext => screen.switch_tab_next(), + ScreenInstruction::SwitchTabPrev => screen.switch_tab_prev(), + ScreenInstruction::CloseTab => screen.close_tab(), + ScreenInstruction::ApplyLayout((layout, new_pane_pids)) => { + screen.apply_layout(layout, new_pane_pids); + command_is_executing.done_opening_new_pane(); + } + ScreenInstruction::Quit => { + break; + } + } + } + } + }) + .unwrap(); + + let wasm_thread = thread::Builder::new() + .name("wasm".to_string()) + .spawn({ + let mut send_pty_instructions = send_pty_instructions.clone(); + let mut send_screen_instructions = send_screen_instructions.clone(); + let mut send_app_instructions = send_app_instructions.clone(); + + 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))); + send_screen_instructions.update(err_ctx); + send_pty_instructions.update(err_ctx); + send_app_instructions.update(err_ctx); + match event { + PluginInstruction::Load(pid_tx, path) => { + let project_dirs = + ProjectDirs::from("org", "Mosaic Contributors", "Mosaic").unwrap(); + let plugin_dir = project_dirs.data_dir().join("plugins/"); + let root_plugin_dir = Path::new(MOSAIC_ROOT_PLUGIN_DIR); + 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"))) + .or_else(|_| { + fs::read(&root_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("mosaic") + .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_pty_instructions: send_pty_instructions.clone(), + send_screen_instructions: send_screen_instructions.clone(), + send_app_instructions: send_app_instructions.clone(), + wasi_env, + }; + + let mosaic = mosaic_imports(&store, &plugin_env); + let instance = Instance::new(&module, &mosaic.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::Draw(buf_tx, pid, rows, cols) => { + let (instance, plugin_env) = plugin_map.get(&pid).unwrap(); + + let draw = instance.exports.get_function("draw").unwrap(); + + draw.call(&[Value::I32(rows as i32), Value::I32(cols as i32)]) + .unwrap(); + + buf_tx.send(wasi_stdout(&plugin_env.wasi_env)).unwrap(); + } + // FIXME: Deduplicate this with the callback below! + PluginInstruction::Input(pid, input_bytes) => { + let (instance, plugin_env) = plugin_map.get(&pid).unwrap(); + + let handle_key = instance.exports.get_function("handle_key").unwrap(); + for key in input_bytes.keys() { + if let Ok(key) = key { + wasi_write_string( + &plugin_env.wasi_env, + &serde_json::to_string(&key).unwrap(), + ); + handle_key.call(&[]).unwrap(); + } + } + + drop(send_screen_instructions.send(ScreenInstruction::Render)); + } + PluginInstruction::GlobalInput(input_bytes) => { + // FIXME: Set up an event subscription system, and timed callbacks + for (instance, plugin_env) in plugin_map.values() { + let handler = + instance.exports.get_function("handle_global_key").unwrap(); + for key in input_bytes.keys() { + if let Ok(key) = key { + wasi_write_string( + &plugin_env.wasi_env, + &serde_json::to_string(&key).unwrap(), + ); + handler.call(&[]).unwrap(); + } + } + } + + drop(send_screen_instructions.send(ScreenInstruction::Render)); + } + PluginInstruction::Unload(pid) => drop(plugin_map.remove(&pid)), + PluginInstruction::Quit => break, + } + } + }) + .unwrap(); + + // TODO: currently we don't wait for this to quit + // because otherwise the app will hang. Need to fix this so it both + // listens to the ipc-bus and is able to quit cleanly + #[cfg(not(test))] + let _ipc_thread = thread::Builder::new() + .name("ipc_server".to_string()) + .spawn({ + use std::io::Read; + let mut send_pty_instructions = send_pty_instructions.clone(); + let mut send_screen_instructions = send_screen_instructions.clone(); + move || { + std::fs::remove_file(MOSAIC_IPC_PIPE).ok(); + let listener = std::os::unix::net::UnixListener::bind(MOSAIC_IPC_PIPE) + .expect("could not listen on ipc socket"); + let mut err_ctx = OPENCALLS.with(|ctx| *ctx.borrow()); + err_ctx.add_call(ContextType::IPCServer); + send_pty_instructions.update(err_ctx); + send_screen_instructions.update(err_ctx); + + for stream in listener.incoming() { + match stream { + Ok(mut stream) => { + let mut buffer = [0; 65535]; // TODO: more accurate + let _ = stream + .read(&mut buffer) + .expect("failed to parse ipc message"); + let decoded: ApiCommand = bincode::deserialize(&buffer) + .expect("failed to deserialize ipc message"); + match &decoded { + ApiCommand::OpenFile(file_name) => { + let path = PathBuf::from(file_name); + send_pty_instructions + .send(PtyInstruction::SpawnTerminal(Some(path))) + .unwrap(); + } + ApiCommand::SplitHorizontally => { + send_pty_instructions + .send(PtyInstruction::SpawnTerminalHorizontally(None)) + .unwrap(); + } + ApiCommand::SplitVertically => { + send_pty_instructions + .send(PtyInstruction::SpawnTerminalVertically(None)) + .unwrap(); + } + ApiCommand::MoveFocus => { + send_screen_instructions + .send(ScreenInstruction::MoveFocus) + .unwrap(); + } + } + } + Err(err) => { + panic!("err {:?}", err); + } + } + } + } + }) + .unwrap(); + + let _stdin_thread = thread::Builder::new() + .name("stdin_handler".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 os_input = os_input.clone(); + move || { + input_loop( + os_input, + command_is_executing, + send_screen_instructions, + send_pty_instructions, + send_plugin_instructions, + send_app_instructions, + ) + } + }); + + #[warn(clippy::never_loop)] + loop { + let (app_instruction, mut err_ctx) = receive_app_instructions + .recv() + .expect("failed to receive app instruction on channel"); + + err_ctx.add_call(ContextType::App(AppContext::from(&app_instruction))); + send_screen_instructions.update(err_ctx); + send_pty_instructions.update(err_ctx); + match app_instruction { + AppInstruction::GetState(state_tx) => drop(state_tx.send(app_state.clone())), + AppInstruction::SetState(state) => app_state = state, + AppInstruction::Exit => { + let _ = send_screen_instructions.send(ScreenInstruction::Quit); + let _ = send_pty_instructions.send(PtyInstruction::Quit); + let _ = send_plugin_instructions.send(PluginInstruction::Quit); + break; + } + AppInstruction::Error(backtrace) => { + let _ = send_screen_instructions.send(ScreenInstruction::Quit); + let _ = screen_thread.join(); + let _ = send_pty_instructions.send(PtyInstruction::Quit); + let _ = pty_thread.join(); + let _ = send_plugin_instructions.send(PluginInstruction::Quit); + let _ = wasm_thread.join(); + os_input.unset_raw_mode(0); + let goto_start_of_last_line = format!("\u{1b}[{};{}H", full_screen_ws.rows, 1); + let error = format!("{}\n{}", goto_start_of_last_line, backtrace); + let _ = os_input + .get_stdout_writer() + .write(error.as_bytes()) + .unwrap(); + std::process::exit(1); + } + } + } + + let _ = send_screen_instructions.send(ScreenInstruction::Quit); + screen_thread.join().unwrap(); + let _ = send_pty_instructions.send(PtyInstruction::Quit); + pty_thread.join().unwrap(); + let _ = send_plugin_instructions.send(PluginInstruction::Quit); + wasm_thread.join().unwrap(); + + // cleanup(); + let reset_style = "\u{1b}[m"; + let show_cursor = "\u{1b}[?25h"; + 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 Mosaic!", + goto_start_of_last_line, restore_snapshot, reset_style, show_cursor + ); + + os_input.unset_raw_mode(0); + let _ = os_input + .get_stdout_writer() + .write(goodbye_message.as_bytes()) + .unwrap(); + os_input.get_stdout_writer().flush().unwrap(); +} diff --git a/src/os_input_output.rs b/src/common/os_input_output.rs similarity index 100% rename from src/os_input_output.rs rename to src/common/os_input_output.rs diff --git a/src/pty_bus.rs b/src/common/pty_bus.rs similarity index 99% rename from src/pty_bus.rs rename to src/common/pty_bus.rs index c7458cfe..d341662d 100644 --- a/src/pty_bus.rs +++ b/src/common/pty_bus.rs @@ -9,6 +9,7 @@ use ::std::time::{Duration, Instant}; use ::vte; use std::path::PathBuf; +use super::{ScreenInstruction, SenderWithContext, OPENCALLS}; use crate::os_input_output::OsApi; use crate::utils::logging::debug_to_file; use crate::{ @@ -16,7 +17,6 @@ use crate::{ panes::PaneId, }; use crate::{layout::Layout, wasm_vm::PluginInstruction}; -use crate::{ScreenInstruction, SenderWithContext, OPENCALLS}; pub struct ReadFromPid { pid: RawFd, diff --git a/src/screen.rs b/src/common/screen.rs similarity index 99% rename from src/screen.rs rename to src/common/screen.rs index 551fff65..2438dba0 100644 --- a/src/screen.rs +++ b/src/common/screen.rs @@ -2,13 +2,13 @@ use std::collections::BTreeMap; use std::os::unix::io::RawFd; use std::sync::mpsc::Receiver; +use super::{AppInstruction, SenderWithContext}; use crate::os_input_output::OsApi; use crate::panes::PositionAndSize; use crate::pty_bus::{PtyInstruction, VteEvent}; use crate::tab::Tab; use crate::{errors::ErrorContext, wasm_vm::PluginInstruction}; use crate::{layout::Layout, panes::PaneId}; -use crate::{AppInstruction, SenderWithContext}; /* * Screen diff --git a/src/utils/consts.rs b/src/common/utils/consts.rs similarity index 100% rename from src/utils/consts.rs rename to src/common/utils/consts.rs diff --git a/src/utils/logging.rs b/src/common/utils/logging.rs similarity index 100% rename from src/utils/logging.rs rename to src/common/utils/logging.rs diff --git a/src/utils/mod.rs b/src/common/utils/mod.rs similarity index 100% rename from src/utils/mod.rs rename to src/common/utils/mod.rs diff --git a/src/utils/shared.rs b/src/common/utils/shared.rs similarity index 100% rename from src/utils/shared.rs rename to src/common/utils/shared.rs diff --git a/src/wasm_vm.rs b/src/common/wasm_vm.rs similarity index 96% rename from src/wasm_vm.rs rename to src/common/wasm_vm.rs index 5a68f636..d678401d 100644 --- a/src/wasm_vm.rs +++ b/src/common/wasm_vm.rs @@ -5,9 +5,9 @@ use std::{ use wasmer::{imports, Function, ImportObject, Store, WasmerEnv}; use wasmer_wasi::WasiEnv; -use crate::{ - input::get_help, panes::PaneId, pty_bus::PtyInstruction, screen::ScreenInstruction, - AppInstruction, SenderWithContext, +use super::{ + input::get_help, pty_bus::PtyInstruction, screen::ScreenInstruction, AppInstruction, PaneId, + SenderWithContext, }; #[derive(Clone, Debug)] diff --git a/src/main.rs b/src/main.rs index 140c95e8..aea289fb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,108 +1,30 @@ #[cfg(test)] mod tests; -mod boundaries; mod cli; -mod command_is_executing; -mod errors; -mod input; -mod layout; -mod os_input_output; -mod panes; -mod pty_bus; -mod screen; -mod tab; -mod utils; +mod common; +// TODO mod server; +mod client; -mod wasm_vm; +use client::{boundaries, layout, panes, tab}; +use common::{ + command_is_executing, errors, ipc, os_input_output, pty_bus, screen, start, utils, wasm_vm, + ApiCommand, +}; use std::io::Write; use std::os::unix::net::UnixStream; -use std::path::{Path, PathBuf}; -use std::sync::mpsc::{channel, sync_channel, Receiver, SendError, Sender, SyncSender}; -use std::thread; -use std::{cell::RefCell, sync::mpsc::TrySendError}; -use std::{collections::HashMap, fs}; -use directories_next::ProjectDirs; -use input::InputMode; -use panes::PaneId; -use serde::{Deserialize, Serialize}; use structopt::StructOpt; -use termion::input::TermRead; -use wasm_vm::PluginEnv; -use wasmer::{ChainableNamedResolver, Instance, Module, Store, Value}; -use wasmer_wasi::{Pipe, WasiState}; use crate::cli::CliArgs; use crate::command_is_executing::CommandIsExecuting; -use crate::errors::{ - AppContext, ContextType, ErrorContext, PluginContext, PtyContext, ScreenContext, -}; -use crate::input::input_loop; -use crate::layout::Layout; -use crate::os_input_output::{get_os_input, OsApi}; -use crate::pty_bus::{PtyBus, PtyInstruction, VteEvent}; -use crate::screen::{Screen, ScreenInstruction}; -use crate::utils::consts::MOSAIC_ROOT_PLUGIN_DIR; +use crate::os_input_output::get_os_input; +use crate::pty_bus::VteEvent; use crate::utils::{ consts::{MOSAIC_IPC_PIPE, MOSAIC_TMP_DIR, MOSAIC_TMP_LOG_DIR}, logging::*, }; -use crate::wasm_vm::{mosaic_imports, wasi_stdout, wasi_write_string, PluginInstruction}; - -thread_local!(static OPENCALLS: RefCell = RefCell::default()); - -#[derive(Serialize, Deserialize, Debug)] -enum ApiCommand { - OpenFile(PathBuf), - SplitHorizontally, - SplitVertically, - MoveFocus, -} - -pub type ChannelWithContext = (Sender<(T, ErrorContext)>, Receiver<(T, ErrorContext)>); -pub type SyncChannelWithContext = (SyncSender<(T, ErrorContext)>, Receiver<(T, ErrorContext)>); - -#[derive(Clone)] -enum SenderType { - Sender(Sender<(T, ErrorContext)>), - SyncSender(SyncSender<(T, ErrorContext)>), -} - -#[derive(Clone)] -pub struct SenderWithContext { - err_ctx: ErrorContext, - sender: SenderType, -} - -impl SenderWithContext { - fn new(err_ctx: ErrorContext, sender: SenderType) -> Self { - Self { err_ctx, sender } - } - - pub fn send(&self, event: T) -> Result<(), SendError<(T, ErrorContext)>> { - match self.sender { - SenderType::Sender(ref s) => s.send((event, self.err_ctx)), - SenderType::SyncSender(ref s) => s.send((event, self.err_ctx)), - } - } - - pub fn try_send(&self, event: T) -> Result<(), TrySendError<(T, ErrorContext)>> { - if let SenderType::SyncSender(ref s) = self.sender { - s.try_send((event, self.err_ctx)) - } else { - panic!("try_send can only be called on SyncSenders!") - } - } - - pub fn update(&mut self, new_ctx: ErrorContext) { - self.err_ctx = new_ctx; - } -} - -unsafe impl Send for SenderWithContext {} -unsafe impl Sync for SenderWithContext {} pub fn main() { let opts = CliArgs::from_args(); @@ -135,578 +57,3 @@ pub fn main() { start(Box::new(os_input), opts); } } - -// FIXME: It would be good to add some more things to this over time -#[derive(Debug, Clone)] -pub struct AppState { - pub input_mode: InputMode, -} - -impl Default for AppState { - fn default() -> Self { - Self { - input_mode: InputMode::Normal, - } - } -} - -// FIXME: Make this a method on the big `Communication` struct, so that app_tx can be extracted -// from self instead of being explicitly passed here -pub fn update_state( - app_tx: &SenderWithContext, - update_fn: impl FnOnce(AppState) -> AppState, -) { - let (state_tx, state_rx) = channel(); - - drop(app_tx.send(AppInstruction::GetState(state_tx))); - let state = state_rx.recv().unwrap(); - - drop(app_tx.send(AppInstruction::SetState(update_fn(state)))) -} - -#[derive(Clone)] -pub enum AppInstruction { - GetState(Sender), - SetState(AppState), - Exit, - Error(String), -} - -pub fn start(mut os_input: Box, opts: CliArgs) { - let take_snapshot = "\u{1b}[?1049h"; - os_input.unset_raw_mode(0); - let _ = os_input - .get_stdout_writer() - .write(take_snapshot.as_bytes()) - .unwrap(); - let mut app_state = AppState::default(); - - let command_is_executing = CommandIsExecuting::new(); - - let full_screen_ws = os_input.get_terminal_size_using_fd(0); - os_input.set_raw_mode(0); - let (send_screen_instructions, receive_screen_instructions): ChannelWithContext< - ScreenInstruction, - > = channel(); - let err_ctx = OPENCALLS.with(|ctx| *ctx.borrow()); - let mut send_screen_instructions = - SenderWithContext::new(err_ctx, SenderType::Sender(send_screen_instructions)); - - let (send_pty_instructions, receive_pty_instructions): ChannelWithContext = - channel(); - let mut send_pty_instructions = - SenderWithContext::new(err_ctx, SenderType::Sender(send_pty_instructions)); - - let (send_plugin_instructions, receive_plugin_instructions): ChannelWithContext< - PluginInstruction, - > = channel(); - let send_plugin_instructions = - SenderWithContext::new(err_ctx, SenderType::Sender(send_plugin_instructions)); - - let (send_app_instructions, receive_app_instructions): SyncChannelWithContext = - sync_channel(0); - let send_app_instructions = - SenderWithContext::new(err_ctx, SenderType::SyncSender(send_app_instructions)); - - let mut pty_bus = PtyBus::new( - receive_pty_instructions, - send_screen_instructions.clone(), - send_plugin_instructions.clone(), - os_input.clone(), - opts.debug, - ); - - // Don't use default layouts in tests, but do everywhere else - #[cfg(not(test))] - let default_layout = Some(PathBuf::from("default")); - #[cfg(test)] - let default_layout = None; - let maybe_layout = opts.layout.or(default_layout).map(Layout::new); - - #[cfg(not(test))] - std::panic::set_hook({ - use crate::errors::handle_panic; - let send_app_instructions = send_app_instructions.clone(); - Box::new(move |info| { - handle_panic(info, &send_app_instructions); - }) - }); - - let pty_thread = thread::Builder::new() - .name("pty".to_string()) - .spawn({ - let mut command_is_executing = command_is_executing.clone(); - send_pty_instructions.send(PtyInstruction::NewTab).unwrap(); - 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))); - pty_bus.send_screen_instructions.update(err_ctx); - 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); - command_is_executing.done_closing_pane(); - } - PtyInstruction::CloseTab(ids) => { - pty_bus.close_tab(ids); - command_is_executing.done_closing_pane(); - } - PtyInstruction::Quit => { - break; - } - } - } - }) - .unwrap(); - - let screen_thread = thread::Builder::new() - .name("screen".to_string()) - .spawn({ - let mut command_is_executing = command_is_executing.clone(); - let os_input = os_input.clone(); - let send_pty_instructions = send_pty_instructions.clone(); - let send_plugin_instructions = send_plugin_instructions.clone(); - let send_app_instructions = send_app_instructions.clone(); - let max_panes = opts.max_panes; - - move || { - let mut screen = Screen::new( - receive_screen_instructions, - send_pty_instructions, - send_plugin_instructions, - send_app_instructions, - &full_screen_ws, - os_input, - max_panes, - ); - 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))); - screen.send_app_instructions.update(err_ctx); - screen.send_pty_instructions.update(err_ctx); - match event { - ScreenInstruction::Pty(pid, vte_event) => { - screen - .get_active_tab_mut() - .unwrap() - .handle_pty_event(pid, vte_event); - } - ScreenInstruction::Render => { - screen.render(); - } - ScreenInstruction::NewPane(pid) => { - screen.get_active_tab_mut().unwrap().new_pane(pid); - command_is_executing.done_opening_new_pane(); - } - ScreenInstruction::HorizontalSplit(pid) => { - screen.get_active_tab_mut().unwrap().horizontal_split(pid); - command_is_executing.done_opening_new_pane(); - } - ScreenInstruction::VerticalSplit(pid) => { - screen.get_active_tab_mut().unwrap().vertical_split(pid); - command_is_executing.done_opening_new_pane(); - } - ScreenInstruction::WriteCharacter(bytes) => { - screen - .get_active_tab_mut() - .unwrap() - .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::MoveFocus => { - screen.get_active_tab_mut().unwrap().move_focus(); - } - 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::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); - // FIXME: Is this needed? - screen.render(); - } - ScreenInstruction::SetMaxHeight(id, max_height) => { - screen - .get_active_tab_mut() - .unwrap() - .set_pane_max_height(id, max_height); - } - 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); - command_is_executing.done_opening_new_pane(); - } - ScreenInstruction::SwitchTabNext => screen.switch_tab_next(), - ScreenInstruction::SwitchTabPrev => screen.switch_tab_prev(), - ScreenInstruction::CloseTab => screen.close_tab(), - ScreenInstruction::ApplyLayout((layout, new_pane_pids)) => { - screen.apply_layout(layout, new_pane_pids); - command_is_executing.done_opening_new_pane(); - } - ScreenInstruction::Quit => { - break; - } - } - } - } - }) - .unwrap(); - - let wasm_thread = thread::Builder::new() - .name("wasm".to_string()) - .spawn({ - let mut send_pty_instructions = send_pty_instructions.clone(); - let mut send_screen_instructions = send_screen_instructions.clone(); - let mut send_app_instructions = send_app_instructions.clone(); - - 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))); - send_screen_instructions.update(err_ctx); - send_pty_instructions.update(err_ctx); - send_app_instructions.update(err_ctx); - match event { - PluginInstruction::Load(pid_tx, path) => { - let project_dirs = - ProjectDirs::from("org", "Mosaic Contributors", "Mosaic").unwrap(); - let plugin_dir = project_dirs.data_dir().join("plugins/"); - let root_plugin_dir = Path::new(MOSAIC_ROOT_PLUGIN_DIR); - 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"))) - .or_else(|_| { - fs::read(&root_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("mosaic") - .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_pty_instructions: send_pty_instructions.clone(), - send_screen_instructions: send_screen_instructions.clone(), - send_app_instructions: send_app_instructions.clone(), - wasi_env, - }; - - let mosaic = mosaic_imports(&store, &plugin_env); - let instance = Instance::new(&module, &mosaic.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::Draw(buf_tx, pid, rows, cols) => { - let (instance, plugin_env) = plugin_map.get(&pid).unwrap(); - - let draw = instance.exports.get_function("draw").unwrap(); - - draw.call(&[Value::I32(rows as i32), Value::I32(cols as i32)]) - .unwrap(); - - buf_tx.send(wasi_stdout(&plugin_env.wasi_env)).unwrap(); - } - // FIXME: Deduplicate this with the callback below! - PluginInstruction::Input(pid, input_bytes) => { - let (instance, plugin_env) = plugin_map.get(&pid).unwrap(); - - let handle_key = instance.exports.get_function("handle_key").unwrap(); - for key in input_bytes.keys() { - if let Ok(key) = key { - wasi_write_string( - &plugin_env.wasi_env, - &serde_json::to_string(&key).unwrap(), - ); - handle_key.call(&[]).unwrap(); - } - } - - drop(send_screen_instructions.send(ScreenInstruction::Render)); - } - PluginInstruction::GlobalInput(input_bytes) => { - // FIXME: Set up an event subscription system, and timed callbacks - for (instance, plugin_env) in plugin_map.values() { - let handler = - instance.exports.get_function("handle_global_key").unwrap(); - for key in input_bytes.keys() { - if let Ok(key) = key { - wasi_write_string( - &plugin_env.wasi_env, - &serde_json::to_string(&key).unwrap(), - ); - handler.call(&[]).unwrap(); - } - } - } - - drop(send_screen_instructions.send(ScreenInstruction::Render)); - } - PluginInstruction::Unload(pid) => drop(plugin_map.remove(&pid)), - PluginInstruction::Quit => break, - } - } - }) - .unwrap(); - - // TODO: currently we don't wait for this to quit - // because otherwise the app will hang. Need to fix this so it both - // listens to the ipc-bus and is able to quit cleanly - #[cfg(not(test))] - let _ipc_thread = thread::Builder::new() - .name("ipc_server".to_string()) - .spawn({ - use std::io::Read; - let mut send_pty_instructions = send_pty_instructions.clone(); - let mut send_screen_instructions = send_screen_instructions.clone(); - move || { - std::fs::remove_file(MOSAIC_IPC_PIPE).ok(); - let listener = std::os::unix::net::UnixListener::bind(MOSAIC_IPC_PIPE) - .expect("could not listen on ipc socket"); - let mut err_ctx = OPENCALLS.with(|ctx| *ctx.borrow()); - err_ctx.add_call(ContextType::IPCServer); - send_pty_instructions.update(err_ctx); - send_screen_instructions.update(err_ctx); - - for stream in listener.incoming() { - match stream { - Ok(mut stream) => { - let mut buffer = [0; 65535]; // TODO: more accurate - let _ = stream - .read(&mut buffer) - .expect("failed to parse ipc message"); - let decoded: ApiCommand = bincode::deserialize(&buffer) - .expect("failed to deserialize ipc message"); - match &decoded { - ApiCommand::OpenFile(file_name) => { - let path = PathBuf::from(file_name); - send_pty_instructions - .send(PtyInstruction::SpawnTerminal(Some(path))) - .unwrap(); - } - ApiCommand::SplitHorizontally => { - send_pty_instructions - .send(PtyInstruction::SpawnTerminalHorizontally(None)) - .unwrap(); - } - ApiCommand::SplitVertically => { - send_pty_instructions - .send(PtyInstruction::SpawnTerminalVertically(None)) - .unwrap(); - } - ApiCommand::MoveFocus => { - send_screen_instructions - .send(ScreenInstruction::MoveFocus) - .unwrap(); - } - } - } - Err(err) => { - panic!("err {:?}", err); - } - } - } - } - }) - .unwrap(); - - let _stdin_thread = thread::Builder::new() - .name("stdin_handler".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 os_input = os_input.clone(); - move || { - input_loop( - os_input, - command_is_executing, - send_screen_instructions, - send_pty_instructions, - send_plugin_instructions, - send_app_instructions, - ) - } - }); - - #[warn(clippy::never_loop)] - loop { - let (app_instruction, mut err_ctx) = receive_app_instructions - .recv() - .expect("failed to receive app instruction on channel"); - - err_ctx.add_call(ContextType::App(AppContext::from(&app_instruction))); - send_screen_instructions.update(err_ctx); - send_pty_instructions.update(err_ctx); - match app_instruction { - AppInstruction::GetState(state_tx) => drop(state_tx.send(app_state.clone())), - AppInstruction::SetState(state) => app_state = state, - AppInstruction::Exit => { - let _ = send_screen_instructions.send(ScreenInstruction::Quit); - let _ = send_pty_instructions.send(PtyInstruction::Quit); - let _ = send_plugin_instructions.send(PluginInstruction::Quit); - break; - } - AppInstruction::Error(backtrace) => { - let _ = send_screen_instructions.send(ScreenInstruction::Quit); - let _ = screen_thread.join(); - let _ = send_pty_instructions.send(PtyInstruction::Quit); - let _ = pty_thread.join(); - let _ = send_plugin_instructions.send(PluginInstruction::Quit); - let _ = wasm_thread.join(); - os_input.unset_raw_mode(0); - let goto_start_of_last_line = format!("\u{1b}[{};{}H", full_screen_ws.rows, 1); - let error = format!("{}\n{}", goto_start_of_last_line, backtrace); - let _ = os_input - .get_stdout_writer() - .write(error.as_bytes()) - .unwrap(); - std::process::exit(1); - } - } - } - - let _ = send_screen_instructions.send(ScreenInstruction::Quit); - screen_thread.join().unwrap(); - let _ = send_pty_instructions.send(PtyInstruction::Quit); - pty_thread.join().unwrap(); - let _ = send_plugin_instructions.send(PluginInstruction::Quit); - wasm_thread.join().unwrap(); - - // cleanup(); - let reset_style = "\u{1b}[m"; - let show_cursor = "\u{1b}[?25h"; - 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 Mosaic!", - goto_start_of_last_line, restore_snapshot, reset_style, show_cursor - ); - - os_input.unset_raw_mode(0); - let _ = os_input - .get_stdout_writer() - .write(goodbye_message.as_bytes()) - .unwrap(); - os_input.get_stdout_writer().flush().unwrap(); -} diff --git a/src/server/mod.rs b/src/server/mod.rs new file mode 100644 index 00000000..626b4e6b --- /dev/null +++ b/src/server/mod.rs @@ -0,0 +1,5 @@ +use super::super::common::{screen}; + +pub fn start_server() { + // TODO +} \ No newline at end of file