diff options
| author | Denis Maximov <denismaximov98@gmail.com> | 2021-01-30 13:45:51 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2021-01-30 13:45:51 +0200 |
| commit | 63a2dc33995fad81c7eec02b23e38afdc3f426e8 (patch) | |
| tree | d060832b4038328b0fb10267a54a0c1baf16c51c /src/common | |
| parent | feat(statusbar): max status bar height (#156) (diff) | |
| parent | chore: formatting (diff) | |
| download | sl0ppij-63a2dc33995fad81c7eec02b23e38afdc3f426e8.tar.gz sl0ppij-63a2dc33995fad81c7eec02b23e38afdc3f426e8.tar.xz sl0ppij-63a2dc33995fad81c7eec02b23e38afdc3f426e8.zip | |
Merge pull request #157 from mosaic-org/restructure
Restructure
Diffstat (limited to 'src/common')
| -rw-r--r-- | src/common/command_is_executing.rs | 53 | ||||
| -rw-r--r-- | src/common/errors.rs | 286 | ||||
| -rw-r--r-- | src/common/input.rs | 359 | ||||
| -rw-r--r-- | src/common/ipc.rs | 46 | ||||
| -rw-r--r-- | src/common/mod.rs | 662 | ||||
| -rw-r--r-- | src/common/os_input_output.rs | 217 | ||||
| -rw-r--r-- | src/common/pty_bus.rs | 317 | ||||
| -rw-r--r-- | src/common/screen.rs | 197 | ||||
| -rw-r--r-- | src/common/utils/consts.rs | 6 | ||||
| -rw-r--r-- | src/common/utils/logging.rs | 76 | ||||
| -rw-r--r-- | src/common/utils/mod.rs | 3 | ||||
| -rw-r--r-- | src/common/utils/shared.rs | 19 | ||||
| -rw-r--r-- | src/common/wasm_vm.rs | 107 |
13 files changed, 2348 insertions, 0 deletions
diff --git a/src/common/command_is_executing.rs b/src/common/command_is_executing.rs new file mode 100644 index 00000000..93c44eb6 --- /dev/null +++ b/src/common/command_is_executing.rs @@ -0,0 +1,53 @@ +#![allow(clippy::mutex_atomic)] +use std::sync::{Arc, Condvar, Mutex}; + +#[derive(Clone)] +pub struct CommandIsExecuting { + opening_new_pane: Arc<(Mutex<bool>, Condvar)>, + closing_pane: Arc<(Mutex<bool>, Condvar)>, +} + +impl CommandIsExecuting { + pub fn new() -> Self { + CommandIsExecuting { + opening_new_pane: Arc::new((Mutex::new(false), Condvar::new())), + closing_pane: Arc::new((Mutex::new(false), Condvar::new())), + } + } + pub fn closing_pane(&mut self) { + let (lock, _cvar) = &*self.closing_pane; + let mut closing_pane = lock.lock().unwrap(); + *closing_pane = true; + } + pub fn done_closing_pane(&mut self) { + let (lock, cvar) = &*self.closing_pane; + let mut closing_pane = lock.lock().unwrap(); + *closing_pane = false; + cvar.notify_one(); + } + pub fn opening_new_pane(&mut self) { + let (lock, _cvar) = &*self.opening_new_pane; + let mut opening_new_pane = lock.lock().unwrap(); + *opening_new_pane = true; + } + pub fn done_opening_new_pane(&mut self) { + let (lock, cvar) = &*self.opening_new_pane; + let mut opening_new_pane = lock.lock().unwrap(); + *opening_new_pane = false; + cvar.notify_one(); + } + pub fn wait_until_pane_is_closed(&self) { + let (lock, cvar) = &*self.closing_pane; + let mut closing_pane = lock.lock().unwrap(); + while *closing_pane { + closing_pane = cvar.wait(closing_pane).unwrap(); + } + } + pub fn wait_until_new_pane_is_opened(&self) { + let (lock, cvar) = &*self.opening_new_pane; + let mut opening_new_pane = lock.lock().unwrap(); + while *opening_new_pane { + opening_new_pane = cvar.wait(opening_new_pane).unwrap(); + } + } +} diff --git a/src/common/errors.rs b/src/common/errors.rs new file mode 100644 index 00000000..90ca6f9e --- /dev/null +++ b/src/common/errors.rs @@ -0,0 +1,286 @@ +use super::{AppInstruction, OPENCALLS}; +use crate::pty_bus::PtyInstruction; +use crate::screen::ScreenInstruction; + +use std::fmt::{Display, Error, Formatter}; + +const MAX_THREAD_CALL_STACK: usize = 6; + +#[cfg(not(test))] +use super::SenderWithContext; +#[cfg(not(test))] +use std::panic::PanicInfo; +#[cfg(not(test))] +pub fn handle_panic( + info: &PanicInfo<'_>, + send_app_instructions: &SenderWithContext<AppInstruction>, +) { + use backtrace::Backtrace; + use std::{process, thread}; + let backtrace = Backtrace::new(); + let thread = thread::current(); + let thread = thread.name().unwrap_or("unnamed"); + + let msg = match info.payload().downcast_ref::<&'static str>() { + Some(s) => Some(*s), + None => info.payload().downcast_ref::<String>().map(|s| &**s), + }; + + let err_ctx = OPENCALLS.with(|ctx| *ctx.borrow()); + + let backtrace = match (info.location(), msg) { + (Some(location), Some(msg)) => format!( + "{}\n\u{1b}[0;0mError: \u{1b}[0;31mthread '{}' panicked at '{}': {}:{}\n\u{1b}[0;0m{:?}", + err_ctx, + thread, + msg, + location.file(), + location.line(), + backtrace + ), + (Some(location), None) => format!( + "{}\n\u{1b}[0;0mError: \u{1b}[0;31mthread '{}' panicked: {}:{}\n\u{1b}[0;0m{:?}", + err_ctx, + thread, + location.file(), + location.line(), + backtrace + ), + (None, Some(msg)) => format!( + "{}\n\u{1b}[0;0mError: \u{1b}[0;31mthread '{}' panicked at '{}'\n\u{1b}[0;0m{:?}", + err_ctx, thread, msg, backtrace + ), + (None, None) => format!( + "{}\n\u{1b}[0;0mError: \u{1b}[0;31mthread '{}' panicked\n\u{1b}[0;0m{:?}", + err_ctx, thread, backtrace + ), + }; + + if thread == "main" { + println!("{}", backtrace); + process::exit(1); + } else { + send_app_instructions + .send(AppInstruction::Error(backtrace)) + .unwrap(); + } +} + +#[derive(Clone, Copy)] +pub struct ErrorContext { + calls: [ContextType; MAX_THREAD_CALL_STACK], +} + +impl ErrorContext { + pub fn new() -> Self { + Self { + calls: [ContextType::Empty; MAX_THREAD_CALL_STACK], + } + } + + pub fn add_call(&mut self, call: ContextType) { + for ctx in self.calls.iter_mut() { + if *ctx == ContextType::Empty { + *ctx = call; + break; + } + } + OPENCALLS.with(|ctx| *ctx.borrow_mut() = *self); + } +} + +impl Default for ErrorContext { + fn default() -> Self { + Self::new() + } +} + +impl Display for ErrorContext { + fn fmt(&self, f: &mut Formatter) -> Result<(), Error> { + writeln!(f, "Originating Thread(s):")?; + for (index, ctx) in self.calls.iter().enumerate() { + if *ctx == ContextType::Empty { + break; + } + writeln!(f, "\u{1b}[0;0m{}. {}", index + 1, ctx)?; + } + Ok(()) + } +} + +#[derive(Copy, Clone, PartialEq)] +pub enum ContextType { + Screen(ScreenContext), + Pty(PtyContext), + + Plugin(PluginContext), + App(AppContext), + IPCServer, + StdinHandler, + AsyncTask, + Empty, +} + +impl Display for ContextType { + fn fmt(&self, f: &mut Formatter) -> Result<(), Error> { + let purple = "\u{1b}[1;35m"; + let green = "\u{1b}[0;32m"; + match *self { + ContextType::Screen(c) => write!(f, "{}screen_thread: {}{:?}", purple, green, c), + ContextType::Pty(c) => write!(f, "{}pty_thread: {}{:?}", purple, green, c), + + ContextType::Plugin(c) => write!(f, "{}plugin_thread: {}{:?}", purple, green, c), + ContextType::App(c) => write!(f, "{}main_thread: {}{:?}", purple, green, c), + ContextType::IPCServer => write!(f, "{}ipc_server: {}AcceptInput", purple, green), + ContextType::StdinHandler => { + write!(f, "{}stdin_handler_thread: {}AcceptInput", purple, green) + } + ContextType::AsyncTask => { + write!(f, "{}stream_terminal_bytes: {}AsyncTask", purple, green) + } + ContextType::Empty => write!(f, ""), + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum ScreenContext { + HandlePtyEvent, + Render, + NewPane, + HorizontalSplit, + VerticalSplit, + WriteCharacter, + ResizeLeft, + ResizeRight, + ResizeDown, + ResizeUp, + MoveFocus, + MoveFocusLeft, + MoveFocusDown, + MoveFocusUp, + MoveFocusRight, + Quit, + ScrollUp, + ScrollDown, + ClearScroll, + CloseFocusedPane, + ToggleActiveTerminalFullscreen, + SetSelectable, + SetMaxHeight, + ClosePane, + ApplyLayout, + NewTab, + SwitchTabNext, + SwitchTabPrev, + CloseTab, +} + +impl From<&ScreenInstruction> for ScreenContext { + fn from(screen_instruction: &ScreenInstruction) -> Self { + match *screen_instruction { + ScreenInstruction::Pty(..) => ScreenContext::HandlePtyEvent, + ScreenInstruction::Render => ScreenContext::Render, + ScreenInstruction::NewPane(_) => ScreenContext::NewPane, + ScreenInstruction::HorizontalSplit(_) => ScreenContext::HorizontalSplit, + ScreenInstruction::VerticalSplit(_) => ScreenContext::VerticalSplit, + ScreenInstruction::WriteCharacter(_) => ScreenContext::WriteCharacter, + ScreenInstruction::ResizeLeft => ScreenContext::ResizeLeft, + ScreenInstruction::ResizeRight => ScreenContext::ResizeRight, + ScreenInstruction::ResizeDown => ScreenContext::ResizeDown, + ScreenInstruction::ResizeUp => ScreenContext::ResizeUp, + ScreenInstruction::MoveFocus => ScreenContext::MoveFocus, + ScreenInstruction::MoveFocusLeft => ScreenContext::MoveFocusLeft, + ScreenInstruction::MoveFocusDown => ScreenContext::MoveFocusDown, + ScreenInstruction::MoveFocusUp => ScreenContext::MoveFocusUp, + ScreenInstruction::MoveFocusRight => ScreenContext::MoveFocusRight, + ScreenInstruction::Quit => ScreenContext::Quit, + ScreenInstruction::ScrollUp => ScreenContext::ScrollUp, + ScreenInstruction::ScrollDown => ScreenContext::ScrollDown, + ScreenInstruction::ClearScroll => ScreenContext::ClearScroll, + ScreenInstruction::CloseFocusedPane => ScreenContext::CloseFocusedPane, + ScreenInstruction::ToggleActiveTerminalFullscreen => { + ScreenContext::ToggleActiveTerminalFullscreen + } + ScreenInstruction::SetSelectable(..) => ScreenContext::SetSelectable, + ScreenInstruction::SetMaxHeight(..) => ScreenContext::SetMaxHeight, + ScreenInstruction::ClosePane(_) => ScreenContext::ClosePane, + ScreenInstruction::ApplyLayout(_) => ScreenContext::ApplyLayout, + ScreenInstruction::NewTab(_) => ScreenContext::NewTab, + ScreenInstruction::SwitchTabNext => ScreenContext::SwitchTabNext, + ScreenInstruction::SwitchTabPrev => ScreenContext::SwitchTabPrev, + ScreenInstruction::CloseTab => ScreenContext::CloseTab, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum PtyContext { + SpawnTerminal, + SpawnTerminalVertically, + SpawnTerminalHorizontally, + NewTab, + ClosePane, + CloseTab, + Quit, +} + +impl From<&PtyInstruction> for PtyContext { + fn from(pty_instruction: &PtyInstruction) -> Self { + match *pty_instruction { + PtyInstruction::SpawnTerminal(_) => PtyContext::SpawnTerminal, + PtyInstruction::SpawnTerminalVertically(_) => PtyContext::SpawnTerminalVertically, + PtyInstruction::SpawnTerminalHorizontally(_) => PtyContext::SpawnTerminalHorizontally, + PtyInstruction::ClosePane(_) => PtyContext::ClosePane, + PtyInstruction::CloseTab(_) => PtyContext::CloseTab, + PtyInstruction::NewTab => PtyContext::NewTab, + PtyInstruction::Quit => PtyContext::Quit, + } + } +} + +// FIXME: This whole pattern *needs* a macro eventually, it's soul-crushing to write + +use crate::wasm_vm::PluginInstruction; + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum PluginContext { + Load, + Draw, + Input, + GlobalInput, + Unload, + Quit, +} + +impl From<&PluginInstruction> for PluginContext { + fn from(plugin_instruction: &PluginInstruction) -> Self { + match *plugin_instruction { + PluginInstruction::Load(..) => PluginContext::Load, + PluginInstruction::Draw(..) => PluginContext::Draw, + PluginInstruction::Input(..) => PluginContext::Input, + PluginInstruction::GlobalInput(_) => PluginContext::GlobalInput, + PluginInstruction::Unload(_) => PluginContext::Unload, + PluginInstruction::Quit => PluginContext::Quit, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum AppContext { + GetState, + SetState, + Exit, + Error, +} + +impl From<&AppInstruction> for AppContext { + fn from(app_instruction: &AppInstruction) -> Self { + match *app_instruction { + AppInstruction::GetState(_) => AppContext::GetState, + AppInstruction::SetState(_) => AppContext::SetState, + AppInstruction::Exit => AppContext::Exit, + AppInstruction::Error(_) => AppContext::Error, + } + } +} diff --git a/src/common/input.rs b/src/common/input.rs new file mode 100644 index 00000000..125b3ced --- /dev/null +++ b/src/common/input.rs @@ -0,0 +1,359 @@ +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}; + +struct InputHandler { + mode: InputMode, + os_input: Box<dyn OsApi>, + command_is_executing: CommandIsExecuting, + send_screen_instructions: SenderWithContext<ScreenInstruction>, + send_pty_instructions: SenderWithContext<PtyInstruction>, + send_plugin_instructions: SenderWithContext<PluginInstruction>, + send_app_instructions: SenderWithContext<AppInstruction>, +} + +impl InputHandler { + fn new( + os_input: Box<dyn OsApi>, + command_is_executing: CommandIsExecuting, + send_screen_instructions: SenderWithContext<ScreenInstruction>, + send_pty_instructions: SenderWithContext<PtyInstruction>, + send_plugin_instructions: SenderWithContext<PluginInstruction>, + send_app_instructions: SenderWithContext<AppInstruction>, + ) -> Self { + InputHandler { + mode: InputMode::Normal, + os_input, + command_is_executing, + send_screen_instructions, + send_pty_instructions, + send_plugin_instructions, + send_app_instructions, + } + } + + /// Main event loop + fn get_input(&mut self) { + let mut err_ctx = OPENCALLS.with(|ctx| *ctx.borrow()); + err_ctx.add_call(ContextType::StdinHandler); + self.send_pty_instructions.update(err_ctx); + self.send_plugin_instructions.update(err_ctx); + self.send_app_instructions.update(err_ctx); + self.send_screen_instructions.update(err_ctx); + loop { + update_state(&self.send_app_instructions, |_| AppState { + input_mode: self.mode, + }); + match self.mode { + InputMode::Normal => self.read_normal_mode(), + InputMode::Command => self.read_command_mode(false), + InputMode::CommandPersistent => self.read_command_mode(true), + InputMode::Exiting => { + self.exit(); + break; + } + } + } + } + + /// Read input to the terminal (or switch to command mode) + fn read_normal_mode(&mut self) { + assert_eq!(self.mode, InputMode::Normal); + + loop { + let stdin_buffer = self.os_input.read_from_stdin(); + #[cfg(not(test))] // Absolutely zero clue why this breaks *all* of the tests + drop( + self.send_plugin_instructions + .send(PluginInstruction::GlobalInput(stdin_buffer.clone())), + ); + match stdin_buffer.as_slice() { + [7] => { + // ctrl-g + self.mode = InputMode::Command; + return; + } + _ => { + self.send_screen_instructions + .send(ScreenInstruction::ClearScroll) + .unwrap(); + self.send_screen_instructions + .send(ScreenInstruction::WriteCharacter(stdin_buffer)) + .unwrap(); + } + } + } + } + + /// Read input and parse it as commands for mosaic + fn read_command_mode(&mut self, persistent: bool) { + //@@@khs26 Add a powerbar type thing that we can write output to + if persistent { + assert_eq!(self.mode, InputMode::CommandPersistent); + } else { + assert_eq!(self.mode, InputMode::Command); + } + + loop { + let stdin_buffer = self.os_input.read_from_stdin(); + #[cfg(not(test))] // Absolutely zero clue why this breaks *all* of the tests + drop( + self.send_plugin_instructions + .send(PluginInstruction::GlobalInput(stdin_buffer.clone())), + ); + // uncomment this to print the entered character to a log file (/tmp/mosaic/mosaic-log.txt) for debugging + // debug_log_to_file(format!("buffer {:?}", stdin_buffer)); + + match stdin_buffer.as_slice() { + [7] => { + // Ctrl-g + // If we're in command mode, this will let us switch to persistent command mode, to execute + // multiple commands. If we're already in persistent mode, it'll return us to normal mode. + match self.mode { + InputMode::Command => self.mode = InputMode::CommandPersistent, + InputMode::CommandPersistent => self.mode = InputMode::Normal, + _ => panic!(), + } + return; + } + [27] => { + // Esc + self.mode = InputMode::Normal; + return; + } + [106] => { + // j + self.send_screen_instructions + .send(ScreenInstruction::ResizeDown) + .unwrap(); + } + [107] => { + // k + self.send_screen_instructions + .send(ScreenInstruction::ResizeUp) + .unwrap(); + } + [112] => { + // p + self.send_screen_instructions + .send(ScreenInstruction::MoveFocus) + .unwrap(); + } + [104] => { + // h + self.send_screen_instructions + .send(ScreenInstruction::ResizeLeft) + .unwrap(); + } + [108] => { + // l + self.send_screen_instructions + .send(ScreenInstruction::ResizeRight) + .unwrap(); + } + [122] => { + // z + self.command_is_executing.opening_new_pane(); + self.send_pty_instructions + .send(PtyInstruction::SpawnTerminal(None)) + .unwrap(); + self.command_is_executing.wait_until_new_pane_is_opened(); + } + [110] => { + // n + self.command_is_executing.opening_new_pane(); + self.send_pty_instructions + .send(PtyInstruction::SpawnTerminalVertically(None)) + .unwrap(); + self.command_is_executing.wait_until_new_pane_is_opened(); + } + [98] => { + // b + self.command_is_executing.opening_new_pane(); + self.send_pty_instructions + .send(PtyInstruction::SpawnTerminalHorizontally(None)) + .unwrap(); + self.command_is_executing.wait_until_new_pane_is_opened(); + } + [113] => { + // q + self.mode = InputMode::Exiting; + return; + } + [27, 91, 53, 126] => { + // PgUp + self.send_screen_instructions + .send(ScreenInstruction::ScrollUp) + .unwrap(); + } + [27, 91, 54, 126] => { + // PgDown + self.send_screen_instructions + .send(ScreenInstruction::ScrollDown) + .unwrap(); + } + [120] => { + // x + self.command_is_executing.closing_pane(); + self.send_screen_instructions + .send(ScreenInstruction::CloseFocusedPane) + .unwrap(); + self.command_is_executing.wait_until_pane_is_closed(); + } + [101] => { + // e + self.send_screen_instructions + .send(ScreenInstruction::ToggleActiveTerminalFullscreen) + .unwrap(); + } + [121] => { + // y + self.send_screen_instructions + .send(ScreenInstruction::MoveFocusLeft) + .unwrap() + } + [117] => { + // u + self.send_screen_instructions + .send(ScreenInstruction::MoveFocusDown) + .unwrap() + } + [105] => { + // i + self.send_screen_instructions + .send(ScreenInstruction::MoveFocusUp) + .unwrap() + } + [111] => { + // o + self.send_screen_instructions + .send(ScreenInstruction::MoveFocusRight) + .unwrap() + } + [49] => { + // 1 + self.command_is_executing.opening_new_pane(); + self.send_pty_instructions + .send(PtyInstruction::NewTab) + .unwrap(); + self.command_is_executing.wait_until_new_pane_is_opened(); + } + [50] => { + // 2 + self.send_screen_instructions + .send(ScreenInstruction::SwitchTabPrev) + .unwrap() + } + [51] => { + // 3 + self.send_screen_instructions + .send(ScreenInstruction::SwitchTabNext) + .unwrap() + } + [52] => { + // 4 + self.command_is_executing.closing_pane(); + self.send_screen_instructions + .send(ScreenInstruction::CloseTab) + .unwrap(); + self.command_is_executing.wait_until_pane_is_closed(); + } + //@@@khs26 Write this to the powerbar? + _ => continue, + } + + if self.mode == InputMode::Command { + self.mode = InputMode::Normal; + return; + } + } + } + + /// Routine to be called when the input handler exits (at the moment this is the + /// same as quitting mosaic) + fn exit(&mut self) { + self.send_screen_instructions + .send(ScreenInstruction::Quit) + .unwrap(); + self.send_pty_instructions + .send(PtyInstruction::Quit) + .unwrap(); + self.send_plugin_instructions + .send(PluginInstruction::Quit) + .unwrap(); + self.send_app_instructions + .send(AppInstruction::Exit) + .unwrap(); + } +} + +/// Dictates whether we're in command mode, persistent command mode, normal mode or exiting: +/// - Normal mode either writes characters to the terminal, or switches to command mode +/// using a particular key control +/// - Command mode intercepts characters to control mosaic itself, before switching immediately +/// back to normal mode +/// - Persistent command mode is the same as command mode, but doesn't return automatically to +/// normal mode +/// - Exiting means that we should start the shutdown process for mosaic or the given +/// input handler +#[derive(Debug, PartialEq, Clone, Copy)] +pub enum InputMode { + Normal, + Command, + CommandPersistent, + Exiting, +} + +// FIXME: This should be auto-generated from the soon-to-be-added `get_default_keybinds` +pub fn get_help(mode: &InputMode) -> Vec<String> { + let command_help = vec![ + "<n/b/z> Split".into(), + "<j/k/h/l> Resize".into(), + "<p> Focus Next".into(), + "<x> Close Pane".into(), + "<q> Quit".into(), + "<PgUp/PgDown> Scroll".into(), + "<1> New Tab".into(), + "<2/3> Move Tab".into(), + "<4> Close Tab".into(), + ]; + match mode { + InputMode::Normal => vec!["<Ctrl-g> Command Mode".into()], + InputMode::Command => [ + vec![ + "<Ctrl-g> Persistent Mode".into(), + "<ESC> Normal Mode".into(), + ], + command_help, + ] + .concat(), + InputMode::CommandPersistent => { + [vec!["<ESC/Ctrl-g> Normal Mode".into()], command_help].concat() + } + InputMode::Exiting => vec!["Bye from Mosaic!".into()], + } +} + +/// Entry point to the module that instantiates a new InputHandler and calls its +/// reading loop +pub fn input_loop( + os_input: Box<dyn OsApi>, + command_is_executing: CommandIsExecuting, + send_screen_instructions: SenderWithContext<ScreenInstruction>, + send_pty_instructions: SenderWithContext<PtyInstruction>, + send_plugin_instructions: SenderWithContext<PluginInstruction>, + send_app_instructions: SenderWithContext<AppInstruction>, +) { + let _handler = InputHandler::new( + os_input, + command_is_executing, + send_screen_instructions, + send_pty_instructions, + send_plugin_instructions, + send_app_instructions, + ) + .get_input(); +} 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<Session>), +} 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<AppInstruction>, + 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<T> = (Sender<(T, ErrorContext)>, Receiver<(T, ErrorContext)>); +pub type SyncChannelWithContext<T> = (SyncSender<(T, ErrorContext)>, Receiver<(T, ErrorContext)>); + +#[derive(Clone)] +enum SenderType<T: Clone> { + Sender(Sender<(T, ErrorContext)>), + SyncSender(SyncSender<(T, ErrorContext)>), +} + +#[derive(Clone)] +pub struct SenderWithContext<T: Clone> { + err_ctx: ErrorContext, + sender: SenderType<T>, +} + +impl<T: Clone> SenderWithContext<T> { + fn new(err_ctx: ErrorContext, sender: SenderType<T>) -> 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<T: Clone> Send for SenderWithContext<T> {} +unsafe impl<T: Clone> Sync for SenderWithContext<T> {} + +thread_local!(static OPENCALLS: RefCell<ErrorContext> = RefCell::default()); + +#[derive(Clone)] +pub enum AppInstruction { + GetState(Sender<AppState>), + SetState(AppState), + Exit, + Error(String), +} + +pub fn start(mut os_input: Box<dyn OsApi>, 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<PtyInstruction> = + 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<AppInstruction> = + 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/common/os_input_output.rs b/src/common/os_input_output.rs new file mode 100644 index 00000000..41ee3b3c --- /dev/null +++ b/src/common/os_input_output.rs @@ -0,0 +1,217 @@ +use crate::panes::PositionAndSize; +use nix::fcntl::{fcntl, FcntlArg, OFlag}; +use nix::pty::{forkpty, Winsize}; +use nix::sys::signal::{kill, Signal}; +use nix::sys::termios::{cfmakeraw, tcdrain, tcgetattr, tcsetattr, SetArg, Termios}; +use nix::sys::wait::waitpid; +use nix::unistd::{read, write, ForkResult, Pid}; +use std::io::prelude::*; +use std::io::{stdin, Write}; +use std::os::unix::io::RawFd; +use std::path::PathBuf; +use std::process::{Child, Command}; +use std::sync::{Arc, Mutex}; + +use std::env; + +fn into_raw_mode(pid: RawFd) { + let mut tio = tcgetattr(pid).expect("could not get terminal attribute"); + cfmakeraw(&mut tio); + match tcsetattr(pid, SetArg::TCSANOW, &tio) { + Ok(_) => {} + Err(e) => panic!("error {:?}", e), + }; +} + +fn unset_raw_mode(pid: RawFd, orig_termios: Termios) { + match tcsetattr(pid, SetArg::TCSANOW, &orig_termios) { + Ok(_) => {} + Err(e) => panic!("error {:?}", e), + }; +} + +pub fn get_terminal_size_using_fd(fd: RawFd) -> PositionAndSize { + // TODO: do this with the nix ioctl + use libc::ioctl; + use libc::TIOCGWINSZ; + + let mut winsize = Winsize { + ws_row: 0, + ws_col: 0, + ws_xpixel: 0, + ws_ypixel: 0, + }; + + unsafe { ioctl(fd, TIOCGWINSZ, &mut winsize) }; + PositionAndSize::from(winsize) +} + +pub fn set_terminal_size_using_fd(fd: RawFd, columns: u16, rows: u16) { + // TODO: do this with the nix ioctl + use libc::ioctl; + use libc::TIOCSWINSZ; + + let winsize = Winsize { + ws_col: columns, + ws_row: rows, + ws_xpixel: 0, + ws_ypixel: 0, + }; + unsafe { ioctl(fd, TIOCSWINSZ, &winsize) }; +} + +fn handle_command_exit(mut child: Child) { + let signals = ::signal_hook::iterator::Signals::new(&[::signal_hook::SIGINT]).unwrap(); + 'handle_exit: loop { + match child.try_wait() { + Ok(Some(_status)) => { + // TODO: handle errors? + break; + } + Ok(None) => { + ::std::thread::sleep(::std::time::Duration::from_millis(100)); + } + Err(e) => panic!("error attempting to wait: {}", e), + } + + for signal in signals.pending() { + // FIXME: We need to handle more signals here! + match signal { + signal_hook::SIGINT => { + child.kill().unwrap(); + child.wait().unwrap(); + break 'handle_exit; + } + _ => {} + } + } + } +} + +fn spawn_terminal(file_to_open: Option<PathBuf>, orig_termios: Termios) -> (RawFd, RawFd) { + let (pid_primary, pid_secondary): (RawFd, RawFd) = { + match forkpty(None, Some(&orig_termios)) { + Ok(fork_pty_res) => { + let pid_primary = fork_pty_res.master; + let pid_secondary = match fork_pty_res.fork_result { + ForkResult::Parent { child } => { + // fcntl(pid_primary, FcntlArg::F_SETFL(OFlag::empty())).expect("could not fcntl"); + fcntl(pid_primary, FcntlArg::F_SETFL(OFlag::O_NONBLOCK)) + .expect("could not fcntl"); + child + } + ForkResult::Child => match file_to_open { + Some(file_to_open) => { + if env::var("EDITOR").is_err() && env::var("VISUAL").is_err() { + panic!("Can't edit files if an editor is not defined. To fix: define the EDITOR or VISUAL environment variables with the path to your editor (eg. /usr/bin/vim)"); + } + let editor = + env::var("EDITOR").unwrap_or_else(|_| env::var("VISUAL").unwrap()); + + let child = Command::new(editor) + .args(&[file_to_open]) + .spawn() + .expect("failed to spawn"); + handle_command_exit(child); + ::std::process::exit(0); + } + None => { + let child = Command::new(env::var("SHELL").unwrap()) + .spawn() + .expect("failed to spawn"); + handle_command_exit(child); + ::std::process::exit(0); + } + }, + }; + (pid_primary, pid_secondary.as_raw()) + } + Err(e) => { + panic!("failed to fork {:?}", e); + } + } + }; + (pid_primary, pid_secondary) +} + +#[derive(Clone)] +pub struct OsInputOutput { + orig_termios: Arc<Mutex<Termios>>, +} + +pub trait OsApi: Send + Sync { + fn get_terminal_size_using_fd(&self, pid: RawFd) -> PositionAndSize; + fn set_terminal_size_using_fd(&mut self, pid: RawFd, cols: u16, rows: u16); + fn set_raw_mode(&mut self, pid: RawFd); + fn unset_raw_mode(&mut self, pid: RawFd); + fn spawn_terminal(&mut self, file_to_open: Option<PathBuf>) -> (RawFd, RawFd); + fn read_from_tty_stdout(&mut self, pid: RawFd, buf: &mut [u8]) -> Result<usize, nix::Error>; + fn write_to_tty_stdin(&mut self, pid: RawFd, buf: &mut [u8]) -> Result<usize, nix::Error>; + fn tcdrain(&mut self, pid: RawFd) -> Result<(), nix::Error>; + fn kill(&mut self, pid: RawFd) -> Result<(), nix::Error>; + fn read_from_stdin(&self) -> Vec<u8>; + fn get_stdout_writer(&self) -> Box<dyn Write>; + fn box_clone(&self) -> Box<dyn OsApi>; +} + +impl OsApi for OsInputOutput { + fn get_terminal_size_using_fd(&self, pid: RawFd) -> PositionAndSize { + get_terminal_size_using_fd(pid) + } + fn set_terminal_size_using_fd(&mut self, pid: RawFd, cols: u16, rows: u16) { + set_terminal_size_using_fd(pid, cols, rows); + } + fn set_raw_mode(&mut self, pid: RawFd) { + into_raw_mode(pid); + } + fn unset_raw_mode(&mut self, pid: RawFd) { + let orig_termios = self.orig_termios.lock().unwrap(); + unset_raw_mode(pid, orig_termios.clone()); + } + fn spawn_terminal(&mut self, file_to_open: Option<PathBuf>) -> (RawFd, RawFd) { + let orig_termios = self.orig_termios.lock().unwrap(); + spawn_terminal(file_to_open, orig_termios.clone()) + } + fn read_from_tty_stdout(&mut self, pid: RawFd, buf: &mut [u8]) -> Result<usize, nix::Error> { + read(pid, buf) + } + fn write_to_tty_stdin(&mut self, pid: RawFd, buf: &mut [u8]) -> Result<usize, nix::Error> { + write(pid, buf) + } + fn tcdrain(&mut self, pid: RawFd) -> Result<(), nix::Error> { + tcdrain(pid) + } + fn box_clone(&self) -> Box<dyn OsApi> { + Box::new((*self).clone()) + } + fn read_from_stdin(&self) -> Vec<u8> { + let stdin = stdin(); + let mut stdin = stdin.lock(); + let buffer = stdin.fill_buf().unwrap(); + let length = buffer.len(); + let read_bytes = Vec::from(buffer); + stdin.consume(length); + read_bytes + } + fn get_stdout_writer(&self) -> Box<dyn Write> { + let stdout = ::std::io::stdout(); + Box::new(stdout) + } + fn kill(&mut self, fd: RawFd) -> Result<(), nix::Error> { + kill(Pid::from_raw(fd), Some(Signal::SIGINT)).unwrap(); + waitpid(Pid::from_raw(fd), None).unwrap(); + Ok(()) + } +} + +impl Clone for Box<dyn OsApi> { + fn clone(&self) -> Box<dyn OsApi> { + self.box_clone() + } +} + +pub fn get_os_input() -> OsInputOutput { + let current_termios = tcgetattr(0).unwrap(); + let orig_termios = Arc::new(Mutex::new(current_termios)); + OsInputOutput { orig_termios } +} diff --git a/src/common/pty_bus.rs b/src/common/pty_bus.rs new file mode 100644 index 00000000..d341662d --- /dev/null +++ b/src/common/pty_bus.rs @@ -0,0 +1,317 @@ +use ::async_std::stream::*; +use ::async_std::task; +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 ::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::{ + errors::{ContextType, ErrorContext}, + panes::PaneId, +}; +use crate::{layout::Layout, wasm_vm::PluginInstruction}; + +pub struct ReadFromPid { + pid: RawFd, + os_input: Box<dyn OsApi>, +} + +impl ReadFromPid { + pub fn new(pid: &RawFd, os_input: Box<dyn OsApi>) -> ReadFromPid { + ReadFromPid { + pid: *pid, + os_input, + } + } +} + +impl Stream for ReadFromPid { + type Item = Vec<u8>; + fn poll_next(mut self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Option<Self::Item>> { + let mut read_buffer = [0; 65535]; + let pid = self.pid; + let read_result = &self.os_input.read_from_tty_stdout(pid, &mut read_buffer); + match read_result { + Ok(res) => { + if *res == 0 { + // indicates end of file + Poll::Ready(None) + } else { + let res = Some(read_buffer[..=*res].to_vec()); + Poll::Ready(res) + } + } + Err(e) => { + match e { + nix::Error::Sys(errno) => { + if *errno == nix::errno::Errno::EAGAIN { + Poll::Ready(Some(vec![])) // TODO: better with timeout waker somehow + } else { + Poll::Ready(None) + } + } + _ => Poll::Ready(None), + } + } + } + } +} + +#[derive(Debug, Clone)] +pub enum VteEvent { + // TODO: try not to allocate Vecs + Print(char), + Execute(u8), // byte + Hook(Vec<i64>, Vec<u8>, bool, char), // params, intermediates, ignore, char + Put(u8), // byte + Unhook, + OscDispatch(Vec<Vec<u8>>, bool), // params, bell_terminated + CsiDispatch(Vec<i64>, Vec<u8>, bool, char), // params, intermediates, ignore, char + EscDispatch(Vec<u8>, bool, u8), // intermediates, ignore, byte +} + +struct VteEventSender { + id: RawFd, + sender: SenderWithContext<ScreenInstruction>, +} + +impl VteEventSender { + pub fn new(id: RawFd, sender: SenderWithContext<ScreenInstruction>) -> Self { + VteEventSender { id, sender } + } +} + +impl vte::Perform for VteEventSender { + fn print(&mut self, c: char) { + let _ = self + .sender + .send(ScreenInstruction::Pty(self.id, VteEvent::Print(c))); + } + fn execute(&mut self, byte: u8) { + let _ = self + .sender + .send(ScreenInstruction::Pty(self.id, VteEvent::Execute(byte))); + } + + fn hook(&mut self, params: &[i64], intermediates: &[u8], ignore: bool, c: char) { + let params = params.iter().copied().collect(); + let intermediates = intermediates.iter().copied().collect(); + let instruction = + ScreenInstruction::Pty(self.id, VteEvent::Hook(params, intermediates, ignore, c)); + let _ = self.sender.send(instruction); + } + + fn put(&mut self, byte: u8) { + let _ = self + .sender + .send(ScreenInstruction::Pty(self.id, VteEvent::Put(byte))); + } + + fn unhook(&mut self) { + let _ = self + .sender + .send(ScreenInstruction::Pty(self.id, VteEvent::Unhook)); + } + + fn osc_dispatch(&mut self, params: &[&[u8]], bell_terminated: bool) { + let params = params.iter().map(|p| p.to_vec()).collect(); + let instruction = + ScreenInstruction::Pty(self.id, VteEvent::OscDispatch(params, bell_terminated)); + let _ = self.sender.send(instruction); + } + + fn csi_dispatch(&mut self, params: &[i64], intermediates: &[u8], ignore: bool, c: char) { + let params = params.iter().copied().collect(); + let intermediates = intermediates.iter().copied().collect(); + let instruction = ScreenInstruction::Pty( + self.id, + VteEvent::CsiDispatch(params, intermediates, ignore, c), + ); + let _ = self.sender.send(instruction); + } + + fn esc_dispatch(&mut self, intermediates: &[u8], ignore: bool, byte: u8) { + let intermediates = intermediates.iter().copied().collect(); + let instruction = + ScreenInstruction::Pty(self.id, VteEvent::EscDispatch(intermediates, ignore, byte)); + let _ = self.sender.send(instruction); + } +} + +#[derive(Clone, Debug)] +pub enum PtyInstruction { + SpawnTerminal(Option<PathBuf>), + SpawnTerminalVertically(Option<PathBuf>), + SpawnTerminalHorizontally(Option<PathBuf>), + NewTab, + ClosePane(PaneId), + CloseTab(Vec<PaneId>), + Quit, +} + +pub struct PtyBus { + pub send_screen_instructions: SenderWithContext<ScreenInstruction>, + pub send_plugin_instructions: SenderWithContext<PluginInstruction>, + pub receive_pty_instructions: Receiver<(PtyInstruction, ErrorContext)>, + pub id_to_child_pid: HashMap<RawFd, RawFd>, + os_input: Box<dyn OsApi>, + debug_to_file: bool, + task_handles: HashMap<RawFd, JoinHandle<()>>, +} + +fn stream_terminal_bytes( + pid: RawFd, + mut send_screen_instructions: SenderWithContext<ScreenInstruction>, + os_input: Box<dyn OsApi>, + debug: bool, +) -> JoinHandle<()> { + let mut err_ctx = OPENCALLS.with(|ctx| *ctx.borrow()); + task::spawn({ + async move { + err_ctx.add_call(ContextType::AsyncTask); + send_screen_instructions.update(err_ctx); + let mut vte_parser = vte::Parser::new(); + let mut vte_event_sender = VteEventSender::new(pid, send_screen_instructions.clone()); + let mut terminal_bytes = ReadFromPid::new(&pid, os_input); + + let mut last_byte_receive_time: Option<Instant> = None; + let mut pending_render = false; + let max_render_pause = Duration::from_millis(30); + + while let Some(bytes) = terminal_bytes.next().await { + let bytes_is_empty = bytes.is_empty(); + for byte in bytes { + if debug { + debug_to_file(byte, pid).unwrap(); + } + vte_parser.advance(&mut vte_event_sender, byte); + } + if !bytes_is_empty { + // for UX reasons, if we got something on the wire, we only send the render notice if: + // 1. there aren't any more bytes on the wire afterwards + // 2. a certain period (currently 30ms) has elapsed since the last render + // (otherwise if we get a large amount of data, the display would hang + // until it's done) + // 3. the stream has ended, and so we render 1 last time + match last_byte_receive_time.as_mut() { + Some(receive_time) => { + if receive_time.elapsed() > max_render_pause { + pending_render = false; + let _ = send_screen_instructions.send(ScreenInstruction::Render); + last_byte_receive_time = Some(Instant::now()); + } else { + pending_render = true; + } + } + None => { + last_byte_receive_time = Some(Instant::now()); + pending_render = true; + } + }; + } else { + if pending_render { + pending_render = false; + let _ = send_screen_instructions.send(ScreenInstruction::Render); + } + last_byte_receive_time = None; + task::sleep(::std::time::Duration::from_millis(10)).await; + } + } + if pending_render { + let _ = send_screen_instructions.send(ScreenInstruction::Render); + } + #[cfg(not(test))] + // this is a little hacky, and is because the tests end the file as soon as + // we read everything, rather than hanging until there is new data + // a better solution would be to fix the test fakes, but this will do for now + send_screen_instructions + .send(ScreenInstruction::ClosePane(PaneId::Terminal(pid))) + .unwrap(); + } + }) +} + +impl PtyBus { + pub fn new( + receive_pty_instructions: Receiver<(PtyInstruction, ErrorContext)>, + send_screen_instructions: SenderWithContext<ScreenInstruction>, + send_plugin_instructions: SenderWithContext<PluginInstruction>, + os_input: Box<dyn OsApi>, + debug_to_file: bool, + ) -> Self { + PtyBus { + send_screen_instructions, + send_plugin_instructions, + receive_pty_instructions, + os_input, + id_to_child_pid: HashMap::new(), + debug_to_file, + task_handles: HashMap::new(), + } + } + pub fn spawn_terminal(&mut self, file_to_open: Option<PathBuf>) -> RawFd { + let (pid_primary, pid_secondary): (RawFd, RawFd) = + self.os_input.spawn_terminal(file_to_open); + let task_handle = stream_terminal_bytes( + pid_primary, + self.send_screen_instructions.clone(), + self.os_input.clone(), + self.debug_to_file, + ); + self.task_handles.insert(pid_primary, task_handle); + self.id_to_child_pid.insert(pid_primary, pid_secondary); + pid_primary + } + pub fn spawn_terminals_for_layout(&mut self, layout: Layout) { + 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); + self.id_to_child_pid.insert(pid_primary, pid_secondary); + new_pane_pids.push(pid_primary); + } + self.send_screen_instructions + .send(ScreenInstruction::ApplyLayout(( + layout, + new_pane_pids.clone(), + ))) + .unwrap(); + for id in new_pane_pids { + let task_handle = stream_terminal_bytes( + id, + self.send_screen_instructions.clone(), + self.os_input.clone(), + self.debug_to_file, + ); + self.task_handles.insert(id, task_handle); + } + } + pub fn close_pane(&mut self, id: PaneId) { + match id { + 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(); + task::block_on(async { + handle.cancel().await; + }); + } + PaneId::Plugin(pid) => drop( + self.send_plugin_instructions + .send(PluginInstruction::Unload(pid)), + ), + } + } + pub fn close_tab(&mut self, ids: Vec<PaneId>) { + ids.iter().for_each(|&id| { + self.close_pane(id); + }); + } +} diff --git a/src/common/screen.rs b/src/common/screen.rs new file mode 100644 index 00000000..2438dba0 --- /dev/null +++ b/src/common/screen.rs @@ -0,0 +1,197 @@ +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}; + +/* + * Screen + * + * this holds multiple tabs, each one holding multiple panes + * it tracks the active tab and controls tab switching, all the rest + * is performed in Tab + * + */ + +#[derive(Debug, Clone)] +pub enum ScreenInstruction { + Pty(RawFd, VteEvent), + Render, + NewPane(PaneId), + HorizontalSplit(PaneId), + VerticalSplit(PaneId), + WriteCharacter(Vec<u8>), + ResizeLeft, + ResizeRight, + ResizeDown, + ResizeUp, + MoveFocus, + MoveFocusLeft, + MoveFocusDown, + MoveFocusUp, + MoveFocusRight, + Quit, + ScrollUp, + ScrollDown, + ClearScroll, + CloseFocusedPane, + ToggleActiveTerminalFullscreen, + SetSelectable(PaneId, bool), + SetMaxHeight(PaneId, usize), + ClosePane(PaneId), + ApplyLayout((Layout, Vec<RawFd>)), + NewTab(RawFd), + SwitchTabNext, + SwitchTabPrev, + CloseTab, +} + +pub struct Screen { + pub receiver: Receiver<(ScreenInstruction, ErrorContext)>, + max_panes: Option<usize>, + tabs: BTreeMap<usize, Tab>, + pub send_pty_instructions: SenderWithContext<PtyInstruction>, + pub send_plugin_instructions: SenderWithContext<PluginInstruction>, + pub send_app_instructions: SenderWithContext<AppInstruction>, + full_screen_ws: PositionAndSize, + active_tab_index: Option<usize>, + os_api: Box<dyn OsApi>, +} + +impl Screen { + pub fn new( + receive_screen_instructions: Receiver<(ScreenInstruction, ErrorContext)>, + send_pty_instructions: SenderWithContext<PtyInstruction>, + send_plugin_instructions: SenderWithContext<PluginInstruction>, + send_app_instructions: SenderWithContext<AppInstruction>, + full_screen_ws: &PositionAndSize, + os_api: Box<dyn OsApi>, + max_panes: Option<usize>, + ) -> Self { + Screen { + receiver: receive_screen_instructions, + max_panes, + send_pty_instructions, + send_plugin_instructions, + send_app_instructions, + full_screen_ws: *full_screen_ws, + active_tab_index: None, + tabs: BTreeMap::new(), + os_api, + } + } + pub fn new_tab(&mut self, pane_id: RawFd) { + let tab_index = self.get_next_tab_index(); + let tab = Tab::new( + tab_index, + &self.full_screen_ws, + self.os_api.clone(), + self.send_pty_instructions.clone(), + self.send_plugin_instructions.clone(), + self.send_app_instructions.clone(), + self.max_panes, + Some(PaneId::Terminal(pane_id)), + ); + self.active_tab_index = Some(tab_index); + self.tabs.insert(tab_index, tab); + self.render(); + } + fn get_next_tab_index(&self) -> usize { + if let Some(index) = self.tabs.keys().last() { + *index + 1 + } else { + 0 + } + } + pub fn switch_tab_next(&mut self) { + let active_tab_id = self.get_active_tab().unwrap().index; + let tab_ids: Vec<usize> = self.tabs.keys().copied().collect(); + let first_tab = tab_ids.get(0).unwrap(); + let active_tab_id_position = tab_ids.iter().position(|id| id == &active_tab_id).unwrap(); + if let Some(next_tab) = tab_ids.get(active_tab_id_position + 1) { + self.active_tab_index = Some(*next_tab); + } else { + self.active_tab_index = Some(*first_tab); + } + self.render(); + } + pub fn switch_tab_prev(&mut self) { + let active_tab_id = self.get_active_tab().unwrap().index; + let tab_ids: Vec<usize> = self.tabs.keys().copied().collect(); + let first_tab = tab_ids.get(0).unwrap(); + let last_tab = tab_ids.last().unwrap(); + + let active_tab_id_position = tab_ids.iter().position(|id| id == &active_tab_id).unwrap(); + if active_tab_id == *first_tab { + self.active_tab_index = Some(*last_tab) + } else if let Some(prev_tab) = tab_ids.get(active_tab_id_position - 1) { + self.active_tab_index = Some(*prev_tab) + } + self.render(); + } + pub fn close_tab(&mut self) { + let active_tab_index = self.active_tab_index.unwrap(); + if self.tabs.len() > 1 { + self.switch_tab_prev(); + } + let active_tab = self.tabs.remove(&active_tab_index).unwrap(); + let pane_ids = active_tab.get_pane_ids(); + self.send_pty_instructions + .send(PtyInstruction::CloseTab(pane_ids)) + .unwrap(); + if self.tabs.is_empty() { + self.active_tab_index = None; + self.send_app_instructions + .send(AppInstruction::Exit) + .unwrap(); + } + } + pub fn render(&mut self) { + if let Some(active_tab) = self.get_active_tab_mut() { + if active_tab.get_active_pane().is_some() { + active_tab.render(); + } else { + self.close_tab(); + } + }; + } + + pub fn get_active_tab(&self) -> Option<&Tab> { + match self.active_tab_index { + Some(tab) => self.tabs.get(&tab), + None => None, + } + } + pub fn get_tabs_mut(&mut self) -> &mut BTreeMap<usize, Tab> { + &mut self.tabs + } + pub fn get_active_tab_mut(&mut self) -> Option<&mut Tab> { + let tab = match self.active_tab_index { + Some(tab) => self.get_tabs_mut().get_mut(&tab), + None => None, + }; + tab + } + pub fn apply_layout(&mut self, layout: Layout, new_pids: Vec<RawFd>) { + let tab_index = self.get_next_tab_index(); + let mut tab = Tab::new( + tab_index, + &self.full_screen_ws, + self.os_api.clone(), + self.send_pty_instructions.clone(), + self.send_plugin_instructions.clone(), + self.send_app_instructions.clone(), + self.max_panes, + None, + ); + tab.apply_layout(layout, new_pids); + self.active_tab_index = Some(tab_index); + self.tabs.insert(tab_index, tab); + } +} diff --git a/src/common/utils/consts.rs b/src/common/utils/consts.rs new file mode 100644 index 00000000..7a0c9ddf --- /dev/null +++ b/src/common/utils/consts.rs @@ -0,0 +1,6 @@ +pub const MOSAIC_TMP_DIR: &str = "/tmp/mosaic"; +pub const MOSAIC_TMP_LOG_DIR: &str = "/tmp/mosaic/mosaic-log"; +pub const MOSAIC_TMP_LOG_FILE: &str = "/tmp/mosaic/mosaic-log/log.txt"; +pub const MOSAIC_IPC_PIPE: &str = "/tmp/mosaic/ipc"; +pub const MOSAIC_ROOT_PLUGIN_DIR: &str = "/usr/share/mosaic/plugins"; +pub const MOSAIC_ROOT_LAYOUT_DIR: &str = "/usr/share/mosaic/layouts"; diff --git a/src/common/utils/logging.rs b/src/common/utils/logging.rs new file mode 100644 index 00000000..05af455c --- /dev/null +++ b/src/common/utils/logging.rs @@ -0,0 +1,76 @@ +use std::{ + fs, + io::{self, prelude::*}, + os::unix::io::RawFd, + path::PathBuf, +}; + +use crate::utils::consts::{MOSAIC_TMP_LOG_DIR, MOSAIC_TMP_LOG_FILE}; + +pub fn atomic_create_file(file_name: &str) { + let _ = fs::OpenOptions::new().create(true).open(file_name); +} + +pub fn atomic_create_dir(dir_name: &str) -> io::Result<()> { + if let Err(e) = fs::create_dir(dir_name) { + if e.kind() == std::io::ErrorKind::AlreadyExists { + Ok(()) + } else { + Err(e) + } + } else { + Ok(()) + } +} + +pub fn debug_log_to_file(mut message: String) -> io::Result<()> { + message.push('\n'); + debug_log_to_file_without_newline(message) +} + +pub fn debug_log_to_file_without_newline(message: String) -> io::Result<()> { + atomic_create_file(MOSAIC_TMP_LOG_FILE); + let mut file = fs::OpenOptions::new() + .append(true) + .create(true) + .open(MOSAIC_TMP_LOG_FILE)?; + file.write_all(message.as_bytes()) +} + +pub fn _debug_log_to_file_pid_3(message: String, pid: RawFd) -> io::Result<()> { + if pid == 3 { + debug_log_to_file(message) + } else { + Ok(()) + } +} + +#[allow(dead_code)] +pub fn delete_log_file() -> io::Result<()> { + if fs::metadata(MOSAIC_TMP_LOG_FILE).is_ok() { + fs::remove_file(MOSAIC_TMP_LOG_FILE) + } else { + Ok(()) + } +} + +#[allow(dead_code)] +pub fn delete_log_dir() -> io::Result<()> { + if fs::metadata(MOSAIC_TMP_LOG_DIR).is_ok() { + fs::remove_dir_all(MOSAIC_TMP_LOG_DIR) + } else { + Ok(()) + } +} + +pub fn debug_to_file(message: u8, pid: RawFd) -> io::Result<()> { + let mut path = PathBuf::new(); + path.push(MOSAIC_TMP_LOG_DIR); + path.push(format!("mosaic-{}.log", pid.to_string())); + + let mut file = fs::OpenOptions::new() + .append(true) + .create(true) + .open(path)?; + file.write_all(&[message]) +} diff --git a/src/common/utils/mod.rs b/src/common/utils/mod.rs new file mode 100644 index 00000000..0ea4c76f --- /dev/null +++ b/src/common/utils/mod.rs @@ -0,0 +1,3 @@ +pub mod consts; +pub mod logging; +pub mod shared; diff --git a/src/common/utils/shared.rs b/src/common/utils/shared.rs new file mode 100644 index 00000000..1d8ca5c3 --- /dev/null +++ b/src/common/utils/shared.rs @@ -0,0 +1,19 @@ +use std::{iter, str::from_utf8}; + +use strip_ansi_escapes::strip; + +pub fn ansi_len(s: &str) -> usize { + from_utf8(&strip(s.as_bytes()).unwrap()) + .unwrap() + .chars() + .count() +} + +pub fn pad_to_size(s: &str, rows: usize, columns: usize) -> String { + s.lines() + .map(|l| [l, &str::repeat(" ", columns - ansi_len(l))].concat()) + .chain(iter::repeat(str::repeat(" ", columns))) + .take(rows) + .collect::<Vec<_>>() + .join("\n\r") +} diff --git a/src/common/wasm_vm.rs b/src/common/wasm_vm.rs new file mode 100644 index 00000000..d678401d --- /dev/null +++ b/src/common/wasm_vm.rs @@ -0,0 +1,107 @@ +use std::{ + path::PathBuf, + sync::mpsc::{channel, Sender}, +}; +use wasmer::{imports, Function, ImportObject, Store, WasmerEnv}; +use wasmer_wasi::WasiEnv; + +use super::{ + input::get_help, pty_bus::PtyInstruction, screen::ScreenInstruction, AppInstruction, PaneId, + SenderWithContext, +}; + +#[derive(Clone, Debug)] +pub enum PluginInstruction { + Load(Sender<u32>, PathBuf), + Draw(Sender<String>, u32, usize, usize), // String buffer, plugin id, rows, cols + Input(u32, Vec<u8>), // plugin id, input bytes + GlobalInput(Vec<u8>), // input bytes + Unload(u32), + Quit, +} + +#[derive(WasmerEnv, Clone)] +pub struct PluginEnv { + pub plugin_id: u32, + pub send_screen_instructions: SenderWithContext<ScreenInstruction>, + pub send_app_instructions: SenderWithContext<AppInstruction>, + pub send_pty_instructions: SenderWithContext<PtyInstruction>, // FIXME: This should be a big bundle of all of the channels + pub wasi_env: WasiEnv, +} + +// Plugin API --------------------------------------------------------------------------------------------------------- + +pub fn mosaic_imports(store: &Store, plugin_env: &PluginEnv) -> ImportObject { + imports! { + "mosaic" => { + "host_open_file" => Function::new_native_with_env(store, plugin_env.clone(), host_open_file), + "host_set_max_height" => Function::new_native_with_env(store, plugin_env.clone(), host_set_max_height), + "host_set_selectable" => Function::new_native_with_env(store, plugin_env.clone(), host_set_selectable), + "host_get_help" => Function::new_native_with_env(store, plugin_env.clone(), host_get_help), + } + } +} + +// FIXME: Bundle up all of the channels! Pair that with WasiEnv? +fn host_open_file(plugin_env: &PluginEnv) { + let path = PathBuf::from(wasi_stdout(&plugin_env.wasi_env).lines().next().unwrap()); + plugin_env + .send_pty_instructions + .send(PtyInstruction::SpawnTerminal(Some(path))) + .unwrap(); +} + +// FIXME: Think about these naming conventions – should everything be prefixed by 'host'? +fn host_set_selectable(plugin_env: &PluginEnv, selectable: i32) { + let selectable = selectable != 0; + plugin_env + .send_screen_instructions + .send(ScreenInstruction::SetSelectable( + PaneId::Plugin(plugin_env.plugin_id), + selectable, + )) + .unwrap() +} + +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( + PaneId::Plugin(plugin_env.plugin_id), + max_height, + )) + .unwrap() +} + +fn host_get_help(plugin_env: &PluginEnv) { + let (state_tx, state_rx) = channel(); + // FIXME: If I changed the application so that threads were sent the termination + // signal and joined one at a time, there would be an order to shutdown, so I + // could get rid of this .is_ok() check and the .try_send() + if plugin_env + .send_app_instructions + .try_send(AppInstruction::GetState(state_tx)) + .is_ok() + { + let help = get_help(&state_rx.recv().unwrap().input_mode); + wasi_write_string(&plugin_env.wasi_env, &serde_json::to_string(&help).unwrap()); + } +} + +// Helper Functions --------------------------------------------------------------------------------------------------- + +// FIXME: Unwrap city +pub fn wasi_stdout(wasi_env: &WasiEnv) -> String { + let mut state = wasi_env.state(); + let wasi_file = state.fs.stdout_mut().unwrap().as_mut().unwrap(); + let mut buf = String::new(); + wasi_file.read_to_string(&mut buf).unwrap(); + buf +} + +pub fn wasi_write_string(wasi_env: &WasiEnv, buf: &str) { + let mut state = wasi_env.state(); + let wasi_file = state.fs.stdin_mut().unwrap().as_mut().unwrap(); + writeln!(wasi_file, "{}\r", buf).unwrap(); +} |
