diff --git a/assets/layouts/default.yaml b/assets/layouts/default.yaml index 8c87e635..e6335691 100644 --- a/assets/layouts/default.yaml +++ b/assets/layouts/default.yaml @@ -5,4 +5,4 @@ parts: - direction: Vertical split_size: Fixed: 1 - plugin: status-bar.wasm \ No newline at end of file + plugin: status-bar \ No newline at end of file diff --git a/assets/layouts/strider.yaml b/assets/layouts/strider.yaml index 40dde92b..e33f516a 100644 --- a/assets/layouts/strider.yaml +++ b/assets/layouts/strider.yaml @@ -6,9 +6,9 @@ parts: - direction: Horizontal split_size: Percent: 20 - plugin: strider.wasm + plugin: strider - direction: Horizontal - direction: Vertical split_size: Fixed: 1 - plugin: status-bar.wasm \ No newline at end of file + plugin: status-bar \ No newline at end of file diff --git a/assets/plugins/status-bar.wasm b/assets/plugins/status-bar.wasm index d434bc1d..57b00f2b 100644 Binary files a/assets/plugins/status-bar.wasm and b/assets/plugins/status-bar.wasm differ diff --git a/src/input.rs b/src/input.rs index c921bdfd..3c5518f8 100644 --- a/src/input.rs +++ b/src/input.rs @@ -317,8 +317,8 @@ pub fn get_help(mode: &InputMode) -> Vec { " Quit".into(), " Scroll".into(), "<1> New Tab".into(), - "<2> Next Tab".into(), - "<3> Last Tab".into(), + "<2/3> Move Tab".into(), + "<4> Close Tab".into(), ]; match mode { InputMode::Normal => vec![" Command Mode".into()], diff --git a/src/layout.rs b/src/layout.rs index 193de93a..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}; @@ -179,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(); diff --git a/src/main.rs b/src/main.rs index bf39d7f8..27e97766 100644 --- a/src/main.rs +++ b/src/main.rs @@ -16,14 +16,15 @@ mod utils; mod wasm_vm; -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}; @@ -208,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({ @@ -224,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; } } } @@ -421,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; @@ -441,114 +441,122 @@ pub fn start(mut os_input: Box, opts: CliArgs) { 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); - send_app_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 { - 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 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(); - } - // 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(); + 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(); } } - - 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, + drop(send_screen_instructions.send(ScreenInstruction::Render)); } + PluginInstruction::Unload(pid) => drop(plugin_map.remove(&pid)), + PluginInstruction::Quit => break, } } })