feat(ui): added a help / status bar for keybindings and modes
To implement this status bar as a plugin (https://github.com/mosaic-org/status-bar/blob/master/src/main.rs), the following bits of infrastructure were changed: - Layouts can now have an exact size constraint (1 row high, for example) - Unconstrained blocks in a layout are now grown to fill the remaining space - Default plugins and layouts are now stored in an OS-dependent data directory. The initial installation of these assets is done a build-time via `build.rs` - All new tabs are created with a user-configurable default layout, unless provided with a different layout - Plugins can now capture *all* key presses detected by Mosaic via `global_handle_key()` - Plugins can now control whether or not they are selectable via `set_selectable()` - Plugins can now fetch a vector of help-strings from Mosaic, which is currently being used to display helpful keybindings, via `get_help()` - Also patched up all remaining compiler warnings and 23 of the 25 clippy lints on main
This commit is contained in:
commit
e7f16ed468
19 changed files with 655 additions and 445 deletions
49
Cargo.lock
generated
49
Cargo.lock
generated
|
|
@ -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",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
8
assets/layouts/default.yaml
Normal file
8
assets/layouts/default.yaml
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
direction: Horizontal
|
||||
parts:
|
||||
- direction: Vertical
|
||||
- direction: Vertical
|
||||
split_size:
|
||||
Fixed: 1
|
||||
plugin: status-bar
|
||||
|
|
@ -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
|
||||
Fixed: 1
|
||||
plugin: status-bar
|
||||
BIN
assets/plugins/status-bar.wasm
Normal file
BIN
assets/plugins/status-bar.wasm
Normal file
Binary file not shown.
Binary file not shown.
23
build.rs
23
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");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
67
src/input.rs
67
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<ScreenInstruction>,
|
||||
send_pty_instructions: SenderWithContext<PtyInstruction>,
|
||||
send_plugin_instructions: SenderWithContext<PluginInstruction>,
|
||||
send_app_instructions: SenderWithContext<AppInstruction>,
|
||||
}
|
||||
|
||||
|
|
@ -21,6 +21,7 @@ impl InputHandler {
|
|||
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 {
|
||||
|
|
@ -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<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(
|
||||
|
|
@ -297,6 +344,7 @@ pub fn input_loop(
|
|||
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(
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
185
src/layout.rs
185
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<u8>,
|
||||
sizes: Vec<Option<SplitSize>>,
|
||||
) -> Vec<PositionAndSize> {
|
||||
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<u8>,
|
||||
sizes: Vec<Option<SplitSize>>,
|
||||
) -> Vec<PositionAndSize> {
|
||||
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<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");
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
let sizes: Vec<Option<SplitSize>> = 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();
|
||||
|
|
|
|||
329
src/main.rs
329
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<T: Clone> SenderWithContext<T> {
|
|||
}
|
||||
}
|
||||
|
||||
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<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))))
|
||||
}
|
||||
|
||||
#[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 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<dyn OsApi>, 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<dyn OsApi>, 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<dyn OsApi>, 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<dyn OsApi>, 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<dyn OsApi>, 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<dyn OsApi>, 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<dyn OsApi>, 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<dyn OsApi>, 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<dyn OsApi>, 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";
|
||||
|
|
|
|||
|
|
@ -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<Row> = 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<Row> = 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<TerminalCharacter> = 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
|
||||
|
|
|
|||
|
|
@ -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<PositionAndSize>,
|
||||
pub send_plugin_instructions: SenderWithContext<PluginInstruction>,
|
||||
|
|
@ -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<String> {
|
||||
// if self.should_render {
|
||||
if true {
|
||||
|
|
|
|||
|
|
@ -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<Grid>, // 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<PositionAndSize>,
|
||||
pub cursor_key_mode: bool, // DECCKM - when set, cursor keys should send ANSI direction codes (eg. "OD") instead of the arrow keys (eg. "[D")
|
||||
|
|
@ -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<String> {
|
||||
// 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<u8>) -> Vec<u8> {
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<PaneId>) {
|
||||
ids.iter().for_each(|&id| self.close_pane(id));
|
||||
ids.iter().for_each(|&id| {
|
||||
self.close_pane(id);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ pub enum ScreenInstruction {
|
|||
ClearScroll,
|
||||
CloseFocusedPane,
|
||||
ToggleActiveTerminalFullscreen,
|
||||
SetSelectable(PaneId, bool),
|
||||
ClosePane(PaneId),
|
||||
ApplyLayout((Layout, Vec<RawFd>)),
|
||||
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))
|
||||
|
|
|
|||
71
src/tab.rs
71
src/tab.rs
|
|
@ -66,6 +66,7 @@ pub struct Tab {
|
|||
pub send_app_instructions: SenderWithContext<AppInstruction>,
|
||||
}
|
||||
|
||||
// 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<PositionAndSize>;
|
||||
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<String>;
|
||||
fn pid(&self) -> PaneId;
|
||||
fn reduce_height_down(&mut self, count: usize);
|
||||
|
|
@ -618,10 +621,24 @@ impl Tab {
|
|||
fn get_panes(&self) -> impl Iterator<Item = (&PaneId, &Box<dyn Pane>)> {
|
||||
self.panes.iter()
|
||||
}
|
||||
// FIXME: This is some shameful duplication...
|
||||
fn get_selectable_panes(&self) -> impl Iterator<Item = (&PaneId, &Box<dyn Pane>)> {
|
||||
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<PaneId>) -> Option<PaneId> {
|
||||
panes
|
||||
.into_iter()
|
||||
.rev()
|
||||
.find(|pid| self.panes.get(pid).unwrap().selectable())
|
||||
}
|
||||
fn pane_ids_directly_left_of(&self, id: &PaneId) -> Option<Vec<PaneId>> {
|
||||
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<PaneId> = self.get_panes().map(|(&pid, _)| pid).collect(); // TODO: better, no allocations
|
||||
let terminal_ids: Vec<PaneId> = 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<Vec<PaneId>> {
|
||||
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<Vec<PaneId>> {
|
||||
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<Vec<PaneId>> {
|
||||
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<Vec<PaneId>> {
|
||||
if let Some(terminal) = &self.panes.get(&id) {
|
||||
fn panes_below_between_aligning_borders(&self, id: PaneId) -> Option<Vec<PaneId>> {
|
||||
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<PaneId> {
|
||||
pub fn get_pane_ids(&self) -> Vec<PaneId> {
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<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,
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue