fixup! Merge branch 'main' of https://github.com/zellij-org/zellij into simple-font-ui

This commit is contained in:
a-kenji 2021-05-10 20:54:39 +02:00
commit 85a4e476f2
27 changed files with 1216 additions and 949 deletions

View File

@ -3,27 +3,27 @@ name: "\U0001F41B Bug Report"
about: "If something isn't working as expected."
labels: bug
---
Thank you for taking the time to file an issue!
You can erase any parts of this template not applicable to your issue.
Thank you for taking the time to file this issue! Please follow the instructions and fill the missing parts below the instructions, if it is meaningful. Try to be brief and concise.
## In Case of Graphical, or Performance Issues
**In Case of Graphical or Performance Issues**
Please:
1. Delete the contents of `/tmp/zellij-1000/zellij-log`.
2. Run `zellij --debug` and then recreate your issue.
1. Delete the contents of `/tmp/zellij-1000/zellij-log`, ie with `cd /tmp/zellij-1000/` and `rm -fr zellij-log/`
2. Run `zellij --debug`
3. Recreate your issue.
3. Quit Zellij immediately with ctrl-q (your bug should ideally still be visible on screen)
Please attach the files that were created in
Please attach the files that were created in `/tmp/zellij-1000/zellij-log/` to the extent you are comfortable with.
`/tmp/zellij-1000/zellij-log/`
To the extent you are comfortable with.
Also please add the size in columns/lines of the terminal in which the bug happened. You can usually find these out with `tput lines` and `tput cols`.
And the name and version of progams you interacted with as well as
the operating system.
## Information
**Basic information**
`zellij --version`:
`tput lines`:
`tput cols`:
`uname -av` or `ver`(Windows):
List of programs you interact with as, `PROGRAM --version`: output cropped meaningful, for example:
`nvim --version`: NVIM v0.5.0-dev+1299-g1c2e504d5 (used the appimage release)
`alacritty --version`: alacritty 0.7.2 (5ac8060b)
**Further information**
Reproduction steps, noticable behavior, related issues etc

View File

@ -9,6 +9,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
* Terminal compatibility: fix support for CSI subparameters (https://github.com/zellij-org/zellij/pull/469)
* Move the sync command to tab mode (https://github.com/zellij-org/zellij/pull/412)
* Add support for requesting a simpler layout from plugins, move `clean` flag from `options` to `setup` (https://github.com/zellij-org/zellij/pull/479)
* Fix exit code of `dump-default-config` (https://github.com/zellij-org/zellij/pull/480)
* Feature: Switch tabs using `Alt + h/l` in normal mode if there are no panes in the direction (https://github.com/zellij-org/zellij/pull/471)
## [0.8.0] - 2021-05-07
* Terminal compatibility: pass vttest 8 (https://github.com/zellij-org/zellij/pull/461)

View File

@ -16,9 +16,9 @@ keybinds:
key: [Ctrl: 'q',]
- action: [NewPane: ]
key: [ Alt: 'n',]
- action: [MoveFocus: Left,]
- action: [MoveFocusOrTab: Left,]
key: [ Alt: 'h',]
- action: [MoveFocus: Right,]
- action: [MoveFocusOrTab: Right,]
key: [ Alt: 'l',]
- action: [MoveFocus: Down,]
key: [ Alt: 'j',]

View File

@ -37,5 +37,5 @@ The Boundaries refer to those lines that are drawn between terminal panes. A few
* The Rect trait is here so that different panes can implement it, giving boundaries a generic way to calculate the size of the pane and draw boundaries around it.
* Here we use the [unicode box drawing characters](https://en.wikipedia.org/wiki/Box-drawing_character) in order to draw the borders. There's some logic here about combining them together for all possible combinations of pane locations.
## PTY Bus (src/pty_bus.rs)
## PTY Bus (src/pty.rs)
The PtyBus keeps track of several asynchronous streams that read from pty sockets (eg. /dev/pts/999), parse those bytes into ANSI/VT events and send them on to the Screen so that they can be received in the relevant TerminalPane.

View File

@ -17,9 +17,9 @@ keybinds:
key: [Ctrl: 'q',]
- action: [NewPane: ]
key: [ Alt: 'n',]
- action: [MoveFocus: Left,]
- action: [MoveFocusOrTab: Left,]
key: [ Alt: 'h',]
- action: [MoveFocus: Right,]
- action: [MoveFocusOrTab: Right,]
key: [ Alt: 'l',]
- action: [MoveFocus: Down,]
key: [ Alt: 'j',]

View File

@ -17,9 +17,9 @@ keybinds:
key: [Ctrl: 'q',]
- action: [NewPane: ]
key: [ Alt: 'n',]
- action: [MoveFocus: Left,]
- action: [MoveFocusOrTab: Left,]
key: [ Alt: 'h',]
- action: [MoveFocus: Right,]
- action: [MoveFocusOrTab: Right,]
key: [ Alt: 'l',]
- action: [MoveFocus: Down,]
key: [ Alt: 'j',]

View File

@ -16,7 +16,7 @@ use crate::common::{
input::config::Config,
input::handler::input_loop,
os_input_output::ClientOsApi,
SenderType, SenderWithContext, SyncChannelWithContext,
thread_bus::{SenderType, SenderWithContext, SyncChannelWithContext},
};
use crate::server::ServerInstruction;

View File

@ -1,9 +1,12 @@
use crate::{common::SenderWithContext, pty_bus::VteBytes, tab::Pane, wasm_vm::PluginInstruction};
use crate::panes::{PaneId, PositionAndSize};
use std::sync::mpsc::channel;
use std::time::Instant;
use std::{sync::mpsc::channel, unimplemented};
use std::unimplemented;
use crate::common::thread_bus::SenderWithContext;
use crate::panes::{PaneId, PositionAndSize};
use crate::pty::VteBytes;
use crate::tab::Pane;
use crate::wasm_vm::PluginInstruction;
pub struct PluginPane {
pub pid: u32,

View File

@ -1,15 +1,16 @@
use crate::tab::Pane;
use ::nix::pty::Winsize;
use ::std::os::unix::io::RawFd;
use serde::{Deserialize, Serialize};
use std::fmt::Debug;
use std::os::unix::io::RawFd;
use std::time::Instant;
use nix::pty::Winsize;
use serde::{Deserialize, Serialize};
use crate::panes::grid::Grid;
use crate::panes::terminal_character::{
CharacterStyles, TerminalCharacter, EMPTY_TERMINAL_CHARACTER,
};
use crate::pty_bus::VteBytes;
use crate::pty::VteBytes;
use crate::tab::Pane;
#[derive(PartialEq, Eq, Ord, PartialOrd, Hash, Clone, Copy, Debug, Serialize, Deserialize)]
pub enum PaneId {

View File

@ -2,11 +2,12 @@
//! as well as how they should be resized
use crate::client::pane_resizer::PaneResizer;
use crate::common::{input::handler::parse_keys, SenderWithContext};
use crate::common::input::handler::parse_keys;
use crate::common::thread_bus::ThreadSenders;
use crate::layout::Layout;
use crate::os_input_output::ServerOsApi;
use crate::panes::{PaneId, PositionAndSize, TerminalPane};
use crate::pty_bus::{PtyInstruction, VteBytes};
use crate::pty::{PtyInstruction, VteBytes};
use crate::server::ServerInstruction;
use crate::utils::shared::adjust_to_size;
use crate::wasm_vm::PluginInstruction;
@ -69,9 +70,7 @@ pub struct Tab {
full_screen_ws: PositionAndSize,
fullscreen_is_active: bool,
os_api: Box<dyn ServerOsApi>,
send_plugin_instructions: SenderWithContext<PluginInstruction>,
send_pty_instructions: SenderWithContext<PtyInstruction>,
send_server_instructions: SenderWithContext<ServerInstruction>,
pub senders: ThreadSenders,
synchronize_is_active: bool,
should_clear_display_before_rendering: bool,
pub mode_info: ModeInfo,
@ -219,7 +218,7 @@ pub trait Pane {
}
impl Tab {
// FIXME: Too many arguments here! Maybe bundle all of the senders for the whole program in a struct?
// FIXME: Still too many arguments for clippy to be happy...
#[allow(clippy::too_many_arguments)]
pub fn new(
index: usize,
@ -227,9 +226,7 @@ impl Tab {
name: String,
full_screen_ws: &PositionAndSize,
mut os_api: Box<dyn ServerOsApi>,
send_plugin_instructions: SenderWithContext<PluginInstruction>,
send_pty_instructions: SenderWithContext<PtyInstruction>,
send_server_instructions: SenderWithContext<ServerInstruction>,
senders: ThreadSenders,
max_panes: Option<usize>,
pane_id: Option<PaneId>,
mode_info: ModeInfo,
@ -261,9 +258,7 @@ impl Tab {
fullscreen_is_active: false,
synchronize_is_active: false,
os_api,
send_plugin_instructions,
send_pty_instructions,
send_server_instructions,
senders,
should_clear_display_before_rendering: false,
mode_info,
input_mode,
@ -315,14 +310,14 @@ impl Tab {
// Just a regular terminal
if let Some(plugin) = &layout.plugin {
let (pid_tx, pid_rx) = channel();
self.send_plugin_instructions
.send(PluginInstruction::Load(pid_tx, plugin.clone()))
self.senders
.send_to_plugin(PluginInstruction::Load(pid_tx, plugin.clone()))
.unwrap();
let pid = pid_rx.recv().unwrap();
let mut new_plugin = PluginPane::new(
pid,
*position_and_size,
self.send_plugin_instructions.clone(),
self.senders.to_plugin.as_ref().unwrap().clone(),
);
if let Some(max_rows) = position_and_size.max_rows {
new_plugin.set_max_height(max_rows);
@ -332,8 +327,8 @@ impl Tab {
}
self.panes.insert(PaneId::Plugin(pid), Box::new(new_plugin));
// Send an initial mode update to the newly loaded plugin only!
self.send_plugin_instructions
.send(PluginInstruction::Update(
self.senders
.send_to_plugin(PluginInstruction::Update(
Some(pid),
Event::ModeUpdate(self.mode_info.clone()),
))
@ -355,8 +350,8 @@ impl Tab {
// this is a bit of a hack and happens because we don't have any central location that
// can query the screen as to how many panes it needs to create a layout
// fixing this will require a bit of an architecture change
self.send_pty_instructions
.send(PtyInstruction::ClosePane(PaneId::Terminal(*unused_pid)))
self.senders
.send_to_pty(PtyInstruction::ClosePane(PaneId::Terminal(*unused_pid)))
.unwrap();
}
self.active_terminal = self.panes.iter().map(|(id, _)| id.to_owned()).next();
@ -400,9 +395,9 @@ impl Tab {
},
);
if terminal_id_to_split.is_none() {
self.send_pty_instructions
.send(PtyInstruction::ClosePane(pid))
.unwrap(); // we can't open this pane, close the pty
self.senders
.send_to_pty(PtyInstruction::ClosePane(pid)) // we can't open this pane, close the pty
.unwrap();
return; // likely no terminal large enough to split
}
let terminal_id_to_split = terminal_id_to_split.unwrap();
@ -481,9 +476,9 @@ impl Tab {
let active_pane_id = &self.get_active_pane_id().unwrap();
let active_pane = self.panes.get_mut(active_pane_id).unwrap();
if active_pane.rows() < MIN_TERMINAL_HEIGHT * 2 + 1 {
self.send_pty_instructions
.send(PtyInstruction::ClosePane(pid))
.unwrap(); // we can't open this pane, close the pty
self.senders
.send_to_pty(PtyInstruction::ClosePane(pid)) // we can't open this pane, close the pty
.unwrap();
return;
}
let terminal_ws = PositionAndSize {
@ -538,9 +533,9 @@ impl Tab {
let active_pane_id = &self.get_active_pane_id().unwrap();
let active_pane = self.panes.get_mut(active_pane_id).unwrap();
if active_pane.columns() < MIN_TERMINAL_WIDTH * 2 + 1 {
self.send_pty_instructions
.send(PtyInstruction::ClosePane(pid))
.unwrap(); // we can't open this pane, close the pty
self.senders
.send_to_pty(PtyInstruction::ClosePane(pid)) // we can't open this pane, close the pty
.unwrap();
return;
}
let terminal_ws = PositionAndSize {
@ -597,7 +592,7 @@ impl Tab {
}
pub fn handle_pty_bytes(&mut self, pid: RawFd, bytes: VteBytes) {
// if we don't have the terminal in self.terminals it's probably because
// of a race condition where the terminal was created in pty_bus but has not
// of a race condition where the terminal was created in pty but has not
// yet been created in Screen. These events are currently not buffered, so
// if you're debugging seemingly randomly missing stdout data, this is
// the reason
@ -628,8 +623,8 @@ impl Tab {
}
PaneId::Plugin(pid) => {
for key in parse_keys(&input_bytes) {
self.send_plugin_instructions
.send(PluginInstruction::Update(Some(pid), Event::KeyPress(key)))
self.senders
.send_to_plugin(PluginInstruction::Update(Some(pid), Event::KeyPress(key)))
.unwrap()
}
}
@ -706,7 +701,7 @@ impl Tab {
pub fn is_sync_panes_active(&self) -> bool {
self.synchronize_is_active
}
pub fn toggle_sync_tab_is_active(&mut self) {
pub fn toggle_sync_panes_is_active(&mut self) {
self.synchronize_is_active = !self.synchronize_is_active;
}
pub fn panes_contain_widechar(&self) -> bool {
@ -781,8 +776,8 @@ impl Tab {
}
}
self.send_server_instructions
.send(ServerInstruction::Render(Some(output)))
self.senders
.send_to_server(ServerInstruction::Render(Some(output)))
.unwrap();
}
fn get_panes(&self) -> impl Iterator<Item = (&PaneId, &Box<dyn Pane>)> {
@ -1853,12 +1848,13 @@ impl Tab {
}
self.render();
}
pub fn move_focus_left(&mut self) {
// returns a boolean that indicates whether the focus moved
pub fn move_focus_left(&mut self) -> bool {
if !self.has_selectable_panes() {
return;
return false;
}
if self.fullscreen_is_active {
return;
return false;
}
let active_terminal = self.get_active_pane();
if let Some(active) = active_terminal {
@ -1873,6 +1869,8 @@ impl Tab {
match next_index {
Some(&p) => {
self.active_terminal = Some(p);
self.render();
return true;
}
None => {
self.active_terminal = Some(active.pid());
@ -1881,7 +1879,7 @@ impl Tab {
} else {
self.active_terminal = Some(active_terminal.unwrap().pid());
}
self.render();
false
}
pub fn move_focus_down(&mut self) {
if !self.has_selectable_panes() {
@ -1943,12 +1941,13 @@ impl Tab {
}
self.render();
}
pub fn move_focus_right(&mut self) {
// returns a boolean that indicates whether the focus moved
pub fn move_focus_right(&mut self) -> bool {
if !self.has_selectable_panes() {
return;
return false;
}
if self.fullscreen_is_active {
return;
return false;
}
let active_terminal = self.get_active_pane();
if let Some(active) = active_terminal {
@ -1963,6 +1962,8 @@ impl Tab {
match next_index {
Some(&p) => {
self.active_terminal = Some(p);
self.render();
return true;
}
None => {
self.active_terminal = Some(active.pid());
@ -1971,7 +1972,7 @@ impl Tab {
} else {
self.active_terminal = Some(active_terminal.unwrap().pid());
}
self.render();
false
}
fn horizontal_borders(&self, terminals: &[PaneId]) -> HashSet<usize> {
terminals.iter().fold(HashSet::new(), |mut borders, t| {
@ -2086,8 +2087,8 @@ impl Tab {
if let Some(max_panes) = self.max_panes {
let terminals = self.get_pane_ids();
for &pid in terminals.iter().skip(max_panes - 1) {
self.send_pty_instructions
.send(PtyInstruction::ClosePane(pid))
self.senders
.send_to_pty(PtyInstruction::ClosePane(pid))
.unwrap();
self.close_pane_without_rerender(pid);
}
@ -2198,8 +2199,8 @@ impl Tab {
pub fn close_focused_pane(&mut self) {
if let Some(active_pane_id) = self.get_active_pane_id() {
self.close_pane(active_pane_id);
self.send_pty_instructions
.send(PtyInstruction::ClosePane(active_pane_id))
self.senders
.send_to_pty(PtyInstruction::ClosePane(active_pane_id))
.unwrap();
}
}

View File

@ -1,20 +1,22 @@
//! Error context system based on a thread-local representation of the call stack, itself based on
//! the instructions that are sent between threads.
use super::{ServerInstruction, ASYNCOPENCALLS, OPENCALLS};
use crate::client::ClientInstruction;
use crate::pty_bus::PtyInstruction;
use crate::screen::ScreenInstruction;
use serde::{Deserialize, Serialize};
use std::fmt::{Display, Error, Formatter};
use crate::client::ClientInstruction;
use crate::common::thread_bus::{ASYNCOPENCALLS, OPENCALLS};
use crate::pty::PtyInstruction;
use crate::screen::ScreenInstruction;
use crate::server::ServerInstruction;
/// The maximum amount of calls an [`ErrorContext`] will keep track
/// of in its stack representation. This is a per-thread maximum.
const MAX_THREAD_CALL_STACK: usize = 6;
#[cfg(not(test))]
use super::SenderWithContext;
use super::thread_bus::SenderWithContext;
#[cfg(not(test))]
use std::panic::PanicInfo;
/// Custom panic handler/hook. Prints the [`ErrorContext`].
@ -190,9 +192,11 @@ pub enum ScreenContext {
FocusNextPane,
FocusPreviousPane,
MoveFocusLeft,
MoveFocusLeftOrPreviousTab,
MoveFocusDown,
MoveFocusUp,
MoveFocusRight,
MoveFocusRightOrNextTab,
Exit,
ScrollUp,
ScrollDown,
@ -235,9 +239,13 @@ impl From<&ScreenInstruction> for ScreenContext {
ScreenInstruction::FocusNextPane => ScreenContext::FocusNextPane,
ScreenInstruction::FocusPreviousPane => ScreenContext::FocusPreviousPane,
ScreenInstruction::MoveFocusLeft => ScreenContext::MoveFocusLeft,
ScreenInstruction::MoveFocusLeftOrPreviousTab => {
ScreenContext::MoveFocusLeftOrPreviousTab
}
ScreenInstruction::MoveFocusDown => ScreenContext::MoveFocusDown,
ScreenInstruction::MoveFocusUp => ScreenContext::MoveFocusUp,
ScreenInstruction::MoveFocusRight => ScreenContext::MoveFocusRight,
ScreenInstruction::MoveFocusRightOrNextTab => ScreenContext::MoveFocusRightOrNextTab,
ScreenInstruction::Exit => ScreenContext::Exit,
ScreenInstruction::ScrollUp => ScreenContext::ScrollUp,
ScreenInstruction::ScrollDown => ScreenContext::ScrollDown,

View File

@ -29,6 +29,9 @@ pub enum Action {
/// Move the focus pane in specified direction.
SwitchFocus,
MoveFocus(Direction),
/// Tries to move the focus pane in specified direction.
/// If there is no pane in the direction, move to previous/next Tab.
MoveFocusOrTab(Direction),
/// Scroll up in focus pane.
ScrollUp,
/// Scroll down in focus pane.

View File

@ -4,7 +4,7 @@ use super::actions::Action;
use super::keybinds::Keybinds;
use crate::client::ClientInstruction;
use crate::common::input::config::Config;
use crate::common::{SenderWithContext, OPENCALLS};
use crate::common::thread_bus::{SenderWithContext, OPENCALLS};
use crate::errors::ContextType;
use crate::os_input_output::ClientOsApi;
use crate::server::ServerInstruction;
@ -119,7 +119,8 @@ impl InputHandler {
| Action::GoToNextTab
| Action::GoToPreviousTab
| Action::CloseTab
| Action::GoToTab(_) => {
| Action::GoToTab(_)
| Action::MoveFocusOrTab(_) => {
self.command_is_executing.blocking_input_thread();
self.os_input
.send_to_server(ServerInstruction::Action(action));

View File

@ -3,73 +3,12 @@ pub mod errors;
pub mod input;
pub mod ipc;
pub mod os_input_output;
pub mod pty_bus;
pub mod pty;
pub mod screen;
pub mod setup;
pub mod thread_bus;
pub mod utils;
pub mod wasm_vm;
use crate::panes::PaneId;
use crate::server::ServerInstruction;
use async_std::task_local;
use errors::{get_current_ctx, ErrorContext};
use std::cell::RefCell;
use std::sync::mpsc;
/// An [MPSC](mpsc) asynchronous channel with added error context.
pub type ChannelWithContext<T> = (
mpsc::Sender<(T, ErrorContext)>,
mpsc::Receiver<(T, ErrorContext)>,
);
/// An [MPSC](mpsc) synchronous channel with added error context.
pub type SyncChannelWithContext<T> = (
mpsc::SyncSender<(T, ErrorContext)>,
mpsc::Receiver<(T, ErrorContext)>,
);
/// Wrappers around the two standard [MPSC](mpsc) sender types, [`mpsc::Sender`] and [`mpsc::SyncSender`], with an additional [`ErrorContext`].
#[derive(Clone)]
pub enum SenderType<T: Clone> {
/// A wrapper around an [`mpsc::Sender`], adding an [`ErrorContext`].
Sender(mpsc::Sender<(T, ErrorContext)>),
/// A wrapper around an [`mpsc::SyncSender`], adding an [`ErrorContext`].
SyncSender(mpsc::SyncSender<(T, ErrorContext)>),
}
/// Sends messages on an [MPSC](std::sync::mpsc) channel, along with an [`ErrorContext`],
/// synchronously or asynchronously depending on the underlying [`SenderType`].
#[derive(Clone)]
pub struct SenderWithContext<T: Clone> {
sender: SenderType<T>,
}
impl<T: Clone> SenderWithContext<T> {
pub fn new(sender: SenderType<T>) -> Self {
Self { sender }
}
/// Sends an event, along with the current [`ErrorContext`], on this
/// [`SenderWithContext`]'s channel.
pub fn send(&self, event: T) -> Result<(), mpsc::SendError<(T, ErrorContext)>> {
let err_ctx = get_current_ctx();
match self.sender {
SenderType::Sender(ref s) => s.send((event, err_ctx)),
SenderType::SyncSender(ref s) => s.send((event, err_ctx)),
}
}
}
unsafe impl<T: Clone> Send for SenderWithContext<T> {}
unsafe impl<T: Clone> Sync for SenderWithContext<T> {}
thread_local!(
/// A key to some thread local storage (TLS) that holds a representation of the thread's call
/// stack in the form of an [`ErrorContext`].
pub static OPENCALLS: RefCell<ErrorContext> = RefCell::default()
);
task_local! {
/// A key to some task local storage that holds a representation of the task's call
/// stack in the form of an [`ErrorContext`].
static ASYNCOPENCALLS: RefCell<ErrorContext> = RefCell::default()
}

View File

@ -1,23 +1,21 @@
use ::async_std::stream::*;
use ::async_std::task;
use ::async_std::task::*;
use ::std::collections::HashMap;
use ::std::os::unix::io::RawFd;
use ::std::pin::*;
use ::std::sync::mpsc::Receiver;
use ::std::time::{Duration, Instant};
use async_std::stream::*;
use async_std::task;
use async_std::task::*;
use std::collections::HashMap;
use std::os::unix::io::RawFd;
use std::path::PathBuf;
use std::pin::*;
use std::time::{Duration, Instant};
use super::SenderWithContext;
use crate::client::panes::PaneId;
use crate::common::errors::{get_current_ctx, ContextType, PtyContext};
use crate::common::screen::ScreenInstruction;
use crate::common::thread_bus::{Bus, ThreadSenders};
use crate::layout::Layout;
use crate::os_input_output::ServerOsApi;
use crate::server::ServerInstruction;
use crate::utils::logging::debug_to_file;
use crate::{
errors::{get_current_ctx, ContextType, ErrorContext},
panes::PaneId,
screen::ScreenInstruction,
wasm_vm::PluginInstruction,
};
use crate::wasm_vm::PluginInstruction;
pub struct ReadFromPid {
pid: RawFd,
@ -79,19 +77,72 @@ pub enum PtyInstruction {
Exit,
}
pub struct PtyBus {
pub receive_pty_instructions: Receiver<(PtyInstruction, ErrorContext)>,
pub struct Pty {
pub bus: Bus<PtyInstruction>,
pub id_to_child_pid: HashMap<RawFd, RawFd>,
pub send_screen_instructions: SenderWithContext<ScreenInstruction>,
send_plugin_instructions: SenderWithContext<PluginInstruction>,
os_input: Box<dyn ServerOsApi>,
debug_to_file: bool,
task_handles: HashMap<RawFd, JoinHandle<()>>,
}
pub fn pty_thread_main(mut pty: Pty, maybe_layout: Option<Layout>) {
loop {
let (event, mut err_ctx) = pty.bus.recv().expect("failed to receive event on channel");
err_ctx.add_call(ContextType::Pty(PtyContext::from(&event)));
match event {
PtyInstruction::SpawnTerminal(file_to_open) => {
let pid = pty.spawn_terminal(file_to_open);
pty.bus
.senders
.send_to_screen(ScreenInstruction::NewPane(PaneId::Terminal(pid)))
.unwrap();
}
PtyInstruction::SpawnTerminalVertically(file_to_open) => {
let pid = pty.spawn_terminal(file_to_open);
pty.bus
.senders
.send_to_screen(ScreenInstruction::VerticalSplit(PaneId::Terminal(pid)))
.unwrap();
}
PtyInstruction::SpawnTerminalHorizontally(file_to_open) => {
let pid = pty.spawn_terminal(file_to_open);
pty.bus
.senders
.send_to_screen(ScreenInstruction::HorizontalSplit(PaneId::Terminal(pid)))
.unwrap();
}
PtyInstruction::NewTab => {
if let Some(layout) = maybe_layout.clone() {
pty.spawn_terminals_for_layout(layout);
} else {
let pid = pty.spawn_terminal(None);
pty.bus
.senders
.send_to_screen(ScreenInstruction::NewTab(pid))
.unwrap();
}
}
PtyInstruction::ClosePane(id) => {
pty.close_pane(id);
pty.bus
.senders
.send_to_server(ServerInstruction::UnblockInputThread)
.unwrap();
}
PtyInstruction::CloseTab(ids) => {
pty.close_tab(ids);
pty.bus
.senders
.send_to_server(ServerInstruction::UnblockInputThread)
.unwrap();
}
PtyInstruction::Exit => break,
}
}
}
fn stream_terminal_bytes(
pid: RawFd,
send_screen_instructions: SenderWithContext<ScreenInstruction>,
senders: ThreadSenders,
os_input: Box<dyn ServerOsApi>,
debug: bool,
) -> JoinHandle<()> {
@ -113,7 +164,7 @@ fn stream_terminal_bytes(
}
}
if !bytes_is_empty {
let _ = send_screen_instructions.send(ScreenInstruction::PtyBytes(pid, bytes));
let _ = senders.send_to_screen(ScreenInstruction::PtyBytes(pid, bytes));
// for UX reasons, if we got something on the wire, we only send the render notice if:
// 1. there aren't any more bytes on the wire afterwards
// 2. a certain period (currently 30ms) has elapsed since the last render
@ -124,9 +175,7 @@ fn stream_terminal_bytes(
Some(receive_time) => {
if receive_time.elapsed() > max_render_pause {
pending_render = false;
send_screen_instructions
.send(ScreenInstruction::Render)
.unwrap();
let _ = senders.send_to_screen(ScreenInstruction::Render);
last_byte_receive_time = Some(Instant::now());
} else {
pending_render = true;
@ -140,53 +189,44 @@ fn stream_terminal_bytes(
} else {
if pending_render {
pending_render = false;
send_screen_instructions
.send(ScreenInstruction::Render)
.unwrap();
let _ = senders.send_to_screen(ScreenInstruction::Render);
}
last_byte_receive_time = None;
task::sleep(::std::time::Duration::from_millis(10)).await;
}
}
send_screen_instructions
.send(ScreenInstruction::Render)
.unwrap();
senders.send_to_screen(ScreenInstruction::Render).unwrap();
#[cfg(not(test))]
// this is a little hacky, and is because the tests end the file as soon as
// we read everything, rather than hanging until there is new data
// a better solution would be to fix the test fakes, but this will do for now
send_screen_instructions
.send(ScreenInstruction::ClosePane(PaneId::Terminal(pid)))
senders
.send_to_screen(ScreenInstruction::ClosePane(PaneId::Terminal(pid)))
.unwrap();
}
})
}
impl PtyBus {
pub fn new(
receive_pty_instructions: Receiver<(PtyInstruction, ErrorContext)>,
send_screen_instructions: SenderWithContext<ScreenInstruction>,
send_plugin_instructions: SenderWithContext<PluginInstruction>,
os_input: Box<dyn ServerOsApi>,
debug_to_file: bool,
) -> Self {
PtyBus {
receive_pty_instructions,
os_input,
impl Pty {
pub fn new(bus: Bus<PtyInstruction>, debug_to_file: bool) -> Self {
Pty {
bus,
id_to_child_pid: HashMap::new(),
send_screen_instructions,
send_plugin_instructions,
debug_to_file,
task_handles: HashMap::new(),
}
}
pub fn spawn_terminal(&mut self, file_to_open: Option<PathBuf>) -> RawFd {
let (pid_primary, pid_secondary): (RawFd, RawFd) =
self.os_input.spawn_terminal(file_to_open);
let (pid_primary, pid_secondary): (RawFd, RawFd) = self
.bus
.os_input
.as_mut()
.unwrap()
.spawn_terminal(file_to_open);
let task_handle = stream_terminal_bytes(
pid_primary,
self.send_screen_instructions.clone(),
self.os_input.clone(),
self.bus.senders.clone(),
self.bus.os_input.as_ref().unwrap().clone(),
self.debug_to_file,
);
self.task_handles.insert(pid_primary, task_handle);
@ -197,12 +237,14 @@ impl PtyBus {
let total_panes = layout.total_terminal_panes();
let mut new_pane_pids = vec![];
for _ in 0..total_panes {
let (pid_primary, pid_secondary): (RawFd, RawFd) = self.os_input.spawn_terminal(None);
let (pid_primary, pid_secondary): (RawFd, RawFd) =
self.bus.os_input.as_mut().unwrap().spawn_terminal(None);
self.id_to_child_pid.insert(pid_primary, pid_secondary);
new_pane_pids.push(pid_primary);
}
self.send_screen_instructions
.send(ScreenInstruction::ApplyLayout(
self.bus
.senders
.send_to_screen(ScreenInstruction::ApplyLayout(
layout,
new_pane_pids.clone(),
))
@ -210,8 +252,8 @@ impl PtyBus {
for id in new_pane_pids {
let task_handle = stream_terminal_bytes(
id,
self.send_screen_instructions.clone(),
self.os_input.clone(),
self.bus.senders.clone(),
self.bus.os_input.as_ref().unwrap().clone(),
self.debug_to_file,
);
self.task_handles.insert(id, task_handle);
@ -222,15 +264,16 @@ impl PtyBus {
PaneId::Terminal(id) => {
let child_pid = self.id_to_child_pid.remove(&id).unwrap();
let handle = self.task_handles.remove(&id).unwrap();
self.os_input.kill(child_pid).unwrap();
self.bus.os_input.as_mut().unwrap().kill(child_pid).unwrap();
task::block_on(async {
handle.cancel().await;
});
}
PaneId::Plugin(pid) => self
.send_plugin_instructions
.send(PluginInstruction::Unload(pid))
.unwrap(),
PaneId::Plugin(pid) => drop(
self.bus
.senders
.send_to_plugin(PluginInstruction::Unload(pid)),
),
}
}
pub fn close_tab(&mut self, ids: Vec<PaneId>) {
@ -240,7 +283,7 @@ impl PtyBus {
}
}
impl Drop for PtyBus {
impl Drop for Pty {
fn drop(&mut self) {
let child_ids: Vec<RawFd> = self.id_to_child_pid.keys().copied().collect();
for id in child_ids {

View File

@ -3,18 +3,20 @@
use std::collections::BTreeMap;
use std::os::unix::io::RawFd;
use std::str;
use std::sync::mpsc::Receiver;
use crate::common::SenderWithContext;
use crate::os_input_output::ServerOsApi;
use crate::common::pty::{PtyInstruction, VteBytes};
use crate::common::thread_bus::Bus;
use crate::common::input::options::Options;
use crate::cli::ConfigCli;
use crate::errors::{ContextType, ScreenContext};
use crate::layout::Layout;
use crate::panes::PaneId;
use crate::panes::PositionAndSize;
use crate::pty_bus::{PtyInstruction, VteBytes};
use crate::server::ServerInstruction;
use crate::tab::Tab;
use crate::{errors::ErrorContext, wasm_vm::PluginInstruction};
use crate::{layout::Layout, panes::PaneId};
use crate::wasm_vm::PluginInstruction;
use zellij_tile::data::{Event, InputMode, ModeInfo, Palette, TabInfo};
use zellij_tile::data::{Event, InputMode, ModeInfo, Palette, TabInfo, PluginCapabilities};
/// Instructions that can be sent to the [`Screen`].
#[derive(Debug, Clone)]
@ -33,9 +35,11 @@ pub enum ScreenInstruction {
FocusNextPane,
FocusPreviousPane,
MoveFocusLeft,
MoveFocusLeftOrPreviousTab,
MoveFocusDown,
MoveFocusUp,
MoveFocusRight,
MoveFocusRightOrNextTab,
Exit,
ScrollUp,
ScrollDown,
@ -63,55 +67,37 @@ pub enum ScreenInstruction {
/// A [`Screen`] holds multiple [`Tab`]s, each one holding multiple [`panes`](crate::client::panes).
/// It only directly controls which tab is active, delegating the rest to the individual `Tab`.
pub struct Screen {
/// A [`ScreenInstruction`] and [`ErrorContext`] receiver.
pub receiver: Receiver<(ScreenInstruction, ErrorContext)>,
/// A Bus for sending and receiving messages with the other threads.
pub bus: Bus<ScreenInstruction>,
/// An optional maximal amount of panes allowed per [`Tab`] in this [`Screen`] instance.
max_panes: Option<usize>,
/// A map between this [`Screen`]'s tabs and their ID/key.
tabs: BTreeMap<usize, Tab>,
/// A [`PluginInstruction`] and [`ErrorContext`] sender.
pub send_plugin_instructions: SenderWithContext<PluginInstruction>,
/// An [`PtyInstruction`] and [`ErrorContext`] sender.
pub send_pty_instructions: SenderWithContext<PtyInstruction>,
/// An [`ServerInstruction`] and [`ErrorContext`] sender.
pub send_server_instructions: SenderWithContext<ServerInstruction>,
/// The full size of this [`Screen`].
full_screen_ws: PositionAndSize,
/// The index of this [`Screen`]'s active [`Tab`].
active_tab_index: Option<usize>,
/// The [`ServerOsApi`] this [`Screen`] uses.
os_api: Box<dyn ServerOsApi>,
mode_info: ModeInfo,
input_mode: InputMode,
colors: Palette,
}
impl Screen {
// FIXME: This lint needs actual fixing! Maybe by bundling the Senders
/// Creates and returns a new [`Screen`].
#[allow(clippy::too_many_arguments)]
pub fn new(
receive_screen_instructions: Receiver<(ScreenInstruction, ErrorContext)>,
send_plugin_instructions: SenderWithContext<PluginInstruction>,
send_pty_instructions: SenderWithContext<PtyInstruction>,
send_server_instructions: SenderWithContext<ServerInstruction>,
bus: Bus<ScreenInstruction>,
full_screen_ws: &PositionAndSize,
os_api: Box<dyn ServerOsApi>,
max_panes: Option<usize>,
mode_info: ModeInfo,
input_mode: InputMode,
colors: Palette,
) -> Self {
Screen {
receiver: receive_screen_instructions,
bus,
max_panes,
send_plugin_instructions,
send_pty_instructions,
send_server_instructions,
full_screen_ws: *full_screen_ws,
active_tab_index: None,
tabs: BTreeMap::new(),
os_api,
mode_info,
input_mode,
colors,
@ -128,10 +114,8 @@ impl Screen {
position,
String::new(),
&self.full_screen_ws,
self.os_api.clone(),
self.send_plugin_instructions.clone(),
self.send_pty_instructions.clone(),
self.send_server_instructions.clone(),
self.bus.os_input.as_ref().unwrap().clone(),
self.bus.senders.clone(),
self.max_panes,
Some(PaneId::Terminal(pane_id)),
self.mode_info.clone(),
@ -215,13 +199,15 @@ impl Screen {
// below we don't check the result of sending the CloseTab instruction to the pty thread
// because this might be happening when the app is closing, at which point the pty thread
// has already closed and this would result in an error
self.send_pty_instructions
.send(PtyInstruction::CloseTab(pane_ids))
self.bus
.senders
.send_to_pty(PtyInstruction::CloseTab(pane_ids))
.unwrap();
if self.tabs.is_empty() {
self.active_tab_index = None;
self.send_server_instructions
.send(ServerInstruction::Render(None))
self.bus
.senders
.send_to_server(ServerInstruction::Render(None))
.unwrap();
} else {
for t in self.tabs.values_mut() {
@ -284,10 +270,8 @@ impl Screen {
position,
String::new(),
&self.full_screen_ws,
self.os_api.clone(),
self.send_plugin_instructions.clone(),
self.send_pty_instructions.clone(),
self.send_server_instructions.clone(),
self.bus.os_input.as_ref().unwrap().clone(),
self.bus.senders.clone(),
self.max_panes,
None,
self.mode_info.clone(),
@ -311,8 +295,9 @@ impl Screen {
is_sync_panes_active: tab.is_sync_panes_active(),
});
}
self.send_plugin_instructions
.send(PluginInstruction::Update(None, Event::TabUpdate(tab_data)))
self.bus
.senders
.send_to_plugin(PluginInstruction::Update(None, Event::TabUpdate(tab_data)))
.unwrap();
}
@ -340,3 +325,276 @@ impl Screen {
}
}
}
pub fn screen_thread_main(
bus: Bus<ScreenInstruction>,
max_panes: Option<usize>,
full_screen_ws: PositionAndSize,
options: Option<ConfigCli>,
config_options: Options,
) {
let colors = bus.os_input.as_ref().unwrap().load_palette();
let capabilities = if let Some(ConfigCli::Options(options)) = options {
options.simplified_ui
} else {
config_options.simplified_ui
};
let mut screen = Screen::new(
bus,
&full_screen_ws,
max_panes,
ModeInfo {
palette: colors,
capabilities: PluginCapabilities {
arrow_fonts: capabilities,
},
..ModeInfo::default()
},
InputMode::Normal,
colors,
);
loop {
let (event, mut err_ctx) = screen
.bus
.recv()
.expect("failed to receive event on channel");
err_ctx.add_call(ContextType::Screen(ScreenContext::from(&event)));
match event {
ScreenInstruction::PtyBytes(pid, vte_bytes) => {
let active_tab = screen.get_active_tab_mut().unwrap();
if active_tab.has_terminal_pid(pid) {
// it's most likely that this event is directed at the active tab
// look there first
active_tab.handle_pty_bytes(pid, vte_bytes);
} else {
// if this event wasn't directed at the active tab, start looking
// in other tabs
let all_tabs = screen.get_tabs_mut();
for tab in all_tabs.values_mut() {
if tab.has_terminal_pid(pid) {
tab.handle_pty_bytes(pid, vte_bytes);
break;
}
}
}
}
ScreenInstruction::Render => {
screen.render();
}
ScreenInstruction::NewPane(pid) => {
screen.get_active_tab_mut().unwrap().new_pane(pid);
screen
.bus
.senders
.send_to_server(ServerInstruction::UnblockInputThread)
.unwrap();
}
ScreenInstruction::HorizontalSplit(pid) => {
screen.get_active_tab_mut().unwrap().horizontal_split(pid);
screen
.bus
.senders
.send_to_server(ServerInstruction::UnblockInputThread)
.unwrap();
}
ScreenInstruction::VerticalSplit(pid) => {
screen.get_active_tab_mut().unwrap().vertical_split(pid);
screen
.bus
.senders
.send_to_server(ServerInstruction::UnblockInputThread)
.unwrap();
}
ScreenInstruction::WriteCharacter(bytes) => {
let active_tab = screen.get_active_tab_mut().unwrap();
match active_tab.is_sync_panes_active() {
true => active_tab.write_to_terminals_on_current_tab(bytes),
false => active_tab.write_to_active_terminal(bytes),
}
}
ScreenInstruction::ResizeLeft => {
screen.get_active_tab_mut().unwrap().resize_left();
}
ScreenInstruction::ResizeRight => {
screen.get_active_tab_mut().unwrap().resize_right();
}
ScreenInstruction::ResizeDown => {
screen.get_active_tab_mut().unwrap().resize_down();
}
ScreenInstruction::ResizeUp => {
screen.get_active_tab_mut().unwrap().resize_up();
}
ScreenInstruction::SwitchFocus => {
screen.get_active_tab_mut().unwrap().move_focus();
}
ScreenInstruction::FocusNextPane => {
screen.get_active_tab_mut().unwrap().focus_next_pane();
}
ScreenInstruction::FocusPreviousPane => {
screen.get_active_tab_mut().unwrap().focus_previous_pane();
}
ScreenInstruction::MoveFocusLeft => {
screen.get_active_tab_mut().unwrap().move_focus_left();
}
ScreenInstruction::MoveFocusLeftOrPreviousTab => {
if !screen.get_active_tab_mut().unwrap().move_focus_left() {
screen.switch_tab_prev();
}
screen
.bus
.senders
.send_to_server(ServerInstruction::UnblockInputThread)
.unwrap();
}
ScreenInstruction::MoveFocusDown => {
screen.get_active_tab_mut().unwrap().move_focus_down();
}
ScreenInstruction::MoveFocusRight => {
screen.get_active_tab_mut().unwrap().move_focus_right();
}
ScreenInstruction::MoveFocusRightOrNextTab => {
if !screen.get_active_tab_mut().unwrap().move_focus_right() {
screen.switch_tab_next();
}
screen
.bus
.senders
.send_to_server(ServerInstruction::UnblockInputThread)
.unwrap();
}
ScreenInstruction::MoveFocusUp => {
screen.get_active_tab_mut().unwrap().move_focus_up();
}
ScreenInstruction::ScrollUp => {
screen
.get_active_tab_mut()
.unwrap()
.scroll_active_terminal_up();
}
ScreenInstruction::ScrollDown => {
screen
.get_active_tab_mut()
.unwrap()
.scroll_active_terminal_down();
}
ScreenInstruction::PageScrollUp => {
screen
.get_active_tab_mut()
.unwrap()
.scroll_active_terminal_up_page();
}
ScreenInstruction::PageScrollDown => {
screen
.get_active_tab_mut()
.unwrap()
.scroll_active_terminal_down_page();
}
ScreenInstruction::ClearScroll => {
screen
.get_active_tab_mut()
.unwrap()
.clear_active_terminal_scroll();
}
ScreenInstruction::CloseFocusedPane => {
screen.get_active_tab_mut().unwrap().close_focused_pane();
screen.render();
}
ScreenInstruction::SetSelectable(id, selectable) => {
screen
.get_active_tab_mut()
.unwrap()
.set_pane_selectable(id, selectable);
}
ScreenInstruction::SetMaxHeight(id, max_height) => {
screen
.get_active_tab_mut()
.unwrap()
.set_pane_max_height(id, max_height);
}
ScreenInstruction::SetInvisibleBorders(id, invisible_borders) => {
screen
.get_active_tab_mut()
.unwrap()
.set_pane_invisible_borders(id, invisible_borders);
screen.render();
}
ScreenInstruction::ClosePane(id) => {
screen.get_active_tab_mut().unwrap().close_pane(id);
screen.render();
}
ScreenInstruction::ToggleActiveTerminalFullscreen => {
screen
.get_active_tab_mut()
.unwrap()
.toggle_active_pane_fullscreen();
}
ScreenInstruction::NewTab(pane_id) => {
screen.new_tab(pane_id);
screen
.bus
.senders
.send_to_server(ServerInstruction::UnblockInputThread)
.unwrap();
}
ScreenInstruction::SwitchTabNext => {
screen.switch_tab_next();
screen
.bus
.senders
.send_to_server(ServerInstruction::UnblockInputThread)
.unwrap();
}
ScreenInstruction::SwitchTabPrev => {
screen.switch_tab_prev();
screen
.bus
.senders
.send_to_server(ServerInstruction::UnblockInputThread)
.unwrap();
}
ScreenInstruction::CloseTab => {
screen.close_tab();
screen
.bus
.senders
.send_to_server(ServerInstruction::UnblockInputThread)
.unwrap();
}
ScreenInstruction::ApplyLayout(layout, new_pane_pids) => {
screen.apply_layout(layout, new_pane_pids);
screen
.bus
.senders
.send_to_server(ServerInstruction::UnblockInputThread)
.unwrap();
}
ScreenInstruction::GoToTab(tab_index) => {
screen.go_to_tab(tab_index as usize);
screen
.bus
.senders
.send_to_server(ServerInstruction::UnblockInputThread)
.unwrap();
}
ScreenInstruction::UpdateTabName(c) => {
screen.update_active_tab_name(c);
}
ScreenInstruction::TerminalResize(new_size) => {
screen.resize_to_screen(new_size);
}
ScreenInstruction::ChangeMode(mode_info) => {
screen.change_mode(mode_info);
}
ScreenInstruction::ToggleActiveSyncTab => {
screen
.get_active_tab_mut()
.unwrap()
.toggle_sync_panes_is_active();
screen.update_tabs();
}
ScreenInstruction::Exit => {
break;
}
}
}
}

142
src/common/thread_bus.rs Normal file
View File

@ -0,0 +1,142 @@
//! Definitions and helpers for sending and receiving messages between threads.
use async_std::task_local;
use std::cell::RefCell;
use std::sync::mpsc;
use crate::common::pty::PtyInstruction;
use crate::common::ServerInstruction;
use crate::errors::{get_current_ctx, ErrorContext};
use crate::os_input_output::ServerOsApi;
use crate::screen::ScreenInstruction;
use crate::wasm_vm::PluginInstruction;
/// An [MPSC](mpsc) asynchronous channel with added error context.
pub type ChannelWithContext<T> = (
mpsc::Sender<(T, ErrorContext)>,
mpsc::Receiver<(T, ErrorContext)>,
);
/// An [MPSC](mpsc) synchronous channel with added error context.
pub type SyncChannelWithContext<T> = (
mpsc::SyncSender<(T, ErrorContext)>,
mpsc::Receiver<(T, ErrorContext)>,
);
/// Wrappers around the two standard [MPSC](mpsc) sender types, [`mpsc::Sender`] and [`mpsc::SyncSender`], with an additional [`ErrorContext`].
#[derive(Clone)]
pub enum SenderType<T: Clone> {
/// A wrapper around an [`mpsc::Sender`], adding an [`ErrorContext`].
Sender(mpsc::Sender<(T, ErrorContext)>),
/// A wrapper around an [`mpsc::SyncSender`], adding an [`ErrorContext`].
SyncSender(mpsc::SyncSender<(T, ErrorContext)>),
}
/// Sends messages on an [MPSC](std::sync::mpsc) channel, along with an [`ErrorContext`],
/// synchronously or asynchronously depending on the underlying [`SenderType`].
#[derive(Clone)]
pub struct SenderWithContext<T: Clone> {
sender: SenderType<T>,
}
impl<T: Clone> SenderWithContext<T> {
pub fn new(sender: SenderType<T>) -> Self {
Self { sender }
}
/// Sends an event, along with the current [`ErrorContext`], on this
/// [`SenderWithContext`]'s channel.
pub fn send(&self, event: T) -> Result<(), mpsc::SendError<(T, ErrorContext)>> {
let err_ctx = get_current_ctx();
match self.sender {
SenderType::Sender(ref s) => s.send((event, err_ctx)),
SenderType::SyncSender(ref s) => s.send((event, err_ctx)),
}
}
}
unsafe impl<T: Clone> Send for SenderWithContext<T> {}
unsafe impl<T: Clone> Sync for SenderWithContext<T> {}
thread_local!(
/// A key to some thread local storage (TLS) that holds a representation of the thread's call
/// stack in the form of an [`ErrorContext`].
pub static OPENCALLS: RefCell<ErrorContext> = RefCell::default()
);
task_local! {
/// A key to some task local storage that holds a representation of the task's call
/// stack in the form of an [`ErrorContext`].
pub static ASYNCOPENCALLS: RefCell<ErrorContext> = RefCell::default()
}
/// A container for senders to the different threads in zellij on the server side
#[derive(Clone)]
pub struct ThreadSenders {
pub to_screen: Option<SenderWithContext<ScreenInstruction>>,
pub to_pty: Option<SenderWithContext<PtyInstruction>>,
pub to_plugin: Option<SenderWithContext<PluginInstruction>>,
pub to_server: Option<SenderWithContext<ServerInstruction>>,
}
impl ThreadSenders {
pub fn send_to_screen(
&self,
instruction: ScreenInstruction,
) -> Result<(), mpsc::SendError<(ScreenInstruction, ErrorContext)>> {
self.to_screen.as_ref().unwrap().send(instruction)
}
pub fn send_to_pty(
&self,
instruction: PtyInstruction,
) -> Result<(), mpsc::SendError<(PtyInstruction, ErrorContext)>> {
self.to_pty.as_ref().unwrap().send(instruction)
}
pub fn send_to_plugin(
&self,
instruction: PluginInstruction,
) -> Result<(), mpsc::SendError<(PluginInstruction, ErrorContext)>> {
self.to_plugin.as_ref().unwrap().send(instruction)
}
pub fn send_to_server(
&self,
instruction: ServerInstruction,
) -> Result<(), mpsc::SendError<(ServerInstruction, ErrorContext)>> {
self.to_server.as_ref().unwrap().send(instruction)
}
}
/// A container for a receiver, OS input and the senders to a given thread
pub struct Bus<T> {
pub receiver: mpsc::Receiver<(T, ErrorContext)>,
pub senders: ThreadSenders,
pub os_input: Option<Box<dyn ServerOsApi>>,
}
impl<T> Bus<T> {
pub fn new(
receiver: mpsc::Receiver<(T, ErrorContext)>,
to_screen: Option<&SenderWithContext<ScreenInstruction>>,
to_pty: Option<&SenderWithContext<PtyInstruction>>,
to_plugin: Option<&SenderWithContext<PluginInstruction>>,
to_server: Option<&SenderWithContext<ServerInstruction>>,
os_input: Option<Box<dyn ServerOsApi>>,
) -> Self {
Bus {
receiver,
senders: ThreadSenders {
to_screen: to_screen.cloned(),
to_pty: to_pty.cloned(),
to_plugin: to_plugin.cloned(),
to_server: to_server.cloned(),
},
os_input: os_input.clone(),
}
}
pub fn recv(&self) -> Result<(T, ErrorContext), mpsc::RecvError> {
self.receiver.recv()
}
}

View File

@ -1,17 +1,25 @@
use std::collections::{HashMap, HashSet};
use std::fs;
use std::path::PathBuf;
use std::process;
use std::str::FromStr;
use std::sync::{mpsc::Sender, Arc, Mutex};
use std::thread;
use std::time::{Duration, Instant};
use serde::{de::DeserializeOwned, Serialize};
use std::{
collections::HashSet,
path::PathBuf,
process,
sync::{mpsc::Sender, Arc, Mutex},
thread,
time::{Duration, Instant},
use wasmer::{
imports, ChainableNamedResolver, Function, ImportObject, Instance, Module, Store, Value,
WasmerEnv,
};
use wasmer::{imports, Function, ImportObject, Store, WasmerEnv};
use wasmer_wasi::WasiEnv;
use wasmer_wasi::{Pipe, WasiEnv, WasiState};
use zellij_tile::data::{Event, EventType, PluginIds};
use super::{pty_bus::PtyInstruction, screen::ScreenInstruction, PaneId, SenderWithContext};
use crate::common::errors::{ContextType, PluginContext};
use crate::common::pty::PtyInstruction;
use crate::common::screen::ScreenInstruction;
use crate::common::thread_bus::{Bus, ThreadSenders};
use crate::common::PaneId;
#[derive(Clone, Debug)]
pub enum PluginInstruction {
@ -25,14 +33,97 @@ pub enum PluginInstruction {
#[derive(WasmerEnv, Clone)]
pub struct PluginEnv {
pub plugin_id: u32,
// FIXME: This should be a big bundle of all of the channels
pub send_screen_instructions: SenderWithContext<ScreenInstruction>,
pub send_pty_instructions: SenderWithContext<PtyInstruction>,
pub send_plugin_instructions: SenderWithContext<PluginInstruction>,
pub senders: ThreadSenders,
pub wasi_env: WasiEnv,
pub subscriptions: Arc<Mutex<HashSet<EventType>>>,
}
// Thread main --------------------------------------------------------------------------------------------------------
pub fn wasm_thread_main(bus: Bus<PluginInstruction>, store: Store, data_dir: PathBuf) {
let mut plugin_id = 0;
let mut plugin_map = HashMap::new();
loop {
let (event, mut err_ctx) = bus.recv().expect("failed to receive event on channel");
err_ctx.add_call(ContextType::Plugin(PluginContext::from(&event)));
match event {
PluginInstruction::Load(pid_tx, path) => {
let plugin_dir = 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()));
// FIXME: Cache this compiled module on disk. I could use `(de)serialize_to_file()` for that
let module = Module::new(&store, &wasm_bytes).unwrap();
let output = Pipe::new();
let input = Pipe::new();
let mut wasi_env = WasiState::new("Zellij")
.env("CLICOLOR_FORCE", "1")
.preopen(|p| {
p.directory(".") // FIXME: Change this to a more meaningful dir
.alias(".")
.read(true)
.write(true)
.create(true)
})
.unwrap()
.stdin(Box::new(input))
.stdout(Box::new(output))
.finalize()
.unwrap();
let wasi = wasi_env.import_object(&module).unwrap();
let plugin_env = PluginEnv {
plugin_id,
senders: bus.senders.clone(),
wasi_env,
subscriptions: Arc::new(Mutex::new(HashSet::new())),
};
let zellij = zellij_exports(&store, &plugin_env);
let instance = Instance::new(&module, &zellij.chain_back(wasi)).unwrap();
let start = instance.exports.get_function("_start").unwrap();
// This eventually calls the `.load()` method
start.call(&[]).unwrap();
plugin_map.insert(plugin_id, (instance, plugin_env));
pid_tx.send(plugin_id).unwrap();
plugin_id += 1;
}
PluginInstruction::Update(pid, event) => {
for (&i, (instance, plugin_env)) in &plugin_map {
let subs = plugin_env.subscriptions.lock().unwrap();
// FIXME: This is very janky... Maybe I should write my own macro for Event -> EventType?
let event_type = EventType::from_str(&event.to_string()).unwrap();
if (pid.is_none() || pid == Some(i)) && subs.contains(&event_type) {
let update = instance.exports.get_function("update").unwrap();
wasi_write_object(&plugin_env.wasi_env, &event);
update.call(&[]).unwrap();
}
}
drop(bus.senders.send_to_screen(ScreenInstruction::Render));
}
PluginInstruction::Render(buf_tx, pid, rows, cols) => {
let (instance, plugin_env) = plugin_map.get(&pid).unwrap();
let render = instance.exports.get_function("render").unwrap();
render
.call(&[Value::I32(rows as i32), Value::I32(cols as i32)])
.unwrap();
buf_tx.send(wasi_read_string(&plugin_env.wasi_env)).unwrap();
}
PluginInstruction::Unload(pid) => drop(plugin_map.remove(&pid)),
PluginInstruction::Exit => break,
}
}
}
// Plugin API ---------------------------------------------------------------------------------------------------------
pub fn zellij_exports(store: &Store, plugin_env: &PluginEnv) -> ImportObject {
@ -74,8 +165,8 @@ fn host_unsubscribe(plugin_env: &PluginEnv) {
fn host_set_selectable(plugin_env: &PluginEnv, selectable: i32) {
let selectable = selectable != 0;
plugin_env
.send_screen_instructions
.send(ScreenInstruction::SetSelectable(
.senders
.send_to_screen(ScreenInstruction::SetSelectable(
PaneId::Plugin(plugin_env.plugin_id),
selectable,
))
@ -85,8 +176,8 @@ fn host_set_selectable(plugin_env: &PluginEnv, selectable: i32) {
fn host_set_max_height(plugin_env: &PluginEnv, max_height: i32) {
let max_height = max_height as usize;
plugin_env
.send_screen_instructions
.send(ScreenInstruction::SetMaxHeight(
.senders
.send_to_screen(ScreenInstruction::SetMaxHeight(
PaneId::Plugin(plugin_env.plugin_id),
max_height,
))
@ -96,8 +187,8 @@ fn host_set_max_height(plugin_env: &PluginEnv, max_height: i32) {
fn host_set_invisible_borders(plugin_env: &PluginEnv, invisible_borders: i32) {
let invisible_borders = invisible_borders != 0;
plugin_env
.send_screen_instructions
.send(ScreenInstruction::SetInvisibleBorders(
.senders
.send_to_screen(ScreenInstruction::SetInvisibleBorders(
PaneId::Plugin(plugin_env.plugin_id),
invisible_borders,
))
@ -115,8 +206,8 @@ fn host_get_plugin_ids(plugin_env: &PluginEnv) {
fn host_open_file(plugin_env: &PluginEnv) {
let path: PathBuf = wasi_read_object(&plugin_env.wasi_env);
plugin_env
.send_pty_instructions
.send(PtyInstruction::SpawnTerminal(Some(path)))
.senders
.send_to_pty(PtyInstruction::SpawnTerminal(Some(path)))
.unwrap();
}
@ -130,7 +221,7 @@ fn host_set_timeout(plugin_env: &PluginEnv, secs: f64) {
// timers as we'd like.
//
// But that's a lot of code, and this is a few lines:
let send_plugin_instructions = plugin_env.send_plugin_instructions.clone();
let send_plugin_instructions = plugin_env.senders.to_plugin.clone();
let update_target = Some(plugin_env.plugin_id);
thread::spawn(move || {
let start_time = Instant::now();
@ -140,6 +231,7 @@ fn host_set_timeout(plugin_env: &PluginEnv, secs: f64) {
let elapsed_time = Instant::now().duration_since(start_time).as_secs_f64();
send_plugin_instructions
.unwrap()
.send(PluginInstruction::Update(
update_target,
Event::Timer(elapsed_time),

View File

@ -6,9 +6,7 @@ mod server;
mod tests;
use client::{boundaries, layout, panes, start_client, tab};
use common::{
command_is_executing, errors, os_input_output, pty_bus, screen, setup, utils, wasm_vm,
};
use common::{command_is_executing, errors, os_input_output, pty, screen, setup, utils, wasm_vm};
use server::start_server;
use structopt::StructOpt;
@ -47,7 +45,7 @@ pub fn main() {
CliArgs::clap().gen_completions_to("zellij", shell, &mut out);
} else if let Some(crate::cli::ConfigCli::Setup { .. }) = opts.option {
setup::dump_default_config().expect("Failed to print to stdout");
std::process::exit(1);
std::process::exit(0);
} else {
atomic_create_dir(&*ZELLIJ_TMP_DIR).unwrap();
atomic_create_dir(&*ZELLIJ_TMP_LOG_DIR).unwrap();

View File

@ -1,36 +1,29 @@
pub mod route;
use interprocess::local_socket::LocalSocketListener;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use std::sync::mpsc::channel;
use std::sync::{Arc, RwLock};
use std::thread;
use std::{collections::HashMap, fs};
use std::{
collections::HashSet,
str::FromStr,
sync::{Arc, Mutex, RwLock},
};
use wasmer::{ChainableNamedResolver, Instance, Module, Store, Value};
use wasmer_wasi::{Pipe, WasiState};
use zellij_tile::data::{Event, EventType, InputMode, ModeInfo, PluginCapabilities};
use std::{path::PathBuf, sync::mpsc::channel};
use wasmer::Store;
use crate::cli::{CliArgs, ConfigCli};
use crate::cli::CliArgs;
use crate::client::ClientInstruction;
use crate::common::thread_bus::{Bus, ThreadSenders};
use crate::common::{
errors::{ContextType, PluginContext, PtyContext, ScreenContext, ServerContext},
input::actions::{Action, Direction},
input::options::Options,
input::handler::get_mode_info,
errors::{ContextType, ServerContext},
input::{actions::Action, options::Options},
os_input_output::{set_permissions, ServerOsApi},
pty_bus::{PtyBus, PtyInstruction},
screen::{Screen, ScreenInstruction},
pty::{pty_thread_main, Pty, PtyInstruction},
screen::{screen_thread_main, ScreenInstruction},
setup::install::populate_data_dir,
thread_bus::{ChannelWithContext, SenderType, SenderWithContext},
utils::consts::{ZELLIJ_IPC_PIPE, ZELLIJ_PROJ_DIR},
wasm_vm::{wasi_read_string, wasi_write_object, zellij_exports, PluginEnv, PluginInstruction},
ChannelWithContext, SenderType, SenderWithContext,
wasm_vm::{wasm_thread_main, PluginInstruction},
};
use crate::layout::Layout;
use crate::panes::PaneId;
use crate::panes::PositionAndSize;
use route::route_thread_main;
/// Instructions related to server-side application including the
/// ones sent by client to server
@ -44,10 +37,8 @@ pub enum ServerInstruction {
ClientExit,
}
struct SessionMetaData {
pub send_pty_instructions: SenderWithContext<PtyInstruction>,
pub send_screen_instructions: SenderWithContext<ScreenInstruction>,
pub send_plugin_instructions: SenderWithContext<PluginInstruction>,
pub struct SessionMetaData {
pub senders: ThreadSenders,
screen_thread: Option<thread::JoinHandle<()>>,
pty_thread: Option<thread::JoinHandle<()>>,
wasm_thread: Option<thread::JoinHandle<()>>,
@ -55,9 +46,9 @@ struct SessionMetaData {
impl Drop for SessionMetaData {
fn drop(&mut self) {
let _ = self.send_pty_instructions.send(PtyInstruction::Exit);
let _ = self.send_screen_instructions.send(ScreenInstruction::Exit);
let _ = self.send_plugin_instructions.send(PluginInstruction::Exit);
let _ = self.senders.send_to_pty(PtyInstruction::Exit);
let _ = self.senders.send_to_screen(ScreenInstruction::Exit);
let _ = self.senders.send_to_plugin(PluginInstruction::Exit);
let _ = self.screen_thread.take().unwrap().join();
let _ = self.pty_thread.take().unwrap().join();
let _ = self.wasm_thread.take().unwrap().join();
@ -65,26 +56,28 @@ impl Drop for SessionMetaData {
}
pub fn start_server(os_input: Box<dyn ServerOsApi>) -> thread::JoinHandle<()> {
let (send_server_instructions, receive_server_instructions): ChannelWithContext<
ServerInstruction,
> = channel();
let send_server_instructions =
SenderWithContext::new(SenderType::Sender(send_server_instructions));
let (to_server, server_receiver): ChannelWithContext<ServerInstruction> = channel();
let to_server = SenderWithContext::new(SenderType::Sender(to_server));
let sessions: Arc<RwLock<Option<SessionMetaData>>> = Arc::new(RwLock::new(None));
#[cfg(test)]
handle_client(
sessions.clone(),
os_input.clone(),
send_server_instructions.clone(),
);
thread::Builder::new()
.name("server_router".to_string())
.spawn({
let sessions = sessions.clone();
let os_input = os_input.clone();
let to_server = to_server.clone();
move || route_thread_main(sessions, os_input, to_server)
})
.unwrap();
#[cfg(not(test))]
let _ = thread::Builder::new()
.name("server_listener".to_string())
.spawn({
let os_input = os_input.clone();
let sessions = sessions.clone();
let send_server_instructions = send_server_instructions.clone();
let to_server = to_server.clone();
move || {
drop(std::fs::remove_file(&*ZELLIJ_IPC_PIPE));
let listener = LocalSocketListener::bind(&**ZELLIJ_IPC_PIPE).unwrap();
@ -95,8 +88,17 @@ pub fn start_server(os_input: Box<dyn ServerOsApi>) -> thread::JoinHandle<()> {
let mut os_input = os_input.clone();
os_input.update_receiver(stream);
let sessions = sessions.clone();
let send_server_instructions = send_server_instructions.clone();
handle_client(sessions, os_input, send_server_instructions);
let to_server = to_server.clone();
thread::Builder::new()
.name("server_router".to_string())
.spawn({
let sessions = sessions.clone();
let os_input = os_input.clone();
let to_server = to_server.clone();
move || route_thread_main(sessions, os_input, to_server)
})
.unwrap();
}
Err(err) => {
panic!("err {:?}", err);
@ -110,7 +112,7 @@ pub fn start_server(os_input: Box<dyn ServerOsApi>) -> thread::JoinHandle<()> {
.name("server_thread".to_string())
.spawn({
move || loop {
let (instruction, mut err_ctx) = receive_server_instructions.recv().unwrap();
let (instruction, mut err_ctx) = server_receiver.recv().unwrap();
err_ctx.add_call(ContextType::IPCServer(ServerContext::from(&instruction)));
match instruction {
ServerInstruction::NewClient(full_screen_ws, opts, config_options) => {
@ -118,7 +120,7 @@ pub fn start_server(os_input: Box<dyn ServerOsApi>) -> thread::JoinHandle<()> {
os_input.clone(),
opts,
config_options,
send_server_instructions.clone(),
to_server.clone(),
full_screen_ws,
);
*sessions.write().unwrap() = Some(session_data);
@ -127,8 +129,8 @@ pub fn start_server(os_input: Box<dyn ServerOsApi>) -> thread::JoinHandle<()> {
.unwrap()
.as_ref()
.unwrap()
.send_pty_instructions
.send(PtyInstruction::NewTab)
.senders
.send_to_pty(PtyInstruction::NewTab)
.unwrap();
}
ServerInstruction::UnblockInputThread => {
@ -150,66 +152,20 @@ pub fn start_server(os_input: Box<dyn ServerOsApi>) -> thread::JoinHandle<()> {
.unwrap()
}
fn handle_client(
sessions: Arc<RwLock<Option<SessionMetaData>>>,
mut os_input: Box<dyn ServerOsApi>,
send_server_instructions: SenderWithContext<ServerInstruction>,
) {
thread::Builder::new()
.name("server_router".to_string())
.spawn(move || loop {
let (instruction, mut err_ctx) = os_input.recv_from_client();
err_ctx.add_call(ContextType::IPCServer(ServerContext::from(&instruction)));
let rlocked_sessions = sessions.read().unwrap();
match instruction {
ServerInstruction::ClientExit => {
send_server_instructions.send(instruction).unwrap();
break;
}
ServerInstruction::Action(action) => {
route_action(action, rlocked_sessions.as_ref().unwrap(), &*os_input);
}
ServerInstruction::TerminalResize(new_size) => {
rlocked_sessions
.as_ref()
.unwrap()
.send_screen_instructions
.send(ScreenInstruction::TerminalResize(new_size))
.unwrap();
}
ServerInstruction::NewClient(..) => {
os_input.add_client_sender();
send_server_instructions.send(instruction).unwrap();
}
_ => {
send_server_instructions.send(instruction).unwrap();
}
}
})
.unwrap();
}
fn init_session(
os_input: Box<dyn ServerOsApi>,
opts: CliArgs,
config_options: Options,
send_server_instructions: SenderWithContext<ServerInstruction>,
to_server: SenderWithContext<ServerInstruction>,
full_screen_ws: PositionAndSize,
) -> SessionMetaData {
let (send_screen_instructions, receive_screen_instructions): ChannelWithContext<
ScreenInstruction,
> = channel();
let send_screen_instructions =
SenderWithContext::new(SenderType::Sender(send_screen_instructions));
let (to_screen, screen_receiver): ChannelWithContext<ScreenInstruction> = channel();
let to_screen = SenderWithContext::new(SenderType::Sender(to_screen));
let (send_plugin_instructions, receive_plugin_instructions): ChannelWithContext<
PluginInstruction,
> = channel();
let send_plugin_instructions =
SenderWithContext::new(SenderType::Sender(send_plugin_instructions));
let (send_pty_instructions, receive_pty_instructions): ChannelWithContext<PtyInstruction> =
channel();
let send_pty_instructions = SenderWithContext::new(SenderType::Sender(send_pty_instructions));
let (to_plugin, plugin_receiver): ChannelWithContext<PluginInstruction> = channel();
let to_plugin = SenderWithContext::new(SenderType::Sender(to_plugin));
let (to_pty, pty_receiver): ChannelWithContext<PtyInstruction> = channel();
let to_pty = SenderWithContext::new(SenderType::Sender(to_pty));
// Determine and initialize the data directory
let data_dir = opts
@ -227,330 +183,41 @@ fn init_session(
.map(|p| Layout::new(&p, &data_dir))
.or_else(|| default_layout.map(|p| Layout::from_defaults(&p, &data_dir)));
let mut pty_bus = PtyBus::new(
receive_pty_instructions,
send_screen_instructions.clone(),
send_plugin_instructions.clone(),
os_input.clone(),
opts.debug,
);
let pty_thread = thread::Builder::new()
.name("pty".to_string())
.spawn({
let send_server_instructions = send_server_instructions.clone();
move || loop {
let (event, mut err_ctx) = pty_bus
.receive_pty_instructions
.recv()
.expect("failed to receive event on channel");
err_ctx.add_call(ContextType::Pty(PtyContext::from(&event)));
match event {
PtyInstruction::SpawnTerminal(file_to_open) => {
let pid = pty_bus.spawn_terminal(file_to_open);
pty_bus
.send_screen_instructions
.send(ScreenInstruction::NewPane(PaneId::Terminal(pid)))
.unwrap();
}
PtyInstruction::SpawnTerminalVertically(file_to_open) => {
let pid = pty_bus.spawn_terminal(file_to_open);
pty_bus
.send_screen_instructions
.send(ScreenInstruction::VerticalSplit(PaneId::Terminal(pid)))
.unwrap();
}
PtyInstruction::SpawnTerminalHorizontally(file_to_open) => {
let pid = pty_bus.spawn_terminal(file_to_open);
pty_bus
.send_screen_instructions
.send(ScreenInstruction::HorizontalSplit(PaneId::Terminal(pid)))
.unwrap();
}
PtyInstruction::NewTab => {
if let Some(layout) = maybe_layout.clone() {
pty_bus.spawn_terminals_for_layout(layout);
} else {
let pid = pty_bus.spawn_terminal(None);
pty_bus
.send_screen_instructions
.send(ScreenInstruction::NewTab(pid))
.unwrap();
}
}
PtyInstruction::ClosePane(id) => {
pty_bus.close_pane(id);
send_server_instructions
.send(ServerInstruction::UnblockInputThread)
.unwrap();
}
PtyInstruction::CloseTab(ids) => {
pty_bus.close_tab(ids);
send_server_instructions
.send(ServerInstruction::UnblockInputThread)
.unwrap();
}
PtyInstruction::Exit => {
break;
}
}
}
let pty = Pty::new(
Bus::new(
pty_receiver,
Some(&to_screen),
None,
Some(&to_plugin),
Some(&to_server),
Some(os_input.clone()),
),
opts.debug,
);
move || pty_thread_main(pty, maybe_layout)
})
.unwrap();
let screen_thread = thread::Builder::new()
.name("screen".to_string())
.spawn({
let os_input = os_input.clone();
let send_plugin_instructions = send_plugin_instructions.clone();
let send_pty_instructions = send_pty_instructions.clone();
let send_server_instructions = send_server_instructions;
let screen_bus = Bus::new(
screen_receiver,
None,
Some(&to_pty),
Some(&to_plugin),
Some(&to_server),
Some(os_input.clone()),
);
let max_panes = opts.max_panes;
let capabilities = if let Some(ConfigCli::Options(options)) = opts.option {
options.simplified_ui
} else {
config_options.simplified_ui
};
let colors = os_input.load_palette();
let options = opts.option;
move || {
let mut screen = Screen::new(
receive_screen_instructions,
send_plugin_instructions,
send_pty_instructions,
send_server_instructions,
&full_screen_ws,
os_input,
max_panes,
ModeInfo {
palette: colors,
capabilities: PluginCapabilities {
arrow_fonts: capabilities,
},
..ModeInfo::default()
},
InputMode::Normal,
colors,
);
loop {
let (event, mut err_ctx) = screen
.receiver
.recv()
.expect("failed to receive event on channel");
err_ctx.add_call(ContextType::Screen(ScreenContext::from(&event)));
match event {
ScreenInstruction::PtyBytes(pid, vte_bytes) => {
let active_tab = screen.get_active_tab_mut().unwrap();
if active_tab.has_terminal_pid(pid) {
// it's most likely that this event is directed at the active tab
// look there first
active_tab.handle_pty_bytes(pid, vte_bytes);
} else {
// if this event wasn't directed at the active tab, start looking
// in other tabs
let all_tabs = screen.get_tabs_mut();
for tab in all_tabs.values_mut() {
if tab.has_terminal_pid(pid) {
tab.handle_pty_bytes(pid, vte_bytes);
break;
}
}
}
}
ScreenInstruction::Render => {
screen.render();
}
ScreenInstruction::NewPane(pid) => {
screen.get_active_tab_mut().unwrap().new_pane(pid);
screen
.send_server_instructions
.send(ServerInstruction::UnblockInputThread)
.unwrap();
}
ScreenInstruction::HorizontalSplit(pid) => {
screen.get_active_tab_mut().unwrap().horizontal_split(pid);
screen
.send_server_instructions
.send(ServerInstruction::UnblockInputThread)
.unwrap();
}
ScreenInstruction::VerticalSplit(pid) => {
screen.get_active_tab_mut().unwrap().vertical_split(pid);
screen
.send_server_instructions
.send(ServerInstruction::UnblockInputThread)
.unwrap();
}
ScreenInstruction::WriteCharacter(bytes) => {
let active_tab = screen.get_active_tab_mut().unwrap();
match active_tab.is_sync_panes_active() {
true => active_tab.write_to_terminals_on_current_tab(bytes),
false => active_tab.write_to_active_terminal(bytes),
}
}
ScreenInstruction::ResizeLeft => {
screen.get_active_tab_mut().unwrap().resize_left();
}
ScreenInstruction::ResizeRight => {
screen.get_active_tab_mut().unwrap().resize_right();
}
ScreenInstruction::ResizeDown => {
screen.get_active_tab_mut().unwrap().resize_down();
}
ScreenInstruction::ResizeUp => {
screen.get_active_tab_mut().unwrap().resize_up();
}
ScreenInstruction::SwitchFocus => {
screen.get_active_tab_mut().unwrap().move_focus();
}
ScreenInstruction::FocusNextPane => {
screen.get_active_tab_mut().unwrap().focus_next_pane();
}
ScreenInstruction::FocusPreviousPane => {
screen.get_active_tab_mut().unwrap().focus_previous_pane();
}
ScreenInstruction::MoveFocusLeft => {
screen.get_active_tab_mut().unwrap().move_focus_left();
}
ScreenInstruction::MoveFocusDown => {
screen.get_active_tab_mut().unwrap().move_focus_down();
}
ScreenInstruction::MoveFocusRight => {
screen.get_active_tab_mut().unwrap().move_focus_right();
}
ScreenInstruction::MoveFocusUp => {
screen.get_active_tab_mut().unwrap().move_focus_up();
}
ScreenInstruction::ScrollUp => {
screen
.get_active_tab_mut()
.unwrap()
.scroll_active_terminal_up();
}
ScreenInstruction::ScrollDown => {
screen
.get_active_tab_mut()
.unwrap()
.scroll_active_terminal_down();
}
ScreenInstruction::PageScrollUp => {
screen
.get_active_tab_mut()
.unwrap()
.scroll_active_terminal_up_page();
}
ScreenInstruction::PageScrollDown => {
screen
.get_active_tab_mut()
.unwrap()
.scroll_active_terminal_down_page();
}
ScreenInstruction::ClearScroll => {
screen
.get_active_tab_mut()
.unwrap()
.clear_active_terminal_scroll();
}
ScreenInstruction::CloseFocusedPane => {
screen.get_active_tab_mut().unwrap().close_focused_pane();
screen.render();
}
ScreenInstruction::SetSelectable(id, selectable) => {
screen
.get_active_tab_mut()
.unwrap()
.set_pane_selectable(id, selectable);
}
ScreenInstruction::SetMaxHeight(id, max_height) => {
screen
.get_active_tab_mut()
.unwrap()
.set_pane_max_height(id, max_height);
}
ScreenInstruction::SetInvisibleBorders(id, invisible_borders) => {
screen
.get_active_tab_mut()
.unwrap()
.set_pane_invisible_borders(id, invisible_borders);
screen.render();
}
ScreenInstruction::ClosePane(id) => {
screen.get_active_tab_mut().unwrap().close_pane(id);
screen.render();
}
ScreenInstruction::ToggleActiveTerminalFullscreen => {
screen
.get_active_tab_mut()
.unwrap()
.toggle_active_pane_fullscreen();
}
ScreenInstruction::NewTab(pane_id) => {
screen.new_tab(pane_id);
screen
.send_server_instructions
.send(ServerInstruction::UnblockInputThread)
.unwrap();
}
ScreenInstruction::SwitchTabNext => {
screen.switch_tab_next();
screen
.send_server_instructions
.send(ServerInstruction::UnblockInputThread)
.unwrap();
}
ScreenInstruction::SwitchTabPrev => {
screen.switch_tab_prev();
screen
.send_server_instructions
.send(ServerInstruction::UnblockInputThread)
.unwrap();
}
ScreenInstruction::CloseTab => {
screen.close_tab();
screen
.send_server_instructions
.send(ServerInstruction::UnblockInputThread)
.unwrap();
}
ScreenInstruction::ApplyLayout(layout, new_pane_pids) => {
screen.apply_layout(layout, new_pane_pids);
screen
.send_server_instructions
.send(ServerInstruction::UnblockInputThread)
.unwrap();
}
ScreenInstruction::GoToTab(tab_index) => {
screen.go_to_tab(tab_index as usize);
screen
.send_server_instructions
.send(ServerInstruction::UnblockInputThread)
.unwrap();
}
ScreenInstruction::UpdateTabName(c) => {
screen.update_active_tab_name(c);
screen
.send_server_instructions
.send(ServerInstruction::UnblockInputThread)
.unwrap();
}
ScreenInstruction::TerminalResize(new_size) => {
screen.resize_to_screen(new_size);
}
ScreenInstruction::ChangeMode(mode_info) => {
screen.change_mode(mode_info);
}
ScreenInstruction::ToggleActiveSyncTab => {
screen
.get_active_tab_mut()
.unwrap()
.toggle_sync_tab_is_active();
screen.update_tabs();
}
ScreenInstruction::Exit => {
break;
}
}
}
screen_thread_main(screen_bus, max_panes, full_screen_ws, options, config_options);
}
})
.unwrap();
@ -558,265 +225,28 @@ fn init_session(
let wasm_thread = thread::Builder::new()
.name("wasm".to_string())
.spawn({
let send_screen_instructions = send_screen_instructions.clone();
let send_pty_instructions = send_pty_instructions.clone();
let send_plugin_instructions = send_plugin_instructions.clone();
let plugin_bus = Bus::new(
plugin_receiver,
Some(&to_screen),
Some(&to_pty),
None,
None,
None,
);
let store = Store::default();
let mut plugin_id = 0;
let mut plugin_map = HashMap::new();
move || loop {
let (event, mut err_ctx) = receive_plugin_instructions
.recv()
.expect("failed to receive event on channel");
err_ctx.add_call(ContextType::Plugin(PluginContext::from(&event)));
match event {
PluginInstruction::Load(pid_tx, path) => {
let plugin_dir = 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()));
// FIXME: Cache this compiled module on disk. I could use `(de)serialize_to_file()` for that
let module = Module::new(&store, &wasm_bytes).unwrap();
let output = Pipe::new();
let input = Pipe::new();
let mut wasi_env = WasiState::new("Zellij")
.env("CLICOLOR_FORCE", "1")
.preopen(|p| {
p.directory(".") // FIXME: Change this to a more meaningful dir
.alias(".")
.read(true)
.write(true)
.create(true)
})
.unwrap()
.stdin(Box::new(input))
.stdout(Box::new(output))
.finalize()
.unwrap();
let wasi = wasi_env.import_object(&module).unwrap();
let plugin_env = PluginEnv {
plugin_id,
send_screen_instructions: send_screen_instructions.clone(),
send_pty_instructions: send_pty_instructions.clone(),
send_plugin_instructions: send_plugin_instructions.clone(),
wasi_env,
subscriptions: Arc::new(Mutex::new(HashSet::new())),
};
let zellij = zellij_exports(&store, &plugin_env);
let instance = Instance::new(&module, &zellij.chain_back(wasi)).unwrap();
let start = instance.exports.get_function("_start").unwrap();
// This eventually calls the `.init()` method
start.call(&[]).unwrap();
plugin_map.insert(plugin_id, (instance, plugin_env));
pid_tx.send(plugin_id).unwrap();
plugin_id += 1;
}
PluginInstruction::Update(pid, event) => {
for (&i, (instance, plugin_env)) in &plugin_map {
let subs = plugin_env.subscriptions.lock().unwrap();
// FIXME: This is very janky... Maybe I should write my own macro for Event -> EventType?
let event_type = EventType::from_str(&event.to_string()).unwrap();
if (pid.is_none() || pid == Some(i)) && subs.contains(&event_type) {
let update = instance.exports.get_function("update").unwrap();
wasi_write_object(&plugin_env.wasi_env, &event);
update.call(&[]).unwrap();
}
}
drop(send_screen_instructions.send(ScreenInstruction::Render));
}
PluginInstruction::Render(buf_tx, pid, rows, cols) => {
let (instance, plugin_env) = plugin_map.get(&pid).unwrap();
let render = instance.exports.get_function("render").unwrap();
render
.call(&[Value::I32(rows as i32), Value::I32(cols as i32)])
.unwrap();
buf_tx.send(wasi_read_string(&plugin_env.wasi_env)).unwrap();
}
PluginInstruction::Unload(pid) => drop(plugin_map.remove(&pid)),
PluginInstruction::Exit => break,
}
}
move || wasm_thread_main(plugin_bus, store, data_dir)
})
.unwrap();
SessionMetaData {
send_plugin_instructions,
send_screen_instructions,
send_pty_instructions,
senders: ThreadSenders {
to_screen: Some(to_screen),
to_pty: Some(to_pty),
to_plugin: Some(to_plugin),
to_server: None,
},
screen_thread: Some(screen_thread),
pty_thread: Some(pty_thread),
wasm_thread: Some(wasm_thread),
}
}
fn route_action(action: Action, session: &SessionMetaData, os_input: &dyn ServerOsApi) {
match action {
Action::Write(val) => {
session
.send_screen_instructions
.send(ScreenInstruction::ClearScroll)
.unwrap();
session
.send_screen_instructions
.send(ScreenInstruction::WriteCharacter(val))
.unwrap();
}
Action::SwitchToMode(mode) => {
let palette = os_input.load_palette();
session
.send_plugin_instructions
.send(PluginInstruction::Update(
None,
Event::ModeUpdate(get_mode_info(mode, palette)),
))
.unwrap();
session
.send_screen_instructions
.send(ScreenInstruction::ChangeMode(get_mode_info(mode, palette)))
.unwrap();
session
.send_screen_instructions
.send(ScreenInstruction::Render)
.unwrap();
}
Action::Resize(direction) => {
let screen_instr = match direction {
Direction::Left => ScreenInstruction::ResizeLeft,
Direction::Right => ScreenInstruction::ResizeRight,
Direction::Up => ScreenInstruction::ResizeUp,
Direction::Down => ScreenInstruction::ResizeDown,
};
session.send_screen_instructions.send(screen_instr).unwrap();
}
Action::SwitchFocus => {
session
.send_screen_instructions
.send(ScreenInstruction::SwitchFocus)
.unwrap();
}
Action::FocusNextPane => {
session
.send_screen_instructions
.send(ScreenInstruction::FocusNextPane)
.unwrap();
}
Action::FocusPreviousPane => {
session
.send_screen_instructions
.send(ScreenInstruction::FocusPreviousPane)
.unwrap();
}
Action::MoveFocus(direction) => {
let screen_instr = match direction {
Direction::Left => ScreenInstruction::MoveFocusLeft,
Direction::Right => ScreenInstruction::MoveFocusRight,
Direction::Up => ScreenInstruction::MoveFocusUp,
Direction::Down => ScreenInstruction::MoveFocusDown,
};
session.send_screen_instructions.send(screen_instr).unwrap();
}
Action::ScrollUp => {
session
.send_screen_instructions
.send(ScreenInstruction::ScrollUp)
.unwrap();
}
Action::ScrollDown => {
session
.send_screen_instructions
.send(ScreenInstruction::ScrollDown)
.unwrap();
}
Action::PageScrollUp => {
session
.send_screen_instructions
.send(ScreenInstruction::PageScrollUp)
.unwrap();
}
Action::PageScrollDown => {
session
.send_screen_instructions
.send(ScreenInstruction::PageScrollDown)
.unwrap();
}
Action::ToggleFocusFullscreen => {
session
.send_screen_instructions
.send(ScreenInstruction::ToggleActiveTerminalFullscreen)
.unwrap();
}
Action::NewPane(direction) => {
let pty_instr = match direction {
Some(Direction::Left) => PtyInstruction::SpawnTerminalVertically(None),
Some(Direction::Right) => PtyInstruction::SpawnTerminalVertically(None),
Some(Direction::Up) => PtyInstruction::SpawnTerminalHorizontally(None),
Some(Direction::Down) => PtyInstruction::SpawnTerminalHorizontally(None),
// No direction specified - try to put it in the biggest available spot
None => PtyInstruction::SpawnTerminal(None),
};
session.send_pty_instructions.send(pty_instr).unwrap();
}
Action::CloseFocus => {
session
.send_screen_instructions
.send(ScreenInstruction::CloseFocusedPane)
.unwrap();
}
Action::NewTab => {
session
.send_pty_instructions
.send(PtyInstruction::NewTab)
.unwrap();
}
Action::GoToNextTab => {
session
.send_screen_instructions
.send(ScreenInstruction::SwitchTabNext)
.unwrap();
}
Action::GoToPreviousTab => {
session
.send_screen_instructions
.send(ScreenInstruction::SwitchTabPrev)
.unwrap();
}
Action::ToggleActiveSyncTab => {
session
.send_screen_instructions
.send(ScreenInstruction::ToggleActiveSyncTab)
.unwrap();
}
Action::CloseTab => {
session
.send_screen_instructions
.send(ScreenInstruction::CloseTab)
.unwrap();
}
Action::GoToTab(i) => {
session
.send_screen_instructions
.send(ScreenInstruction::GoToTab(i))
.unwrap();
}
Action::TabNameInput(c) => {
session
.send_screen_instructions
.send(ScreenInstruction::UpdateTabName(c))
.unwrap();
}
Action::NoOp => {}
Action::Quit => panic!("Received unexpected action"),
}
}

214
src/server/route.rs Normal file
View File

@ -0,0 +1,214 @@
use std::sync::{Arc, RwLock};
use zellij_tile::data::Event;
use crate::common::errors::{ContextType, ServerContext};
use crate::common::input::actions::{Action, Direction};
use crate::common::input::handler::get_mode_info;
use crate::common::os_input_output::ServerOsApi;
use crate::common::pty::PtyInstruction;
use crate::common::screen::ScreenInstruction;
use crate::common::thread_bus::SenderWithContext;
use crate::common::wasm_vm::PluginInstruction;
use crate::server::{ServerInstruction, SessionMetaData};
fn route_action(action: Action, session: &SessionMetaData, os_input: &dyn ServerOsApi) {
match action {
Action::Write(val) => {
session
.senders
.send_to_screen(ScreenInstruction::ClearScroll)
.unwrap();
session
.senders
.send_to_screen(ScreenInstruction::WriteCharacter(val))
.unwrap();
}
Action::SwitchToMode(mode) => {
let palette = os_input.load_palette();
session
.senders
.send_to_plugin(PluginInstruction::Update(
None,
Event::ModeUpdate(get_mode_info(mode, palette)),
))
.unwrap();
session
.senders
.send_to_screen(ScreenInstruction::ChangeMode(get_mode_info(mode, palette)))
.unwrap();
session
.senders
.send_to_screen(ScreenInstruction::Render)
.unwrap();
}
Action::Resize(direction) => {
let screen_instr = match direction {
Direction::Left => ScreenInstruction::ResizeLeft,
Direction::Right => ScreenInstruction::ResizeRight,
Direction::Up => ScreenInstruction::ResizeUp,
Direction::Down => ScreenInstruction::ResizeDown,
};
session.senders.send_to_screen(screen_instr).unwrap();
}
Action::SwitchFocus => {
session
.senders
.send_to_screen(ScreenInstruction::SwitchFocus)
.unwrap();
}
Action::FocusNextPane => {
session
.senders
.send_to_screen(ScreenInstruction::FocusNextPane)
.unwrap();
}
Action::FocusPreviousPane => {
session
.senders
.send_to_screen(ScreenInstruction::FocusPreviousPane)
.unwrap();
}
Action::MoveFocus(direction) => {
let screen_instr = match direction {
Direction::Left => ScreenInstruction::MoveFocusLeft,
Direction::Right => ScreenInstruction::MoveFocusRight,
Direction::Up => ScreenInstruction::MoveFocusUp,
Direction::Down => ScreenInstruction::MoveFocusDown,
};
session.senders.send_to_screen(screen_instr).unwrap();
}
Action::MoveFocusOrTab(direction) => {
let screen_instr = match direction {
Direction::Left => ScreenInstruction::MoveFocusLeftOrPreviousTab,
Direction::Right => ScreenInstruction::MoveFocusRightOrNextTab,
_ => unreachable!(),
};
session.senders.send_to_screen(screen_instr).unwrap();
}
Action::ScrollUp => {
session
.senders
.send_to_screen(ScreenInstruction::ScrollUp)
.unwrap();
}
Action::ScrollDown => {
session
.senders
.send_to_screen(ScreenInstruction::ScrollDown)
.unwrap();
}
Action::PageScrollUp => {
session
.senders
.send_to_screen(ScreenInstruction::PageScrollUp)
.unwrap();
}
Action::PageScrollDown => {
session
.senders
.send_to_screen(ScreenInstruction::PageScrollDown)
.unwrap();
}
Action::ToggleFocusFullscreen => {
session
.senders
.send_to_screen(ScreenInstruction::ToggleActiveTerminalFullscreen)
.unwrap();
}
Action::NewPane(direction) => {
let pty_instr = match direction {
Some(Direction::Left) => PtyInstruction::SpawnTerminalVertically(None),
Some(Direction::Right) => PtyInstruction::SpawnTerminalVertically(None),
Some(Direction::Up) => PtyInstruction::SpawnTerminalHorizontally(None),
Some(Direction::Down) => PtyInstruction::SpawnTerminalHorizontally(None),
// No direction specified - try to put it in the biggest available spot
None => PtyInstruction::SpawnTerminal(None),
};
session.senders.send_to_pty(pty_instr).unwrap();
}
Action::CloseFocus => {
session
.senders
.send_to_screen(ScreenInstruction::CloseFocusedPane)
.unwrap();
}
Action::NewTab => {
session.senders.send_to_pty(PtyInstruction::NewTab).unwrap();
}
Action::GoToNextTab => {
session
.senders
.send_to_screen(ScreenInstruction::SwitchTabNext)
.unwrap();
}
Action::GoToPreviousTab => {
session
.senders
.send_to_screen(ScreenInstruction::SwitchTabPrev)
.unwrap();
}
Action::ToggleActiveSyncTab => {
session
.senders
.send_to_screen(ScreenInstruction::ToggleActiveSyncTab)
.unwrap();
}
Action::CloseTab => {
session
.senders
.send_to_screen(ScreenInstruction::CloseTab)
.unwrap();
}
Action::GoToTab(i) => {
session
.senders
.send_to_screen(ScreenInstruction::GoToTab(i))
.unwrap();
}
Action::TabNameInput(c) => {
session
.senders
.send_to_screen(ScreenInstruction::UpdateTabName(c))
.unwrap();
}
Action::NoOp => {}
Action::Quit => panic!("Received unexpected action"),
}
}
pub fn route_thread_main(
sessions: Arc<RwLock<Option<SessionMetaData>>>,
mut os_input: Box<dyn ServerOsApi>,
to_server: SenderWithContext<ServerInstruction>,
) {
loop {
let (instruction, mut err_ctx) = os_input.recv_from_client();
err_ctx.add_call(ContextType::IPCServer(ServerContext::from(&instruction)));
let rlocked_sessions = sessions.read().unwrap();
match instruction {
ServerInstruction::ClientExit => {
to_server.send(instruction).unwrap();
break;
}
ServerInstruction::Action(action) => {
route_action(action, rlocked_sessions.as_ref().unwrap(), &*os_input);
}
ServerInstruction::TerminalResize(new_size) => {
rlocked_sessions
.as_ref()
.unwrap()
.senders
.send_to_screen(ScreenInstruction::TerminalResize(new_size))
.unwrap();
}
ServerInstruction::NewClient(..) => {
os_input.add_client_sender();
to_server.send(instruction).unwrap();
}
_ => {
to_server.send(instruction).unwrap();
}
}
}
}

View File

@ -8,7 +8,7 @@ use std::sync::{mpsc, Arc, Condvar, Mutex};
use std::time::{Duration, Instant};
use crate::client::ClientInstruction;
use crate::common::{ChannelWithContext, SenderType, SenderWithContext};
use crate::common::thread_bus::{ChannelWithContext, SenderType, SenderWithContext};
use crate::errors::ErrorContext;
use crate::os_input_output::{ClientOsApi, ServerOsApi};
use crate::server::ServerInstruction;

View File

@ -7,8 +7,9 @@ use crate::{start, CliArgs};
use crate::common::input::config::Config;
use crate::tests::utils::commands::{
MOVE_FOCUS_LEFT_IN_PANE_MODE, MOVE_FOCUS_RIGHT_IN_PANE_MODE, PANE_MODE, QUIT,
SPLIT_DOWN_IN_PANE_MODE, SPLIT_RIGHT_IN_PANE_MODE,
ENTER, MOVE_FOCUS_LEFT_IN_NORMAL_MODE, MOVE_FOCUS_LEFT_IN_PANE_MODE,
MOVE_FOCUS_RIGHT_IN_PANE_MODE, NEW_TAB_IN_TAB_MODE, PANE_MODE, QUIT, SPLIT_DOWN_IN_PANE_MODE,
SPLIT_RIGHT_IN_PANE_MODE, TAB_MODE,
};
fn get_fake_os_input(fake_win_size: &PositionAndSize) -> FakeInputOutput {
@ -86,3 +87,41 @@ pub fn move_focus_left_to_the_most_recently_used_pane() {
get_next_to_last_snapshot(snapshots).expect("could not find snapshot");
assert_snapshot!(snapshot_before_quit);
}
#[test]
pub fn move_focus_left_changes_tab() {
let fake_win_size = PositionAndSize {
columns: 121,
rows: 20,
x: 0,
y: 0,
..Default::default()
};
let mut fake_input_output = get_fake_os_input(&fake_win_size);
fake_input_output.add_terminal_input(&[
&PANE_MODE,
&SPLIT_DOWN_IN_PANE_MODE,
&ENTER,
&TAB_MODE,
&NEW_TAB_IN_TAB_MODE,
&ENTER,
&MOVE_FOCUS_LEFT_IN_NORMAL_MODE,
&QUIT,
]);
start(
Box::new(fake_input_output.clone()),
CliArgs::default(),
Box::new(fake_input_output.clone()),
Config::default(),
);
let output_frames = fake_input_output
.stdout_writer
.output_frames
.lock()
.unwrap();
let snapshots = get_output_frame_snapshots(&output_frames, &fake_win_size);
let snapshot_before_quit =
get_next_to_last_snapshot(snapshots).expect("could not find snapshot");
assert_snapshot!(snapshot_before_quit);
}

View File

@ -7,8 +7,9 @@ use crate::{start, CliArgs};
use crate::common::input::config::Config;
use crate::tests::utils::commands::{
MOVE_FOCUS_LEFT_IN_PANE_MODE, MOVE_FOCUS_RIGHT_IN_PANE_MODE, PANE_MODE, QUIT,
SPLIT_DOWN_IN_PANE_MODE, SPLIT_RIGHT_IN_PANE_MODE,
ENTER, MOVE_FOCUS_LEFT_IN_PANE_MODE, MOVE_FOCUS_RIGHT_IN_NORMAL_MODE,
MOVE_FOCUS_RIGHT_IN_PANE_MODE, NEW_TAB_IN_TAB_MODE, PANE_MODE, QUIT, SPLIT_DOWN_IN_PANE_MODE,
SPLIT_RIGHT_IN_PANE_MODE, TAB_MODE,
};
fn get_fake_os_input(fake_win_size: &PositionAndSize) -> FakeInputOutput {
@ -86,3 +87,41 @@ pub fn move_focus_right_to_the_most_recently_used_pane() {
get_next_to_last_snapshot(snapshots).expect("could not find snapshot");
assert_snapshot!(snapshot_before_quit);
}
#[test]
pub fn move_focus_right_changes_tab() {
let fake_win_size = PositionAndSize {
columns: 121,
rows: 20,
x: 0,
y: 0,
..Default::default()
};
let mut fake_input_output = get_fake_os_input(&fake_win_size);
fake_input_output.add_terminal_input(&[
&PANE_MODE,
&SPLIT_DOWN_IN_PANE_MODE,
&ENTER,
&TAB_MODE,
&NEW_TAB_IN_TAB_MODE,
&ENTER,
&MOVE_FOCUS_RIGHT_IN_NORMAL_MODE,
&QUIT,
]);
start(
Box::new(fake_input_output.clone()),
CliArgs::default(),
Box::new(fake_input_output.clone()),
Config::default(),
);
let output_frames = fake_input_output
.stdout_writer
.output_frames
.lock()
.unwrap();
let snapshots = get_output_frame_snapshots(&output_frames, &fake_win_size);
let snapshot_before_quit =
get_next_to_last_snapshot(snapshots).expect("could not find snapshot");
assert_snapshot!(snapshot_before_quit);
}

View File

@ -0,0 +1,25 @@
---
source: src/tests/integration/move_focus_left.rs
expression: snapshot_before_quit
---
line11-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
line12-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
line13-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
line14-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
line15-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
line16-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
line17-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
line18-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
line19-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
prompt $
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
line12-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
line13-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
line14-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
line15-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
line16-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
line17-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
line18-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
line19-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
prompt $ █

View File

@ -0,0 +1,25 @@
---
source: src/tests/integration/move_focus_right.rs
expression: snapshot_before_quit
---
line11-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
line12-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
line13-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
line14-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
line15-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
line16-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
line17-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
line18-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
line19-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
prompt $
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
line12-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
line13-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
line14-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
line15-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
line16-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
line17-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
line18-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
line19-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
prompt $ █

View File

@ -47,6 +47,10 @@ pub fn get_next_to_last_snapshot(mut snapshots: Vec<String>) -> Option<String> {
pub mod commands {
pub const QUIT: [u8; 1] = [17]; // ctrl-q
pub const ESC: [u8; 1] = [27];
pub const ENTER: [u8; 1] = [10]; // char '\n'
pub const MOVE_FOCUS_LEFT_IN_NORMAL_MODE: [u8; 2] = [27, 104]; // alt-h
pub const MOVE_FOCUS_RIGHT_IN_NORMAL_MODE: [u8; 2] = [27, 108]; // alt-l
pub const PANE_MODE: [u8; 1] = [16]; // ctrl-p
pub const SPAWN_TERMINAL_IN_PANE_MODE: [u8; 1] = [110]; // n