aboutsummaryrefslogtreecommitdiffstats
path: root/src/common
diff options
context:
space:
mode:
authordenis <denismaximov98@gmail.com>2021-01-24 18:38:46 +0200
committerdenis <denismaximov98@gmail.com>2021-01-24 18:38:46 +0200
commitb23ef332c7531b8e9bb202bd25e7f37fd157c22c (patch)
tree2ad09a6173e8528a91c37609e00c59d9b8a0b04b /src/common
parentMerge branch 'main' into server-client (diff)
downloadsl0ppij-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.rs53
-rw-r--r--src/common/errors.rs284
-rw-r--r--src/common/input.rs359
-rw-r--r--src/common/ipc.rs46
-rw-r--r--src/common/mod.rs8
-rw-r--r--src/common/os_input_output.rs217
-rw-r--r--src/common/pty_bus.rs313
-rw-r--r--src/common/screen.rs196
-rw-r--r--src/common/wasm_vm.rs95
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();
+}