aboutsummaryrefslogtreecommitdiffstats
path: root/src/common
diff options
context:
space:
mode:
authorDenis Maximov <denismaximov98@gmail.com>2021-01-30 13:45:51 +0200
committerGitHub <noreply@github.com>2021-01-30 13:45:51 +0200
commit63a2dc33995fad81c7eec02b23e38afdc3f426e8 (patch)
treed060832b4038328b0fb10267a54a0c1baf16c51c /src/common
parentfeat(statusbar): max status bar height (#156) (diff)
parentchore: formatting (diff)
downloadsl0ppij-63a2dc33995fad81c7eec02b23e38afdc3f426e8.tar.gz
sl0ppij-63a2dc33995fad81c7eec02b23e38afdc3f426e8.tar.xz
sl0ppij-63a2dc33995fad81c7eec02b23e38afdc3f426e8.zip
Merge pull request #157 from mosaic-org/restructure
Restructure
Diffstat (limited to 'src/common')
-rw-r--r--src/common/command_is_executing.rs53
-rw-r--r--src/common/errors.rs286
-rw-r--r--src/common/input.rs359
-rw-r--r--src/common/ipc.rs46
-rw-r--r--src/common/mod.rs662
-rw-r--r--src/common/os_input_output.rs217
-rw-r--r--src/common/pty_bus.rs317
-rw-r--r--src/common/screen.rs197
-rw-r--r--src/common/utils/consts.rs6
-rw-r--r--src/common/utils/logging.rs76
-rw-r--r--src/common/utils/mod.rs3
-rw-r--r--src/common/utils/shared.rs19
-rw-r--r--src/common/wasm_vm.rs107
13 files changed, 2348 insertions, 0 deletions
diff --git a/src/common/command_is_executing.rs b/src/common/command_is_executing.rs
new file mode 100644
index 00000000..93c44eb6
--- /dev/null
+++ b/src/common/command_is_executing.rs
@@ -0,0 +1,53 @@
+#![allow(clippy::mutex_atomic)]
+use std::sync::{Arc, Condvar, Mutex};
+
+#[derive(Clone)]
+pub struct CommandIsExecuting {
+ opening_new_pane: Arc<(Mutex<bool>, Condvar)>,
+ closing_pane: Arc<(Mutex<bool>, Condvar)>,
+}
+
+impl CommandIsExecuting {
+ pub fn new() -> Self {
+ CommandIsExecuting {
+ opening_new_pane: Arc::new((Mutex::new(false), Condvar::new())),
+ closing_pane: Arc::new((Mutex::new(false), Condvar::new())),
+ }
+ }
+ pub fn closing_pane(&mut self) {
+ let (lock, _cvar) = &*self.closing_pane;
+ let mut closing_pane = lock.lock().unwrap();
+ *closing_pane = true;
+ }
+ pub fn done_closing_pane(&mut self) {
+ let (lock, cvar) = &*self.closing_pane;
+ let mut closing_pane = lock.lock().unwrap();
+ *closing_pane = false;
+ cvar.notify_one();
+ }
+ pub fn opening_new_pane(&mut self) {
+ let (lock, _cvar) = &*self.opening_new_pane;
+ let mut opening_new_pane = lock.lock().unwrap();
+ *opening_new_pane = true;
+ }
+ pub fn done_opening_new_pane(&mut self) {
+ let (lock, cvar) = &*self.opening_new_pane;
+ let mut opening_new_pane = lock.lock().unwrap();
+ *opening_new_pane = false;
+ cvar.notify_one();
+ }
+ pub fn wait_until_pane_is_closed(&self) {
+ let (lock, cvar) = &*self.closing_pane;
+ let mut closing_pane = lock.lock().unwrap();
+ while *closing_pane {
+ closing_pane = cvar.wait(closing_pane).unwrap();
+ }
+ }
+ pub fn wait_until_new_pane_is_opened(&self) {
+ let (lock, cvar) = &*self.opening_new_pane;
+ let mut opening_new_pane = lock.lock().unwrap();
+ while *opening_new_pane {
+ opening_new_pane = cvar.wait(opening_new_pane).unwrap();
+ }
+ }
+}
diff --git a/src/common/errors.rs b/src/common/errors.rs
new file mode 100644
index 00000000..90ca6f9e
--- /dev/null
+++ b/src/common/errors.rs
@@ -0,0 +1,286 @@
+use super::{AppInstruction, OPENCALLS};
+use crate::pty_bus::PtyInstruction;
+use crate::screen::ScreenInstruction;
+
+use std::fmt::{Display, Error, Formatter};
+
+const MAX_THREAD_CALL_STACK: usize = 6;
+
+#[cfg(not(test))]
+use super::SenderWithContext;
+#[cfg(not(test))]
+use std::panic::PanicInfo;
+#[cfg(not(test))]
+pub fn handle_panic(
+ info: &PanicInfo<'_>,
+ send_app_instructions: &SenderWithContext<AppInstruction>,
+) {
+ use backtrace::Backtrace;
+ use std::{process, thread};
+ let backtrace = Backtrace::new();
+ let thread = thread::current();
+ let thread = thread.name().unwrap_or("unnamed");
+
+ let msg = match info.payload().downcast_ref::<&'static str>() {
+ Some(s) => Some(*s),
+ None => info.payload().downcast_ref::<String>().map(|s| &**s),
+ };
+
+ let err_ctx = OPENCALLS.with(|ctx| *ctx.borrow());
+
+ let backtrace = match (info.location(), msg) {
+ (Some(location), Some(msg)) => format!(
+ "{}\n\u{1b}[0;0mError: \u{1b}[0;31mthread '{}' panicked at '{}': {}:{}\n\u{1b}[0;0m{:?}",
+ err_ctx,
+ thread,
+ msg,
+ location.file(),
+ location.line(),
+ backtrace
+ ),
+ (Some(location), None) => format!(
+ "{}\n\u{1b}[0;0mError: \u{1b}[0;31mthread '{}' panicked: {}:{}\n\u{1b}[0;0m{:?}",
+ err_ctx,
+ thread,
+ location.file(),
+ location.line(),
+ backtrace
+ ),
+ (None, Some(msg)) => format!(
+ "{}\n\u{1b}[0;0mError: \u{1b}[0;31mthread '{}' panicked at '{}'\n\u{1b}[0;0m{:?}",
+ err_ctx, thread, msg, backtrace
+ ),
+ (None, None) => format!(
+ "{}\n\u{1b}[0;0mError: \u{1b}[0;31mthread '{}' panicked\n\u{1b}[0;0m{:?}",
+ err_ctx, thread, backtrace
+ ),
+ };
+
+ if thread == "main" {
+ println!("{}", backtrace);
+ process::exit(1);
+ } else {
+ send_app_instructions
+ .send(AppInstruction::Error(backtrace))
+ .unwrap();
+ }
+}
+
+#[derive(Clone, Copy)]
+pub struct ErrorContext {
+ calls: [ContextType; MAX_THREAD_CALL_STACK],
+}
+
+impl ErrorContext {
+ pub fn new() -> Self {
+ Self {
+ calls: [ContextType::Empty; MAX_THREAD_CALL_STACK],
+ }
+ }
+
+ pub fn add_call(&mut self, call: ContextType) {
+ for ctx in self.calls.iter_mut() {
+ if *ctx == ContextType::Empty {
+ *ctx = call;
+ break;
+ }
+ }
+ OPENCALLS.with(|ctx| *ctx.borrow_mut() = *self);
+ }
+}
+
+impl Default for ErrorContext {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+impl Display for ErrorContext {
+ fn fmt(&self, f: &mut Formatter) -> Result<(), Error> {
+ writeln!(f, "Originating Thread(s):")?;
+ for (index, ctx) in self.calls.iter().enumerate() {
+ if *ctx == ContextType::Empty {
+ break;
+ }
+ writeln!(f, "\u{1b}[0;0m{}. {}", index + 1, ctx)?;
+ }
+ Ok(())
+ }
+}
+
+#[derive(Copy, Clone, PartialEq)]
+pub enum ContextType {
+ Screen(ScreenContext),
+ Pty(PtyContext),
+
+ Plugin(PluginContext),
+ App(AppContext),
+ IPCServer,
+ StdinHandler,
+ AsyncTask,
+ Empty,
+}
+
+impl Display for ContextType {
+ fn fmt(&self, f: &mut Formatter) -> Result<(), Error> {
+ let purple = "\u{1b}[1;35m";
+ let green = "\u{1b}[0;32m";
+ match *self {
+ ContextType::Screen(c) => write!(f, "{}screen_thread: {}{:?}", purple, green, c),
+ ContextType::Pty(c) => write!(f, "{}pty_thread: {}{:?}", purple, green, c),
+
+ ContextType::Plugin(c) => write!(f, "{}plugin_thread: {}{:?}", purple, green, c),
+ ContextType::App(c) => write!(f, "{}main_thread: {}{:?}", purple, green, c),
+ ContextType::IPCServer => write!(f, "{}ipc_server: {}AcceptInput", purple, green),
+ ContextType::StdinHandler => {
+ write!(f, "{}stdin_handler_thread: {}AcceptInput", purple, green)
+ }
+ ContextType::AsyncTask => {
+ write!(f, "{}stream_terminal_bytes: {}AsyncTask", purple, green)
+ }
+ ContextType::Empty => write!(f, ""),
+ }
+ }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub enum ScreenContext {
+ HandlePtyEvent,
+ Render,
+ NewPane,
+ HorizontalSplit,
+ VerticalSplit,
+ WriteCharacter,
+ ResizeLeft,
+ ResizeRight,
+ ResizeDown,
+ ResizeUp,
+ MoveFocus,
+ MoveFocusLeft,
+ MoveFocusDown,
+ MoveFocusUp,
+ MoveFocusRight,
+ Quit,
+ ScrollUp,
+ ScrollDown,
+ ClearScroll,
+ CloseFocusedPane,
+ ToggleActiveTerminalFullscreen,
+ SetSelectable,
+ SetMaxHeight,
+ ClosePane,
+ ApplyLayout,
+ NewTab,
+ SwitchTabNext,
+ SwitchTabPrev,
+ CloseTab,
+}
+
+impl From<&ScreenInstruction> for ScreenContext {
+ fn from(screen_instruction: &ScreenInstruction) -> Self {
+ match *screen_instruction {
+ ScreenInstruction::Pty(..) => ScreenContext::HandlePtyEvent,
+ ScreenInstruction::Render => ScreenContext::Render,
+ ScreenInstruction::NewPane(_) => ScreenContext::NewPane,
+ ScreenInstruction::HorizontalSplit(_) => ScreenContext::HorizontalSplit,
+ ScreenInstruction::VerticalSplit(_) => ScreenContext::VerticalSplit,
+ ScreenInstruction::WriteCharacter(_) => ScreenContext::WriteCharacter,
+ ScreenInstruction::ResizeLeft => ScreenContext::ResizeLeft,
+ ScreenInstruction::ResizeRight => ScreenContext::ResizeRight,
+ ScreenInstruction::ResizeDown => ScreenContext::ResizeDown,
+ ScreenInstruction::ResizeUp => ScreenContext::ResizeUp,
+ ScreenInstruction::MoveFocus => ScreenContext::MoveFocus,
+ ScreenInstruction::MoveFocusLeft => ScreenContext::MoveFocusLeft,
+ ScreenInstruction::MoveFocusDown => ScreenContext::MoveFocusDown,
+ ScreenInstruction::MoveFocusUp => ScreenContext::MoveFocusUp,
+ ScreenInstruction::MoveFocusRight => ScreenContext::MoveFocusRight,
+ ScreenInstruction::Quit => ScreenContext::Quit,
+ ScreenInstruction::ScrollUp => ScreenContext::ScrollUp,
+ ScreenInstruction::ScrollDown => ScreenContext::ScrollDown,
+ ScreenInstruction::ClearScroll => ScreenContext::ClearScroll,
+ ScreenInstruction::CloseFocusedPane => ScreenContext::CloseFocusedPane,
+ ScreenInstruction::ToggleActiveTerminalFullscreen => {
+ ScreenContext::ToggleActiveTerminalFullscreen
+ }
+ ScreenInstruction::SetSelectable(..) => ScreenContext::SetSelectable,
+ ScreenInstruction::SetMaxHeight(..) => ScreenContext::SetMaxHeight,
+ ScreenInstruction::ClosePane(_) => ScreenContext::ClosePane,
+ ScreenInstruction::ApplyLayout(_) => ScreenContext::ApplyLayout,
+ ScreenInstruction::NewTab(_) => ScreenContext::NewTab,
+ ScreenInstruction::SwitchTabNext => ScreenContext::SwitchTabNext,
+ ScreenInstruction::SwitchTabPrev => ScreenContext::SwitchTabPrev,
+ ScreenInstruction::CloseTab => ScreenContext::CloseTab,
+ }
+ }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub enum PtyContext {
+ SpawnTerminal,
+ SpawnTerminalVertically,
+ SpawnTerminalHorizontally,
+ NewTab,
+ ClosePane,
+ CloseTab,
+ Quit,
+}
+
+impl From<&PtyInstruction> for PtyContext {
+ fn from(pty_instruction: &PtyInstruction) -> Self {
+ match *pty_instruction {
+ PtyInstruction::SpawnTerminal(_) => PtyContext::SpawnTerminal,
+ PtyInstruction::SpawnTerminalVertically(_) => PtyContext::SpawnTerminalVertically,
+ PtyInstruction::SpawnTerminalHorizontally(_) => PtyContext::SpawnTerminalHorizontally,
+ PtyInstruction::ClosePane(_) => PtyContext::ClosePane,
+ PtyInstruction::CloseTab(_) => PtyContext::CloseTab,
+ PtyInstruction::NewTab => PtyContext::NewTab,
+ PtyInstruction::Quit => PtyContext::Quit,
+ }
+ }
+}
+
+// FIXME: This whole pattern *needs* a macro eventually, it's soul-crushing to write
+
+use crate::wasm_vm::PluginInstruction;
+
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub enum PluginContext {
+ Load,
+ Draw,
+ Input,
+ GlobalInput,
+ Unload,
+ Quit,
+}
+
+impl From<&PluginInstruction> for PluginContext {
+ fn from(plugin_instruction: &PluginInstruction) -> Self {
+ match *plugin_instruction {
+ PluginInstruction::Load(..) => PluginContext::Load,
+ PluginInstruction::Draw(..) => PluginContext::Draw,
+ PluginInstruction::Input(..) => PluginContext::Input,
+ PluginInstruction::GlobalInput(_) => PluginContext::GlobalInput,
+ PluginInstruction::Unload(_) => PluginContext::Unload,
+ PluginInstruction::Quit => PluginContext::Quit,
+ }
+ }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub enum AppContext {
+ GetState,
+ SetState,
+ Exit,
+ Error,
+}
+
+impl From<&AppInstruction> for AppContext {
+ fn from(app_instruction: &AppInstruction) -> Self {
+ match *app_instruction {
+ AppInstruction::GetState(_) => AppContext::GetState,
+ AppInstruction::SetState(_) => AppContext::SetState,
+ AppInstruction::Exit => AppContext::Exit,
+ AppInstruction::Error(_) => AppContext::Error,
+ }
+ }
+}
diff --git a/src/common/input.rs b/src/common/input.rs
new file mode 100644
index 00000000..125b3ced
--- /dev/null
+++ b/src/common/input.rs
@@ -0,0 +1,359 @@
+use super::{os_input_output::OsApi, update_state, AppState};
+use super::{AppInstruction, SenderWithContext, OPENCALLS};
+use crate::pty_bus::PtyInstruction;
+use crate::screen::ScreenInstruction;
+use crate::CommandIsExecuting;
+use crate::{errors::ContextType, wasm_vm::PluginInstruction};
+
+struct InputHandler {
+ mode: InputMode,
+ os_input: Box<dyn OsApi>,
+ command_is_executing: CommandIsExecuting,
+ send_screen_instructions: SenderWithContext<ScreenInstruction>,
+ send_pty_instructions: SenderWithContext<PtyInstruction>,
+ send_plugin_instructions: SenderWithContext<PluginInstruction>,
+ send_app_instructions: SenderWithContext<AppInstruction>,
+}
+
+impl InputHandler {
+ fn new(
+ os_input: Box<dyn OsApi>,
+ command_is_executing: CommandIsExecuting,
+ send_screen_instructions: SenderWithContext<ScreenInstruction>,
+ send_pty_instructions: SenderWithContext<PtyInstruction>,
+ send_plugin_instructions: SenderWithContext<PluginInstruction>,
+ send_app_instructions: SenderWithContext<AppInstruction>,
+ ) -> Self {
+ InputHandler {
+ mode: InputMode::Normal,
+ os_input,
+ command_is_executing,
+ send_screen_instructions,
+ send_pty_instructions,
+ send_plugin_instructions,
+ send_app_instructions,
+ }
+ }
+
+ /// Main event loop
+ fn get_input(&mut self) {
+ let mut err_ctx = OPENCALLS.with(|ctx| *ctx.borrow());
+ err_ctx.add_call(ContextType::StdinHandler);
+ self.send_pty_instructions.update(err_ctx);
+ self.send_plugin_instructions.update(err_ctx);
+ self.send_app_instructions.update(err_ctx);
+ self.send_screen_instructions.update(err_ctx);
+ loop {
+ update_state(&self.send_app_instructions, |_| AppState {
+ input_mode: self.mode,
+ });
+ match self.mode {
+ InputMode::Normal => self.read_normal_mode(),
+ InputMode::Command => self.read_command_mode(false),
+ InputMode::CommandPersistent => self.read_command_mode(true),
+ InputMode::Exiting => {
+ self.exit();
+ break;
+ }
+ }
+ }
+ }
+
+ /// Read input to the terminal (or switch to command mode)
+ fn read_normal_mode(&mut self) {
+ assert_eq!(self.mode, InputMode::Normal);
+
+ loop {
+ let stdin_buffer = self.os_input.read_from_stdin();
+ #[cfg(not(test))] // Absolutely zero clue why this breaks *all* of the tests
+ drop(
+ self.send_plugin_instructions
+ .send(PluginInstruction::GlobalInput(stdin_buffer.clone())),
+ );
+ match stdin_buffer.as_slice() {
+ [7] => {
+ // ctrl-g
+ self.mode = InputMode::Command;
+ return;
+ }
+ _ => {
+ self.send_screen_instructions
+ .send(ScreenInstruction::ClearScroll)
+ .unwrap();
+ self.send_screen_instructions
+ .send(ScreenInstruction::WriteCharacter(stdin_buffer))
+ .unwrap();
+ }
+ }
+ }
+ }
+
+ /// Read input and parse it as commands for mosaic
+ fn read_command_mode(&mut self, persistent: bool) {
+ //@@@khs26 Add a powerbar type thing that we can write output to
+ if persistent {
+ assert_eq!(self.mode, InputMode::CommandPersistent);
+ } else {
+ assert_eq!(self.mode, InputMode::Command);
+ }
+
+ loop {
+ let stdin_buffer = self.os_input.read_from_stdin();
+ #[cfg(not(test))] // Absolutely zero clue why this breaks *all* of the tests
+ drop(
+ self.send_plugin_instructions
+ .send(PluginInstruction::GlobalInput(stdin_buffer.clone())),
+ );
+ // uncomment this to print the entered character to a log file (/tmp/mosaic/mosaic-log.txt) for debugging
+ // debug_log_to_file(format!("buffer {:?}", stdin_buffer));
+
+ match stdin_buffer.as_slice() {
+ [7] => {
+ // Ctrl-g
+ // If we're in command mode, this will let us switch to persistent command mode, to execute
+ // multiple commands. If we're already in persistent mode, it'll return us to normal mode.
+ match self.mode {
+ InputMode::Command => self.mode = InputMode::CommandPersistent,
+ InputMode::CommandPersistent => self.mode = InputMode::Normal,
+ _ => panic!(),
+ }
+ return;
+ }
+ [27] => {
+ // Esc
+ self.mode = InputMode::Normal;
+ return;
+ }
+ [106] => {
+ // j
+ self.send_screen_instructions
+ .send(ScreenInstruction::ResizeDown)
+ .unwrap();
+ }
+ [107] => {
+ // k
+ self.send_screen_instructions
+ .send(ScreenInstruction::ResizeUp)
+ .unwrap();
+ }
+ [112] => {
+ // p
+ self.send_screen_instructions
+ .send(ScreenInstruction::MoveFocus)
+ .unwrap();
+ }
+ [104] => {
+ // h
+ self.send_screen_instructions
+ .send(ScreenInstruction::ResizeLeft)
+ .unwrap();
+ }
+ [108] => {
+ // l
+ self.send_screen_instructions
+ .send(ScreenInstruction::ResizeRight)
+ .unwrap();
+ }
+ [122] => {
+ // z
+ self.command_is_executing.opening_new_pane();
+ self.send_pty_instructions
+ .send(PtyInstruction::SpawnTerminal(None))
+ .unwrap();
+ self.command_is_executing.wait_until_new_pane_is_opened();
+ }
+ [110] => {
+ // n
+ self.command_is_executing.opening_new_pane();
+ self.send_pty_instructions
+ .send(PtyInstruction::SpawnTerminalVertically(None))
+ .unwrap();
+ self.command_is_executing.wait_until_new_pane_is_opened();
+ }
+ [98] => {
+ // b
+ self.command_is_executing.opening_new_pane();
+ self.send_pty_instructions
+ .send(PtyInstruction::SpawnTerminalHorizontally(None))
+ .unwrap();
+ self.command_is_executing.wait_until_new_pane_is_opened();
+ }
+ [113] => {
+ // q
+ self.mode = InputMode::Exiting;
+ return;
+ }
+ [27, 91, 53, 126] => {
+ // PgUp
+ self.send_screen_instructions
+ .send(ScreenInstruction::ScrollUp)
+ .unwrap();
+ }
+ [27, 91, 54, 126] => {
+ // PgDown
+ self.send_screen_instructions
+ .send(ScreenInstruction::ScrollDown)
+ .unwrap();
+ }
+ [120] => {
+ // x
+ self.command_is_executing.closing_pane();
+ self.send_screen_instructions
+ .send(ScreenInstruction::CloseFocusedPane)
+ .unwrap();
+ self.command_is_executing.wait_until_pane_is_closed();
+ }
+ [101] => {
+ // e
+ self.send_screen_instructions
+ .send(ScreenInstruction::ToggleActiveTerminalFullscreen)
+ .unwrap();
+ }
+ [121] => {
+ // y
+ self.send_screen_instructions
+ .send(ScreenInstruction::MoveFocusLeft)
+ .unwrap()
+ }
+ [117] => {
+ // u
+ self.send_screen_instructions
+ .send(ScreenInstruction::MoveFocusDown)
+ .unwrap()
+ }
+ [105] => {
+ // i
+ self.send_screen_instructions
+ .send(ScreenInstruction::MoveFocusUp)
+ .unwrap()
+ }
+ [111] => {
+ // o
+ self.send_screen_instructions
+ .send(ScreenInstruction::MoveFocusRight)
+ .unwrap()
+ }
+ [49] => {
+ // 1
+ self.command_is_executing.opening_new_pane();
+ self.send_pty_instructions
+ .send(PtyInstruction::NewTab)
+ .unwrap();
+ self.command_is_executing.wait_until_new_pane_is_opened();
+ }
+ [50] => {
+ // 2
+ self.send_screen_instructions
+ .send(ScreenInstruction::SwitchTabPrev)
+ .unwrap()
+ }
+ [51] => {
+ // 3
+ self.send_screen_instructions
+ .send(ScreenInstruction::SwitchTabNext)
+ .unwrap()
+ }
+ [52] => {
+ // 4
+ self.command_is_executing.closing_pane();
+ self.send_screen_instructions
+ .send(ScreenInstruction::CloseTab)
+ .unwrap();
+ self.command_is_executing.wait_until_pane_is_closed();
+ }
+ //@@@khs26 Write this to the powerbar?
+ _ => continue,
+ }
+
+ if self.mode == InputMode::Command {
+ self.mode = InputMode::Normal;
+ return;
+ }
+ }
+ }
+
+ /// Routine to be called when the input handler exits (at the moment this is the
+ /// same as quitting mosaic)
+ fn exit(&mut self) {
+ self.send_screen_instructions
+ .send(ScreenInstruction::Quit)
+ .unwrap();
+ self.send_pty_instructions
+ .send(PtyInstruction::Quit)
+ .unwrap();
+ self.send_plugin_instructions
+ .send(PluginInstruction::Quit)
+ .unwrap();
+ self.send_app_instructions
+ .send(AppInstruction::Exit)
+ .unwrap();
+ }
+}
+
+/// Dictates whether we're in command mode, persistent command mode, normal mode or exiting:
+/// - Normal mode either writes characters to the terminal, or switches to command mode
+/// using a particular key control
+/// - Command mode intercepts characters to control mosaic itself, before switching immediately
+/// back to normal mode
+/// - Persistent command mode is the same as command mode, but doesn't return automatically to
+/// normal mode
+/// - Exiting means that we should start the shutdown process for mosaic or the given
+/// input handler
+#[derive(Debug, PartialEq, Clone, Copy)]
+pub enum InputMode {
+ Normal,
+ Command,
+ CommandPersistent,
+ Exiting,
+}
+
+// FIXME: This should be auto-generated from the soon-to-be-added `get_default_keybinds`
+pub fn get_help(mode: &InputMode) -> Vec<String> {
+ let command_help = vec![
+ "<n/b/z> Split".into(),
+ "<j/k/h/l> Resize".into(),
+ "<p> Focus Next".into(),
+ "<x> Close Pane".into(),
+ "<q> Quit".into(),
+ "<PgUp/PgDown> Scroll".into(),
+ "<1> New Tab".into(),
+ "<2/3> Move Tab".into(),
+ "<4> Close Tab".into(),
+ ];
+ match mode {
+ InputMode::Normal => vec!["<Ctrl-g> Command Mode".into()],
+ InputMode::Command => [
+ vec![
+ "<Ctrl-g> Persistent Mode".into(),
+ "<ESC> Normal Mode".into(),
+ ],
+ command_help,
+ ]
+ .concat(),
+ InputMode::CommandPersistent => {
+ [vec!["<ESC/Ctrl-g> Normal Mode".into()], command_help].concat()
+ }
+ InputMode::Exiting => vec!["Bye from Mosaic!".into()],
+ }
+}
+
+/// Entry point to the module that instantiates a new InputHandler and calls its
+/// reading loop
+pub fn input_loop(
+ os_input: Box<dyn OsApi>,
+ command_is_executing: CommandIsExecuting,
+ send_screen_instructions: SenderWithContext<ScreenInstruction>,
+ send_pty_instructions: SenderWithContext<PtyInstruction>,
+ send_plugin_instructions: SenderWithContext<PluginInstruction>,
+ send_app_instructions: SenderWithContext<AppInstruction>,
+) {
+ let _handler = InputHandler::new(
+ os_input,
+ command_is_executing,
+ send_screen_instructions,
+ send_pty_instructions,
+ send_plugin_instructions,
+ send_app_instructions,
+ )
+ .get_input();
+}
diff --git a/src/common/ipc.rs b/src/common/ipc.rs
new file mode 100644
index 00000000..425d81d6
--- /dev/null
+++ b/src/common/ipc.rs
@@ -0,0 +1,46 @@
+// IPC stuff for starting to split things into a client and server model
+use serde::{Deserialize, Serialize};
+use std::collections::HashSet;
+
+type SessionID = u64;
+
+#[derive(PartialEq, Eq, Serialize, Deserialize, Hash)]
+pub struct Session {
+ // Unique ID for this session
+ id: SessionID,
+ // Identifier for the underlying IPC primitive (socket, pipe)
+ conn_name: String,
+ // User configured alias for the session
+ alias: String,
+}
+
+// How do we want to connect to a session?
+#[derive(Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
+pub enum ClientType {
+ Reader,
+ Writer,
+}
+
+// Types of messages sent from the client to the server
+#[derive(Serialize, Deserialize)]
+pub enum ClientToServerMsg {
+ // List which sessions are available
+ ListSessions,
+ // Create a new session
+ CreateSession,
+ // Attach to a running session
+ AttachToSession(SessionID, ClientType),
+ // Force detach
+ DetachSession(SessionID),
+ // Disconnect from the session we're connected to
+ DisconnectFromSession,
+}
+
+// Types of messages sent from the server to the client
+// @@@ Implement Serialize and Deserialize for this...
+pub enum ServerToClientMsg {
+ // Info about a particular session
+ SessionInfo(Session),
+ // A list of sessions
+ SessionList(HashSet<Session>),
+}
diff --git a/src/common/mod.rs b/src/common/mod.rs
new file mode 100644
index 00000000..ef9919a6
--- /dev/null
+++ b/src/common/mod.rs
@@ -0,0 +1,662 @@
+pub mod command_is_executing;
+pub mod errors;
+pub mod input;
+pub mod ipc;
+pub mod os_input_output;
+pub mod pty_bus;
+pub mod screen;
+pub mod utils;
+pub mod wasm_vm;
+
+use std::io::Write;
+use std::path::{Path, PathBuf};
+use std::sync::mpsc::{channel, sync_channel, Receiver, SendError, Sender, SyncSender};
+use std::thread;
+use std::{cell::RefCell, sync::mpsc::TrySendError};
+use std::{collections::HashMap, fs};
+
+use crate::panes::PaneId;
+use directories_next::ProjectDirs;
+use input::InputMode;
+use serde::{Deserialize, Serialize};
+use termion::input::TermRead;
+use wasm_vm::PluginEnv;
+use wasmer::{ChainableNamedResolver, Instance, Module, Store, Value};
+use wasmer_wasi::{Pipe, WasiState};
+
+use crate::cli::CliArgs;
+use crate::layout::Layout;
+use command_is_executing::CommandIsExecuting;
+use errors::{AppContext, ContextType, ErrorContext, PluginContext, PtyContext, ScreenContext};
+use input::input_loop;
+use os_input_output::OsApi;
+use pty_bus::{PtyBus, PtyInstruction};
+use screen::{Screen, ScreenInstruction};
+use utils::consts::{MOSAIC_IPC_PIPE, MOSAIC_ROOT_PLUGIN_DIR};
+use wasm_vm::{mosaic_imports, wasi_stdout, wasi_write_string, PluginInstruction};
+
+#[derive(Serialize, Deserialize, Debug)]
+pub enum ApiCommand {
+ OpenFile(PathBuf),
+ SplitHorizontally,
+ SplitVertically,
+ MoveFocus,
+}
+// FIXME: It would be good to add some more things to this over time
+#[derive(Debug, Clone)]
+pub struct AppState {
+ pub input_mode: InputMode,
+}
+
+impl Default for AppState {
+ fn default() -> Self {
+ Self {
+ input_mode: InputMode::Normal,
+ }
+ }
+}
+// FIXME: Make this a method on the big `Communication` struct, so that app_tx can be extracted
+// from self instead of being explicitly passed here
+pub fn update_state(
+ app_tx: &SenderWithContext<AppInstruction>,
+ update_fn: impl FnOnce(AppState) -> AppState,
+) {
+ let (state_tx, state_rx) = channel();
+
+ drop(app_tx.send(AppInstruction::GetState(state_tx)));
+ let state = state_rx.recv().unwrap();
+
+ drop(app_tx.send(AppInstruction::SetState(update_fn(state))))
+}
+
+pub type ChannelWithContext<T> = (Sender<(T, ErrorContext)>, Receiver<(T, ErrorContext)>);
+pub type SyncChannelWithContext<T> = (SyncSender<(T, ErrorContext)>, Receiver<(T, ErrorContext)>);
+
+#[derive(Clone)]
+enum SenderType<T: Clone> {
+ Sender(Sender<(T, ErrorContext)>),
+ SyncSender(SyncSender<(T, ErrorContext)>),
+}
+
+#[derive(Clone)]
+pub struct SenderWithContext<T: Clone> {
+ err_ctx: ErrorContext,
+ sender: SenderType<T>,
+}
+
+impl<T: Clone> SenderWithContext<T> {
+ fn new(err_ctx: ErrorContext, sender: SenderType<T>) -> Self {
+ Self { err_ctx, sender }
+ }
+
+ pub fn send(&self, event: T) -> Result<(), SendError<(T, ErrorContext)>> {
+ match self.sender {
+ SenderType::Sender(ref s) => s.send((event, self.err_ctx)),
+ SenderType::SyncSender(ref s) => s.send((event, self.err_ctx)),
+ }
+ }
+
+ pub fn try_send(&self, event: T) -> Result<(), TrySendError<(T, ErrorContext)>> {
+ if let SenderType::SyncSender(ref s) = self.sender {
+ s.try_send((event, self.err_ctx))
+ } else {
+ panic!("try_send can only be called on SyncSenders!")
+ }
+ }
+
+ pub fn update(&mut self, new_ctx: ErrorContext) {
+ self.err_ctx = new_ctx;
+ }
+}
+
+unsafe impl<T: Clone> Send for SenderWithContext<T> {}
+unsafe impl<T: Clone> Sync for SenderWithContext<T> {}
+
+thread_local!(static OPENCALLS: RefCell<ErrorContext> = RefCell::default());
+
+#[derive(Clone)]
+pub enum AppInstruction {
+ GetState(Sender<AppState>),
+ SetState(AppState),
+ Exit,
+ Error(String),
+}
+
+pub fn start(mut os_input: Box<dyn OsApi>, opts: CliArgs) {
+ let take_snapshot = "\u{1b}[?1049h";
+ os_input.unset_raw_mode(0);
+ let _ = os_input
+ .get_stdout_writer()
+ .write(take_snapshot.as_bytes())
+ .unwrap();
+ let mut app_state = AppState::default();
+
+ let command_is_executing = CommandIsExecuting::new();
+
+ let full_screen_ws = os_input.get_terminal_size_using_fd(0);
+ os_input.set_raw_mode(0);
+ let (send_screen_instructions, receive_screen_instructions): ChannelWithContext<
+ ScreenInstruction,
+ > = channel();
+ let err_ctx = OPENCALLS.with(|ctx| *ctx.borrow());
+ let mut send_screen_instructions =
+ SenderWithContext::new(err_ctx, SenderType::Sender(send_screen_instructions));
+
+ let (send_pty_instructions, receive_pty_instructions): ChannelWithContext<PtyInstruction> =
+ channel();
+ let mut send_pty_instructions =
+ SenderWithContext::new(err_ctx, SenderType::Sender(send_pty_instructions));
+
+ let (send_plugin_instructions, receive_plugin_instructions): ChannelWithContext<
+ PluginInstruction,
+ > = channel();
+ let send_plugin_instructions =
+ SenderWithContext::new(err_ctx, SenderType::Sender(send_plugin_instructions));
+
+ let (send_app_instructions, receive_app_instructions): SyncChannelWithContext<AppInstruction> =
+ sync_channel(0);
+ let send_app_instructions =
+ SenderWithContext::new(err_ctx, SenderType::SyncSender(send_app_instructions));
+
+ let mut pty_bus = PtyBus::new(
+ receive_pty_instructions,
+ send_screen_instructions.clone(),
+ send_plugin_instructions.clone(),
+ os_input.clone(),
+ opts.debug,
+ );
+
+ // Don't use default layouts in tests, but do everywhere else
+ #[cfg(not(test))]
+ let default_layout = Some(PathBuf::from("default"));
+ #[cfg(test)]
+ let default_layout = None;
+ let maybe_layout = opts.layout.or(default_layout).map(Layout::new);
+
+ #[cfg(not(test))]
+ std::panic::set_hook({
+ use errors::handle_panic;
+ let send_app_instructions = send_app_instructions.clone();
+ Box::new(move |info| {
+ handle_panic(info, &send_app_instructions);
+ })
+ });
+
+ let pty_thread = thread::Builder::new()
+ .name("pty".to_string())
+ .spawn({
+ let mut command_is_executing = command_is_executing.clone();
+ send_pty_instructions.send(PtyInstruction::NewTab).unwrap();
+ move || loop {
+ let (event, mut err_ctx) = pty_bus
+ .receive_pty_instructions
+ .recv()
+ .expect("failed to receive event on channel");
+ err_ctx.add_call(ContextType::Pty(PtyContext::from(&event)));
+ pty_bus.send_screen_instructions.update(err_ctx);
+ match event {
+ PtyInstruction::SpawnTerminal(file_to_open) => {
+ let pid = pty_bus.spawn_terminal(file_to_open);
+ pty_bus
+ .send_screen_instructions
+ .send(ScreenInstruction::NewPane(PaneId::Terminal(pid)))
+ .unwrap();
+ }
+ PtyInstruction::SpawnTerminalVertically(file_to_open) => {
+ let pid = pty_bus.spawn_terminal(file_to_open);
+ pty_bus
+ .send_screen_instructions
+ .send(ScreenInstruction::VerticalSplit(PaneId::Terminal(pid)))
+ .unwrap();
+ }
+ PtyInstruction::SpawnTerminalHorizontally(file_to_open) => {
+ let pid = pty_bus.spawn_terminal(file_to_open);
+ pty_bus
+ .send_screen_instructions
+ .send(ScreenInstruction::HorizontalSplit(PaneId::Terminal(pid)))
+ .unwrap();
+ }
+ PtyInstruction::NewTab => {
+ if let Some(layout) = maybe_layout.clone() {
+ pty_bus.spawn_terminals_for_layout(layout);
+ } else {
+ let pid = pty_bus.spawn_terminal(None);
+ pty_bus
+ .send_screen_instructions
+ .send(ScreenInstruction::NewTab(pid))
+ .unwrap();
+ }
+ }
+ PtyInstruction::ClosePane(id) => {
+ pty_bus.close_pane(id);
+ command_is_executing.done_closing_pane();
+ }
+ PtyInstruction::CloseTab(ids) => {
+ pty_bus.close_tab(ids);
+ command_is_executing.done_closing_pane();
+ }
+ PtyInstruction::Quit => {
+ break;
+ }
+ }
+ }
+ })
+ .unwrap();
+
+ let screen_thread = thread::Builder::new()
+ .name("screen".to_string())
+ .spawn({
+ let mut command_is_executing = command_is_executing.clone();
+ let os_input = os_input.clone();
+ let send_pty_instructions = send_pty_instructions.clone();
+ let send_plugin_instructions = send_plugin_instructions.clone();
+ let send_app_instructions = send_app_instructions.clone();
+ let max_panes = opts.max_panes;
+
+ move || {
+ let mut screen = Screen::new(
+ receive_screen_instructions,
+ send_pty_instructions,
+ send_plugin_instructions,
+ send_app_instructions,
+ &full_screen_ws,
+ os_input,
+ max_panes,
+ );
+ loop {
+ let (event, mut err_ctx) = screen
+ .receiver
+ .recv()
+ .expect("failed to receive event on channel");
+ err_ctx.add_call(ContextType::Screen(ScreenContext::from(&event)));
+ screen.send_app_instructions.update(err_ctx);
+ screen.send_pty_instructions.update(err_ctx);
+ match event {
+ ScreenInstruction::Pty(pid, vte_event) => {
+ screen
+ .get_active_tab_mut()
+ .unwrap()
+ .handle_pty_event(pid, vte_event);
+ }
+ ScreenInstruction::Render => {
+ screen.render();
+ }
+ ScreenInstruction::NewPane(pid) => {
+ screen.get_active_tab_mut().unwrap().new_pane(pid);
+ command_is_executing.done_opening_new_pane();
+ }
+ ScreenInstruction::HorizontalSplit(pid) => {
+ screen.get_active_tab_mut().unwrap().horizontal_split(pid);
+ command_is_executing.done_opening_new_pane();
+ }
+ ScreenInstruction::VerticalSplit(pid) => {
+ screen.get_active_tab_mut().unwrap().vertical_split(pid);
+ command_is_executing.done_opening_new_pane();
+ }
+ ScreenInstruction::WriteCharacter(bytes) => {
+ screen
+ .get_active_tab_mut()
+ .unwrap()
+ .write_to_active_terminal(bytes);
+ }
+ ScreenInstruction::ResizeLeft => {
+ screen.get_active_tab_mut().unwrap().resize_left();
+ }
+ ScreenInstruction::ResizeRight => {
+ screen.get_active_tab_mut().unwrap().resize_right();
+ }
+ ScreenInstruction::ResizeDown => {
+ screen.get_active_tab_mut().unwrap().resize_down();
+ }
+ ScreenInstruction::ResizeUp => {
+ screen.get_active_tab_mut().unwrap().resize_up();
+ }
+ ScreenInstruction::MoveFocus => {
+ screen.get_active_tab_mut().unwrap().move_focus();
+ }
+ ScreenInstruction::MoveFocusLeft => {
+ screen.get_active_tab_mut().unwrap().move_focus_left();
+ }
+ ScreenInstruction::MoveFocusDown => {
+ screen.get_active_tab_mut().unwrap().move_focus_down();
+ }
+ ScreenInstruction::MoveFocusRight => {
+ screen.get_active_tab_mut().unwrap().move_focus_right();
+ }
+ ScreenInstruction::MoveFocusUp => {
+ screen.get_active_tab_mut().unwrap().move_focus_up();
+ }
+ ScreenInstruction::ScrollUp => {
+ screen
+ .get_active_tab_mut()
+ .unwrap()
+ .scroll_active_terminal_up();
+ }
+ ScreenInstruction::ScrollDown => {
+ screen
+ .get_active_tab_mut()
+ .unwrap()
+ .scroll_active_terminal_down();
+ }
+ ScreenInstruction::ClearScroll => {
+ screen
+ .get_active_tab_mut()
+ .unwrap()
+ .clear_active_terminal_scroll();
+ }
+ ScreenInstruction::CloseFocusedPane => {
+ screen.get_active_tab_mut().unwrap().close_focused_pane();
+ screen.render();
+ }
+ ScreenInstruction::SetSelectable(id, selectable) => {
+ screen
+ .get_active_tab_mut()
+ .unwrap()
+ .set_pane_selectable(id, selectable);
+ // FIXME: Is this needed?
+ screen.render();
+ }
+ ScreenInstruction::SetMaxHeight(id, max_height) => {
+ screen
+ .get_active_tab_mut()
+ .unwrap()
+ .set_pane_max_height(id, max_height);
+ }
+ ScreenInstruction::ClosePane(id) => {
+ screen.get_active_tab_mut().unwrap().close_pane(id);
+ screen.render();
+ }
+ ScreenInstruction::ToggleActiveTerminalFullscreen => {
+ screen
+ .get_active_tab_mut()
+ .unwrap()
+ .toggle_active_pane_fullscreen();
+ }
+ ScreenInstruction::NewTab(pane_id) => {
+ screen.new_tab(pane_id);
+ command_is_executing.done_opening_new_pane();
+ }
+ ScreenInstruction::SwitchTabNext => screen.switch_tab_next(),
+ ScreenInstruction::SwitchTabPrev => screen.switch_tab_prev(),
+ ScreenInstruction::CloseTab => screen.close_tab(),
+ ScreenInstruction::ApplyLayout((layout, new_pane_pids)) => {
+ screen.apply_layout(layout, new_pane_pids);
+ command_is_executing.done_opening_new_pane();
+ }
+ ScreenInstruction::Quit => {
+ break;
+ }
+ }
+ }
+ }
+ })
+ .unwrap();
+
+ let wasm_thread = thread::Builder::new()
+ .name("wasm".to_string())
+ .spawn({
+ let mut send_pty_instructions = send_pty_instructions.clone();
+ let mut send_screen_instructions = send_screen_instructions.clone();
+ let mut send_app_instructions = send_app_instructions.clone();
+
+ let store = Store::default();
+ let mut plugin_id = 0;
+ let mut plugin_map = HashMap::new();
+
+ move || loop {
+ let (event, mut err_ctx) = receive_plugin_instructions
+ .recv()
+ .expect("failed to receive event on channel");
+ err_ctx.add_call(ContextType::Plugin(PluginContext::from(&event)));
+ send_screen_instructions.update(err_ctx);
+ send_pty_instructions.update(err_ctx);
+ send_app_instructions.update(err_ctx);
+ match event {
+ PluginInstruction::Load(pid_tx, path) => {
+ let project_dirs =
+ ProjectDirs::from("org", "Mosaic Contributors", "Mosaic").unwrap();
+ let plugin_dir = project_dirs.data_dir().join("plugins/");
+ let root_plugin_dir = Path::new(MOSAIC_ROOT_PLUGIN_DIR);
+ let wasm_bytes = fs::read(&path)
+ .or_else(|_| fs::read(&path.with_extension("wasm")))
+ .or_else(|_| fs::read(&plugin_dir.join(&path).with_extension("wasm")))
+ .or_else(|_| {
+ fs::read(&root_plugin_dir.join(&path).with_extension("wasm"))
+ })
+ .unwrap_or_else(|_| panic!("cannot find plugin {}", &path.display()));
+
+ // FIXME: Cache this compiled module on disk. I could use `(de)serialize_to_file()` for that
+ let module = Module::new(&store, &wasm_bytes).unwrap();
+
+ let output = Pipe::new();
+ let input = Pipe::new();
+ let mut wasi_env = WasiState::new("mosaic")
+ .env("CLICOLOR_FORCE", "1")
+ .preopen(|p| {
+ p.directory(".") // FIXME: Change this to a more meaningful dir
+ .alias(".")
+ .read(true)
+ .write(true)
+ .create(true)
+ })
+ .unwrap()
+ .stdin(Box::new(input))
+ .stdout(Box::new(output))
+ .finalize()
+ .unwrap();
+
+ let wasi = wasi_env.import_object(&module).unwrap();
+
+ let plugin_env = PluginEnv {
+ plugin_id,
+ send_pty_instructions: send_pty_instructions.clone(),
+ send_screen_instructions: send_screen_instructions.clone(),
+ send_app_instructions: send_app_instructions.clone(),
+ wasi_env,
+ };
+
+ let mosaic = mosaic_imports(&store, &plugin_env);
+ let instance = Instance::new(&module, &mosaic.chain_back(wasi)).unwrap();
+
+ let start = instance.exports.get_function("_start").unwrap();
+
+ // This eventually calls the `.init()` method
+ start.call(&[]).unwrap();
+
+ plugin_map.insert(plugin_id, (instance, plugin_env));
+ pid_tx.send(plugin_id).unwrap();
+ plugin_id += 1;
+ }
+ PluginInstruction::Draw(buf_tx, pid, rows, cols) => {
+ let (instance, plugin_env) = plugin_map.get(&pid).unwrap();
+
+ let draw = instance.exports.get_function("draw").unwrap();
+
+ draw.call(&[Value::I32(rows as i32), Value::I32(cols as i32)])
+ .unwrap();
+
+ buf_tx.send(wasi_stdout(&plugin_env.wasi_env)).unwrap();
+ }
+ // FIXME: Deduplicate this with the callback below!
+ PluginInstruction::Input(pid, input_bytes) => {
+ let (instance, plugin_env) = plugin_map.get(&pid).unwrap();
+
+ let handle_key = instance.exports.get_function("handle_key").unwrap();
+ for key in input_bytes.keys() {
+ if let Ok(key) = key {
+ wasi_write_string(
+ &plugin_env.wasi_env,
+ &serde_json::to_string(&key).unwrap(),
+ );
+ handle_key.call(&[]).unwrap();
+ }
+ }
+
+ drop(send_screen_instructions.send(ScreenInstruction::Render));
+ }
+ PluginInstruction::GlobalInput(input_bytes) => {
+ // FIXME: Set up an event subscription system, and timed callbacks
+ for (instance, plugin_env) in plugin_map.values() {
+ let handler =
+ instance.exports.get_function("handle_global_key").unwrap();
+ for key in input_bytes.keys() {
+ if let Ok(key) = key {
+ wasi_write_string(
+ &plugin_env.wasi_env,
+ &serde_json::to_string(&key).unwrap(),
+ );
+ handler.call(&[]).unwrap();
+ }
+ }
+ }
+
+ drop(send_screen_instructions.send(ScreenInstruction::Render));
+ }
+ PluginInstruction::Unload(pid) => drop(plugin_map.remove(&pid)),
+ PluginInstruction::Quit => break,
+ }
+ }
+ })
+ .unwrap();
+
+ // TODO: currently we don't wait for this to quit
+ // because otherwise the app will hang. Need to fix this so it both
+ // listens to the ipc-bus and is able to quit cleanly
+ #[cfg(not(test))]
+ let _ipc_thread = thread::Builder::new()
+ .name("ipc_server".to_string())
+ .spawn({
+ use std::io::Read;
+ let mut send_pty_instructions = send_pty_instructions.clone();
+ let mut send_screen_instructions = send_screen_instructions.clone();
+ move || {
+ std::fs::remove_file(MOSAIC_IPC_PIPE).ok();
+ let listener = std::os::unix::net::UnixListener::bind(MOSAIC_IPC_PIPE)
+ .expect("could not listen on ipc socket");
+ let mut err_ctx = OPENCALLS.with(|ctx| *ctx.borrow());
+ err_ctx.add_call(ContextType::IPCServer);
+ send_pty_instructions.update(err_ctx);
+ send_screen_instructions.update(err_ctx);
+
+ for stream in listener.incoming() {
+ match stream {
+ Ok(mut stream) => {
+ let mut buffer = [0; 65535]; // TODO: more accurate
+ let _ = stream
+ .read(&mut buffer)
+ .expect("failed to parse ipc message");
+ let decoded: ApiCommand = bincode::deserialize(&buffer)
+ .expect("failed to deserialize ipc message");
+ match &decoded {
+ ApiCommand::OpenFile(file_name) => {
+ let path = PathBuf::from(file_name);
+ send_pty_instructions
+ .send(PtyInstruction::SpawnTerminal(Some(path)))
+ .unwrap();
+ }
+ ApiCommand::SplitHorizontally => {
+ send_pty_instructions
+ .send(PtyInstruction::SpawnTerminalHorizontally(None))
+ .unwrap();
+ }
+ ApiCommand::SplitVertically => {
+ send_pty_instructions
+ .send(PtyInstruction::SpawnTerminalVertically(None))
+ .unwrap();
+ }
+ ApiCommand::MoveFocus => {
+ send_screen_instructions
+ .send(ScreenInstruction::MoveFocus)
+ .unwrap();
+ }
+ }
+ }
+ Err(err) => {
+ panic!("err {:?}", err);
+ }
+ }
+ }
+ }
+ })
+ .unwrap();
+
+ let _stdin_thread = thread::Builder::new()
+ .name("stdin_handler".to_string())
+ .spawn({
+ let send_screen_instructions = send_screen_instructions.clone();
+ let send_pty_instructions = send_pty_instructions.clone();
+ let send_plugin_instructions = send_plugin_instructions.clone();
+ let os_input = os_input.clone();
+ move || {
+ input_loop(
+ os_input,
+ command_is_executing,
+ send_screen_instructions,
+ send_pty_instructions,
+ send_plugin_instructions,
+ send_app_instructions,
+ )
+ }
+ });
+
+ #[warn(clippy::never_loop)]
+ loop {
+ let (app_instruction, mut err_ctx) = receive_app_instructions
+ .recv()
+ .expect("failed to receive app instruction on channel");
+
+ err_ctx.add_call(ContextType::App(AppContext::from(&app_instruction)));
+ send_screen_instructions.update(err_ctx);
+ send_pty_instructions.update(err_ctx);
+ match app_instruction {
+ AppInstruction::GetState(state_tx) => drop(state_tx.send(app_state.clone())),
+ AppInstruction::SetState(state) => app_state = state,
+ AppInstruction::Exit => {
+ let _ = send_screen_instructions.send(ScreenInstruction::Quit);
+ let _ = send_pty_instructions.send(PtyInstruction::Quit);
+ let _ = send_plugin_instructions.send(PluginInstruction::Quit);
+ break;
+ }
+ AppInstruction::Error(backtrace) => {
+ let _ = send_screen_instructions.send(ScreenInstruction::Quit);
+ let _ = screen_thread.join();
+ let _ = send_pty_instructions.send(PtyInstruction::Quit);
+ let _ = pty_thread.join();
+ let _ = send_plugin_instructions.send(PluginInstruction::Quit);
+ let _ = wasm_thread.join();
+ os_input.unset_raw_mode(0);
+ let goto_start_of_last_line = format!("\u{1b}[{};{}H", full_screen_ws.rows, 1);
+ let error = format!("{}\n{}", goto_start_of_last_line, backtrace);
+ let _ = os_input
+ .get_stdout_writer()
+ .write(error.as_bytes())
+ .unwrap();
+ std::process::exit(1);
+ }
+ }
+ }
+
+ let _ = send_screen_instructions.send(ScreenInstruction::Quit);
+ screen_thread.join().unwrap();
+ let _ = send_pty_instructions.send(PtyInstruction::Quit);
+ pty_thread.join().unwrap();
+ let _ = send_plugin_instructions.send(PluginInstruction::Quit);
+ wasm_thread.join().unwrap();
+
+ // cleanup();
+ let reset_style = "\u{1b}[m";
+ let show_cursor = "\u{1b}[?25h";
+ let restore_snapshot = "\u{1b}[?1049l";
+ let goto_start_of_last_line = format!("\u{1b}[{};{}H", full_screen_ws.rows, 1);
+ let goodbye_message = format!(
+ "{}\n{}{}{}Bye from Mosaic!",
+ goto_start_of_last_line, restore_snapshot, reset_style, show_cursor
+ );
+
+ os_input.unset_raw_mode(0);
+ let _ = os_input
+ .get_stdout_writer()
+ .write(goodbye_message.as_bytes())
+ .unwrap();
+ os_input.get_stdout_writer().flush().unwrap();
+}
diff --git a/src/common/os_input_output.rs b/src/common/os_input_output.rs
new file mode 100644
index 00000000..41ee3b3c
--- /dev/null
+++ b/src/common/os_input_output.rs
@@ -0,0 +1,217 @@
+use crate::panes::PositionAndSize;
+use nix::fcntl::{fcntl, FcntlArg, OFlag};
+use nix::pty::{forkpty, Winsize};
+use nix::sys::signal::{kill, Signal};
+use nix::sys::termios::{cfmakeraw, tcdrain, tcgetattr, tcsetattr, SetArg, Termios};
+use nix::sys::wait::waitpid;
+use nix::unistd::{read, write, ForkResult, Pid};
+use std::io::prelude::*;
+use std::io::{stdin, Write};
+use std::os::unix::io::RawFd;
+use std::path::PathBuf;
+use std::process::{Child, Command};
+use std::sync::{Arc, Mutex};
+
+use std::env;
+
+fn into_raw_mode(pid: RawFd) {
+ let mut tio = tcgetattr(pid).expect("could not get terminal attribute");
+ cfmakeraw(&mut tio);
+ match tcsetattr(pid, SetArg::TCSANOW, &tio) {
+ Ok(_) => {}
+ Err(e) => panic!("error {:?}", e),
+ };
+}
+
+fn unset_raw_mode(pid: RawFd, orig_termios: Termios) {
+ match tcsetattr(pid, SetArg::TCSANOW, &orig_termios) {
+ Ok(_) => {}
+ Err(e) => panic!("error {:?}", e),
+ };
+}
+
+pub fn get_terminal_size_using_fd(fd: RawFd) -> PositionAndSize {
+ // TODO: do this with the nix ioctl
+ use libc::ioctl;
+ use libc::TIOCGWINSZ;
+
+ let mut winsize = Winsize {
+ ws_row: 0,
+ ws_col: 0,
+ ws_xpixel: 0,
+ ws_ypixel: 0,
+ };
+
+ unsafe { ioctl(fd, TIOCGWINSZ, &mut winsize) };
+ PositionAndSize::from(winsize)
+}
+
+pub fn set_terminal_size_using_fd(fd: RawFd, columns: u16, rows: u16) {
+ // TODO: do this with the nix ioctl
+ use libc::ioctl;
+ use libc::TIOCSWINSZ;
+
+ let winsize = Winsize {
+ ws_col: columns,
+ ws_row: rows,
+ ws_xpixel: 0,
+ ws_ypixel: 0,
+ };
+ unsafe { ioctl(fd, TIOCSWINSZ, &winsize) };
+}
+
+fn handle_command_exit(mut child: Child) {
+ let signals = ::signal_hook::iterator::Signals::new(&[::signal_hook::SIGINT]).unwrap();
+ 'handle_exit: loop {
+ match child.try_wait() {
+ Ok(Some(_status)) => {
+ // TODO: handle errors?
+ break;
+ }
+ Ok(None) => {
+ ::std::thread::sleep(::std::time::Duration::from_millis(100));
+ }
+ Err(e) => panic!("error attempting to wait: {}", e),
+ }
+
+ for signal in signals.pending() {
+ // FIXME: We need to handle more signals here!
+ match signal {
+ signal_hook::SIGINT => {
+ child.kill().unwrap();
+ child.wait().unwrap();
+ break 'handle_exit;
+ }
+ _ => {}
+ }
+ }
+ }
+}
+
+fn spawn_terminal(file_to_open: Option<PathBuf>, orig_termios: Termios) -> (RawFd, RawFd) {
+ let (pid_primary, pid_secondary): (RawFd, RawFd) = {
+ match forkpty(None, Some(&orig_termios)) {
+ Ok(fork_pty_res) => {
+ let pid_primary = fork_pty_res.master;
+ let pid_secondary = match fork_pty_res.fork_result {
+ ForkResult::Parent { child } => {
+ // fcntl(pid_primary, FcntlArg::F_SETFL(OFlag::empty())).expect("could not fcntl");
+ fcntl(pid_primary, FcntlArg::F_SETFL(OFlag::O_NONBLOCK))
+ .expect("could not fcntl");
+ child
+ }
+ ForkResult::Child => match file_to_open {
+ Some(file_to_open) => {
+ if env::var("EDITOR").is_err() && env::var("VISUAL").is_err() {
+ panic!("Can't edit files if an editor is not defined. To fix: define the EDITOR or VISUAL environment variables with the path to your editor (eg. /usr/bin/vim)");
+ }
+ let editor =
+ env::var("EDITOR").unwrap_or_else(|_| env::var("VISUAL").unwrap());
+
+ let child = Command::new(editor)
+ .args(&[file_to_open])
+ .spawn()
+ .expect("failed to spawn");
+ handle_command_exit(child);
+ ::std::process::exit(0);
+ }
+ None => {
+ let child = Command::new(env::var("SHELL").unwrap())
+ .spawn()
+ .expect("failed to spawn");
+ handle_command_exit(child);
+ ::std::process::exit(0);
+ }
+ },
+ };
+ (pid_primary, pid_secondary.as_raw())
+ }
+ Err(e) => {
+ panic!("failed to fork {:?}", e);
+ }
+ }
+ };
+ (pid_primary, pid_secondary)
+}
+
+#[derive(Clone)]
+pub struct OsInputOutput {
+ orig_termios: Arc<Mutex<Termios>>,
+}
+
+pub trait OsApi: Send + Sync {
+ fn get_terminal_size_using_fd(&self, pid: RawFd) -> PositionAndSize;
+ fn set_terminal_size_using_fd(&mut self, pid: RawFd, cols: u16, rows: u16);
+ fn set_raw_mode(&mut self, pid: RawFd);
+ fn unset_raw_mode(&mut self, pid: RawFd);
+ fn spawn_terminal(&mut self, file_to_open: Option<PathBuf>) -> (RawFd, RawFd);
+ fn read_from_tty_stdout(&mut self, pid: RawFd, buf: &mut [u8]) -> Result<usize, nix::Error>;
+ fn write_to_tty_stdin(&mut self, pid: RawFd, buf: &mut [u8]) -> Result<usize, nix::Error>;
+ fn tcdrain(&mut self, pid: RawFd) -> Result<(), nix::Error>;
+ fn kill(&mut self, pid: RawFd) -> Result<(), nix::Error>;
+ fn read_from_stdin(&self) -> Vec<u8>;
+ fn get_stdout_writer(&self) -> Box<dyn Write>;
+ fn box_clone(&self) -> Box<dyn OsApi>;
+}
+
+impl OsApi for OsInputOutput {
+ fn get_terminal_size_using_fd(&self, pid: RawFd) -> PositionAndSize {
+ get_terminal_size_using_fd(pid)
+ }
+ fn set_terminal_size_using_fd(&mut self, pid: RawFd, cols: u16, rows: u16) {
+ set_terminal_size_using_fd(pid, cols, rows);
+ }
+ fn set_raw_mode(&mut self, pid: RawFd) {
+ into_raw_mode(pid);
+ }
+ fn unset_raw_mode(&mut self, pid: RawFd) {
+ let orig_termios = self.orig_termios.lock().unwrap();
+ unset_raw_mode(pid, orig_termios.clone());
+ }
+ fn spawn_terminal(&mut self, file_to_open: Option<PathBuf>) -> (RawFd, RawFd) {
+ let orig_termios = self.orig_termios.lock().unwrap();
+ spawn_terminal(file_to_open, orig_termios.clone())
+ }
+ fn read_from_tty_stdout(&mut self, pid: RawFd, buf: &mut [u8]) -> Result<usize, nix::Error> {
+ read(pid, buf)
+ }
+ fn write_to_tty_stdin(&mut self, pid: RawFd, buf: &mut [u8]) -> Result<usize, nix::Error> {
+ write(pid, buf)
+ }
+ fn tcdrain(&mut self, pid: RawFd) -> Result<(), nix::Error> {
+ tcdrain(pid)
+ }
+ fn box_clone(&self) -> Box<dyn OsApi> {
+ Box::new((*self).clone())
+ }
+ fn read_from_stdin(&self) -> Vec<u8> {
+ let stdin = stdin();
+ let mut stdin = stdin.lock();
+ let buffer = stdin.fill_buf().unwrap();
+ let length = buffer.len();
+ let read_bytes = Vec::from(buffer);
+ stdin.consume(length);
+ read_bytes
+ }
+ fn get_stdout_writer(&self) -> Box<dyn Write> {
+ let stdout = ::std::io::stdout();
+ Box::new(stdout)
+ }
+ fn kill(&mut self, fd: RawFd) -> Result<(), nix::Error> {
+ kill(Pid::from_raw(fd), Some(Signal::SIGINT)).unwrap();
+ waitpid(Pid::from_raw(fd), None).unwrap();
+ Ok(())
+ }
+}
+
+impl Clone for Box<dyn OsApi> {
+ fn clone(&self) -> Box<dyn OsApi> {
+ self.box_clone()
+ }
+}
+
+pub fn get_os_input() -> OsInputOutput {
+ let current_termios = tcgetattr(0).unwrap();
+ let orig_termios = Arc::new(Mutex::new(current_termios));
+ OsInputOutput { orig_termios }
+}
diff --git a/src/common/pty_bus.rs b/src/common/pty_bus.rs
new file mode 100644
index 00000000..d341662d
--- /dev/null
+++ b/src/common/pty_bus.rs
@@ -0,0 +1,317 @@
+use ::async_std::stream::*;
+use ::async_std::task;
+use ::async_std::task::*;
+use ::std::collections::HashMap;
+use ::std::os::unix::io::RawFd;
+use ::std::pin::*;
+use ::std::sync::mpsc::Receiver;
+use ::std::time::{Duration, Instant};
+use ::vte;
+use std::path::PathBuf;
+
+use super::{ScreenInstruction, SenderWithContext, OPENCALLS};
+use crate::os_input_output::OsApi;
+use crate::utils::logging::debug_to_file;
+use crate::{
+ errors::{ContextType, ErrorContext},
+ panes::PaneId,
+};
+use crate::{layout::Layout, wasm_vm::PluginInstruction};
+
+pub struct ReadFromPid {
+ pid: RawFd,
+ os_input: Box<dyn OsApi>,
+}
+
+impl ReadFromPid {
+ pub fn new(pid: &RawFd, os_input: Box<dyn OsApi>) -> ReadFromPid {
+ ReadFromPid {
+ pid: *pid,
+ os_input,
+ }
+ }
+}
+
+impl Stream for ReadFromPid {
+ type Item = Vec<u8>;
+ fn poll_next(mut self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
+ let mut read_buffer = [0; 65535];
+ let pid = self.pid;
+ let read_result = &self.os_input.read_from_tty_stdout(pid, &mut read_buffer);
+ match read_result {
+ Ok(res) => {
+ if *res == 0 {
+ // indicates end of file
+ Poll::Ready(None)
+ } else {
+ let res = Some(read_buffer[..=*res].to_vec());
+ Poll::Ready(res)
+ }
+ }
+ Err(e) => {
+ match e {
+ nix::Error::Sys(errno) => {
+ if *errno == nix::errno::Errno::EAGAIN {
+ Poll::Ready(Some(vec![])) // TODO: better with timeout waker somehow
+ } else {
+ Poll::Ready(None)
+ }
+ }
+ _ => Poll::Ready(None),
+ }
+ }
+ }
+ }
+}
+
+#[derive(Debug, Clone)]
+pub enum VteEvent {
+ // TODO: try not to allocate Vecs
+ Print(char),
+ Execute(u8), // byte
+ Hook(Vec<i64>, Vec<u8>, bool, char), // params, intermediates, ignore, char
+ Put(u8), // byte
+ Unhook,
+ OscDispatch(Vec<Vec<u8>>, bool), // params, bell_terminated
+ CsiDispatch(Vec<i64>, Vec<u8>, bool, char), // params, intermediates, ignore, char
+ EscDispatch(Vec<u8>, bool, u8), // intermediates, ignore, byte
+}
+
+struct VteEventSender {
+ id: RawFd,
+ sender: SenderWithContext<ScreenInstruction>,
+}
+
+impl VteEventSender {
+ pub fn new(id: RawFd, sender: SenderWithContext<ScreenInstruction>) -> Self {
+ VteEventSender { id, sender }
+ }
+}
+
+impl vte::Perform for VteEventSender {
+ fn print(&mut self, c: char) {
+ let _ = self
+ .sender
+ .send(ScreenInstruction::Pty(self.id, VteEvent::Print(c)));
+ }
+ fn execute(&mut self, byte: u8) {
+ let _ = self
+ .sender
+ .send(ScreenInstruction::Pty(self.id, VteEvent::Execute(byte)));
+ }
+
+ fn hook(&mut self, params: &[i64], intermediates: &[u8], ignore: bool, c: char) {
+ let params = params.iter().copied().collect();
+ let intermediates = intermediates.iter().copied().collect();
+ let instruction =
+ ScreenInstruction::Pty(self.id, VteEvent::Hook(params, intermediates, ignore, c));
+ let _ = self.sender.send(instruction);
+ }
+
+ fn put(&mut self, byte: u8) {
+ let _ = self
+ .sender
+ .send(ScreenInstruction::Pty(self.id, VteEvent::Put(byte)));
+ }
+
+ fn unhook(&mut self) {
+ let _ = self
+ .sender
+ .send(ScreenInstruction::Pty(self.id, VteEvent::Unhook));
+ }
+
+ fn osc_dispatch(&mut self, params: &[&[u8]], bell_terminated: bool) {
+ let params = params.iter().map(|p| p.to_vec()).collect();
+ let instruction =
+ ScreenInstruction::Pty(self.id, VteEvent::OscDispatch(params, bell_terminated));
+ let _ = self.sender.send(instruction);
+ }
+
+ fn csi_dispatch(&mut self, params: &[i64], intermediates: &[u8], ignore: bool, c: char) {
+ let params = params.iter().copied().collect();
+ let intermediates = intermediates.iter().copied().collect();
+ let instruction = ScreenInstruction::Pty(
+ self.id,
+ VteEvent::CsiDispatch(params, intermediates, ignore, c),
+ );
+ let _ = self.sender.send(instruction);
+ }
+
+ fn esc_dispatch(&mut self, intermediates: &[u8], ignore: bool, byte: u8) {
+ let intermediates = intermediates.iter().copied().collect();
+ let instruction =
+ ScreenInstruction::Pty(self.id, VteEvent::EscDispatch(intermediates, ignore, byte));
+ let _ = self.sender.send(instruction);
+ }
+}
+
+#[derive(Clone, Debug)]
+pub enum PtyInstruction {
+ SpawnTerminal(Option<PathBuf>),
+ SpawnTerminalVertically(Option<PathBuf>),
+ SpawnTerminalHorizontally(Option<PathBuf>),
+ NewTab,
+ ClosePane(PaneId),
+ CloseTab(Vec<PaneId>),
+ Quit,
+}
+
+pub struct PtyBus {
+ pub send_screen_instructions: SenderWithContext<ScreenInstruction>,
+ pub send_plugin_instructions: SenderWithContext<PluginInstruction>,
+ pub receive_pty_instructions: Receiver<(PtyInstruction, ErrorContext)>,
+ pub id_to_child_pid: HashMap<RawFd, RawFd>,
+ os_input: Box<dyn OsApi>,
+ debug_to_file: bool,
+ task_handles: HashMap<RawFd, JoinHandle<()>>,
+}
+
+fn stream_terminal_bytes(
+ pid: RawFd,
+ mut send_screen_instructions: SenderWithContext<ScreenInstruction>,
+ os_input: Box<dyn OsApi>,
+ debug: bool,
+) -> JoinHandle<()> {
+ let mut err_ctx = OPENCALLS.with(|ctx| *ctx.borrow());
+ task::spawn({
+ async move {
+ err_ctx.add_call(ContextType::AsyncTask);
+ send_screen_instructions.update(err_ctx);
+ let mut vte_parser = vte::Parser::new();
+ let mut vte_event_sender = VteEventSender::new(pid, send_screen_instructions.clone());
+ let mut terminal_bytes = ReadFromPid::new(&pid, os_input);
+
+ let mut last_byte_receive_time: Option<Instant> = None;
+ let mut pending_render = false;
+ let max_render_pause = Duration::from_millis(30);
+
+ while let Some(bytes) = terminal_bytes.next().await {
+ let bytes_is_empty = bytes.is_empty();
+ for byte in bytes {
+ if debug {
+ debug_to_file(byte, pid).unwrap();
+ }
+ vte_parser.advance(&mut vte_event_sender, byte);
+ }
+ if !bytes_is_empty {
+ // for UX reasons, if we got something on the wire, we only send the render notice if:
+ // 1. there aren't any more bytes on the wire afterwards
+ // 2. a certain period (currently 30ms) has elapsed since the last render
+ // (otherwise if we get a large amount of data, the display would hang
+ // until it's done)
+ // 3. the stream has ended, and so we render 1 last time
+ match last_byte_receive_time.as_mut() {
+ Some(receive_time) => {
+ if receive_time.elapsed() > max_render_pause {
+ pending_render = false;
+ let _ = send_screen_instructions.send(ScreenInstruction::Render);
+ last_byte_receive_time = Some(Instant::now());
+ } else {
+ pending_render = true;
+ }
+ }
+ None => {
+ last_byte_receive_time = Some(Instant::now());
+ pending_render = true;
+ }
+ };
+ } else {
+ if pending_render {
+ pending_render = false;
+ let _ = send_screen_instructions.send(ScreenInstruction::Render);
+ }
+ last_byte_receive_time = None;
+ task::sleep(::std::time::Duration::from_millis(10)).await;
+ }
+ }
+ if pending_render {
+ let _ = send_screen_instructions.send(ScreenInstruction::Render);
+ }
+ #[cfg(not(test))]
+ // this is a little hacky, and is because the tests end the file as soon as
+ // we read everything, rather than hanging until there is new data
+ // a better solution would be to fix the test fakes, but this will do for now
+ send_screen_instructions
+ .send(ScreenInstruction::ClosePane(PaneId::Terminal(pid)))
+ .unwrap();
+ }
+ })
+}
+
+impl PtyBus {
+ pub fn new(
+ receive_pty_instructions: Receiver<(PtyInstruction, ErrorContext)>,
+ send_screen_instructions: SenderWithContext<ScreenInstruction>,
+ send_plugin_instructions: SenderWithContext<PluginInstruction>,
+ os_input: Box<dyn OsApi>,
+ debug_to_file: bool,
+ ) -> Self {
+ PtyBus {
+ send_screen_instructions,
+ send_plugin_instructions,
+ receive_pty_instructions,
+ os_input,
+ id_to_child_pid: HashMap::new(),
+ debug_to_file,
+ task_handles: HashMap::new(),
+ }
+ }
+ pub fn spawn_terminal(&mut self, file_to_open: Option<PathBuf>) -> RawFd {
+ let (pid_primary, pid_secondary): (RawFd, RawFd) =
+ self.os_input.spawn_terminal(file_to_open);
+ let task_handle = stream_terminal_bytes(
+ pid_primary,
+ self.send_screen_instructions.clone(),
+ self.os_input.clone(),
+ self.debug_to_file,
+ );
+ self.task_handles.insert(pid_primary, task_handle);
+ self.id_to_child_pid.insert(pid_primary, pid_secondary);
+ pid_primary
+ }
+ pub fn spawn_terminals_for_layout(&mut self, layout: Layout) {
+ let total_panes = layout.total_terminal_panes();
+ let mut new_pane_pids = vec![];
+ for _ in 0..total_panes {
+ let (pid_primary, pid_secondary): (RawFd, RawFd) = self.os_input.spawn_terminal(None);
+ self.id_to_child_pid.insert(pid_primary, pid_secondary);
+ new_pane_pids.push(pid_primary);
+ }
+ self.send_screen_instructions
+ .send(ScreenInstruction::ApplyLayout((
+ layout,
+ new_pane_pids.clone(),
+ )))
+ .unwrap();
+ for id in new_pane_pids {
+ let task_handle = stream_terminal_bytes(
+ id,
+ self.send_screen_instructions.clone(),
+ self.os_input.clone(),
+ self.debug_to_file,
+ );
+ self.task_handles.insert(id, task_handle);
+ }
+ }
+ pub fn close_pane(&mut self, id: PaneId) {
+ match id {
+ PaneId::Terminal(id) => {
+ let child_pid = self.id_to_child_pid.remove(&id).unwrap();
+ let handle = self.task_handles.remove(&id).unwrap();
+ self.os_input.kill(child_pid).unwrap();
+ task::block_on(async {
+ handle.cancel().await;
+ });
+ }
+ PaneId::Plugin(pid) => drop(
+ self.send_plugin_instructions
+ .send(PluginInstruction::Unload(pid)),
+ ),
+ }
+ }
+ pub fn close_tab(&mut self, ids: Vec<PaneId>) {
+ ids.iter().for_each(|&id| {
+ self.close_pane(id);
+ });
+ }
+}
diff --git a/src/common/screen.rs b/src/common/screen.rs
new file mode 100644
index 00000000..2438dba0
--- /dev/null
+++ b/src/common/screen.rs
@@ -0,0 +1,197 @@
+use std::collections::BTreeMap;
+use std::os::unix::io::RawFd;
+use std::sync::mpsc::Receiver;
+
+use super::{AppInstruction, SenderWithContext};
+use crate::os_input_output::OsApi;
+use crate::panes::PositionAndSize;
+use crate::pty_bus::{PtyInstruction, VteEvent};
+use crate::tab::Tab;
+use crate::{errors::ErrorContext, wasm_vm::PluginInstruction};
+use crate::{layout::Layout, panes::PaneId};
+
+/*
+ * Screen
+ *
+ * this holds multiple tabs, each one holding multiple panes
+ * it tracks the active tab and controls tab switching, all the rest
+ * is performed in Tab
+ *
+ */
+
+#[derive(Debug, Clone)]
+pub enum ScreenInstruction {
+ Pty(RawFd, VteEvent),
+ Render,
+ NewPane(PaneId),
+ HorizontalSplit(PaneId),
+ VerticalSplit(PaneId),
+ WriteCharacter(Vec<u8>),
+ ResizeLeft,
+ ResizeRight,
+ ResizeDown,
+ ResizeUp,
+ MoveFocus,
+ MoveFocusLeft,
+ MoveFocusDown,
+ MoveFocusUp,
+ MoveFocusRight,
+ Quit,
+ ScrollUp,
+ ScrollDown,
+ ClearScroll,
+ CloseFocusedPane,
+ ToggleActiveTerminalFullscreen,
+ SetSelectable(PaneId, bool),
+ SetMaxHeight(PaneId, usize),
+ ClosePane(PaneId),
+ ApplyLayout((Layout, Vec<RawFd>)),
+ NewTab(RawFd),
+ SwitchTabNext,
+ SwitchTabPrev,
+ CloseTab,
+}
+
+pub struct Screen {
+ pub receiver: Receiver<(ScreenInstruction, ErrorContext)>,
+ max_panes: Option<usize>,
+ tabs: BTreeMap<usize, Tab>,
+ pub send_pty_instructions: SenderWithContext<PtyInstruction>,
+ pub send_plugin_instructions: SenderWithContext<PluginInstruction>,
+ pub send_app_instructions: SenderWithContext<AppInstruction>,
+ full_screen_ws: PositionAndSize,
+ active_tab_index: Option<usize>,
+ os_api: Box<dyn OsApi>,
+}
+
+impl Screen {
+ pub fn new(
+ receive_screen_instructions: Receiver<(ScreenInstruction, ErrorContext)>,
+ send_pty_instructions: SenderWithContext<PtyInstruction>,
+ send_plugin_instructions: SenderWithContext<PluginInstruction>,
+ send_app_instructions: SenderWithContext<AppInstruction>,
+ full_screen_ws: &PositionAndSize,
+ os_api: Box<dyn OsApi>,
+ max_panes: Option<usize>,
+ ) -> Self {
+ Screen {
+ receiver: receive_screen_instructions,
+ max_panes,
+ send_pty_instructions,
+ send_plugin_instructions,
+ send_app_instructions,
+ full_screen_ws: *full_screen_ws,
+ active_tab_index: None,
+ tabs: BTreeMap::new(),
+ os_api,
+ }
+ }
+ pub fn new_tab(&mut self, pane_id: RawFd) {
+ let tab_index = self.get_next_tab_index();
+ let tab = Tab::new(
+ tab_index,
+ &self.full_screen_ws,
+ self.os_api.clone(),
+ self.send_pty_instructions.clone(),
+ self.send_plugin_instructions.clone(),
+ self.send_app_instructions.clone(),
+ self.max_panes,
+ Some(PaneId::Terminal(pane_id)),
+ );
+ self.active_tab_index = Some(tab_index);
+ self.tabs.insert(tab_index, tab);
+ self.render();
+ }
+ fn get_next_tab_index(&self) -> usize {
+ if let Some(index) = self.tabs.keys().last() {
+ *index + 1
+ } else {
+ 0
+ }
+ }
+ pub fn switch_tab_next(&mut self) {
+ let active_tab_id = self.get_active_tab().unwrap().index;
+ let tab_ids: Vec<usize> = self.tabs.keys().copied().collect();
+ let first_tab = tab_ids.get(0).unwrap();
+ let active_tab_id_position = tab_ids.iter().position(|id| id == &active_tab_id).unwrap();
+ if let Some(next_tab) = tab_ids.get(active_tab_id_position + 1) {
+ self.active_tab_index = Some(*next_tab);
+ } else {
+ self.active_tab_index = Some(*first_tab);
+ }
+ self.render();
+ }
+ pub fn switch_tab_prev(&mut self) {
+ let active_tab_id = self.get_active_tab().unwrap().index;
+ let tab_ids: Vec<usize> = self.tabs.keys().copied().collect();
+ let first_tab = tab_ids.get(0).unwrap();
+ let last_tab = tab_ids.last().unwrap();
+
+ let active_tab_id_position = tab_ids.iter().position(|id| id == &active_tab_id).unwrap();
+ if active_tab_id == *first_tab {
+ self.active_tab_index = Some(*last_tab)
+ } else if let Some(prev_tab) = tab_ids.get(active_tab_id_position - 1) {
+ self.active_tab_index = Some(*prev_tab)
+ }
+ self.render();
+ }
+ pub fn close_tab(&mut self) {
+ let active_tab_index = self.active_tab_index.unwrap();
+ if self.tabs.len() > 1 {
+ self.switch_tab_prev();
+ }
+ let active_tab = self.tabs.remove(&active_tab_index).unwrap();
+ let pane_ids = active_tab.get_pane_ids();
+ self.send_pty_instructions
+ .send(PtyInstruction::CloseTab(pane_ids))
+ .unwrap();
+ if self.tabs.is_empty() {
+ self.active_tab_index = None;
+ self.send_app_instructions
+ .send(AppInstruction::Exit)
+ .unwrap();
+ }
+ }
+ pub fn render(&mut self) {
+ if let Some(active_tab) = self.get_active_tab_mut() {
+ if active_tab.get_active_pane().is_some() {
+ active_tab.render();
+ } else {
+ self.close_tab();
+ }
+ };
+ }
+
+ pub fn get_active_tab(&self) -> Option<&Tab> {
+ match self.active_tab_index {
+ Some(tab) => self.tabs.get(&tab),
+ None => None,
+ }
+ }
+ pub fn get_tabs_mut(&mut self) -> &mut BTreeMap<usize, Tab> {
+ &mut self.tabs
+ }
+ pub fn get_active_tab_mut(&mut self) -> Option<&mut Tab> {
+ let tab = match self.active_tab_index {
+ Some(tab) => self.get_tabs_mut().get_mut(&tab),
+ None => None,
+ };
+ tab
+ }
+ pub fn apply_layout(&mut self, layout: Layout, new_pids: Vec<RawFd>) {
+ let tab_index = self.get_next_tab_index();
+ let mut tab = Tab::new(
+ tab_index,
+ &self.full_screen_ws,
+ self.os_api.clone(),
+ self.send_pty_instructions.clone(),
+ self.send_plugin_instructions.clone(),
+ self.send_app_instructions.clone(),
+ self.max_panes,
+ None,
+ );
+ tab.apply_layout(layout, new_pids);
+ self.active_tab_index = Some(tab_index);
+ self.tabs.insert(tab_index, tab);
+ }
+}
diff --git a/src/common/utils/consts.rs b/src/common/utils/consts.rs
new file mode 100644
index 00000000..7a0c9ddf
--- /dev/null
+++ b/src/common/utils/consts.rs
@@ -0,0 +1,6 @@
+pub const MOSAIC_TMP_DIR: &str = "/tmp/mosaic";
+pub const MOSAIC_TMP_LOG_DIR: &str = "/tmp/mosaic/mosaic-log";
+pub const MOSAIC_TMP_LOG_FILE: &str = "/tmp/mosaic/mosaic-log/log.txt";
+pub const MOSAIC_IPC_PIPE: &str = "/tmp/mosaic/ipc";
+pub const MOSAIC_ROOT_PLUGIN_DIR: &str = "/usr/share/mosaic/plugins";
+pub const MOSAIC_ROOT_LAYOUT_DIR: &str = "/usr/share/mosaic/layouts";
diff --git a/src/common/utils/logging.rs b/src/common/utils/logging.rs
new file mode 100644
index 00000000..05af455c
--- /dev/null
+++ b/src/common/utils/logging.rs
@@ -0,0 +1,76 @@
+use std::{
+ fs,
+ io::{self, prelude::*},
+ os::unix::io::RawFd,
+ path::PathBuf,
+};
+
+use crate::utils::consts::{MOSAIC_TMP_LOG_DIR, MOSAIC_TMP_LOG_FILE};
+
+pub fn atomic_create_file(file_name: &str) {
+ let _ = fs::OpenOptions::new().create(true).open(file_name);
+}
+
+pub fn atomic_create_dir(dir_name: &str) -> io::Result<()> {
+ if let Err(e) = fs::create_dir(dir_name) {
+ if e.kind() == std::io::ErrorKind::AlreadyExists {
+ Ok(())
+ } else {
+ Err(e)
+ }
+ } else {
+ Ok(())
+ }
+}
+
+pub fn debug_log_to_file(mut message: String) -> io::Result<()> {
+ message.push('\n');
+ debug_log_to_file_without_newline(message)
+}
+
+pub fn debug_log_to_file_without_newline(message: String) -> io::Result<()> {
+ atomic_create_file(MOSAIC_TMP_LOG_FILE);
+ let mut file = fs::OpenOptions::new()
+ .append(true)
+ .create(true)
+ .open(MOSAIC_TMP_LOG_FILE)?;
+ file.write_all(message.as_bytes())
+}
+
+pub fn _debug_log_to_file_pid_3(message: String, pid: RawFd) -> io::Result<()> {
+ if pid == 3 {
+ debug_log_to_file(message)
+ } else {
+ Ok(())
+ }
+}
+
+#[allow(dead_code)]
+pub fn delete_log_file() -> io::Result<()> {
+ if fs::metadata(MOSAIC_TMP_LOG_FILE).is_ok() {
+ fs::remove_file(MOSAIC_TMP_LOG_FILE)
+ } else {
+ Ok(())
+ }
+}
+
+#[allow(dead_code)]
+pub fn delete_log_dir() -> io::Result<()> {
+ if fs::metadata(MOSAIC_TMP_LOG_DIR).is_ok() {
+ fs::remove_dir_all(MOSAIC_TMP_LOG_DIR)
+ } else {
+ Ok(())
+ }
+}
+
+pub fn debug_to_file(message: u8, pid: RawFd) -> io::Result<()> {
+ let mut path = PathBuf::new();
+ path.push(MOSAIC_TMP_LOG_DIR);
+ path.push(format!("mosaic-{}.log", pid.to_string()));
+
+ let mut file = fs::OpenOptions::new()
+ .append(true)
+ .create(true)
+ .open(path)?;
+ file.write_all(&[message])
+}
diff --git a/src/common/utils/mod.rs b/src/common/utils/mod.rs
new file mode 100644
index 00000000..0ea4c76f
--- /dev/null
+++ b/src/common/utils/mod.rs
@@ -0,0 +1,3 @@
+pub mod consts;
+pub mod logging;
+pub mod shared;
diff --git a/src/common/utils/shared.rs b/src/common/utils/shared.rs
new file mode 100644
index 00000000..1d8ca5c3
--- /dev/null
+++ b/src/common/utils/shared.rs
@@ -0,0 +1,19 @@
+use std::{iter, str::from_utf8};
+
+use strip_ansi_escapes::strip;
+
+pub fn ansi_len(s: &str) -> usize {
+ from_utf8(&strip(s.as_bytes()).unwrap())
+ .unwrap()
+ .chars()
+ .count()
+}
+
+pub fn pad_to_size(s: &str, rows: usize, columns: usize) -> String {
+ s.lines()
+ .map(|l| [l, &str::repeat(" ", columns - ansi_len(l))].concat())
+ .chain(iter::repeat(str::repeat(" ", columns)))
+ .take(rows)
+ .collect::<Vec<_>>()
+ .join("\n\r")
+}
diff --git a/src/common/wasm_vm.rs b/src/common/wasm_vm.rs
new file mode 100644
index 00000000..d678401d
--- /dev/null
+++ b/src/common/wasm_vm.rs
@@ -0,0 +1,107 @@
+use std::{
+ path::PathBuf,
+ sync::mpsc::{channel, Sender},
+};
+use wasmer::{imports, Function, ImportObject, Store, WasmerEnv};
+use wasmer_wasi::WasiEnv;
+
+use super::{
+ input::get_help, pty_bus::PtyInstruction, screen::ScreenInstruction, AppInstruction, PaneId,
+ SenderWithContext,
+};
+
+#[derive(Clone, Debug)]
+pub enum PluginInstruction {
+ Load(Sender<u32>, PathBuf),
+ Draw(Sender<String>, u32, usize, usize), // String buffer, plugin id, rows, cols
+ Input(u32, Vec<u8>), // plugin id, input bytes
+ GlobalInput(Vec<u8>), // input bytes
+ Unload(u32),
+ Quit,
+}
+
+#[derive(WasmerEnv, Clone)]
+pub struct PluginEnv {
+ pub plugin_id: u32,
+ pub send_screen_instructions: SenderWithContext<ScreenInstruction>,
+ pub send_app_instructions: SenderWithContext<AppInstruction>,
+ pub send_pty_instructions: SenderWithContext<PtyInstruction>, // FIXME: This should be a big bundle of all of the channels
+ pub wasi_env: WasiEnv,
+}
+
+// Plugin API ---------------------------------------------------------------------------------------------------------
+
+pub fn mosaic_imports(store: &Store, plugin_env: &PluginEnv) -> ImportObject {
+ imports! {
+ "mosaic" => {
+ "host_open_file" => Function::new_native_with_env(store, plugin_env.clone(), host_open_file),
+ "host_set_max_height" => Function::new_native_with_env(store, plugin_env.clone(), host_set_max_height),
+ "host_set_selectable" => Function::new_native_with_env(store, plugin_env.clone(), host_set_selectable),
+ "host_get_help" => Function::new_native_with_env(store, plugin_env.clone(), host_get_help),
+ }
+ }
+}
+
+// FIXME: Bundle up all of the channels! Pair that with WasiEnv?
+fn host_open_file(plugin_env: &PluginEnv) {
+ let path = PathBuf::from(wasi_stdout(&plugin_env.wasi_env).lines().next().unwrap());
+ plugin_env
+ .send_pty_instructions
+ .send(PtyInstruction::SpawnTerminal(Some(path)))
+ .unwrap();
+}
+
+// FIXME: Think about these naming conventions – should everything be prefixed by 'host'?
+fn host_set_selectable(plugin_env: &PluginEnv, selectable: i32) {
+ let selectable = selectable != 0;
+ plugin_env
+ .send_screen_instructions
+ .send(ScreenInstruction::SetSelectable(
+ PaneId::Plugin(plugin_env.plugin_id),
+ selectable,
+ ))
+ .unwrap()
+}
+
+fn host_set_max_height(plugin_env: &PluginEnv, max_height: i32) {
+ let max_height = max_height as usize;
+ plugin_env
+ .send_screen_instructions
+ .send(ScreenInstruction::SetMaxHeight(
+ PaneId::Plugin(plugin_env.plugin_id),
+ max_height,
+ ))
+ .unwrap()
+}
+
+fn host_get_help(plugin_env: &PluginEnv) {
+ let (state_tx, state_rx) = channel();
+ // FIXME: If I changed the application so that threads were sent the termination
+ // signal and joined one at a time, there would be an order to shutdown, so I
+ // could get rid of this .is_ok() check and the .try_send()
+ if plugin_env
+ .send_app_instructions
+ .try_send(AppInstruction::GetState(state_tx))
+ .is_ok()
+ {
+ let help = get_help(&state_rx.recv().unwrap().input_mode);
+ wasi_write_string(&plugin_env.wasi_env, &serde_json::to_string(&help).unwrap());
+ }
+}
+
+// Helper Functions ---------------------------------------------------------------------------------------------------
+
+// FIXME: Unwrap city
+pub fn wasi_stdout(wasi_env: &WasiEnv) -> String {
+ let mut state = wasi_env.state();
+ let wasi_file = state.fs.stdout_mut().unwrap().as_mut().unwrap();
+ let mut buf = String::new();
+ wasi_file.read_to_string(&mut buf).unwrap();
+ buf
+}
+
+pub fn wasi_write_string(wasi_env: &WasiEnv, buf: &str) {
+ let mut state = wasi_env.state();
+ let wasi_file = state.fs.stdin_mut().unwrap().as_mut().unwrap();
+ writeln!(wasi_file, "{}\r", buf).unwrap();
+}