diff --git a/Cargo.lock b/Cargo.lock index a8656836..f869fa22 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -407,6 +407,27 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "524cbf6897b527295dff137cec09ecf3a05f4fddffd7dfcd1585403449e74198" +[[package]] +name = "directories-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "339ee130d97a610ea5a5872d2bbb130fdf68884ff09d3028b81bec8a1ac23bbc" +dependencies = [ + "cfg-if 1.0.0", + "dirs-sys-next", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + [[package]] name = "dtoa" version = "0.4.6" @@ -882,6 +903,7 @@ dependencies = [ "async-std", "backtrace", "bincode", + "directories-next", "futures", "insta", "libc", @@ -1142,13 +1164,32 @@ version = "0.1.57" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" +[[package]] +name = "redox_syscall" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05ec8ca9416c5ea37062b502703cd7fcb207736bc294f6e0cf367ac6fc234570" +dependencies = [ + "bitflags", +] + [[package]] name = "redox_termios" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e891cfe48e9100a70a3b6eb652fef28920c117d366339687bd5576160db0f76" dependencies = [ - "redox_syscall", + "redox_syscall 0.1.57", +] + +[[package]] +name = "redox_users" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64" +dependencies = [ + "getrandom 0.2.0", + "redox_syscall 0.2.4", ] [[package]] @@ -1356,7 +1397,7 @@ checksum = "03088793f677dce356f3ccc2edb1b314ad191ab702a5de3faf49304f7e104918" dependencies = [ "cfg-if 0.1.10", "libc", - "redox_syscall", + "redox_syscall 0.1.57", "winapi", ] @@ -1437,7 +1478,7 @@ dependencies = [ "cfg-if 0.1.10", "libc", "rand", - "redox_syscall", + "redox_syscall 0.1.57", "remove_dir_all", "winapi", ] @@ -1459,7 +1500,7 @@ source = "git+https://gitlab.com/TheLostLambda/termion.git#70159e07c59c02dc681db dependencies = [ "libc", "numtoa", - "redox_syscall", + "redox_syscall 0.1.57", "redox_termios", "serde", ] diff --git a/Cargo.toml b/Cargo.toml index f1964cb4..ef2c47ea 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,7 @@ edition = "2018" [dependencies] backtrace = "0.3.55" bincode = "1.3.1" +directories-next = "2.0" futures = "0.3.5" libc = "0.2" nix = "0.17.0" @@ -34,6 +35,7 @@ features = ["unstable"] insta = "0.16.1" [build-dependencies] +directories-next = "2.0" structopt = "0.3" [profile.release] diff --git a/assets/layouts/default.yaml b/assets/layouts/default.yaml new file mode 100644 index 00000000..e6335691 --- /dev/null +++ b/assets/layouts/default.yaml @@ -0,0 +1,8 @@ +--- +direction: Horizontal +parts: + - direction: Vertical + - direction: Vertical + split_size: + Fixed: 1 + plugin: status-bar \ No newline at end of file diff --git a/src/tests/fixtures/layouts/panes-with-plugins.yaml b/assets/layouts/strider.yaml similarity index 63% rename from src/tests/fixtures/layouts/panes-with-plugins.yaml rename to assets/layouts/strider.yaml index 7fb13b2e..e33f516a 100644 --- a/src/tests/fixtures/layouts/panes-with-plugins.yaml +++ b/assets/layouts/strider.yaml @@ -6,12 +6,9 @@ parts: - direction: Horizontal split_size: Percent: 20 - plugin: strider.wasm + plugin: strider - direction: Horizontal - split_size: - Percent: 80 - split_size: - Percent: 80 - direction: Vertical split_size: - Percent: 20 \ No newline at end of file + Fixed: 1 + plugin: status-bar \ No newline at end of file diff --git a/assets/plugins/status-bar.wasm b/assets/plugins/status-bar.wasm new file mode 100644 index 00000000..0b012080 Binary files /dev/null and b/assets/plugins/status-bar.wasm differ diff --git a/strider.wasm b/assets/plugins/strider.wasm similarity index 77% rename from strider.wasm rename to assets/plugins/strider.wasm index e0062773..77b07b01 100644 Binary files a/strider.wasm and b/assets/plugins/strider.wasm differ diff --git a/build.rs b/build.rs index f0f92e61..c405d27e 100644 --- a/build.rs +++ b/build.rs @@ -1,4 +1,5 @@ -use std::fs; +use directories_next::ProjectDirs; +use std::{fs, path::Path}; use structopt::clap::Shell; include!("src/cli.rs"); @@ -6,8 +7,9 @@ include!("src/cli.rs"); const BIN_NAME: &str = "mosaic"; fn main() { + // Generate Shell Completions let mut clap_app = CliArgs::clap(); - println!("cargo:rerun-if-changed=src/app.rs"); + println!("cargo:rerun-if-changed=src/cli.rs"); let mut out_dir = std::env::var_os("CARGO_MANIFEST_DIR").unwrap(); out_dir.push("/assets/completions"); @@ -19,4 +21,21 @@ fn main() { clap_app.gen_completions(BIN_NAME, Shell::Bash, &out_dir); clap_app.gen_completions(BIN_NAME, Shell::Zsh, &out_dir); clap_app.gen_completions(BIN_NAME, Shell::Fish, &out_dir); + + // Install Default Plugins and Layouts + let assets = vec![ + "plugins/status-bar.wasm", + "plugins/strider.wasm", + "layouts/default.yaml", + "layouts/strider.yaml", + ]; + let project_dirs = ProjectDirs::from("org", "Mosaic Contributors", "Mosaic").unwrap(); + let data_dir = project_dirs.data_dir(); + fs::create_dir_all(data_dir.join("plugins")).unwrap(); + fs::create_dir_all(data_dir.join("layouts")).unwrap(); + for asset in assets { + println!("cargo:rerun-if-changed=assets/{}", asset); + fs::copy(Path::new("assets/").join(asset), data_dir.join(asset)) + .expect("Failed to copy asset files"); + } } diff --git a/src/errors.rs b/src/errors.rs index e53bbc30..cb483bbc 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -166,6 +166,7 @@ pub enum ScreenContext { ClearScroll, CloseFocusedPane, ToggleActiveTerminalFullscreen, + SetSelectable, ClosePane, ApplyLayout, NewTab, @@ -200,6 +201,7 @@ impl From<&ScreenInstruction> for ScreenContext { ScreenInstruction::ToggleActiveTerminalFullscreen => { ScreenContext::ToggleActiveTerminalFullscreen } + ScreenInstruction::SetSelectable(..) => ScreenContext::SetSelectable, ScreenInstruction::ClosePane(_) => ScreenContext::ClosePane, ScreenInstruction::ApplyLayout(_) => ScreenContext::ApplyLayout, ScreenInstruction::NewTab(_) => ScreenContext::NewTab, @@ -244,6 +246,7 @@ pub enum PluginContext { Load, Draw, Input, + GlobalInput, Unload, Quit, } @@ -254,6 +257,7 @@ impl From<&PluginInstruction> for PluginContext { PluginInstruction::Load(..) => PluginContext::Load, PluginInstruction::Draw(..) => PluginContext::Draw, PluginInstruction::Input(..) => PluginContext::Input, + PluginInstruction::GlobalInput(_) => PluginContext::GlobalInput, PluginInstruction::Unload(_) => PluginContext::Unload, PluginInstruction::Quit => PluginContext::Quit, } @@ -262,6 +266,8 @@ impl From<&PluginInstruction> for PluginContext { #[derive(Debug, Clone, Copy, PartialEq)] pub enum AppContext { + GetState, + SetState, Exit, Error, } @@ -269,6 +275,8 @@ pub enum AppContext { 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/input.rs b/src/input.rs index cc144361..3c5518f8 100644 --- a/src/input.rs +++ b/src/input.rs @@ -1,9 +1,8 @@ -/// Module for handling input -use crate::errors::ContextType; -use crate::os_input_output::OsApi; 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 { @@ -12,6 +11,7 @@ struct InputHandler { command_is_executing: CommandIsExecuting, send_screen_instructions: SenderWithContext, send_pty_instructions: SenderWithContext, + send_plugin_instructions: SenderWithContext, send_app_instructions: SenderWithContext, } @@ -21,6 +21,7 @@ impl InputHandler { command_is_executing: CommandIsExecuting, send_screen_instructions: SenderWithContext, send_pty_instructions: SenderWithContext, + send_plugin_instructions: SenderWithContext, send_app_instructions: SenderWithContext, ) -> Self { InputHandler { @@ -29,6 +30,7 @@ impl InputHandler { command_is_executing, send_screen_instructions, send_pty_instructions, + send_plugin_instructions, send_app_instructions, } } @@ -38,9 +40,13 @@ impl InputHandler { 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), @@ -59,6 +65,11 @@ impl InputHandler { 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 @@ -88,6 +99,11 @@ impl InputHandler { 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)); @@ -98,12 +114,10 @@ impl InputHandler { // 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; - return; - } + InputMode::CommandPersistent => self.mode = InputMode::Normal, _ => panic!(), } + return; } [27] => { // Esc @@ -248,7 +262,7 @@ impl InputHandler { self.command_is_executing.wait_until_pane_is_closed(); } //@@@khs26 Write this to the powerbar? - _ => {} + _ => continue, } if self.mode == InputMode::Command { @@ -267,6 +281,9 @@ impl InputHandler { self.send_pty_instructions .send(PtyInstruction::Quit) .unwrap(); + self.send_plugin_instructions + .send(PluginInstruction::Quit) + .unwrap(); self.send_app_instructions .send(AppInstruction::Exit) .unwrap(); @@ -282,7 +299,7 @@ impl InputHandler { /// normal mode /// - Exiting means that we should start the shutdown process for mosaic or the given /// input handler -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Clone, Copy)] pub enum InputMode { Normal, Command, @@ -290,6 +307,36 @@ pub enum InputMode { Exiting, } +// FIXME: This should be auto-generated from the soon-to-be-added `get_default_keybinds` +pub fn get_help(mode: &InputMode) -> Vec { + let command_help = vec![ + " Split".into(), + " Resize".into(), + "

Focus Next".into(), + " Close Pane".into(), + " Quit".into(), + " Scroll".into(), + "<1> New Tab".into(), + "<2/3> Move Tab".into(), + "<4> Close Tab".into(), + ]; + match mode { + InputMode::Normal => vec![" Command Mode".into()], + InputMode::Command => [ + vec![ + " Persistent Mode".into(), + " Normal Mode".into(), + ], + command_help, + ] + .concat(), + InputMode::CommandPersistent => { + [vec![" 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( @@ -297,6 +344,7 @@ pub fn input_loop( command_is_executing: CommandIsExecuting, send_screen_instructions: SenderWithContext, send_pty_instructions: SenderWithContext, + send_plugin_instructions: SenderWithContext, send_app_instructions: SenderWithContext, ) { let _handler = InputHandler::new( @@ -304,6 +352,7 @@ pub fn input_loop( command_is_executing, send_screen_instructions, send_pty_instructions, + send_plugin_instructions, send_app_instructions, ) .get_input(); diff --git a/src/layout.rs b/src/layout.rs index c82fc404..2935d42d 100644 --- a/src/layout.rs +++ b/src/layout.rs @@ -1,3 +1,4 @@ +use directories_next::ProjectDirs; use serde::{Deserialize, Serialize}; use std::{fs::File, io::prelude::*, path::PathBuf}; @@ -5,58 +6,128 @@ use crate::panes::PositionAndSize; fn split_space_to_parts_vertically( space_to_split: &PositionAndSize, - percentages: Vec, + sizes: Vec>, ) -> Vec { - let mut split_parts = vec![]; + let mut split_parts = Vec::new(); let mut current_x_position = space_to_split.x; - let width = space_to_split.columns - (percentages.len() - 1); // minus space for gaps - for percentage in percentages.iter() { - let columns = (width as f32 * (*percentage as f32 / 100.0)) as usize; // TODO: round properly + let mut current_width = 0; + let max_width = space_to_split.columns - (sizes.len() - 1); // minus space for gaps + + let mut parts_to_grow = Vec::new(); + + // First fit in the parameterized sizes + for size in sizes { + let columns = match size { + Some(SplitSize::Percent(percent)) => { + (max_width as f32 * (percent as f32 / 100.0)) as usize + } // TODO: round properly + Some(SplitSize::Fixed(size)) => size as usize, + None => { + parts_to_grow.push(current_x_position); + 1 // This is grown later on + } + }; split_parts.push(PositionAndSize { x: current_x_position, y: space_to_split.y, columns, rows: space_to_split.rows, }); + current_width += columns; current_x_position += columns + 1; // 1 for gap } - let total_width = split_parts - .iter() - .fold(0, |total_width, part| total_width + part.columns); - if total_width < width { - // we have some extra space left, let's add it to the last part - let last_part_index = split_parts.len() - 1; - let mut last_part = split_parts.get_mut(last_part_index).unwrap(); - last_part.columns += width - total_width; + + if current_width > max_width { + panic!("Layout contained too many columns to fit onto the screen!"); + } + + let mut last_flexible_index = split_parts.len() - 1; + if let Some(new_columns) = (max_width - current_width).checked_div(parts_to_grow.len()) { + current_width = 0; + current_x_position = 0; + for (idx, part) in split_parts.iter_mut().enumerate() { + part.x = current_x_position; + if parts_to_grow.contains(&part.x) { + part.columns = new_columns; + last_flexible_index = idx; + } + current_width += part.columns; + current_x_position += part.columns + 1; // 1 for gap + } + } + + if current_width < max_width { + // we have some extra space left, let's add it to the last flexible part + let extra = max_width - current_width; + let mut last_part = split_parts.get_mut(last_flexible_index).unwrap(); + last_part.columns += extra; + for part in (&mut split_parts[last_flexible_index + 1..]).iter_mut() { + part.x += extra; + } } split_parts } fn split_space_to_parts_horizontally( space_to_split: &PositionAndSize, - percentages: Vec, + sizes: Vec>, ) -> Vec { - let mut split_parts = vec![]; + let mut split_parts = Vec::new(); let mut current_y_position = space_to_split.y; - let height = space_to_split.rows - (percentages.len() - 1); // minus space for gaps - for percentage in percentages.iter() { - let rows = (height as f32 * (*percentage as f32 / 100.0)) as usize; // TODO: round properly + let mut current_height = 0; + let max_height = space_to_split.rows - (sizes.len() - 1); // minus space for gaps + + let mut parts_to_grow = Vec::new(); + + for size in sizes { + let rows = match size { + Some(SplitSize::Percent(percent)) => { + (max_height as f32 * (percent as f32 / 100.0)) as usize + } // TODO: round properly + Some(SplitSize::Fixed(size)) => size as usize, + None => { + parts_to_grow.push(current_y_position); + 1 // This is grown later on + } + }; split_parts.push(PositionAndSize { x: space_to_split.x, y: current_y_position, columns: space_to_split.columns, rows, }); + current_height += rows; current_y_position += rows + 1; // 1 for gap } - let total_height = split_parts - .iter() - .fold(0, |total_height, part| total_height + part.rows); - if total_height < height { - // we have some extra space left, let's add it to the last part - let last_part_index = split_parts.len() - 1; - let mut last_part = split_parts.get_mut(last_part_index).unwrap(); - last_part.rows += height - total_height; + + if current_height > max_height { + panic!("Layout contained too many rows to fit onto the screen!"); + } + + let mut last_flexible_index = split_parts.len() - 1; + if let Some(new_rows) = (max_height - current_height).checked_div(parts_to_grow.len()) { + current_height = 0; + current_y_position = 0; + + for (idx, part) in split_parts.iter_mut().enumerate() { + part.y = current_y_position; + if parts_to_grow.contains(&part.y) { + part.rows = new_rows; + last_flexible_index = idx; + } + current_height += part.rows; + current_y_position += part.rows + 1; // 1 for gap + } + } + + if current_height < max_height { + // we have some extra space left, let's add it to the last flexible part + let extra = max_height - current_height; + let mut last_part = split_parts.get_mut(last_flexible_index).unwrap(); + last_part.rows += extra; + for part in (&mut split_parts[last_flexible_index + 1..]).iter_mut() { + part.y += extra; + } } split_parts } @@ -66,24 +137,11 @@ fn split_space( layout: &Layout, ) -> Vec<(Layout, PositionAndSize)> { let mut pane_positions = Vec::new(); - let percentages: Vec = layout - .parts - .iter() - .map(|part| { - let split_size = part.split_size.as_ref(); - match split_size { - Some(SplitSize::Percent(percent)) => *percent, - None => { - // TODO: if there is no split size, it should get the remaining "free space" - panic!("Please enter the percentage of the screen part"); - } - } - }) - .collect(); + let sizes: Vec> = layout.parts.iter().map(|part| part.split_size).collect(); let split_parts = match layout.direction { - Direction::Vertical => split_space_to_parts_vertically(space_to_split, percentages), - Direction::Horizontal => split_space_to_parts_horizontally(space_to_split, percentages), + Direction::Vertical => split_space_to_parts_vertically(space_to_split, sizes), + Direction::Horizontal => split_space_to_parts_horizontally(space_to_split, sizes), }; for (i, part) in layout.parts.iter().enumerate() { let part_position_and_size = split_parts.get(i).unwrap(); @@ -97,43 +155,16 @@ fn split_space( pane_positions } -fn validate_layout_percentage_total(layout: &Layout) -> bool { - let total_percentages: u8 = layout - .parts - .iter() - .map(|part| { - let split_size = part.split_size.as_ref(); - match split_size { - Some(SplitSize::Percent(percent)) => *percent, - None => { - // TODO: if there is no split size, it should get the remaining "free space" - panic!("Please enter the percentage of the screen part"); - } - } - }) - .sum(); - if total_percentages != 100 { - return false; - } - - for part in layout.parts.iter() { - if !part.parts.is_empty() { - return validate_layout_percentage_total(part); - } - } - - true -} - #[derive(Debug, Serialize, Deserialize, Clone)] pub enum Direction { Horizontal, Vertical, } -#[derive(Debug, Serialize, Deserialize, Clone)] +#[derive(Debug, Serialize, Deserialize, Clone, Copy)] pub enum SplitSize { Percent(u8), // 1 to 100 + Fixed(u16), // An absolute number of columns or rows } #[derive(Debug, Serialize, Deserialize, Clone)] @@ -149,7 +180,11 @@ pub struct Layout { impl Layout { pub fn new(layout_path: PathBuf) -> Self { + let project_dirs = ProjectDirs::from("org", "Mosaic Contributors", "Mosaic").unwrap(); + let layout_dir = project_dirs.data_dir().join("layouts/"); let mut layout_file = File::open(&layout_path) + .or_else(|_| File::open(&layout_path.with_extension("yaml"))) + .or_else(|_| File::open(&layout_dir.join(&layout_path).with_extension("yaml"))) .unwrap_or_else(|_| panic!("cannot find layout {}", &layout_path.display())); let mut layout = String::new(); @@ -158,17 +193,9 @@ impl Layout { .unwrap_or_else(|_| panic!("could not read layout {}", &layout_path.display())); let layout: Layout = serde_yaml::from_str(&layout) .unwrap_or_else(|_| panic!("could not parse layout {}", &layout_path.display())); - layout.validate(); - layout } - pub fn validate(&self) { - if !validate_layout_percentage_total(&self) { - panic!("The total percent for each part should equal 100."); - } - } - pub fn total_terminal_panes(&self) -> usize { let mut total_panes = 0; total_panes += self.parts.len(); diff --git a/src/main.rs b/src/main.rs index 84dfac62..27e97766 100644 --- a/src/main.rs +++ b/src/main.rs @@ -16,14 +16,16 @@ mod utils; mod wasm_vm; -use std::cell::RefCell; -use std::collections::HashMap; use std::io::Write; use std::os::unix::net::UnixStream; use std::path::PathBuf; use std::sync::mpsc::{channel, sync_channel, Receiver, SendError, Sender, SyncSender}; use std::thread; +use std::{cell::RefCell, sync::mpsc::TrySendError}; +use std::{collections::HashMap, fs}; +use directories_next::ProjectDirs; +use input::InputMode; use panes::PaneId; use serde::{Deserialize, Serialize}; use structopt::StructOpt; @@ -85,6 +87,14 @@ impl SenderWithContext { } } + 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; } @@ -125,13 +135,44 @@ pub fn main() { } } +// FIXME: It would be good to add some more things to this over time +#[derive(Debug, Clone)] +pub struct AppState { + pub input_mode: InputMode, +} + +impl Default for AppState { + fn default() -> Self { + Self { + input_mode: InputMode::Normal, + } + } +} + +// FIXME: Make this a method on the big `Communication` struct, so that app_tx can be extracted +// from self instead of being explicitly passed here +pub fn update_state( + app_tx: &SenderWithContext, + update_fn: impl FnOnce(AppState) -> AppState, +) { + let (state_tx, state_rx) = channel(); + + drop(app_tx.send(AppInstruction::GetState(state_tx))); + let state = state_rx.recv().unwrap(); + + drop(app_tx.send(AppInstruction::SetState(update_fn(state)))) +} + #[derive(Clone)] pub enum AppInstruction { + GetState(Sender), + SetState(AppState), Exit, Error(String), } pub fn start(mut os_input: Box, opts: CliArgs) { + let mut app_state = AppState::default(); let mut active_threads = vec![]; let command_is_executing = CommandIsExecuting::new(); @@ -168,7 +209,12 @@ pub fn start(mut os_input: Box, opts: CliArgs) { os_input.clone(), opts.debug, ); - let maybe_layout = opts.layout.map(Layout::new); + // 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({ @@ -184,64 +230,57 @@ pub fn start(mut os_input: Box, opts: CliArgs) { .name("pty".to_string()) .spawn({ let mut command_is_executing = command_is_executing.clone(); - move || { - if let Some(layout) = maybe_layout { - 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(); - } - - 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 => { + 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; - } + } + 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; } } } @@ -355,6 +394,14 @@ pub fn start(mut os_input: Box, opts: CliArgs) { 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::ClosePane(id) => { screen.get_active_tab_mut().unwrap().close_pane(id); screen.render(); @@ -373,7 +420,8 @@ pub fn start(mut os_input: Box, opts: CliArgs) { ScreenInstruction::SwitchTabPrev => screen.switch_tab_prev(), ScreenInstruction::CloseTab => screen.close_tab(), ScreenInstruction::ApplyLayout((layout, new_pane_pids)) => { - screen.apply_layout(layout, new_pane_pids) + screen.apply_layout(layout, new_pane_pids); + command_is_executing.done_opening_new_pane(); } ScreenInstruction::Quit => { break; @@ -391,94 +439,124 @@ pub fn start(mut os_input: Box, opts: CliArgs) { .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(); - move || { - let store = Store::default(); + let store = Store::default(); + let mut plugin_id = 0; + let mut plugin_map = HashMap::new(); - 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 wasm_bytes = fs::read(&path) + .or_else(|_| fs::read(&path.with_extension("wasm"))) + .or_else(|_| { + fs::read(&plugin_dir.join(&path).with_extension("wasm")) + }) + .unwrap_or_else(|_| { + panic!("cannot find plugin {}", &path.display()) + }); - 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); - match event { - PluginInstruction::Load(pid_tx, path) => { - // FIXME: Cache this compiled module on disk. I could use `(de)serialize_to_file()` for that - let module = Module::from_file(&store, &path).unwrap(); + // 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 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 wasi = wasi_env.import_object(&module).unwrap(); - let plugin_env = PluginEnv { - send_pty_instructions: send_pty_instructions.clone(), - wasi_env, - }; + 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 mosaic = mosaic_imports(&store, &plugin_env); + let instance = + Instance::new(&module, &mosaic.chain_back(wasi)).unwrap(); - let start = instance.exports.get_function("_start").unwrap(); + let start = instance.exports.get_function("_start").unwrap(); - // This eventually calls the `.init()` method - start.call(&[]).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; + 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(); + } } - 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(); - } - PluginInstruction::Input(pid, input_bytes) => { - let (instance, plugin_env) = plugin_map.get(&pid).unwrap(); - - let handle_key = - instance.exports.get_function("handle_key").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(), ); - handle_key.call(&[]).unwrap(); + handler.call(&[]).unwrap(); } } - - send_screen_instructions - .send(ScreenInstruction::Render) - .unwrap(); } - PluginInstruction::Unload(pid) => drop(plugin_map.remove(&pid)), - PluginInstruction::Quit => break, + + drop(send_screen_instructions.send(ScreenInstruction::Render)); } + PluginInstruction::Unload(pid) => drop(plugin_map.remove(&pid)), + PluginInstruction::Quit => break, } } }) @@ -551,6 +629,7 @@ pub fn start(mut os_input: Box, opts: CliArgs) { .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( @@ -558,6 +637,7 @@ pub fn start(mut os_input: Box, opts: CliArgs) { command_is_executing, send_screen_instructions, send_pty_instructions, + send_plugin_instructions, send_app_instructions, ) } @@ -573,17 +653,17 @@ pub fn start(mut os_input: Box, opts: CliArgs) { 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 _ = send_pty_instructions.send(PtyInstruction::Quit); - let _ = send_plugin_instructions.send(PluginInstruction::Quit); os_input.unset_raw_mode(0); let goto_start_of_last_line = format!("\u{1b}[{};{}H", full_screen_ws.rows, 1); @@ -603,6 +683,7 @@ pub fn start(mut os_input: Box, opts: CliArgs) { for thread_handler in active_threads { thread_handler.join().unwrap(); } + // cleanup(); let reset_style = "\u{1b}[m"; let show_cursor = "\u{1b}[?25h"; diff --git a/src/panes/grid.rs b/src/panes/grid.rs index 27dd462c..e00b6c73 100644 --- a/src/panes/grid.rs +++ b/src/panes/grid.rs @@ -1,4 +1,7 @@ -use std::fmt::{self, Debug, Formatter}; +use std::{ + cmp::Ordering, + fmt::{self, Debug, Formatter}, +}; use crate::panes::terminal_character::{ CharacterStyles, TerminalCharacter, EMPTY_TERMINAL_CHARACTER, @@ -97,7 +100,7 @@ fn transfer_rows_up( let mut next_lines: Vec = vec![]; for _ in 0..count { if next_lines.is_empty() { - if source.len() > 0 { + if !source.is_empty() { let next_line = source.remove(0); if !next_line.is_canonical { let mut bottom_canonical_row_and_wraps_in_dst = @@ -214,7 +217,7 @@ impl Grid { y_coordinates } pub fn scroll_up_one_line(&mut self) { - if self.lines_above.len() > 0 && self.viewport.len() == self.height { + if !self.lines_above.is_empty() && self.viewport.len() == self.height { let line_to_push_down = self.viewport.pop().unwrap(); self.lines_below.insert(0, line_to_push_down); let line_to_insert_at_viewport_top = self.lines_above.pop().unwrap(); @@ -222,7 +225,7 @@ impl Grid { } } pub fn scroll_down_one_line(&mut self) { - if self.lines_below.len() > 0 && self.viewport.len() == self.height { + if !self.lines_below.is_empty() && self.viewport.len() == self.height { let mut line_to_push_up = self.viewport.remove(0); if line_to_push_up.is_canonical { self.lines_above.push(line_to_push_up); @@ -243,7 +246,7 @@ impl Grid { for mut row in self.viewport.drain(..) { if !row.is_canonical && viewport_canonical_lines.is_empty() - && self.lines_above.len() > 0 + && !self.lines_above.is_empty() { let mut first_line_above = self.lines_above.pop().unwrap(); first_line_above.append(&mut row.columns); @@ -269,7 +272,7 @@ impl Grid { let mut new_viewport_rows = vec![]; for mut canonical_line in viewport_canonical_lines { let mut canonical_line_parts: Vec = vec![]; - while canonical_line.columns.len() > 0 { + while !canonical_line.columns.is_empty() { let next_wrap = if canonical_line.len() > new_columns { canonical_line.columns.drain(..new_columns) } else { @@ -279,7 +282,7 @@ impl Grid { // if there are no more parts, this row is canonical as long as it originall // was canonical (it might not have been for example if it's the first row in // the viewport, and the actual canonical row is above it in the scrollback) - let row = if canonical_line_parts.len() == 0 && canonical_line.is_canonical { + let row = if canonical_line_parts.is_empty() && canonical_line.is_canonical { row.canonical() } else { row @@ -294,62 +297,70 @@ impl Grid { let new_cursor_x = (cursor_index_in_canonical_line / new_columns) + (cursor_index_in_canonical_line % new_columns); let current_viewport_row_count = self.viewport.len(); - if current_viewport_row_count < self.height { - let row_count_to_transfer = self.height - current_viewport_row_count; - transfer_rows_down( - &mut self.lines_above, - &mut self.viewport, - row_count_to_transfer, - None, - Some(new_columns), - ); - let rows_pulled = self.viewport.len() - current_viewport_row_count; - new_cursor_y += rows_pulled; - } else if current_viewport_row_count > self.height { - let row_count_to_transfer = current_viewport_row_count - self.height; - if row_count_to_transfer > new_cursor_y { - new_cursor_y = 0; - } else { - new_cursor_y -= row_count_to_transfer; + match current_viewport_row_count.cmp(&self.height) { + Ordering::Less => { + let row_count_to_transfer = self.height - current_viewport_row_count; + transfer_rows_down( + &mut self.lines_above, + &mut self.viewport, + row_count_to_transfer, + None, + Some(new_columns), + ); + let rows_pulled = self.viewport.len() - current_viewport_row_count; + new_cursor_y += rows_pulled; } - transfer_rows_up( - &mut self.viewport, - &mut self.lines_above, - row_count_to_transfer, - Some(new_columns), - None, - ); + Ordering::Greater => { + let row_count_to_transfer = current_viewport_row_count - self.height; + if row_count_to_transfer > new_cursor_y { + new_cursor_y = 0; + } else { + new_cursor_y -= row_count_to_transfer; + } + transfer_rows_up( + &mut self.viewport, + &mut self.lines_above, + row_count_to_transfer, + Some(new_columns), + None, + ); + } + Ordering::Equal => {} } self.cursor.y = new_cursor_y; self.cursor.x = new_cursor_x; } if new_rows != self.height { let current_viewport_row_count = self.viewport.len(); - if current_viewport_row_count < new_rows { - let row_count_to_transfer = new_rows - current_viewport_row_count; - transfer_rows_down( - &mut self.lines_above, - &mut self.viewport, - row_count_to_transfer, - None, - Some(new_columns), - ); - let rows_pulled = self.viewport.len() - current_viewport_row_count; - self.cursor.y += rows_pulled; - } else if current_viewport_row_count > new_rows { - let row_count_to_transfer = current_viewport_row_count - new_rows; - if row_count_to_transfer > self.cursor.y { - self.cursor.y = 0; - } else { - self.cursor.y -= row_count_to_transfer; + match current_viewport_row_count.cmp(&new_rows) { + Ordering::Less => { + let row_count_to_transfer = new_rows - current_viewport_row_count; + transfer_rows_down( + &mut self.lines_above, + &mut self.viewport, + row_count_to_transfer, + None, + Some(new_columns), + ); + let rows_pulled = self.viewport.len() - current_viewport_row_count; + self.cursor.y += rows_pulled; } - transfer_rows_up( - &mut self.viewport, - &mut self.lines_above, - row_count_to_transfer, - Some(new_columns), - None, - ); + Ordering::Greater => { + let row_count_to_transfer = current_viewport_row_count - new_rows; + if row_count_to_transfer > self.cursor.y { + self.cursor.y = 0; + } else { + self.cursor.y -= row_count_to_transfer; + } + transfer_rows_up( + &mut self.viewport, + &mut self.lines_above, + row_count_to_transfer, + Some(new_columns), + None, + ); + } + Ordering::Equal => {} } } self.height = new_rows; @@ -364,10 +375,8 @@ impl Grid { .iter() .map(|r| { let mut line: Vec = r.columns.iter().copied().collect(); - for _ in line.len()..self.width { - // pad line - line.push(EMPTY_TERMINAL_CHARACTER); - } + // pad line + line.resize(self.width, EMPTY_TERMINAL_CHARACTER); line }) .collect(); @@ -719,17 +728,17 @@ impl Row { self } pub fn add_character_at(&mut self, terminal_character: TerminalCharacter, x: usize) { - if x == self.columns.len() { - self.columns.push(terminal_character); - } else if x > self.columns.len() { - for _ in self.columns.len()..x { - self.columns.push(EMPTY_TERMINAL_CHARACTER); + match self.columns.len().cmp(&x) { + Ordering::Equal => self.columns.push(terminal_character), + Ordering::Less => { + self.columns.resize(x, EMPTY_TERMINAL_CHARACTER); + self.columns.push(terminal_character); + } + Ordering::Greater => { + // this is much more performant than remove/insert + self.columns.push(terminal_character); + self.columns.swap_remove(x); } - self.columns.push(terminal_character); - } else { - // this is much more performant than remove/insert - self.columns.push(terminal_character); - self.columns.swap_remove(x); } } pub fn replace_character_at(&mut self, terminal_character: TerminalCharacter, x: usize) { @@ -767,10 +776,10 @@ impl Row { } current_part.push(character); } - if current_part.len() > 0 { + if !current_part.is_empty() { parts.push(Row::from_columns(current_part)) }; - if parts.len() > 0 && self.is_canonical { + if !parts.is_empty() && self.is_canonical { parts.get_mut(0).unwrap().is_canonical = true; } parts diff --git a/src/panes/plugin_pane.rs b/src/panes/plugin_pane.rs index 1573d577..a93ff8c0 100644 --- a/src/panes/plugin_pane.rs +++ b/src/panes/plugin_pane.rs @@ -9,6 +9,7 @@ use crate::panes::{PaneId, PositionAndSize}; pub struct PluginPane { pub pid: u32, pub should_render: bool, + pub selectable: bool, pub position_and_size: PositionAndSize, pub position_and_size_override: Option, pub send_plugin_instructions: SenderWithContext, @@ -23,6 +24,7 @@ impl PluginPane { Self { pid, should_render: true, + selectable: true, position_and_size, position_and_size_override: None, send_plugin_instructions, @@ -92,6 +94,12 @@ impl Pane for PluginPane { fn set_should_render(&mut self, should_render: bool) { self.should_render = should_render; } + fn selectable(&self) -> bool { + self.selectable + } + fn set_selectable(&mut self, selectable: bool) { + self.selectable = selectable; + } fn render(&mut self) -> Option { // if self.should_render { if true { diff --git a/src/panes/terminal_pane.rs b/src/panes/terminal_pane.rs index b63c6cc8..b9369fea 100644 --- a/src/panes/terminal_pane.rs +++ b/src/panes/terminal_pane.rs @@ -4,10 +4,9 @@ use crate::tab::Pane; use ::nix::pty::Winsize; use ::std::os::unix::io::RawFd; use ::vte::Perform; -use std::fmt::{self, Debug, Formatter}; +use std::fmt::Debug; -use crate::boundaries::Rect; -use crate::panes::grid::{Grid, Row}; +use crate::panes::grid::Grid; use crate::panes::terminal_character::{ CharacterStyles, TerminalCharacter, EMPTY_TERMINAL_CHARACTER, }; @@ -43,6 +42,7 @@ pub struct TerminalPane { pub alternative_grid: Option, // for 1049h/l instructions which tell us to switch between these two pub pid: RawFd, pub should_render: bool, + pub selectable: bool, pub position_and_size: PositionAndSize, pub position_and_size_override: Option, pub cursor_key_mode: bool, // DECCKM - when set, cursor keys should send ANSI direction codes (eg. "OD") instead of the arrow keys (eg. "") @@ -170,6 +170,12 @@ impl Pane for TerminalPane { fn set_should_render(&mut self, should_render: bool) { self.should_render = should_render; } + fn selectable(&self) -> bool { + self.selectable + } + fn set_selectable(&mut self, selectable: bool) { + self.selectable = selectable; + } fn render(&mut self) -> Option { // if self.should_render { if true { @@ -280,6 +286,7 @@ impl TerminalPane { grid, alternative_grid: None, should_render: true, + selectable: true, pending_styles, position_and_size, position_and_size_override: None, @@ -330,14 +337,6 @@ impl TerminalPane { // (x, y) self.grid.cursor_coordinates() } - pub fn scroll_up(&mut self, count: usize) { - self.grid.move_viewport_up(count); - self.mark_for_rerender(); - } - pub fn scroll_down(&mut self, count: usize) { - self.grid.move_viewport_down(count); - self.mark_for_rerender(); - } pub fn rotate_scroll_region_up(&mut self, count: usize) { self.grid.rotate_scroll_region_up(count); self.mark_for_rerender(); @@ -346,68 +345,6 @@ impl TerminalPane { self.grid.rotate_scroll_region_down(count); self.mark_for_rerender(); } - pub fn clear_scroll(&mut self) { - self.grid.reset_viewport(); - self.mark_for_rerender(); - } - pub fn override_size_and_position(&mut self, x: usize, y: usize, size: &PositionAndSize) { - let position_and_size_override = PositionAndSize { - x, - y, - rows: size.rows, - columns: size.columns, - }; - self.position_and_size_override = Some(position_and_size_override); - self.reflow_lines(); - self.mark_for_rerender(); - } - pub fn reset_size_and_position_override(&mut self) { - self.position_and_size_override = None; - self.reflow_lines(); - self.mark_for_rerender(); - } - pub fn adjust_input_to_terminal(&self, input_bytes: Vec) -> Vec { - // there are some cases in which the terminal state means that input sent to it - // needs to be adjusted. - // here we match against those cases - if need be, we adjust the input and if not - // we send back the original input - match input_bytes.as_slice() { - [27, 91, 68] => { - // left arrow - if self.cursor_key_mode { - // please note that in the line below, there is an ANSI escape code (27) at the beginning of the string, - // some editors will not show this - return "OD".as_bytes().to_vec(); - } - } - [27, 91, 67] => { - // right arrow - if self.cursor_key_mode { - // please note that in the line below, there is an ANSI escape code (27) at the beginning of the string, - // some editors will not show this - return "OC".as_bytes().to_vec(); - } - } - [27, 91, 65] => { - // up arrow - if self.cursor_key_mode { - // please note that in the line below, there is an ANSI escape code (27) at the beginning of the string, - // some editors will not show this - return "OA".as_bytes().to_vec(); - } - } - [27, 91, 66] => { - // down arrow - if self.cursor_key_mode { - // please note that in the line below, there is an ANSI escape code (27) at the beginning of the string, - // some editors will not show this - return "OB".as_bytes().to_vec(); - } - } - _ => {} - }; - input_bytes - } fn add_newline(&mut self) { self.grid.add_canonical_line(); // self.reset_all_ansi_codes(); // TODO: find out if we should be resetting here or not @@ -698,11 +635,8 @@ impl vte::Perform for TerminalPane { } fn esc_dispatch(&mut self, intermediates: &[u8], _ignore: bool, byte: u8) { - match (byte, intermediates.get(0)) { - (b'M', None) => { - self.grid.move_cursor_up_with_scrolling(1); - } - _ => {} + if let (b'M', None) = (byte, intermediates.get(0)) { + self.grid.move_cursor_up_with_scrolling(1); } } } diff --git a/src/pty_bus.rs b/src/pty_bus.rs index e635b176..ba5a608b 100644 --- a/src/pty_bus.rs +++ b/src/pty_bus.rs @@ -299,13 +299,15 @@ impl PtyBus { let child_pid = self.id_to_child_pid.get(&id).unwrap(); self.os_input.kill(*child_pid).unwrap(); } - PaneId::Plugin(pid) => self - .send_plugin_instructions - .send(PluginInstruction::Unload(pid)) - .unwrap(), + PaneId::Plugin(pid) => drop( + self.send_plugin_instructions + .send(PluginInstruction::Unload(pid)), + ), } } pub fn close_tab(&mut self, ids: Vec) { - ids.iter().for_each(|&id| self.close_pane(id)); + ids.iter().for_each(|&id| { + self.close_pane(id); + }); } } diff --git a/src/screen.rs b/src/screen.rs index 365d1d7a..5e4d1ae9 100644 --- a/src/screen.rs +++ b/src/screen.rs @@ -42,6 +42,7 @@ pub enum ScreenInstruction { ClearScroll, CloseFocusedPane, ToggleActiveTerminalFullscreen, + SetSelectable(PaneId, bool), ClosePane(PaneId), ApplyLayout((Layout, Vec)), NewTab(RawFd), @@ -138,7 +139,7 @@ impl Screen { if self.tabs.len() > 1 { self.switch_tab_prev(); } - let mut active_tab = self.tabs.remove(&active_tab_index).unwrap(); + 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)) diff --git a/src/tab.rs b/src/tab.rs index 7a56508c..6c65958f 100644 --- a/src/tab.rs +++ b/src/tab.rs @@ -66,6 +66,7 @@ pub struct Tab { pub send_app_instructions: SenderWithContext, } +// FIXME: Use a struct that has a pane_type enum, to reduce all of the duplication pub trait Pane { fn x(&self) -> usize; fn y(&self) -> usize; @@ -81,6 +82,8 @@ pub trait Pane { fn position_and_size_override(&self) -> Option; fn should_render(&self) -> bool; fn set_should_render(&mut self, should_render: bool); + fn selectable(&self) -> bool; + fn set_selectable(&mut self, selectable: bool); fn render(&mut self) -> Option; fn pid(&self) -> PaneId; fn reduce_height_down(&mut self, count: usize); @@ -618,10 +621,24 @@ impl Tab { fn get_panes(&self) -> impl Iterator)> { self.panes.iter() } + // FIXME: This is some shameful duplication... + fn get_selectable_panes(&self) -> impl Iterator)> { + self.panes.iter().filter(|(_, p)| p.selectable()) + } fn has_panes(&self) -> bool { let mut all_terminals = self.get_panes(); all_terminals.next().is_some() } + fn has_selectable_panes(&self) -> bool { + let mut all_terminals = self.get_selectable_panes(); + all_terminals.next().is_some() + } + fn next_active_pane(&self, panes: Vec) -> Option { + panes + .into_iter() + .rev() + .find(|pid| self.panes.get(pid).unwrap().selectable()) + } fn pane_ids_directly_left_of(&self, id: &PaneId) -> Option> { let mut ids = vec![]; let terminal_to_check = self.panes.get(id).unwrap(); @@ -1421,14 +1438,14 @@ impl Tab { } } pub fn move_focus(&mut self) { - if !self.has_panes() { + if !self.has_selectable_panes() { return; } if self.fullscreen_is_active { return; } let active_terminal_id = self.get_active_pane_id().unwrap(); - let terminal_ids: Vec = self.get_panes().map(|(&pid, _)| pid).collect(); // TODO: better, no allocations + let terminal_ids: Vec = self.get_selectable_panes().map(|(&pid, _)| pid).collect(); // TODO: better, no allocations let first_terminal = terminal_ids.get(0).unwrap(); let active_terminal_id_position = terminal_ids .iter() @@ -1442,7 +1459,7 @@ impl Tab { self.render(); } pub fn move_focus_left(&mut self) { - if !self.has_panes() { + if !self.has_selectable_panes() { return; } if self.fullscreen_is_active { @@ -1450,7 +1467,7 @@ impl Tab { } let active_terminal = self.get_active_pane(); if let Some(active) = active_terminal { - let terminals = self.get_panes(); + let terminals = self.get_selectable_panes(); let next_index = terminals .enumerate() .filter(|(_, (_, c))| { @@ -1472,7 +1489,7 @@ impl Tab { self.render(); } pub fn move_focus_down(&mut self) { - if !self.has_panes() { + if !self.has_selectable_panes() { return; } if self.fullscreen_is_active { @@ -1480,7 +1497,7 @@ impl Tab { } let active_terminal = self.get_active_pane(); if let Some(active) = active_terminal { - let terminals = self.get_panes(); + let terminals = self.get_selectable_panes(); let next_index = terminals .enumerate() .filter(|(_, (_, c))| { @@ -1502,7 +1519,7 @@ impl Tab { self.render(); } pub fn move_focus_up(&mut self) { - if !self.has_panes() { + if !self.has_selectable_panes() { return; } if self.fullscreen_is_active { @@ -1510,7 +1527,7 @@ impl Tab { } let active_terminal = self.get_active_pane(); if let Some(active) = active_terminal { - let terminals = self.get_panes(); + let terminals = self.get_selectable_panes(); let next_index = terminals .enumerate() .filter(|(_, (_, c))| { @@ -1532,7 +1549,7 @@ impl Tab { self.render(); } pub fn move_focus_right(&mut self) { - if !self.has_panes() { + if !self.has_selectable_panes() { return; } if self.fullscreen_is_active { @@ -1540,7 +1557,7 @@ impl Tab { } let active_terminal = self.get_active_pane(); if let Some(active) = active_terminal { - let terminals = self.get_panes(); + let terminals = self.get_selectable_panes(); let next_index = terminals .enumerate() .filter(|(_, (_, c))| { @@ -1578,7 +1595,7 @@ impl Tab { }) } fn panes_to_the_left_between_aligning_borders(&self, id: PaneId) -> Option> { - if let Some(terminal) = &self.panes.get(&id) { + if let Some(terminal) = self.panes.get(&id) { let upper_close_border = terminal.y(); let lower_close_border = terminal.y() + terminal.rows() + 1; @@ -1601,7 +1618,7 @@ impl Tab { None } fn panes_to_the_right_between_aligning_borders(&self, id: PaneId) -> Option> { - if let Some(terminal) = &self.panes.get(&id) { + if let Some(terminal) = self.panes.get(&id) { let upper_close_border = terminal.y(); let lower_close_border = terminal.y() + terminal.rows() + 1; @@ -1625,7 +1642,7 @@ impl Tab { None } fn panes_above_between_aligning_borders(&self, id: PaneId) -> Option> { - if let Some(terminal) = &self.panes.get(&id) { + if let Some(terminal) = self.panes.get(&id) { let left_close_border = terminal.x(); let right_close_border = terminal.x() + terminal.columns() + 1; @@ -1647,8 +1664,8 @@ impl Tab { } None } - fn terminals_below_between_aligning_borders(&self, id: PaneId) -> Option> { - if let Some(terminal) = &self.panes.get(&id) { + fn panes_below_between_aligning_borders(&self, id: PaneId) -> Option> { + if let Some(terminal) = self.panes.get(&id) { let left_close_border = terminal.x(); let right_close_border = terminal.x() + terminal.columns() + 1; @@ -1681,9 +1698,17 @@ impl Tab { } } } - pub fn get_pane_ids(&mut self) -> Vec { + pub fn get_pane_ids(&self) -> Vec { self.get_panes().map(|(&pid, _)| pid).collect() } + pub fn set_pane_selectable(&mut self, id: PaneId, selectable: bool) { + if let Some(pane) = self.panes.get_mut(&id) { + pane.set_selectable(selectable); + if self.get_active_pane_id() == Some(id) && !selectable { + self.active_terminal = self.next_active_pane(self.get_pane_ids()) + } + } + } pub fn close_pane(&mut self, id: PaneId) { if self.panes.get(&id).is_some() { self.close_pane_without_rerender(id); @@ -1699,7 +1724,7 @@ impl Tab { // 1 for the border } if self.active_terminal == Some(id) { - self.active_terminal = Some(*terminals.last().unwrap()); + self.active_terminal = self.next_active_pane(terminals); } } else if let Some(terminals) = self.panes_to_the_right_between_aligning_borders(id) { for terminal_id in terminals.iter() { @@ -1707,7 +1732,7 @@ impl Tab { // 1 for the border } if self.active_terminal == Some(id) { - self.active_terminal = Some(*terminals.last().unwrap()); + self.active_terminal = self.next_active_pane(terminals); } } else if let Some(terminals) = self.panes_above_between_aligning_borders(id) { for terminal_id in terminals.iter() { @@ -1715,21 +1740,21 @@ impl Tab { // 1 for the border } if self.active_terminal == Some(id) { - self.active_terminal = Some(*terminals.last().unwrap()); + self.active_terminal = self.next_active_pane(terminals); } - } else if let Some(terminals) = self.terminals_below_between_aligning_borders(id) { + } else if let Some(terminals) = self.panes_below_between_aligning_borders(id) { for terminal_id in terminals.iter() { self.increase_pane_height_up(&terminal_id, terminal_to_close_height + 1); // 1 for the border } if self.active_terminal == Some(id) { - self.active_terminal = Some(*terminals.last().unwrap()); + self.active_terminal = self.next_active_pane(terminals); } } else { } self.panes.remove(&id); - if !self.has_panes() { - self.active_terminal = None; + if self.active_terminal.is_none() { + self.active_terminal = self.next_active_pane(self.get_pane_ids()); } } } diff --git a/src/tests/integration/layouts.rs b/src/tests/integration/layouts.rs index 35ff7188..2086ac72 100644 --- a/src/tests/integration/layouts.rs +++ b/src/tests/integration/layouts.rs @@ -50,43 +50,3 @@ pub fn accepts_basic_layout() { assert_snapshot!(next_to_last_snapshot); assert_snapshot!(last_snapshot); } - -#[test] -#[should_panic(expected = "The total percent for each part should equal 100.")] -pub fn should_throw_for_more_than_100_percent_total() { - let fake_win_size = PositionAndSize { - columns: 121, - rows: 20, - x: 0, - y: 0, - }; - let mut fake_input_output = get_fake_os_input(&fake_win_size); - fake_input_output.add_terminal_input(&[&QUIT]); - - let mut opts = CliArgs::default(); - opts.layout = Some(PathBuf::from( - "src/tests/fixtures/layouts/parts-total-more-than-100-percent.yaml", - )); - - start(Box::new(fake_input_output.clone()), opts); -} - -#[test] -#[should_panic(expected = "The total percent for each part should equal 100.")] -pub fn should_throw_for_less_than_100_percent_total() { - let fake_win_size = PositionAndSize { - columns: 121, - rows: 20, - x: 0, - y: 0, - }; - let mut fake_input_output = get_fake_os_input(&fake_win_size); - fake_input_output.add_terminal_input(&[&QUIT]); - - let mut opts = CliArgs::default(); - opts.layout = Some(PathBuf::from( - "src/tests/fixtures/layouts/parts-total-less-than-100-percent.yaml", - )); - - start(Box::new(fake_input_output.clone()), opts); -} diff --git a/src/wasm_vm.rs b/src/wasm_vm.rs index 51bd9375..97a1a587 100644 --- a/src/wasm_vm.rs +++ b/src/wasm_vm.rs @@ -1,20 +1,30 @@ -use std::{path::PathBuf, sync::mpsc::Sender}; +use std::{ + path::PathBuf, + sync::mpsc::{channel, Sender}, +}; use wasmer::{imports, Function, ImportObject, Store, WasmerEnv}; use wasmer_wasi::WasiEnv; -use crate::{pty_bus::PtyInstruction, SenderWithContext}; +use crate::{ + input::get_help, panes::PaneId, pty_bus::PtyInstruction, screen::ScreenInstruction, + AppInstruction, SenderWithContext, +}; #[derive(Clone, Debug)] pub enum PluginInstruction { Load(Sender, PathBuf), Draw(Sender, u32, usize, usize), // String buffer, plugin id, rows, cols Input(u32, Vec), // plugin id, input bytes + GlobalInput(Vec), // input bytes Unload(u32), Quit, } #[derive(WasmerEnv, Clone)] pub struct PluginEnv { + pub plugin_id: u32, + pub send_screen_instructions: SenderWithContext, + pub send_app_instructions: SenderWithContext, pub send_pty_instructions: SenderWithContext, // FIXME: This should be a big bundle of all of the channels pub wasi_env: WasiEnv, } @@ -24,7 +34,9 @@ pub struct PluginEnv { 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_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), } } } @@ -38,6 +50,33 @@ fn host_open_file(plugin_env: &PluginEnv) { .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