diff options
| author | denis <denismaximov98@gmail.com> | 2021-01-24 18:38:46 +0200 |
|---|---|---|
| committer | denis <denismaximov98@gmail.com> | 2021-01-24 18:38:46 +0200 |
| commit | b23ef332c7531b8e9bb202bd25e7f37fd157c22c (patch) | |
| tree | 2ad09a6173e8528a91c37609e00c59d9b8a0b04b /src/common | |
| parent | Merge branch 'main' into server-client (diff) | |
| download | sl0ppij-b23ef332c7531b8e9bb202bd25e7f37fd157c22c.tar.gz sl0ppij-b23ef332c7531b8e9bb202bd25e7f37fd157c22c.tar.xz sl0ppij-b23ef332c7531b8e9bb202bd25e7f37fd157c22c.zip | |
wip: restructing in progress, to prep for server-client
Diffstat (limited to 'src/common')
| -rw-r--r-- | src/common/command_is_executing.rs | 53 | ||||
| -rw-r--r-- | src/common/errors.rs | 284 | ||||
| -rw-r--r-- | src/common/input.rs | 359 | ||||
| -rw-r--r-- | src/common/ipc.rs | 46 | ||||
| -rw-r--r-- | src/common/mod.rs | 8 | ||||
| -rw-r--r-- | src/common/os_input_output.rs | 217 | ||||
| -rw-r--r-- | src/common/pty_bus.rs | 313 | ||||
| -rw-r--r-- | src/common/screen.rs | 196 | ||||
| -rw-r--r-- | src/common/wasm_vm.rs | 95 |
9 files changed, 1571 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..cb483bbc --- /dev/null +++ b/src/common/errors.rs @@ -0,0 +1,284 @@ +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; +#[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, + 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::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..3c5518f8 --- /dev/null +++ b/src/common/input.rs @@ -0,0 +1,359 @@ +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, + 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..0bfc0451 --- /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 std::collections::HashSet; +use serde::{Serialize, Deserialize}; + +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>), +}
\ No newline at end of file diff --git a/src/common/mod.rs b/src/common/mod.rs new file mode 100644 index 00000000..7daeb94f --- /dev/null +++ b/src/common/mod.rs @@ -0,0 +1,8 @@ +pub mod input; +pub mod os_input_output; +pub mod pty_bus; +pub mod screen; +pub mod ipc; +pub mod wasm_vm; +pub mod command_is_executing; +pub mod errors;
\ No newline at end of file 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..ba5a608b --- /dev/null +++ b/src/common/pty_bus.rs @@ -0,0 +1,313 @@ +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 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}; +use crate::{ScreenInstruction, SenderWithContext, OPENCALLS}; + +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, +} + +fn stream_terminal_bytes( + pid: RawFd, + mut send_screen_instructions: SenderWithContext<ScreenInstruction>, + os_input: Box<dyn OsApi>, + debug: bool, +) { + 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; + send_screen_instructions + .send(ScreenInstruction::Render) + .unwrap(); + 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; + send_screen_instructions + .send(ScreenInstruction::Render) + .unwrap(); + } + last_byte_receive_time = None; + task::sleep(::std::time::Duration::from_millis(10)).await; + } + } + send_screen_instructions + .send(ScreenInstruction::Render) + .unwrap(); + #[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, + } + } + 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); + stream_terminal_bytes( + pid_primary, + self.send_screen_instructions.clone(), + self.os_input.clone(), + self.debug_to_file, + ); + 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 { + stream_terminal_bytes( + id, + self.send_screen_instructions.clone(), + self.os_input.clone(), + self.debug_to_file, + ); + } + } + pub fn close_pane(&mut self, id: PaneId) { + match id { + PaneId::Terminal(id) => { + let child_pid = self.id_to_child_pid.get(&id).unwrap(); + self.os_input.kill(*child_pid).unwrap(); + } + 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..7fb19b6c --- /dev/null +++ b/src/common/screen.rs @@ -0,0 +1,196 @@ +use std::collections::BTreeMap; +use std::os::unix::io::RawFd; +use std::sync::mpsc::Receiver; + +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 + * + * 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), + 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/wasm_vm.rs b/src/common/wasm_vm.rs new file mode 100644 index 00000000..97a1a587 --- /dev/null +++ b/src/common/wasm_vm.rs @@ -0,0 +1,95 @@ +use std::{ + path::PathBuf, + sync::mpsc::{channel, Sender}, +}; +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, +}; + +#[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_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_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(); +} |
