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:
Brooks Rady 2021-01-13 13:10:48 +00:00 committed by GitHub
commit e7f16ed468
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 655 additions and 445 deletions

49
Cargo.lock generated
View file

@ -407,6 +407,27 @@ version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "524cbf6897b527295dff137cec09ecf3a05f4fddffd7dfcd1585403449e74198" 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]] [[package]]
name = "dtoa" name = "dtoa"
version = "0.4.6" version = "0.4.6"
@ -882,6 +903,7 @@ dependencies = [
"async-std", "async-std",
"backtrace", "backtrace",
"bincode", "bincode",
"directories-next",
"futures", "futures",
"insta", "insta",
"libc", "libc",
@ -1142,13 +1164,32 @@ version = "0.1.57"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" 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]] [[package]]
name = "redox_termios" name = "redox_termios"
version = "0.1.1" version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e891cfe48e9100a70a3b6eb652fef28920c117d366339687bd5576160db0f76" checksum = "7e891cfe48e9100a70a3b6eb652fef28920c117d366339687bd5576160db0f76"
dependencies = [ 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]] [[package]]
@ -1356,7 +1397,7 @@ checksum = "03088793f677dce356f3ccc2edb1b314ad191ab702a5de3faf49304f7e104918"
dependencies = [ dependencies = [
"cfg-if 0.1.10", "cfg-if 0.1.10",
"libc", "libc",
"redox_syscall", "redox_syscall 0.1.57",
"winapi", "winapi",
] ]
@ -1437,7 +1478,7 @@ dependencies = [
"cfg-if 0.1.10", "cfg-if 0.1.10",
"libc", "libc",
"rand", "rand",
"redox_syscall", "redox_syscall 0.1.57",
"remove_dir_all", "remove_dir_all",
"winapi", "winapi",
] ]
@ -1459,7 +1500,7 @@ source = "git+https://gitlab.com/TheLostLambda/termion.git#70159e07c59c02dc681db
dependencies = [ dependencies = [
"libc", "libc",
"numtoa", "numtoa",
"redox_syscall", "redox_syscall 0.1.57",
"redox_termios", "redox_termios",
"serde", "serde",
] ]

View file

@ -9,6 +9,7 @@ edition = "2018"
[dependencies] [dependencies]
backtrace = "0.3.55" backtrace = "0.3.55"
bincode = "1.3.1" bincode = "1.3.1"
directories-next = "2.0"
futures = "0.3.5" futures = "0.3.5"
libc = "0.2" libc = "0.2"
nix = "0.17.0" nix = "0.17.0"
@ -34,6 +35,7 @@ features = ["unstable"]
insta = "0.16.1" insta = "0.16.1"
[build-dependencies] [build-dependencies]
directories-next = "2.0"
structopt = "0.3" structopt = "0.3"
[profile.release] [profile.release]

View file

@ -0,0 +1,8 @@
---
direction: Horizontal
parts:
- direction: Vertical
- direction: Vertical
split_size:
Fixed: 1
plugin: status-bar

View file

@ -6,12 +6,9 @@ parts:
- direction: Horizontal - direction: Horizontal
split_size: split_size:
Percent: 20 Percent: 20
plugin: strider.wasm plugin: strider
- direction: Horizontal - direction: Horizontal
split_size:
Percent: 80
split_size:
Percent: 80
- direction: Vertical - direction: Vertical
split_size: split_size:
Percent: 20 Fixed: 1
plugin: status-bar

Binary file not shown.

View file

@ -1,4 +1,5 @@
use std::fs; use directories_next::ProjectDirs;
use std::{fs, path::Path};
use structopt::clap::Shell; use structopt::clap::Shell;
include!("src/cli.rs"); include!("src/cli.rs");
@ -6,8 +7,9 @@ include!("src/cli.rs");
const BIN_NAME: &str = "mosaic"; const BIN_NAME: &str = "mosaic";
fn main() { fn main() {
// Generate Shell Completions
let mut clap_app = CliArgs::clap(); 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(); let mut out_dir = std::env::var_os("CARGO_MANIFEST_DIR").unwrap();
out_dir.push("/assets/completions"); 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::Bash, &out_dir);
clap_app.gen_completions(BIN_NAME, Shell::Zsh, &out_dir); clap_app.gen_completions(BIN_NAME, Shell::Zsh, &out_dir);
clap_app.gen_completions(BIN_NAME, Shell::Fish, &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");
}
} }

View file

@ -166,6 +166,7 @@ pub enum ScreenContext {
ClearScroll, ClearScroll,
CloseFocusedPane, CloseFocusedPane,
ToggleActiveTerminalFullscreen, ToggleActiveTerminalFullscreen,
SetSelectable,
ClosePane, ClosePane,
ApplyLayout, ApplyLayout,
NewTab, NewTab,
@ -200,6 +201,7 @@ impl From<&ScreenInstruction> for ScreenContext {
ScreenInstruction::ToggleActiveTerminalFullscreen => { ScreenInstruction::ToggleActiveTerminalFullscreen => {
ScreenContext::ToggleActiveTerminalFullscreen ScreenContext::ToggleActiveTerminalFullscreen
} }
ScreenInstruction::SetSelectable(..) => ScreenContext::SetSelectable,
ScreenInstruction::ClosePane(_) => ScreenContext::ClosePane, ScreenInstruction::ClosePane(_) => ScreenContext::ClosePane,
ScreenInstruction::ApplyLayout(_) => ScreenContext::ApplyLayout, ScreenInstruction::ApplyLayout(_) => ScreenContext::ApplyLayout,
ScreenInstruction::NewTab(_) => ScreenContext::NewTab, ScreenInstruction::NewTab(_) => ScreenContext::NewTab,
@ -244,6 +246,7 @@ pub enum PluginContext {
Load, Load,
Draw, Draw,
Input, Input,
GlobalInput,
Unload, Unload,
Quit, Quit,
} }
@ -254,6 +257,7 @@ impl From<&PluginInstruction> for PluginContext {
PluginInstruction::Load(..) => PluginContext::Load, PluginInstruction::Load(..) => PluginContext::Load,
PluginInstruction::Draw(..) => PluginContext::Draw, PluginInstruction::Draw(..) => PluginContext::Draw,
PluginInstruction::Input(..) => PluginContext::Input, PluginInstruction::Input(..) => PluginContext::Input,
PluginInstruction::GlobalInput(_) => PluginContext::GlobalInput,
PluginInstruction::Unload(_) => PluginContext::Unload, PluginInstruction::Unload(_) => PluginContext::Unload,
PluginInstruction::Quit => PluginContext::Quit, PluginInstruction::Quit => PluginContext::Quit,
} }
@ -262,6 +266,8 @@ impl From<&PluginInstruction> for PluginContext {
#[derive(Debug, Clone, Copy, PartialEq)] #[derive(Debug, Clone, Copy, PartialEq)]
pub enum AppContext { pub enum AppContext {
GetState,
SetState,
Exit, Exit,
Error, Error,
} }
@ -269,6 +275,8 @@ pub enum AppContext {
impl From<&AppInstruction> for AppContext { impl From<&AppInstruction> for AppContext {
fn from(app_instruction: &AppInstruction) -> Self { fn from(app_instruction: &AppInstruction) -> Self {
match *app_instruction { match *app_instruction {
AppInstruction::GetState(_) => AppContext::GetState,
AppInstruction::SetState(_) => AppContext::SetState,
AppInstruction::Exit => AppContext::Exit, AppInstruction::Exit => AppContext::Exit,
AppInstruction::Error(_) => AppContext::Error, AppInstruction::Error(_) => AppContext::Error,
} }

View file

@ -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::pty_bus::PtyInstruction;
use crate::screen::ScreenInstruction; use crate::screen::ScreenInstruction;
use crate::CommandIsExecuting; use crate::CommandIsExecuting;
use crate::{errors::ContextType, wasm_vm::PluginInstruction};
use crate::{os_input_output::OsApi, update_state, AppState};
use crate::{AppInstruction, SenderWithContext, OPENCALLS}; use crate::{AppInstruction, SenderWithContext, OPENCALLS};
struct InputHandler { struct InputHandler {
@ -12,6 +11,7 @@ struct InputHandler {
command_is_executing: CommandIsExecuting, command_is_executing: CommandIsExecuting,
send_screen_instructions: SenderWithContext<ScreenInstruction>, send_screen_instructions: SenderWithContext<ScreenInstruction>,
send_pty_instructions: SenderWithContext<PtyInstruction>, send_pty_instructions: SenderWithContext<PtyInstruction>,
send_plugin_instructions: SenderWithContext<PluginInstruction>,
send_app_instructions: SenderWithContext<AppInstruction>, send_app_instructions: SenderWithContext<AppInstruction>,
} }
@ -21,6 +21,7 @@ impl InputHandler {
command_is_executing: CommandIsExecuting, command_is_executing: CommandIsExecuting,
send_screen_instructions: SenderWithContext<ScreenInstruction>, send_screen_instructions: SenderWithContext<ScreenInstruction>,
send_pty_instructions: SenderWithContext<PtyInstruction>, send_pty_instructions: SenderWithContext<PtyInstruction>,
send_plugin_instructions: SenderWithContext<PluginInstruction>,
send_app_instructions: SenderWithContext<AppInstruction>, send_app_instructions: SenderWithContext<AppInstruction>,
) -> Self { ) -> Self {
InputHandler { InputHandler {
@ -29,6 +30,7 @@ impl InputHandler {
command_is_executing, command_is_executing,
send_screen_instructions, send_screen_instructions,
send_pty_instructions, send_pty_instructions,
send_plugin_instructions,
send_app_instructions, send_app_instructions,
} }
} }
@ -38,9 +40,13 @@ impl InputHandler {
let mut err_ctx = OPENCALLS.with(|ctx| *ctx.borrow()); let mut err_ctx = OPENCALLS.with(|ctx| *ctx.borrow());
err_ctx.add_call(ContextType::StdinHandler); err_ctx.add_call(ContextType::StdinHandler);
self.send_pty_instructions.update(err_ctx); self.send_pty_instructions.update(err_ctx);
self.send_plugin_instructions.update(err_ctx);
self.send_app_instructions.update(err_ctx); self.send_app_instructions.update(err_ctx);
self.send_screen_instructions.update(err_ctx); self.send_screen_instructions.update(err_ctx);
loop { loop {
update_state(&self.send_app_instructions, |_| AppState {
input_mode: self.mode,
});
match self.mode { match self.mode {
InputMode::Normal => self.read_normal_mode(), InputMode::Normal => self.read_normal_mode(),
InputMode::Command => self.read_command_mode(false), InputMode::Command => self.read_command_mode(false),
@ -59,6 +65,11 @@ impl InputHandler {
loop { loop {
let stdin_buffer = self.os_input.read_from_stdin(); 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() { match stdin_buffer.as_slice() {
[7] => { [7] => {
// ctrl-g // ctrl-g
@ -88,6 +99,11 @@ impl InputHandler {
loop { loop {
let stdin_buffer = self.os_input.read_from_stdin(); 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 // 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)); // 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. // multiple commands. If we're already in persistent mode, it'll return us to normal mode.
match self.mode { match self.mode {
InputMode::Command => self.mode = InputMode::CommandPersistent, InputMode::Command => self.mode = InputMode::CommandPersistent,
InputMode::CommandPersistent => { InputMode::CommandPersistent => self.mode = InputMode::Normal,
self.mode = InputMode::Normal;
return;
}
_ => panic!(), _ => panic!(),
} }
return;
} }
[27] => { [27] => {
// Esc // Esc
@ -248,7 +262,7 @@ impl InputHandler {
self.command_is_executing.wait_until_pane_is_closed(); self.command_is_executing.wait_until_pane_is_closed();
} }
//@@@khs26 Write this to the powerbar? //@@@khs26 Write this to the powerbar?
_ => {} _ => continue,
} }
if self.mode == InputMode::Command { if self.mode == InputMode::Command {
@ -267,6 +281,9 @@ impl InputHandler {
self.send_pty_instructions self.send_pty_instructions
.send(PtyInstruction::Quit) .send(PtyInstruction::Quit)
.unwrap(); .unwrap();
self.send_plugin_instructions
.send(PluginInstruction::Quit)
.unwrap();
self.send_app_instructions self.send_app_instructions
.send(AppInstruction::Exit) .send(AppInstruction::Exit)
.unwrap(); .unwrap();
@ -282,7 +299,7 @@ impl InputHandler {
/// normal mode /// normal mode
/// - Exiting means that we should start the shutdown process for mosaic or the given /// - Exiting means that we should start the shutdown process for mosaic or the given
/// input handler /// input handler
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq, Clone, Copy)]
pub enum InputMode { pub enum InputMode {
Normal, Normal,
Command, Command,
@ -290,6 +307,36 @@ pub enum InputMode {
Exiting, 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 /// Entry point to the module that instantiates a new InputHandler and calls its
/// reading loop /// reading loop
pub fn input_loop( pub fn input_loop(
@ -297,6 +344,7 @@ pub fn input_loop(
command_is_executing: CommandIsExecuting, command_is_executing: CommandIsExecuting,
send_screen_instructions: SenderWithContext<ScreenInstruction>, send_screen_instructions: SenderWithContext<ScreenInstruction>,
send_pty_instructions: SenderWithContext<PtyInstruction>, send_pty_instructions: SenderWithContext<PtyInstruction>,
send_plugin_instructions: SenderWithContext<PluginInstruction>,
send_app_instructions: SenderWithContext<AppInstruction>, send_app_instructions: SenderWithContext<AppInstruction>,
) { ) {
let _handler = InputHandler::new( let _handler = InputHandler::new(
@ -304,6 +352,7 @@ pub fn input_loop(
command_is_executing, command_is_executing,
send_screen_instructions, send_screen_instructions,
send_pty_instructions, send_pty_instructions,
send_plugin_instructions,
send_app_instructions, send_app_instructions,
) )
.get_input(); .get_input();

View file

@ -1,3 +1,4 @@
use directories_next::ProjectDirs;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::{fs::File, io::prelude::*, path::PathBuf}; use std::{fs::File, io::prelude::*, path::PathBuf};
@ -5,58 +6,128 @@ use crate::panes::PositionAndSize;
fn split_space_to_parts_vertically( fn split_space_to_parts_vertically(
space_to_split: &PositionAndSize, space_to_split: &PositionAndSize,
percentages: Vec<u8>, sizes: Vec<Option<SplitSize>>,
) -> Vec<PositionAndSize> { ) -> Vec<PositionAndSize> {
let mut split_parts = vec![]; let mut split_parts = Vec::new();
let mut current_x_position = space_to_split.x; let mut current_x_position = space_to_split.x;
let width = space_to_split.columns - (percentages.len() - 1); // minus space for gaps let mut current_width = 0;
for percentage in percentages.iter() { let max_width = space_to_split.columns - (sizes.len() - 1); // minus space for gaps
let columns = (width as f32 * (*percentage as f32 / 100.0)) as usize; // TODO: round properly
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 { split_parts.push(PositionAndSize {
x: current_x_position, x: current_x_position,
y: space_to_split.y, y: space_to_split.y,
columns, columns,
rows: space_to_split.rows, rows: space_to_split.rows,
}); });
current_width += columns;
current_x_position += columns + 1; // 1 for gap current_x_position += columns + 1; // 1 for gap
} }
let total_width = split_parts
.iter() if current_width > max_width {
.fold(0, |total_width, part| total_width + part.columns); panic!("Layout contained too many columns to fit onto the screen!");
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_flexible_index = split_parts.len() - 1;
let mut last_part = split_parts.get_mut(last_part_index).unwrap(); if let Some(new_columns) = (max_width - current_width).checked_div(parts_to_grow.len()) {
last_part.columns += width - total_width; 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 split_parts
} }
fn split_space_to_parts_horizontally( fn split_space_to_parts_horizontally(
space_to_split: &PositionAndSize, space_to_split: &PositionAndSize,
percentages: Vec<u8>, sizes: Vec<Option<SplitSize>>,
) -> Vec<PositionAndSize> { ) -> Vec<PositionAndSize> {
let mut split_parts = vec![]; let mut split_parts = Vec::new();
let mut current_y_position = space_to_split.y; let mut current_y_position = space_to_split.y;
let height = space_to_split.rows - (percentages.len() - 1); // minus space for gaps let mut current_height = 0;
for percentage in percentages.iter() { let max_height = space_to_split.rows - (sizes.len() - 1); // minus space for gaps
let rows = (height as f32 * (*percentage as f32 / 100.0)) as usize; // TODO: round properly
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 { split_parts.push(PositionAndSize {
x: space_to_split.x, x: space_to_split.x,
y: current_y_position, y: current_y_position,
columns: space_to_split.columns, columns: space_to_split.columns,
rows, rows,
}); });
current_height += rows;
current_y_position += rows + 1; // 1 for gap current_y_position += rows + 1; // 1 for gap
} }
let total_height = split_parts
.iter() if current_height > max_height {
.fold(0, |total_height, part| total_height + part.rows); panic!("Layout contained too many rows to fit onto the screen!");
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_flexible_index = split_parts.len() - 1;
let mut last_part = split_parts.get_mut(last_part_index).unwrap(); if let Some(new_rows) = (max_height - current_height).checked_div(parts_to_grow.len()) {
last_part.rows += height - total_height; 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 split_parts
} }
@ -66,24 +137,11 @@ fn split_space(
layout: &Layout, layout: &Layout,
) -> Vec<(Layout, PositionAndSize)> { ) -> Vec<(Layout, PositionAndSize)> {
let mut pane_positions = Vec::new(); let mut pane_positions = Vec::new();
let percentages: Vec<u8> = layout let sizes: Vec<Option<SplitSize>> = layout.parts.iter().map(|part| part.split_size).collect();
.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 split_parts = match layout.direction { let split_parts = match layout.direction {
Direction::Vertical => split_space_to_parts_vertically(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, percentages), Direction::Horizontal => split_space_to_parts_horizontally(space_to_split, sizes),
}; };
for (i, part) in layout.parts.iter().enumerate() { for (i, part) in layout.parts.iter().enumerate() {
let part_position_and_size = split_parts.get(i).unwrap(); let part_position_and_size = split_parts.get(i).unwrap();
@ -97,43 +155,16 @@ fn split_space(
pane_positions 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)] #[derive(Debug, Serialize, Deserialize, Clone)]
pub enum Direction { pub enum Direction {
Horizontal, Horizontal,
Vertical, Vertical,
} }
#[derive(Debug, Serialize, Deserialize, Clone)] #[derive(Debug, Serialize, Deserialize, Clone, Copy)]
pub enum SplitSize { pub enum SplitSize {
Percent(u8), // 1 to 100 Percent(u8), // 1 to 100
Fixed(u16), // An absolute number of columns or rows
} }
#[derive(Debug, Serialize, Deserialize, Clone)] #[derive(Debug, Serialize, Deserialize, Clone)]
@ -149,7 +180,11 @@ pub struct Layout {
impl Layout { impl Layout {
pub fn new(layout_path: PathBuf) -> Self { 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) 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())); .unwrap_or_else(|_| panic!("cannot find layout {}", &layout_path.display()));
let mut layout = String::new(); let mut layout = String::new();
@ -158,17 +193,9 @@ impl Layout {
.unwrap_or_else(|_| panic!("could not read layout {}", &layout_path.display())); .unwrap_or_else(|_| panic!("could not read layout {}", &layout_path.display()));
let layout: Layout = serde_yaml::from_str(&layout) let layout: Layout = serde_yaml::from_str(&layout)
.unwrap_or_else(|_| panic!("could not parse layout {}", &layout_path.display())); .unwrap_or_else(|_| panic!("could not parse layout {}", &layout_path.display()));
layout.validate();
layout 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 { pub fn total_terminal_panes(&self) -> usize {
let mut total_panes = 0; let mut total_panes = 0;
total_panes += self.parts.len(); total_panes += self.parts.len();

View file

@ -16,14 +16,16 @@ mod utils;
mod wasm_vm; mod wasm_vm;
use std::cell::RefCell;
use std::collections::HashMap;
use std::io::Write; use std::io::Write;
use std::os::unix::net::UnixStream; use std::os::unix::net::UnixStream;
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::mpsc::{channel, sync_channel, Receiver, SendError, Sender, SyncSender}; use std::sync::mpsc::{channel, sync_channel, Receiver, SendError, Sender, SyncSender};
use std::thread; 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 panes::PaneId;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use structopt::StructOpt; 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) { pub fn update(&mut self, new_ctx: ErrorContext) {
self.err_ctx = new_ctx; 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)] #[derive(Clone)]
pub enum AppInstruction { pub enum AppInstruction {
GetState(Sender<AppState>),
SetState(AppState),
Exit, Exit,
Error(String), Error(String),
} }
pub fn start(mut os_input: Box<dyn OsApi>, opts: CliArgs) { pub fn start(mut os_input: Box<dyn OsApi>, opts: CliArgs) {
let mut app_state = AppState::default();
let mut active_threads = vec![]; let mut active_threads = vec![];
let command_is_executing = CommandIsExecuting::new(); let command_is_executing = CommandIsExecuting::new();
@ -168,7 +209,12 @@ pub fn start(mut os_input: Box<dyn OsApi>, opts: CliArgs) {
os_input.clone(), os_input.clone(),
opts.debug, 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))] #[cfg(not(test))]
std::panic::set_hook({ std::panic::set_hook({
@ -184,64 +230,57 @@ pub fn start(mut os_input: Box<dyn OsApi>, opts: CliArgs) {
.name("pty".to_string()) .name("pty".to_string())
.spawn({ .spawn({
let mut command_is_executing = command_is_executing.clone(); let mut command_is_executing = command_is_executing.clone();
move || { send_pty_instructions.send(PtyInstruction::NewTab).unwrap();
if let Some(layout) = maybe_layout { move || loop {
pty_bus.spawn_terminals_for_layout(layout); let (event, mut err_ctx) = pty_bus
} else { .receive_pty_instructions
let pid = pty_bus.spawn_terminal(None); .recv()
pty_bus .expect("failed to receive event on channel");
.send_screen_instructions err_ctx.add_call(ContextType::Pty(PtyContext::from(&event)));
.send(ScreenInstruction::NewTab(pid)) pty_bus.send_screen_instructions.update(err_ctx);
.unwrap(); match event {
} PtyInstruction::SpawnTerminal(file_to_open) => {
let pid = pty_bus.spawn_terminal(file_to_open);
loop { pty_bus
let (event, mut err_ctx) = pty_bus .send_screen_instructions
.receive_pty_instructions .send(ScreenInstruction::NewPane(PaneId::Terminal(pid)))
.recv() .unwrap();
.expect("failed to receive event on channel"); }
err_ctx.add_call(ContextType::Pty(PtyContext::from(&event))); PtyInstruction::SpawnTerminalVertically(file_to_open) => {
pty_bus.send_screen_instructions.update(err_ctx); let pid = pty_bus.spawn_terminal(file_to_open);
match event { pty_bus
PtyInstruction::SpawnTerminal(file_to_open) => { .send_screen_instructions
let pid = pty_bus.spawn_terminal(file_to_open); .send(ScreenInstruction::VerticalSplit(PaneId::Terminal(pid)))
pty_bus .unwrap();
.send_screen_instructions }
.send(ScreenInstruction::NewPane(PaneId::Terminal(pid))) PtyInstruction::SpawnTerminalHorizontally(file_to_open) => {
.unwrap(); let pid = pty_bus.spawn_terminal(file_to_open);
} pty_bus
PtyInstruction::SpawnTerminalVertically(file_to_open) => { .send_screen_instructions
let pid = pty_bus.spawn_terminal(file_to_open); .send(ScreenInstruction::HorizontalSplit(PaneId::Terminal(pid)))
pty_bus .unwrap();
.send_screen_instructions }
.send(ScreenInstruction::VerticalSplit(PaneId::Terminal(pid))) PtyInstruction::NewTab => {
.unwrap(); if let Some(layout) = maybe_layout.clone() {
} pty_bus.spawn_terminals_for_layout(layout);
PtyInstruction::SpawnTerminalHorizontally(file_to_open) => { } else {
let pid = pty_bus.spawn_terminal(file_to_open);
pty_bus
.send_screen_instructions
.send(ScreenInstruction::HorizontalSplit(PaneId::Terminal(pid)))
.unwrap();
}
PtyInstruction::NewTab => {
let pid = pty_bus.spawn_terminal(None); let pid = pty_bus.spawn_terminal(None);
pty_bus pty_bus
.send_screen_instructions .send_screen_instructions
.send(ScreenInstruction::NewTab(pid)) .send(ScreenInstruction::NewTab(pid))
.unwrap(); .unwrap();
} }
PtyInstruction::ClosePane(id) => { }
pty_bus.close_pane(id); PtyInstruction::ClosePane(id) => {
command_is_executing.done_closing_pane(); pty_bus.close_pane(id);
} command_is_executing.done_closing_pane();
PtyInstruction::CloseTab(ids) => { }
pty_bus.close_tab(ids); PtyInstruction::CloseTab(ids) => {
command_is_executing.done_closing_pane(); pty_bus.close_tab(ids);
} command_is_executing.done_closing_pane();
PtyInstruction::Quit => { }
break; 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.get_active_tab_mut().unwrap().close_focused_pane();
screen.render(); 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) => { ScreenInstruction::ClosePane(id) => {
screen.get_active_tab_mut().unwrap().close_pane(id); screen.get_active_tab_mut().unwrap().close_pane(id);
screen.render(); screen.render();
@ -373,7 +420,8 @@ pub fn start(mut os_input: Box<dyn OsApi>, opts: CliArgs) {
ScreenInstruction::SwitchTabPrev => screen.switch_tab_prev(), ScreenInstruction::SwitchTabPrev => screen.switch_tab_prev(),
ScreenInstruction::CloseTab => screen.close_tab(), ScreenInstruction::CloseTab => screen.close_tab(),
ScreenInstruction::ApplyLayout((layout, new_pane_pids)) => { 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 => { ScreenInstruction::Quit => {
break; break;
@ -391,94 +439,124 @@ pub fn start(mut os_input: Box<dyn OsApi>, opts: CliArgs) {
.spawn({ .spawn({
let mut send_pty_instructions = send_pty_instructions.clone(); let mut send_pty_instructions = send_pty_instructions.clone();
let mut send_screen_instructions = send_screen_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; move || loop {
let mut plugin_map = HashMap::new(); 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 { // FIXME: Cache this compiled module on disk. I could use `(de)serialize_to_file()` for that
let (event, mut err_ctx) = receive_plugin_instructions let module = Module::new(&store, &wasm_bytes).unwrap();
.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();
let output = Pipe::new(); let output = Pipe::new();
let input = Pipe::new(); let input = Pipe::new();
let mut wasi_env = WasiState::new("mosaic") let mut wasi_env = WasiState::new("mosaic")
.env("CLICOLOR_FORCE", "1") .env("CLICOLOR_FORCE", "1")
.preopen(|p| { .preopen(|p| {
p.directory(".") // FIXME: Change this to a more meaningful dir p.directory(".") // FIXME: Change this to a more meaningful dir
.alias(".") .alias(".")
.read(true) .read(true)
.write(true) .write(true)
.create(true) .create(true)
}) })
.unwrap() .unwrap()
.stdin(Box::new(input)) .stdin(Box::new(input))
.stdout(Box::new(output)) .stdout(Box::new(output))
.finalize() .finalize()
.unwrap(); .unwrap();
let wasi = wasi_env.import_object(&module).unwrap(); let wasi = wasi_env.import_object(&module).unwrap();
let plugin_env = PluginEnv { let plugin_env = PluginEnv {
send_pty_instructions: send_pty_instructions.clone(), plugin_id,
wasi_env, 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 mosaic = mosaic_imports(&store, &plugin_env);
let instance = let instance =
Instance::new(&module, &mosaic.chain_back(wasi)).unwrap(); 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 // This eventually calls the `.init()` method
start.call(&[]).unwrap(); start.call(&[]).unwrap();
plugin_map.insert(plugin_id, (instance, plugin_env)); plugin_map.insert(plugin_id, (instance, plugin_env));
pid_tx.send(plugin_id).unwrap(); pid_tx.send(plugin_id).unwrap();
plugin_id += 1; 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(); drop(send_screen_instructions.send(ScreenInstruction::Render));
}
draw.call(&[Value::I32(rows as i32), Value::I32(cols as i32)]) PluginInstruction::GlobalInput(input_bytes) => {
.unwrap(); // FIXME: Set up an event subscription system, and timed callbacks
for (instance, plugin_env) in plugin_map.values() {
buf_tx.send(wasi_stdout(&plugin_env.wasi_env)).unwrap(); let handler =
} instance.exports.get_function("handle_global_key").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();
for key in input_bytes.keys() { for key in input_bytes.keys() {
if let Ok(key) = key { if let Ok(key) = key {
wasi_write_string( wasi_write_string(
&plugin_env.wasi_env, &plugin_env.wasi_env,
&serde_json::to_string(&key).unwrap(), &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({ .spawn({
let send_screen_instructions = send_screen_instructions.clone(); let send_screen_instructions = send_screen_instructions.clone();
let send_pty_instructions = send_pty_instructions.clone(); let send_pty_instructions = send_pty_instructions.clone();
let send_plugin_instructions = send_plugin_instructions.clone();
let os_input = os_input.clone(); let os_input = os_input.clone();
move || { move || {
input_loop( input_loop(
@ -558,6 +637,7 @@ pub fn start(mut os_input: Box<dyn OsApi>, opts: CliArgs) {
command_is_executing, command_is_executing,
send_screen_instructions, send_screen_instructions,
send_pty_instructions, send_pty_instructions,
send_plugin_instructions,
send_app_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_screen_instructions.update(err_ctx);
send_pty_instructions.update(err_ctx); send_pty_instructions.update(err_ctx);
match app_instruction { match app_instruction {
AppInstruction::GetState(state_tx) => drop(state_tx.send(app_state.clone())),
AppInstruction::SetState(state) => app_state = state,
AppInstruction::Exit => { AppInstruction::Exit => {
let _ = send_screen_instructions.send(ScreenInstruction::Quit); let _ = send_screen_instructions.send(ScreenInstruction::Quit);
let _ = send_pty_instructions.send(PtyInstruction::Quit); let _ = send_pty_instructions.send(PtyInstruction::Quit);
let _ = send_plugin_instructions.send(PluginInstruction::Quit); let _ = send_plugin_instructions.send(PluginInstruction::Quit);
break; break;
} }
AppInstruction::Error(backtrace) => { AppInstruction::Error(backtrace) => {
let _ = send_screen_instructions.send(ScreenInstruction::Quit); let _ = send_screen_instructions.send(ScreenInstruction::Quit);
let _ = send_pty_instructions.send(PtyInstruction::Quit); let _ = send_pty_instructions.send(PtyInstruction::Quit);
let _ = send_plugin_instructions.send(PluginInstruction::Quit); let _ = send_plugin_instructions.send(PluginInstruction::Quit);
os_input.unset_raw_mode(0); os_input.unset_raw_mode(0);
let goto_start_of_last_line = format!("\u{1b}[{};{}H", full_screen_ws.rows, 1); 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 { for thread_handler in active_threads {
thread_handler.join().unwrap(); thread_handler.join().unwrap();
} }
// cleanup(); // cleanup();
let reset_style = "\u{1b}[m"; let reset_style = "\u{1b}[m";
let show_cursor = "\u{1b}[?25h"; let show_cursor = "\u{1b}[?25h";

View file

@ -1,4 +1,7 @@
use std::fmt::{self, Debug, Formatter}; use std::{
cmp::Ordering,
fmt::{self, Debug, Formatter},
};
use crate::panes::terminal_character::{ use crate::panes::terminal_character::{
CharacterStyles, TerminalCharacter, EMPTY_TERMINAL_CHARACTER, CharacterStyles, TerminalCharacter, EMPTY_TERMINAL_CHARACTER,
@ -97,7 +100,7 @@ fn transfer_rows_up(
let mut next_lines: Vec<Row> = vec![]; let mut next_lines: Vec<Row> = vec![];
for _ in 0..count { for _ in 0..count {
if next_lines.is_empty() { if next_lines.is_empty() {
if source.len() > 0 { if !source.is_empty() {
let next_line = source.remove(0); let next_line = source.remove(0);
if !next_line.is_canonical { if !next_line.is_canonical {
let mut bottom_canonical_row_and_wraps_in_dst = let mut bottom_canonical_row_and_wraps_in_dst =
@ -214,7 +217,7 @@ impl Grid {
y_coordinates y_coordinates
} }
pub fn scroll_up_one_line(&mut self) { 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(); let line_to_push_down = self.viewport.pop().unwrap();
self.lines_below.insert(0, line_to_push_down); self.lines_below.insert(0, line_to_push_down);
let line_to_insert_at_viewport_top = self.lines_above.pop().unwrap(); 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) { 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); let mut line_to_push_up = self.viewport.remove(0);
if line_to_push_up.is_canonical { if line_to_push_up.is_canonical {
self.lines_above.push(line_to_push_up); self.lines_above.push(line_to_push_up);
@ -243,7 +246,7 @@ impl Grid {
for mut row in self.viewport.drain(..) { for mut row in self.viewport.drain(..) {
if !row.is_canonical if !row.is_canonical
&& viewport_canonical_lines.is_empty() && 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(); let mut first_line_above = self.lines_above.pop().unwrap();
first_line_above.append(&mut row.columns); first_line_above.append(&mut row.columns);
@ -269,7 +272,7 @@ impl Grid {
let mut new_viewport_rows = vec![]; let mut new_viewport_rows = vec![];
for mut canonical_line in viewport_canonical_lines { for mut canonical_line in viewport_canonical_lines {
let mut canonical_line_parts: Vec<Row> = vec![]; 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 { let next_wrap = if canonical_line.len() > new_columns {
canonical_line.columns.drain(..new_columns) canonical_line.columns.drain(..new_columns)
} else { } else {
@ -279,7 +282,7 @@ impl Grid {
// if there are no more parts, this row is canonical as long as it originall // 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 // 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) // 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() row.canonical()
} else { } else {
row row
@ -294,62 +297,70 @@ impl Grid {
let new_cursor_x = (cursor_index_in_canonical_line / new_columns) let new_cursor_x = (cursor_index_in_canonical_line / new_columns)
+ (cursor_index_in_canonical_line % new_columns); + (cursor_index_in_canonical_line % new_columns);
let current_viewport_row_count = self.viewport.len(); let current_viewport_row_count = self.viewport.len();
if current_viewport_row_count < self.height { match current_viewport_row_count.cmp(&self.height) {
let row_count_to_transfer = self.height - current_viewport_row_count; Ordering::Less => {
transfer_rows_down( let row_count_to_transfer = self.height - current_viewport_row_count;
&mut self.lines_above, transfer_rows_down(
&mut self.viewport, &mut self.lines_above,
row_count_to_transfer, &mut self.viewport,
None, row_count_to_transfer,
Some(new_columns), None,
); Some(new_columns),
let rows_pulled = self.viewport.len() - current_viewport_row_count; );
new_cursor_y += rows_pulled; let rows_pulled = self.viewport.len() - current_viewport_row_count;
} else if current_viewport_row_count > self.height { new_cursor_y += rows_pulled;
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( Ordering::Greater => {
&mut self.viewport, let row_count_to_transfer = current_viewport_row_count - self.height;
&mut self.lines_above, if row_count_to_transfer > new_cursor_y {
row_count_to_transfer, new_cursor_y = 0;
Some(new_columns), } else {
None, 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.y = new_cursor_y;
self.cursor.x = new_cursor_x; self.cursor.x = new_cursor_x;
} }
if new_rows != self.height { if new_rows != self.height {
let current_viewport_row_count = self.viewport.len(); let current_viewport_row_count = self.viewport.len();
if current_viewport_row_count < new_rows { match current_viewport_row_count.cmp(&new_rows) {
let row_count_to_transfer = new_rows - current_viewport_row_count; Ordering::Less => {
transfer_rows_down( let row_count_to_transfer = new_rows - current_viewport_row_count;
&mut self.lines_above, transfer_rows_down(
&mut self.viewport, &mut self.lines_above,
row_count_to_transfer, &mut self.viewport,
None, row_count_to_transfer,
Some(new_columns), None,
); Some(new_columns),
let rows_pulled = self.viewport.len() - current_viewport_row_count; );
self.cursor.y += rows_pulled; let rows_pulled = self.viewport.len() - current_viewport_row_count;
} else if current_viewport_row_count > new_rows { self.cursor.y += rows_pulled;
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( Ordering::Greater => {
&mut self.viewport, let row_count_to_transfer = current_viewport_row_count - new_rows;
&mut self.lines_above, if row_count_to_transfer > self.cursor.y {
row_count_to_transfer, self.cursor.y = 0;
Some(new_columns), } else {
None, 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; self.height = new_rows;
@ -364,10 +375,8 @@ impl Grid {
.iter() .iter()
.map(|r| { .map(|r| {
let mut line: Vec<TerminalCharacter> = r.columns.iter().copied().collect(); let mut line: Vec<TerminalCharacter> = r.columns.iter().copied().collect();
for _ in line.len()..self.width { // pad line
// pad line line.resize(self.width, EMPTY_TERMINAL_CHARACTER);
line.push(EMPTY_TERMINAL_CHARACTER);
}
line line
}) })
.collect(); .collect();
@ -719,17 +728,17 @@ impl Row {
self self
} }
pub fn add_character_at(&mut self, terminal_character: TerminalCharacter, x: usize) { pub fn add_character_at(&mut self, terminal_character: TerminalCharacter, x: usize) {
if x == self.columns.len() { match self.columns.len().cmp(&x) {
self.columns.push(terminal_character); Ordering::Equal => self.columns.push(terminal_character),
} else if x > self.columns.len() { Ordering::Less => {
for _ in self.columns.len()..x { self.columns.resize(x, EMPTY_TERMINAL_CHARACTER);
self.columns.push(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) { pub fn replace_character_at(&mut self, terminal_character: TerminalCharacter, x: usize) {
@ -767,10 +776,10 @@ impl Row {
} }
current_part.push(character); current_part.push(character);
} }
if current_part.len() > 0 { if !current_part.is_empty() {
parts.push(Row::from_columns(current_part)) 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.get_mut(0).unwrap().is_canonical = true;
} }
parts parts

View file

@ -9,6 +9,7 @@ use crate::panes::{PaneId, PositionAndSize};
pub struct PluginPane { pub struct PluginPane {
pub pid: u32, pub pid: u32,
pub should_render: bool, pub should_render: bool,
pub selectable: bool,
pub position_and_size: PositionAndSize, pub position_and_size: PositionAndSize,
pub position_and_size_override: Option<PositionAndSize>, pub position_and_size_override: Option<PositionAndSize>,
pub send_plugin_instructions: SenderWithContext<PluginInstruction>, pub send_plugin_instructions: SenderWithContext<PluginInstruction>,
@ -23,6 +24,7 @@ impl PluginPane {
Self { Self {
pid, pid,
should_render: true, should_render: true,
selectable: true,
position_and_size, position_and_size,
position_and_size_override: None, position_and_size_override: None,
send_plugin_instructions, send_plugin_instructions,
@ -92,6 +94,12 @@ impl Pane for PluginPane {
fn set_should_render(&mut self, should_render: bool) { fn set_should_render(&mut self, should_render: bool) {
self.should_render = should_render; 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> { fn render(&mut self) -> Option<String> {
// if self.should_render { // if self.should_render {
if true { if true {

View file

@ -4,10 +4,9 @@ use crate::tab::Pane;
use ::nix::pty::Winsize; use ::nix::pty::Winsize;
use ::std::os::unix::io::RawFd; use ::std::os::unix::io::RawFd;
use ::vte::Perform; use ::vte::Perform;
use std::fmt::{self, Debug, Formatter}; use std::fmt::Debug;
use crate::boundaries::Rect; use crate::panes::grid::Grid;
use crate::panes::grid::{Grid, Row};
use crate::panes::terminal_character::{ use crate::panes::terminal_character::{
CharacterStyles, TerminalCharacter, EMPTY_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 alternative_grid: Option<Grid>, // for 1049h/l instructions which tell us to switch between these two
pub pid: RawFd, pub pid: RawFd,
pub should_render: bool, pub should_render: bool,
pub selectable: bool,
pub position_and_size: PositionAndSize, pub position_and_size: PositionAndSize,
pub position_and_size_override: Option<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. "") 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) { fn set_should_render(&mut self, should_render: bool) {
self.should_render = should_render; 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> { fn render(&mut self) -> Option<String> {
// if self.should_render { // if self.should_render {
if true { if true {
@ -280,6 +286,7 @@ impl TerminalPane {
grid, grid,
alternative_grid: None, alternative_grid: None,
should_render: true, should_render: true,
selectable: true,
pending_styles, pending_styles,
position_and_size, position_and_size,
position_and_size_override: None, position_and_size_override: None,
@ -330,14 +337,6 @@ impl TerminalPane {
// (x, y) // (x, y)
self.grid.cursor_coordinates() 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) { pub fn rotate_scroll_region_up(&mut self, count: usize) {
self.grid.rotate_scroll_region_up(count); self.grid.rotate_scroll_region_up(count);
self.mark_for_rerender(); self.mark_for_rerender();
@ -346,68 +345,6 @@ impl TerminalPane {
self.grid.rotate_scroll_region_down(count); self.grid.rotate_scroll_region_down(count);
self.mark_for_rerender(); 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) { fn add_newline(&mut self) {
self.grid.add_canonical_line(); self.grid.add_canonical_line();
// self.reset_all_ansi_codes(); // TODO: find out if we should be resetting here or not // 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) { fn esc_dispatch(&mut self, intermediates: &[u8], _ignore: bool, byte: u8) {
match (byte, intermediates.get(0)) { if let (b'M', None) = (byte, intermediates.get(0)) {
(b'M', None) => { self.grid.move_cursor_up_with_scrolling(1);
self.grid.move_cursor_up_with_scrolling(1);
}
_ => {}
} }
} }
} }

View file

@ -299,13 +299,15 @@ impl PtyBus {
let child_pid = self.id_to_child_pid.get(&id).unwrap(); let child_pid = self.id_to_child_pid.get(&id).unwrap();
self.os_input.kill(*child_pid).unwrap(); self.os_input.kill(*child_pid).unwrap();
} }
PaneId::Plugin(pid) => self PaneId::Plugin(pid) => drop(
.send_plugin_instructions self.send_plugin_instructions
.send(PluginInstruction::Unload(pid)) .send(PluginInstruction::Unload(pid)),
.unwrap(), ),
} }
} }
pub fn close_tab(&mut self, ids: Vec<PaneId>) { 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);
});
} }
} }

View file

@ -42,6 +42,7 @@ pub enum ScreenInstruction {
ClearScroll, ClearScroll,
CloseFocusedPane, CloseFocusedPane,
ToggleActiveTerminalFullscreen, ToggleActiveTerminalFullscreen,
SetSelectable(PaneId, bool),
ClosePane(PaneId), ClosePane(PaneId),
ApplyLayout((Layout, Vec<RawFd>)), ApplyLayout((Layout, Vec<RawFd>)),
NewTab(RawFd), NewTab(RawFd),
@ -138,7 +139,7 @@ impl Screen {
if self.tabs.len() > 1 { if self.tabs.len() > 1 {
self.switch_tab_prev(); 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(); let pane_ids = active_tab.get_pane_ids();
self.send_pty_instructions self.send_pty_instructions
.send(PtyInstruction::CloseTab(pane_ids)) .send(PtyInstruction::CloseTab(pane_ids))

View file

@ -66,6 +66,7 @@ pub struct Tab {
pub send_app_instructions: SenderWithContext<AppInstruction>, 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 { pub trait Pane {
fn x(&self) -> usize; fn x(&self) -> usize;
fn y(&self) -> usize; fn y(&self) -> usize;
@ -81,6 +82,8 @@ pub trait Pane {
fn position_and_size_override(&self) -> Option<PositionAndSize>; fn position_and_size_override(&self) -> Option<PositionAndSize>;
fn should_render(&self) -> bool; fn should_render(&self) -> bool;
fn set_should_render(&mut self, should_render: 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 render(&mut self) -> Option<String>;
fn pid(&self) -> PaneId; fn pid(&self) -> PaneId;
fn reduce_height_down(&mut self, count: usize); 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>)> { fn get_panes(&self) -> impl Iterator<Item = (&PaneId, &Box<dyn Pane>)> {
self.panes.iter() 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 { fn has_panes(&self) -> bool {
let mut all_terminals = self.get_panes(); let mut all_terminals = self.get_panes();
all_terminals.next().is_some() 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>> { fn pane_ids_directly_left_of(&self, id: &PaneId) -> Option<Vec<PaneId>> {
let mut ids = vec![]; let mut ids = vec![];
let terminal_to_check = self.panes.get(id).unwrap(); let terminal_to_check = self.panes.get(id).unwrap();
@ -1421,14 +1438,14 @@ impl Tab {
} }
} }
pub fn move_focus(&mut self) { pub fn move_focus(&mut self) {
if !self.has_panes() { if !self.has_selectable_panes() {
return; return;
} }
if self.fullscreen_is_active { if self.fullscreen_is_active {
return; return;
} }
let active_terminal_id = self.get_active_pane_id().unwrap(); 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 first_terminal = terminal_ids.get(0).unwrap();
let active_terminal_id_position = terminal_ids let active_terminal_id_position = terminal_ids
.iter() .iter()
@ -1442,7 +1459,7 @@ impl Tab {
self.render(); self.render();
} }
pub fn move_focus_left(&mut self) { pub fn move_focus_left(&mut self) {
if !self.has_panes() { if !self.has_selectable_panes() {
return; return;
} }
if self.fullscreen_is_active { if self.fullscreen_is_active {
@ -1450,7 +1467,7 @@ impl Tab {
} }
let active_terminal = self.get_active_pane(); let active_terminal = self.get_active_pane();
if let Some(active) = active_terminal { if let Some(active) = active_terminal {
let terminals = self.get_panes(); let terminals = self.get_selectable_panes();
let next_index = terminals let next_index = terminals
.enumerate() .enumerate()
.filter(|(_, (_, c))| { .filter(|(_, (_, c))| {
@ -1472,7 +1489,7 @@ impl Tab {
self.render(); self.render();
} }
pub fn move_focus_down(&mut self) { pub fn move_focus_down(&mut self) {
if !self.has_panes() { if !self.has_selectable_panes() {
return; return;
} }
if self.fullscreen_is_active { if self.fullscreen_is_active {
@ -1480,7 +1497,7 @@ impl Tab {
} }
let active_terminal = self.get_active_pane(); let active_terminal = self.get_active_pane();
if let Some(active) = active_terminal { if let Some(active) = active_terminal {
let terminals = self.get_panes(); let terminals = self.get_selectable_panes();
let next_index = terminals let next_index = terminals
.enumerate() .enumerate()
.filter(|(_, (_, c))| { .filter(|(_, (_, c))| {
@ -1502,7 +1519,7 @@ impl Tab {
self.render(); self.render();
} }
pub fn move_focus_up(&mut self) { pub fn move_focus_up(&mut self) {
if !self.has_panes() { if !self.has_selectable_panes() {
return; return;
} }
if self.fullscreen_is_active { if self.fullscreen_is_active {
@ -1510,7 +1527,7 @@ impl Tab {
} }
let active_terminal = self.get_active_pane(); let active_terminal = self.get_active_pane();
if let Some(active) = active_terminal { if let Some(active) = active_terminal {
let terminals = self.get_panes(); let terminals = self.get_selectable_panes();
let next_index = terminals let next_index = terminals
.enumerate() .enumerate()
.filter(|(_, (_, c))| { .filter(|(_, (_, c))| {
@ -1532,7 +1549,7 @@ impl Tab {
self.render(); self.render();
} }
pub fn move_focus_right(&mut self) { pub fn move_focus_right(&mut self) {
if !self.has_panes() { if !self.has_selectable_panes() {
return; return;
} }
if self.fullscreen_is_active { if self.fullscreen_is_active {
@ -1540,7 +1557,7 @@ impl Tab {
} }
let active_terminal = self.get_active_pane(); let active_terminal = self.get_active_pane();
if let Some(active) = active_terminal { if let Some(active) = active_terminal {
let terminals = self.get_panes(); let terminals = self.get_selectable_panes();
let next_index = terminals let next_index = terminals
.enumerate() .enumerate()
.filter(|(_, (_, c))| { .filter(|(_, (_, c))| {
@ -1578,7 +1595,7 @@ impl Tab {
}) })
} }
fn panes_to_the_left_between_aligning_borders(&self, id: PaneId) -> Option<Vec<PaneId>> { 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 upper_close_border = terminal.y();
let lower_close_border = terminal.y() + terminal.rows() + 1; let lower_close_border = terminal.y() + terminal.rows() + 1;
@ -1601,7 +1618,7 @@ impl Tab {
None None
} }
fn panes_to_the_right_between_aligning_borders(&self, id: PaneId) -> Option<Vec<PaneId>> { 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 upper_close_border = terminal.y();
let lower_close_border = terminal.y() + terminal.rows() + 1; let lower_close_border = terminal.y() + terminal.rows() + 1;
@ -1625,7 +1642,7 @@ impl Tab {
None None
} }
fn panes_above_between_aligning_borders(&self, id: PaneId) -> Option<Vec<PaneId>> { 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 left_close_border = terminal.x();
let right_close_border = terminal.x() + terminal.columns() + 1; let right_close_border = terminal.x() + terminal.columns() + 1;
@ -1647,8 +1664,8 @@ impl Tab {
} }
None None
} }
fn terminals_below_between_aligning_borders(&self, id: PaneId) -> Option<Vec<PaneId>> { fn panes_below_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 left_close_border = terminal.x();
let right_close_border = terminal.x() + terminal.columns() + 1; 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() 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) { pub fn close_pane(&mut self, id: PaneId) {
if self.panes.get(&id).is_some() { if self.panes.get(&id).is_some() {
self.close_pane_without_rerender(id); self.close_pane_without_rerender(id);
@ -1699,7 +1724,7 @@ impl Tab {
// 1 for the border // 1 for the border
} }
if self.active_terminal == Some(id) { 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) { } else if let Some(terminals) = self.panes_to_the_right_between_aligning_borders(id) {
for terminal_id in terminals.iter() { for terminal_id in terminals.iter() {
@ -1707,7 +1732,7 @@ impl Tab {
// 1 for the border // 1 for the border
} }
if self.active_terminal == Some(id) { 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) { } else if let Some(terminals) = self.panes_above_between_aligning_borders(id) {
for terminal_id in terminals.iter() { for terminal_id in terminals.iter() {
@ -1715,21 +1740,21 @@ impl Tab {
// 1 for the border // 1 for the border
} }
if self.active_terminal == Some(id) { 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() { for terminal_id in terminals.iter() {
self.increase_pane_height_up(&terminal_id, terminal_to_close_height + 1); self.increase_pane_height_up(&terminal_id, terminal_to_close_height + 1);
// 1 for the border // 1 for the border
} }
if self.active_terminal == Some(id) { if self.active_terminal == Some(id) {
self.active_terminal = Some(*terminals.last().unwrap()); self.active_terminal = self.next_active_pane(terminals);
} }
} else { } else {
} }
self.panes.remove(&id); self.panes.remove(&id);
if !self.has_panes() { if self.active_terminal.is_none() {
self.active_terminal = None; self.active_terminal = self.next_active_pane(self.get_pane_ids());
} }
} }
} }

View file

@ -50,43 +50,3 @@ pub fn accepts_basic_layout() {
assert_snapshot!(next_to_last_snapshot); assert_snapshot!(next_to_last_snapshot);
assert_snapshot!(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);
}

View file

@ -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::{imports, Function, ImportObject, Store, WasmerEnv};
use wasmer_wasi::WasiEnv; 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)] #[derive(Clone, Debug)]
pub enum PluginInstruction { pub enum PluginInstruction {
Load(Sender<u32>, PathBuf), Load(Sender<u32>, PathBuf),
Draw(Sender<String>, u32, usize, usize), // String buffer, plugin id, rows, cols Draw(Sender<String>, u32, usize, usize), // String buffer, plugin id, rows, cols
Input(u32, Vec<u8>), // plugin id, input bytes Input(u32, Vec<u8>), // plugin id, input bytes
GlobalInput(Vec<u8>), // input bytes
Unload(u32), Unload(u32),
Quit, Quit,
} }
#[derive(WasmerEnv, Clone)] #[derive(WasmerEnv, Clone)]
pub struct PluginEnv { 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 send_pty_instructions: SenderWithContext<PtyInstruction>, // FIXME: This should be a big bundle of all of the channels
pub wasi_env: WasiEnv, pub wasi_env: WasiEnv,
} }
@ -24,7 +34,9 @@ pub struct PluginEnv {
pub fn mosaic_imports(store: &Store, plugin_env: &PluginEnv) -> ImportObject { pub fn mosaic_imports(store: &Store, plugin_env: &PluginEnv) -> ImportObject {
imports! { imports! {
"mosaic" => { "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(); .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 --------------------------------------------------------------------------------------------------- // Helper Functions ---------------------------------------------------------------------------------------------------
// FIXME: Unwrap city // FIXME: Unwrap city