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"
|
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",
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -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]
|
||||||
|
|
|
||||||
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
|
- 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
|
||||||
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;
|
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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
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::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();
|
||||||
|
|
|
||||||
185
src/layout.rs
185
src/layout.rs
|
|
@ -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();
|
||||||
|
|
|
||||||
329
src/main.rs
329
src/main.rs
|
|
@ -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";
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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. "[D")
|
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) {
|
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);
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
|
|
|
||||||
71
src/tab.rs
71
src/tab.rs
|
|
@ -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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue