docs: first documentation step (#185)

* added some documentation to the input module

* added a bunch of documentation already, doing this non-linearly

* added more comments

* forgot cargo ftm again oop

* PR change requests applied, some forgotten/imcomplete doc added
This commit is contained in:
categorille 2021-02-17 15:55:09 +01:00 committed by GitHub
parent 8be470fbc0
commit 6e5607ba26
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 261 additions and 111 deletions

1
.gitignore vendored
View file

@ -1,3 +1,4 @@
/target /target
*.new *.new
.vscode .vscode
.vim

View file

@ -18,6 +18,9 @@ pub enum PaneId {
Terminal(RawFd), Terminal(RawFd),
Plugin(u32), // FIXME: Drop the trait object, make this a wrapper for the struct? Plugin(u32), // FIXME: Drop the trait object, make this a wrapper for the struct?
} }
/// Contains the position and size of a [`Pane`], or more generally of any terminal, measured
/// in character rows and columns.
#[derive(Clone, Copy, Debug, Default)] #[derive(Clone, Copy, Debug, Default)]
pub struct PositionAndSize { pub struct PositionAndSize {
pub x: usize, pub x: usize,

View file

@ -1,3 +1,5 @@
//! `Tab`s holds multiple panes. It tracks their coordinates (x/y) and size, as well as how they should be resized
use crate::common::{AppInstruction, SenderWithContext}; use crate::common::{AppInstruction, SenderWithContext};
use crate::panes::{PaneId, PositionAndSize, TerminalPane}; use crate::panes::{PaneId, PositionAndSize, TerminalPane};
use crate::pty_bus::{PtyInstruction, VteEvent}; use crate::pty_bus::{PtyInstruction, VteEvent};
@ -13,15 +15,6 @@ use std::{io::Write, sync::mpsc::channel};
use crate::utils::logging::debug_log_to_file; use crate::utils::logging::debug_log_to_file;
/*
* Tab
*
* this holds multiple panes (currently terminal panes) which are currently displayed
* when this tab is active.
* it tracks their coordinates (x/y) and size, as well as how they should be resized
*
*/
const CURSOR_HEIGHT_WIDTH_RATIO: usize = 4; // this is not accurate and kind of a magic number, TODO: look into this const CURSOR_HEIGHT_WIDTH_RATIO: usize = 4; // this is not accurate and kind of a magic number, TODO: look into this
const MIN_TERMINAL_HEIGHT: usize = 2; const MIN_TERMINAL_HEIGHT: usize = 2;
const MIN_TERMINAL_WIDTH: usize = 4; const MIN_TERMINAL_WIDTH: usize = 4;

View file

@ -1,15 +1,21 @@
//! 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::{AppInstruction, OPENCALLS}; use super::{AppInstruction, OPENCALLS};
use crate::pty_bus::PtyInstruction; use crate::pty_bus::PtyInstruction;
use crate::screen::ScreenInstruction; use crate::screen::ScreenInstruction;
use std::fmt::{Display, Error, Formatter}; use std::fmt::{Display, Error, Formatter};
/// 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; const MAX_THREAD_CALL_STACK: usize = 6;
#[cfg(not(test))] #[cfg(not(test))]
use super::SenderWithContext; use super::SenderWithContext;
#[cfg(not(test))] #[cfg(not(test))]
use std::panic::PanicInfo; use std::panic::PanicInfo;
/// Custom panic handler/hook. Prints the [`ErrorContext`].
#[cfg(not(test))] #[cfg(not(test))]
pub fn handle_panic( pub fn handle_panic(
info: &PanicInfo<'_>, info: &PanicInfo<'_>,
@ -66,18 +72,22 @@ pub fn handle_panic(
} }
} }
/// A representation of the call stack.
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
pub struct ErrorContext { pub struct ErrorContext {
calls: [ContextType; MAX_THREAD_CALL_STACK], calls: [ContextType; MAX_THREAD_CALL_STACK],
} }
impl ErrorContext { impl ErrorContext {
/// Returns a new, blank [`ErrorContext`] containing only [`Empty`](ContextType::Empty)
/// calls.
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
calls: [ContextType::Empty; MAX_THREAD_CALL_STACK], calls: [ContextType::Empty; MAX_THREAD_CALL_STACK],
} }
} }
/// Adds a call to this [`ErrorContext`]'s call stack representation.
pub fn add_call(&mut self, call: ContextType) { pub fn add_call(&mut self, call: ContextType) {
for ctx in self.calls.iter_mut() { for ctx in self.calls.iter_mut() {
if *ctx == ContextType::Empty { if *ctx == ContextType::Empty {
@ -108,19 +118,30 @@ impl Display for ErrorContext {
} }
} }
/// Different types of calls that form an [`ErrorContext`] call stack.
///
/// Complex variants store a variant of a related enum, whose variants can be built from
/// the corresponding Zellij MSPC instruction enum variants ([`ScreenInstruction`],
/// [`PtyInstruction`], [`AppInstruction`], etc).
#[derive(Copy, Clone, PartialEq)] #[derive(Copy, Clone, PartialEq)]
pub enum ContextType { pub enum ContextType {
/// A screen-related call.
Screen(ScreenContext), Screen(ScreenContext),
/// A PTY-related call.
Pty(PtyContext), Pty(PtyContext),
/// A plugin-related call.
Plugin(PluginContext), Plugin(PluginContext),
/// An app-related call.
App(AppContext), App(AppContext),
IPCServer, IPCServer,
StdinHandler, StdinHandler,
AsyncTask, AsyncTask,
/// An empty, placeholder call. This should be thought of as representing no call at all.
/// A call stack representation filled with these is the representation of an empty call stack.
Empty, Empty,
} }
// TODO use the `colored` crate for color formatting
impl Display for ContextType { impl Display for ContextType {
fn fmt(&self, f: &mut Formatter) -> Result<(), Error> { fn fmt(&self, f: &mut Formatter) -> Result<(), Error> {
let purple = "\u{1b}[1;35m"; let purple = "\u{1b}[1;35m";
@ -143,6 +164,7 @@ impl Display for ContextType {
} }
} }
/// Stack call representations corresponding to the different types of [`ScreenInstruction`]s.
#[derive(Debug, Clone, Copy, PartialEq)] #[derive(Debug, Clone, Copy, PartialEq)]
pub enum ScreenContext { pub enum ScreenContext {
HandlePtyEvent, HandlePtyEvent,
@ -216,6 +238,7 @@ impl From<&ScreenInstruction> for ScreenContext {
} }
} }
/// Stack call representations corresponding to the different types of [`PtyInstruction`]s.
#[derive(Debug, Clone, Copy, PartialEq)] #[derive(Debug, Clone, Copy, PartialEq)]
pub enum PtyContext { pub enum PtyContext {
SpawnTerminal, SpawnTerminal,
@ -245,6 +268,7 @@ impl From<&PtyInstruction> for PtyContext {
use crate::wasm_vm::PluginInstruction; use crate::wasm_vm::PluginInstruction;
/// Stack call representations corresponding to the different types of [`PluginInstruction`]s.
#[derive(Debug, Clone, Copy, PartialEq)] #[derive(Debug, Clone, Copy, PartialEq)]
pub enum PluginContext { pub enum PluginContext {
Load, Load,
@ -268,6 +292,7 @@ impl From<&PluginInstruction> for PluginContext {
} }
} }
/// Stack call representations corresponding to the different types of [`AppInstruction`]s.
#[derive(Debug, Clone, Copy, PartialEq)] #[derive(Debug, Clone, Copy, PartialEq)]
pub enum AppContext { pub enum AppContext {
GetState, GetState,

View file

@ -1,8 +1,8 @@
/// This module is for defining the set of actions that can be taken in //! Definition of the actions that can be bound to keys.
/// response to a keybind and also passing actions back to the handler
/// for dispatch.
use super::handler; use super::handler;
/// The four directions (left, right, up, down).
#[derive(Clone)] #[derive(Clone)]
pub enum Direction { pub enum Direction {
Left, Left,
@ -11,6 +11,7 @@ pub enum Direction {
Down, Down,
} }
/// Actions that can be bound to keys.
#[derive(Clone)] #[derive(Clone)]
pub enum Action { pub enum Action {
/// Quit Zellij. /// Quit Zellij.

View file

@ -1,7 +1,8 @@
//! Main input logic.
use super::actions::Action; use super::actions::Action;
use super::keybinds::get_default_keybinds; use super::keybinds::get_default_keybinds;
use crate::common::{update_state, AppInstruction, AppState, SenderWithContext, OPENCALLS}; use crate::common::{update_state, AppInstruction, AppState, SenderWithContext, OPENCALLS};
/// Module for handling input
use crate::errors::ContextType; use crate::errors::ContextType;
use crate::os_input_output::OsApi; use crate::os_input_output::OsApi;
use crate::pty_bus::PtyInstruction; use crate::pty_bus::PtyInstruction;
@ -16,8 +17,9 @@ use termion::input::TermReadEventsAndRaw;
use super::keybinds::key_to_actions; use super::keybinds::key_to_actions;
/// Handles the dispatching of [`Action`]s according to the current /// Handles the dispatching of [`Action`]s according to the current
/// [`InputMode`], as well as changes to that mode. /// [`InputMode`], and keep tracks of the current [`InputMode`].
struct InputHandler { struct InputHandler {
/// The current input mode
mode: InputMode, mode: InputMode,
os_input: Box<dyn OsApi>, os_input: Box<dyn OsApi>,
command_is_executing: CommandIsExecuting, command_is_executing: CommandIsExecuting,
@ -28,6 +30,7 @@ struct InputHandler {
} }
impl InputHandler { impl InputHandler {
/// Returns a new [`InputHandler`] with the attributes specified as arguments.
fn new( fn new(
os_input: Box<dyn OsApi>, os_input: Box<dyn OsApi>,
command_is_executing: CommandIsExecuting, command_is_executing: CommandIsExecuting,
@ -47,10 +50,9 @@ impl InputHandler {
} }
} }
/// Main event loop. Interprets the terminal [`Event`](termion::event::Event)s /// Main input event loop. Interprets the terminal [`Event`](termion::event::Event)s
/// as [`Action`]s according to the current [`InputMode`], and dispatches those /// as [`Action`]s according to the current [`InputMode`], and dispatches those actions.
/// actions. fn handle_input(&mut self) {
fn get_input(&mut self) {
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);
@ -99,6 +101,17 @@ impl InputHandler {
} }
} }
/// Dispatches an [`Action`].
///
/// This function's body dictates what each [`Action`] actually does when
/// dispatched.
///
/// # Return value
/// Currently, this function returns a boolean that indicates whether
/// [`Self::handle_input()`] should break after this action is dispatched.
/// This is a temporary measure that is only necessary due to the way that the
/// framework works, and shouldn't be necessary anymore once the test framework
/// is revised. See [issue#183](https://github.com/zellij-org/zellij/issues/183).
fn dispatch_action(&mut self, action: Action) -> bool { fn dispatch_action(&mut self, action: Action) -> bool {
let mut should_break = false; let mut should_break = false;
@ -220,7 +233,7 @@ impl InputHandler {
} }
/// Routine to be called when the input handler exits (at the moment this is the /// Routine to be called when the input handler exits (at the moment this is the
/// same as quitting zellij) /// same as quitting Zellij).
fn exit(&mut self) { fn exit(&mut self) {
self.send_app_instructions self.send_app_instructions
.send(AppInstruction::Exit) .send(AppInstruction::Exit)
@ -228,28 +241,29 @@ impl InputHandler {
} }
} }
/// Dictates the input mode, which is the way that keystrokes will be interpreted: /// Describes the different input modes, which change the way that keystrokes will be interpreted.
/// - Normal mode either writes characters to the terminal, or switches to Command mode
/// using a particular key control
/// - Command mode is a menu that allows choosing another mode, like Resize or Pane
/// - Resize mode is for resizing the different panes already present
/// - Pane mode is for creating and closing panes in different directions
/// - Tab mode is for creating tabs and moving between them
/// - Scroll mode is for scrolling up and down within a pane
#[derive(Debug, PartialEq, Eq, Hash, Copy, Clone, EnumIter, Serialize, Deserialize)] #[derive(Debug, PartialEq, Eq, Hash, Copy, Clone, EnumIter, Serialize, Deserialize)]
pub enum InputMode { pub enum InputMode {
/// In `Normal` mode, input is always written to the terminal, except for one special input that
/// triggers the switch to [`InputMode::Command`] mode.
Normal, Normal,
/// In `Command` mode, input is bound to actions (more precisely, sequences of actions).
/// `Command` mode gives access to the other modes non-`InputMode::Normal` modes.
/// etc.
Command, Command,
/// `Resize` mode allows resizing the different existing panes.
Resize, Resize,
/// `Pane` mode allows creating and closing panes, as well as moving between them.
Pane, Pane,
/// `Tab` mode allows creating and closing tabs, as well as moving between them.
Tab, Tab,
/// `Scroll` mode allows scrolling up and down within a pane.
Scroll, Scroll,
Exiting,
} }
/// Represents the help message that is printed in the status bar, indicating /// Represents the contents of the help message that is printed in the status bar,
/// the current [`InputMode`], whether that mode is persistent, and what the /// which indicates the current [`InputMode`] and what the keybinds for that mode
/// keybinds for that mode are. /// are. Related to the default `status-bar` plugin.
#[derive(Default, Debug, Clone, Serialize, Deserialize)] #[derive(Default, Debug, Clone, Serialize, Deserialize)]
pub struct Help { pub struct Help {
pub mode: InputMode, pub mode: InputMode,
@ -262,12 +276,13 @@ impl Default for InputMode {
} }
} }
/// Creates a [`Help`] struct holding the current [`InputMode`] and its keybinds. /// Creates a [`Help`] struct indicating the current [`InputMode`] and its keybinds
/// (as pairs of [`String`]s).
// TODO this should probably be automatically generated in some way // TODO this should probably be automatically generated in some way
pub fn get_help(mode: InputMode) -> Help { pub fn get_help(mode: InputMode) -> Help {
let mut keybinds: Vec<(String, String)> = vec![]; let mut keybinds: Vec<(String, String)> = vec![];
match mode { match mode {
InputMode::Normal | InputMode::Command | InputMode::Exiting => { InputMode::Normal | InputMode::Command => {
keybinds.push((format!("p"), format!("PANE"))); keybinds.push((format!("p"), format!("PANE")));
keybinds.push((format!("t"), format!("TAB"))); keybinds.push((format!("t"), format!("TAB")));
keybinds.push((format!("r"), format!("RESIZE"))); keybinds.push((format!("r"), format!("RESIZE")));
@ -299,8 +314,8 @@ pub fn get_help(mode: InputMode) -> Help {
Help { mode, keybinds } Help { mode, keybinds }
} }
/// Entry point to the module. Instantiates a new InputHandler and calls its /// Entry point to the module. Instantiates an [`InputHandler`] and starts
/// input loop. /// its [`InputHandler::handle_input()`] loop.
pub fn input_loop( pub fn input_loop(
os_input: Box<dyn OsApi>, os_input: Box<dyn OsApi>,
command_is_executing: CommandIsExecuting, command_is_executing: CommandIsExecuting,
@ -317,5 +332,5 @@ pub fn input_loop(
send_plugin_instructions, send_plugin_instructions,
send_app_instructions, send_app_instructions,
) )
.get_input(); .handle_input();
} }

View file

@ -1,4 +1,4 @@
//! Mapping of inputs to sequences of actions //! Mapping of inputs to sequences of actions.
use super::actions::{Action, Direction}; use super::actions::{Action, Direction};
use super::handler::InputMode; use super::handler::InputMode;
@ -152,7 +152,6 @@ fn get_defaults_for_mode(mode: &InputMode) -> Result<ModeKeybinds, String> {
); );
defaults.insert(Key::Esc, vec![Action::SwitchToMode(InputMode::Command)]); defaults.insert(Key::Esc, vec![Action::SwitchToMode(InputMode::Command)]);
} }
InputMode::Exiting => {}
} }
Ok(defaults) Ok(defaults)

View file

@ -1,3 +1,5 @@
//! The way terminal iput is handled.
pub mod actions; pub mod actions;
pub mod handler; pub mod handler;
pub mod keybinds; pub mod keybinds;

View file

@ -1,4 +1,5 @@
// IPC stuff for starting to split things into a client and server model //! IPC stuff for starting to split things into a client and server model.
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::HashSet; use std::collections::HashSet;

View file

@ -11,7 +11,7 @@ pub mod wasm_vm;
use std::io::Write; use std::io::Write;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::sync::mpsc::{channel, sync_channel, Receiver, SendError, Sender, SyncSender}; use std::sync::mpsc;
use std::thread; use std::thread;
use std::{cell::RefCell, sync::mpsc::TrySendError}; use std::{cell::RefCell, sync::mpsc::TrySendError};
use std::{collections::HashMap, fs}; use std::{collections::HashMap, fs};
@ -55,7 +55,7 @@ pub fn update_state(
app_tx: &SenderWithContext<AppInstruction>, app_tx: &SenderWithContext<AppInstruction>,
update_fn: impl FnOnce(AppState) -> AppState, update_fn: impl FnOnce(AppState) -> AppState,
) { ) {
let (state_tx, state_rx) = channel(); let (state_tx, state_rx) = mpsc::channel();
drop(app_tx.send(AppInstruction::GetState(state_tx))); drop(app_tx.send(AppInstruction::GetState(state_tx)));
let state = state_rx.recv().unwrap(); let state = state_rx.recv().unwrap();
@ -63,15 +63,28 @@ pub fn update_state(
drop(app_tx.send(AppInstruction::SetState(update_fn(state)))) drop(app_tx.send(AppInstruction::SetState(update_fn(state))))
} }
pub type ChannelWithContext<T> = (Sender<(T, ErrorContext)>, Receiver<(T, ErrorContext)>); /// An [MPSC](mpsc) asynchronous channel with added error context.
pub type SyncChannelWithContext<T> = (SyncSender<(T, ErrorContext)>, Receiver<(T, ErrorContext)>); 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)] #[derive(Clone)]
enum SenderType<T: Clone> { enum SenderType<T: Clone> {
Sender(Sender<(T, ErrorContext)>), /// A wrapper around an [`mpsc::Sender`], adding an [`ErrorContext`].
SyncSender(SyncSender<(T, 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)] #[derive(Clone)]
pub struct SenderWithContext<T: Clone> { pub struct SenderWithContext<T: Clone> {
err_ctx: ErrorContext, err_ctx: ErrorContext,
@ -83,13 +96,20 @@ impl<T: Clone> SenderWithContext<T> {
Self { err_ctx, sender } Self { err_ctx, sender }
} }
pub fn send(&self, event: T) -> Result<(), SendError<(T, ErrorContext)>> { /// Sends an event, along with the current [`ErrorContext`], on this
/// [`SenderWithContext`]'s channel.
pub fn send(&self, event: T) -> Result<(), mpsc::SendError<(T, ErrorContext)>> {
match self.sender { match self.sender {
SenderType::Sender(ref s) => s.send((event, self.err_ctx)), SenderType::Sender(ref s) => s.send((event, self.err_ctx)),
SenderType::SyncSender(ref s) => s.send((event, self.err_ctx)), SenderType::SyncSender(ref s) => s.send((event, self.err_ctx)),
} }
} }
/// Attempts to send an event on this sender's channel, terminating instead of blocking
/// if the event could not be sent (buffer full or connection closed).
///
/// This can only be called on [`SyncSender`](SenderType::SyncSender)s, and will
/// panic if called on an asynchronous [`Sender`](SenderType::Sender).
pub fn try_send(&self, event: T) -> Result<(), TrySendError<(T, ErrorContext)>> { pub fn try_send(&self, event: T) -> Result<(), TrySendError<(T, ErrorContext)>> {
if let SenderType::SyncSender(ref s) = self.sender { if let SenderType::SyncSender(ref s) = self.sender {
s.try_send((event, self.err_ctx)) s.try_send((event, self.err_ctx))
@ -98,6 +118,11 @@ impl<T: Clone> SenderWithContext<T> {
} }
} }
/// Updates this [`SenderWithContext`]'s [`ErrorContext`]. This is the way one adds
/// a call to the error context.
///
/// Updating [`ErrorContext`]s works in this way so that these contexts are only ever
/// allocated on the stack (which is thread-specific), and not on the heap.
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;
} }
@ -106,16 +131,23 @@ impl<T: Clone> SenderWithContext<T> {
unsafe impl<T: Clone> Send for SenderWithContext<T> {} unsafe impl<T: Clone> Send for SenderWithContext<T> {}
unsafe impl<T: Clone> Sync for SenderWithContext<T> {} unsafe impl<T: Clone> Sync for SenderWithContext<T> {}
thread_local!(static OPENCALLS: RefCell<ErrorContext> = RefCell::default()); 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`].
static OPENCALLS: RefCell<ErrorContext> = RefCell::default()
);
/// Instructions related to the entire application.
#[derive(Clone)] #[derive(Clone)]
pub enum AppInstruction { pub enum AppInstruction {
GetState(Sender<AppState>), GetState(mpsc::Sender<AppState>),
SetState(AppState), SetState(AppState),
Exit, Exit,
Error(String), Error(String),
} }
/// Start Zellij with the specified [`OsApi`] and command-line arguments.
// FIXME this should definitely be modularized and split into different functions.
pub fn start(mut os_input: Box<dyn OsApi>, opts: CliArgs) { pub fn start(mut os_input: Box<dyn OsApi>, opts: CliArgs) {
let take_snapshot = "\u{1b}[?1049h"; let take_snapshot = "\u{1b}[?1049h";
os_input.unset_raw_mode(0); os_input.unset_raw_mode(0);
@ -131,24 +163,24 @@ pub fn start(mut os_input: Box<dyn OsApi>, opts: CliArgs) {
os_input.set_raw_mode(0); os_input.set_raw_mode(0);
let (send_screen_instructions, receive_screen_instructions): ChannelWithContext< let (send_screen_instructions, receive_screen_instructions): ChannelWithContext<
ScreenInstruction, ScreenInstruction,
> = channel(); > = mpsc::channel();
let err_ctx = OPENCALLS.with(|ctx| *ctx.borrow()); let err_ctx = OPENCALLS.with(|ctx| *ctx.borrow());
let mut send_screen_instructions = let mut send_screen_instructions =
SenderWithContext::new(err_ctx, SenderType::Sender(send_screen_instructions)); SenderWithContext::new(err_ctx, SenderType::Sender(send_screen_instructions));
let (send_pty_instructions, receive_pty_instructions): ChannelWithContext<PtyInstruction> = let (send_pty_instructions, receive_pty_instructions): ChannelWithContext<PtyInstruction> =
channel(); mpsc::channel();
let mut send_pty_instructions = let mut send_pty_instructions =
SenderWithContext::new(err_ctx, SenderType::Sender(send_pty_instructions)); SenderWithContext::new(err_ctx, SenderType::Sender(send_pty_instructions));
let (send_plugin_instructions, receive_plugin_instructions): ChannelWithContext< let (send_plugin_instructions, receive_plugin_instructions): ChannelWithContext<
PluginInstruction, PluginInstruction,
> = channel(); > = mpsc::channel();
let send_plugin_instructions = let send_plugin_instructions =
SenderWithContext::new(err_ctx, SenderType::Sender(send_plugin_instructions)); SenderWithContext::new(err_ctx, SenderType::Sender(send_plugin_instructions));
let (send_app_instructions, receive_app_instructions): SyncChannelWithContext<AppInstruction> = let (send_app_instructions, receive_app_instructions): SyncChannelWithContext<AppInstruction> =
sync_channel(0); mpsc::sync_channel(0);
let send_app_instructions = let send_app_instructions =
SenderWithContext::new(err_ctx, SenderType::SyncSender(send_app_instructions)); SenderWithContext::new(err_ctx, SenderType::SyncSender(send_app_instructions));

View file

@ -2,11 +2,12 @@ use crate::panes::PositionAndSize;
use nix::fcntl::{fcntl, FcntlArg, OFlag}; use nix::fcntl::{fcntl, FcntlArg, OFlag};
use nix::pty::{forkpty, Winsize}; use nix::pty::{forkpty, Winsize};
use nix::sys::signal::{kill, Signal}; use nix::sys::signal::{kill, Signal};
use nix::sys::termios::{cfmakeraw, tcdrain, tcgetattr, tcsetattr, SetArg, Termios}; use nix::sys::termios;
use nix::sys::wait::waitpid; use nix::sys::wait::waitpid;
use nix::unistd::{read, write, ForkResult, Pid}; use nix::unistd;
use nix::unistd::{ForkResult, Pid};
use std::io;
use std::io::prelude::*; use std::io::prelude::*;
use std::io::{stdin, Write};
use std::os::unix::io::RawFd; use std::os::unix::io::RawFd;
use std::path::PathBuf; use std::path::PathBuf;
use std::process::{Child, Command}; use std::process::{Child, Command};
@ -15,16 +16,16 @@ use std::sync::{Arc, Mutex};
use std::env; use std::env;
fn into_raw_mode(pid: RawFd) { fn into_raw_mode(pid: RawFd) {
let mut tio = tcgetattr(pid).expect("could not get terminal attribute"); let mut tio = termios::tcgetattr(pid).expect("could not get terminal attribute");
cfmakeraw(&mut tio); termios::cfmakeraw(&mut tio);
match tcsetattr(pid, SetArg::TCSANOW, &tio) { match termios::tcsetattr(pid, termios::SetArg::TCSANOW, &tio) {
Ok(_) => {} Ok(_) => {}
Err(e) => panic!("error {:?}", e), Err(e) => panic!("error {:?}", e),
}; };
} }
fn unset_raw_mode(pid: RawFd, orig_termios: Termios) { fn unset_raw_mode(pid: RawFd, orig_termios: termios::Termios) {
match tcsetattr(pid, SetArg::TCSANOW, &orig_termios) { match termios::tcsetattr(pid, termios::SetArg::TCSANOW, &orig_termios) {
Ok(_) => {} Ok(_) => {}
Err(e) => panic!("error {:?}", e), Err(e) => panic!("error {:?}", e),
}; };
@ -60,13 +61,19 @@ pub fn set_terminal_size_using_fd(fd: RawFd, columns: u16, rows: u16) {
unsafe { ioctl(fd, TIOCSWINSZ, &winsize) }; unsafe { ioctl(fd, TIOCSWINSZ, &winsize) };
} }
/// Handle some signals for the child process. This will loop until the child
/// process exits.
fn handle_command_exit(mut child: Child) { fn handle_command_exit(mut child: Child) {
// register the SIGINT signal (TODO handle more signals)
let signals = ::signal_hook::iterator::Signals::new(&[::signal_hook::SIGINT]).unwrap(); let signals = ::signal_hook::iterator::Signals::new(&[::signal_hook::SIGINT]).unwrap();
'handle_exit: loop { 'handle_exit: loop {
// test whether the child process has exited
match child.try_wait() { match child.try_wait() {
Ok(Some(_status)) => { Ok(Some(_status)) => {
// if the child process has exited, break outside of the loop
// and exit this function
// TODO: handle errors? // TODO: handle errors?
break; break 'handle_exit;
} }
Ok(None) => { Ok(None) => {
::std::thread::sleep(::std::time::Duration::from_millis(100)); ::std::thread::sleep(::std::time::Duration::from_millis(100));
@ -88,7 +95,21 @@ fn handle_command_exit(mut child: Child) {
} }
} }
fn spawn_terminal(file_to_open: Option<PathBuf>, orig_termios: Termios) -> (RawFd, RawFd) { /// Spawns a new terminal from the parent terminal with [`termios`](termios::Termios)
/// `orig_termios`.
///
/// If a `file_to_open` is given, the text editor specified by environment variable `EDITOR`
/// (or `VISUAL`, if `EDITOR` is not set) will be started in the new terminal, with the given
/// file open. If no file is given, the shell specified by environment variable `SHELL` will
/// be started in the new terminal.
///
/// # Panics
///
/// This function will panic if both the `EDITOR` and `VISUAL` environment variables are not
/// set.
// FIXME this should probably be split into different functions, or at least have less levels
// of indentation in some way
fn spawn_terminal(file_to_open: Option<PathBuf>, orig_termios: termios::Termios) -> (RawFd, RawFd) {
let (pid_primary, pid_secondary): (RawFd, RawFd) = { let (pid_primary, pid_secondary): (RawFd, RawFd) = {
match forkpty(None, Some(&orig_termios)) { match forkpty(None, Some(&orig_termios)) {
Ok(fork_pty_res) => { Ok(fork_pty_res) => {
@ -136,56 +157,75 @@ fn spawn_terminal(file_to_open: Option<PathBuf>, orig_termios: Termios) -> (RawF
#[derive(Clone)] #[derive(Clone)]
pub struct OsInputOutput { pub struct OsInputOutput {
orig_termios: Arc<Mutex<Termios>>, orig_termios: Arc<Mutex<termios::Termios>>,
} }
/// The `OsApi` trait represents an abstract interface to the features of an operating system that
/// Zellij requires.
pub trait OsApi: Send + Sync { pub trait OsApi: Send + Sync {
fn get_terminal_size_using_fd(&self, pid: RawFd) -> PositionAndSize; /// Returns the size of the terminal associated to file descriptor `fd`.
fn set_terminal_size_using_fd(&mut self, pid: RawFd, cols: u16, rows: u16); fn get_terminal_size_using_fd(&self, fd: RawFd) -> PositionAndSize;
fn set_raw_mode(&mut self, pid: RawFd); /// Sets the size of the terminal associated to file descriptor `fd`.
fn unset_raw_mode(&mut self, pid: RawFd); fn set_terminal_size_using_fd(&mut self, fd: RawFd, cols: u16, rows: u16);
/// Set the terminal associated to file descriptor `fd` to
/// [raw mode](https://en.wikipedia.org/wiki/Terminal_mode).
fn set_raw_mode(&mut self, fd: RawFd);
/// Set the terminal associated to file descriptor `fd` to
/// [cooked mode](https://en.wikipedia.org/wiki/Terminal_mode).
fn unset_raw_mode(&mut self, fd: RawFd);
/// Spawn a new terminal, with an optional file to open in a terminal program.
fn spawn_terminal(&mut self, file_to_open: Option<PathBuf>) -> (RawFd, RawFd); fn spawn_terminal(&mut self, file_to_open: Option<PathBuf>) -> (RawFd, RawFd);
fn read_from_tty_stdout(&mut self, pid: RawFd, buf: &mut [u8]) -> Result<usize, nix::Error>; /// Read bytes from the standard output of the virtual terminal referred to by `fd`.
fn write_to_tty_stdin(&mut self, pid: RawFd, buf: &mut [u8]) -> Result<usize, nix::Error>; fn read_from_tty_stdout(&mut self, fd: RawFd, buf: &mut [u8]) -> Result<usize, nix::Error>;
fn tcdrain(&mut self, pid: RawFd) -> Result<(), nix::Error>; /// Write bytes to the standard input of the virtual terminal referred to by `fd`.
fn write_to_tty_stdin(&mut self, fd: RawFd, buf: &mut [u8]) -> Result<usize, nix::Error>;
/// Wait until all output written to the object referred to by `fd` has been transmitted.
fn tcdrain(&mut self, fd: RawFd) -> Result<(), nix::Error>;
/// Terminate the process with process ID `pid`.
// FIXME `RawFd` is semantically the wrong type here. It should either be a raw libc::pid_t,
// or a nix::unistd::Pid. See `man kill.3`, nix::sys::signal::kill (both take an argument
// called `pid` and of type `pid_t`, and not `fd`)
fn kill(&mut self, pid: RawFd) -> Result<(), nix::Error>; fn kill(&mut self, pid: RawFd) -> Result<(), nix::Error>;
/// Returns the raw contents of standard input.
fn read_from_stdin(&self) -> Vec<u8>; fn read_from_stdin(&self) -> Vec<u8>;
fn get_stdout_writer(&self) -> Box<dyn Write>; /// Returns the writer that allows writing to standard output.
fn get_stdout_writer(&self) -> Box<dyn io::Write>;
/// Returns a [`Box`] pointer to this [`OsApi`] struct.
fn box_clone(&self) -> Box<dyn OsApi>; fn box_clone(&self) -> Box<dyn OsApi>;
} }
impl OsApi for OsInputOutput { impl OsApi for OsInputOutput {
fn get_terminal_size_using_fd(&self, pid: RawFd) -> PositionAndSize { fn get_terminal_size_using_fd(&self, fd: RawFd) -> PositionAndSize {
get_terminal_size_using_fd(pid) get_terminal_size_using_fd(fd)
} }
fn set_terminal_size_using_fd(&mut self, pid: RawFd, cols: u16, rows: u16) { fn set_terminal_size_using_fd(&mut self, fd: RawFd, cols: u16, rows: u16) {
set_terminal_size_using_fd(pid, cols, rows); set_terminal_size_using_fd(fd, cols, rows);
} }
fn set_raw_mode(&mut self, pid: RawFd) { fn set_raw_mode(&mut self, fd: RawFd) {
into_raw_mode(pid); into_raw_mode(fd);
} }
fn unset_raw_mode(&mut self, pid: RawFd) { fn unset_raw_mode(&mut self, fd: RawFd) {
let orig_termios = self.orig_termios.lock().unwrap(); let orig_termios = self.orig_termios.lock().unwrap();
unset_raw_mode(pid, orig_termios.clone()); unset_raw_mode(fd, orig_termios.clone());
} }
fn spawn_terminal(&mut self, file_to_open: Option<PathBuf>) -> (RawFd, RawFd) { fn spawn_terminal(&mut self, file_to_open: Option<PathBuf>) -> (RawFd, RawFd) {
let orig_termios = self.orig_termios.lock().unwrap(); let orig_termios = self.orig_termios.lock().unwrap();
spawn_terminal(file_to_open, orig_termios.clone()) spawn_terminal(file_to_open, orig_termios.clone())
} }
fn read_from_tty_stdout(&mut self, pid: RawFd, buf: &mut [u8]) -> Result<usize, nix::Error> { fn read_from_tty_stdout(&mut self, fd: RawFd, buf: &mut [u8]) -> Result<usize, nix::Error> {
read(pid, buf) unistd::read(fd, buf)
} }
fn write_to_tty_stdin(&mut self, pid: RawFd, buf: &mut [u8]) -> Result<usize, nix::Error> { fn write_to_tty_stdin(&mut self, fd: RawFd, buf: &mut [u8]) -> Result<usize, nix::Error> {
write(pid, buf) unistd::write(fd, buf)
} }
fn tcdrain(&mut self, pid: RawFd) -> Result<(), nix::Error> { fn tcdrain(&mut self, fd: RawFd) -> Result<(), nix::Error> {
tcdrain(pid) termios::tcdrain(fd)
} }
fn box_clone(&self) -> Box<dyn OsApi> { fn box_clone(&self) -> Box<dyn OsApi> {
Box::new((*self).clone()) Box::new((*self).clone())
} }
fn read_from_stdin(&self) -> Vec<u8> { fn read_from_stdin(&self) -> Vec<u8> {
let stdin = stdin(); let stdin = std::io::stdin();
let mut stdin = stdin.lock(); let mut stdin = stdin.lock();
let buffer = stdin.fill_buf().unwrap(); let buffer = stdin.fill_buf().unwrap();
let length = buffer.len(); let length = buffer.len();
@ -193,13 +233,13 @@ impl OsApi for OsInputOutput {
stdin.consume(length); stdin.consume(length);
read_bytes read_bytes
} }
fn get_stdout_writer(&self) -> Box<dyn Write> { fn get_stdout_writer(&self) -> Box<dyn io::Write> {
let stdout = ::std::io::stdout(); let stdout = ::std::io::stdout();
Box::new(stdout) Box::new(stdout)
} }
fn kill(&mut self, fd: RawFd) -> Result<(), nix::Error> { fn kill(&mut self, pid: RawFd) -> Result<(), nix::Error> {
kill(Pid::from_raw(fd), Some(Signal::SIGINT)).unwrap(); kill(Pid::from_raw(pid), Some(Signal::SIGINT)).unwrap();
waitpid(Pid::from_raw(fd), None).unwrap(); waitpid(Pid::from_raw(pid), None).unwrap();
Ok(()) Ok(())
} }
} }
@ -211,7 +251,7 @@ impl Clone for Box<dyn OsApi> {
} }
pub fn get_os_input() -> OsInputOutput { pub fn get_os_input() -> OsInputOutput {
let current_termios = tcgetattr(0).unwrap(); let current_termios = termios::tcgetattr(0).unwrap();
let orig_termios = Arc::new(Mutex::new(current_termios)); let orig_termios = Arc::new(Mutex::new(current_termios));
OsInputOutput { orig_termios } OsInputOutput { orig_termios }
} }

View file

@ -145,6 +145,7 @@ impl vte::Perform for VteEventSender {
} }
} }
/// Instructions related to PTYs (pseudoterminals).
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub enum PtyInstruction { pub enum PtyInstruction {
SpawnTerminal(Option<PathBuf>), SpawnTerminal(Option<PathBuf>),

View file

@ -1,3 +1,5 @@
//! Things related to [`Screen`]s.
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::os::unix::io::RawFd; use std::os::unix::io::RawFd;
use std::sync::mpsc::Receiver; use std::sync::mpsc::Receiver;
@ -10,15 +12,7 @@ use crate::tab::Tab;
use crate::{errors::ErrorContext, wasm_vm::PluginInstruction}; use crate::{errors::ErrorContext, wasm_vm::PluginInstruction};
use crate::{layout::Layout, panes::PaneId}; use crate::{layout::Layout, panes::PaneId};
/* /// Instructions that can be sent to the [`Screen`].
* Screen
*
* this holds multiple tabs, each one holding multiple panes
* it tracks the active tab and controls tab switching, all the rest
* is performed in Tab
*
*/
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum ScreenInstruction { pub enum ScreenInstruction {
Pty(RawFd, VteEvent), Pty(RawFd, VteEvent),
@ -53,19 +47,31 @@ pub enum ScreenInstruction {
CloseTab, CloseTab,
} }
/// 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 { pub struct Screen {
/// A [`ScreenInstruction`] and [`ErrorContext`] receiver.
pub receiver: Receiver<(ScreenInstruction, ErrorContext)>, pub receiver: Receiver<(ScreenInstruction, ErrorContext)>,
/// An optional maximal amount of panes allowed per [`Tab`] in this [`Screen`] instance.
max_panes: Option<usize>, max_panes: Option<usize>,
/// A map between this [`Screen`]'s tabs and their ID/key.
tabs: BTreeMap<usize, Tab>, tabs: BTreeMap<usize, Tab>,
/// A [`PtyInstruction`] and [`ErrorContext`] sender.
pub send_pty_instructions: SenderWithContext<PtyInstruction>, pub send_pty_instructions: SenderWithContext<PtyInstruction>,
/// A [`PluginInstruction`] and [`ErrorContext`] sender.
pub send_plugin_instructions: SenderWithContext<PluginInstruction>, pub send_plugin_instructions: SenderWithContext<PluginInstruction>,
/// An [`AppInstruction`] and [`ErrorContext`] sender.
pub send_app_instructions: SenderWithContext<AppInstruction>, pub send_app_instructions: SenderWithContext<AppInstruction>,
/// The full size of this [`Screen`].
full_screen_ws: PositionAndSize, full_screen_ws: PositionAndSize,
/// The index of this [`Screen`]'s active [`Tab`].
active_tab_index: Option<usize>, active_tab_index: Option<usize>,
/// The [`OsApi`] this [`Screen`] uses.
os_api: Box<dyn OsApi>, os_api: Box<dyn OsApi>,
} }
impl Screen { impl Screen {
/// Creates and returns a new [`Screen`].
pub fn new( pub fn new(
receive_screen_instructions: Receiver<(ScreenInstruction, ErrorContext)>, receive_screen_instructions: Receiver<(ScreenInstruction, ErrorContext)>,
send_pty_instructions: SenderWithContext<PtyInstruction>, send_pty_instructions: SenderWithContext<PtyInstruction>,
@ -87,8 +93,11 @@ impl Screen {
os_api, os_api,
} }
} }
/// Creates a new [`Tab`] in this [`Screen`], containing a single
/// [pane](crate::client::panes) with PTY file descriptor `pane_id`.
pub fn new_tab(&mut self, pane_id: RawFd) { pub fn new_tab(&mut self, pane_id: RawFd) {
let tab_index = self.get_next_tab_index(); let tab_index = self.get_new_tab_index();
let tab = Tab::new( let tab = Tab::new(
tab_index, tab_index,
&self.full_screen_ws, &self.full_screen_ws,
@ -103,13 +112,19 @@ impl Screen {
self.tabs.insert(tab_index, tab); self.tabs.insert(tab_index, tab);
self.render(); self.render();
} }
fn get_next_tab_index(&self) -> usize {
/// Returns the index where a new [`Tab`] should be created in this [`Screen`].
/// Currently, this is right after the last currently existing tab, or `0` if
/// no tabs exist in this screen yet.
fn get_new_tab_index(&self) -> usize {
if let Some(index) = self.tabs.keys().last() { if let Some(index) = self.tabs.keys().last() {
*index + 1 *index + 1
} else { } else {
0 0
} }
} }
/// Sets this [`Screen`]'s active [`Tab`] to the next tab.
pub fn switch_tab_next(&mut self) { pub fn switch_tab_next(&mut self) {
let active_tab_id = self.get_active_tab().unwrap().index; let active_tab_id = self.get_active_tab().unwrap().index;
let tab_ids: Vec<usize> = self.tabs.keys().copied().collect(); let tab_ids: Vec<usize> = self.tabs.keys().copied().collect();
@ -122,6 +137,8 @@ impl Screen {
} }
self.render(); self.render();
} }
/// Sets this [`Screen`]'s active [`Tab`] to the previous tab.
pub fn switch_tab_prev(&mut self) { pub fn switch_tab_prev(&mut self) {
let active_tab_id = self.get_active_tab().unwrap().index; let active_tab_id = self.get_active_tab().unwrap().index;
let tab_ids: Vec<usize> = self.tabs.keys().copied().collect(); let tab_ids: Vec<usize> = self.tabs.keys().copied().collect();
@ -136,6 +153,9 @@ impl Screen {
} }
self.render(); self.render();
} }
/// Closes this [`Screen`]'s active [`Tab`], exiting the application if it happens
/// to be the last tab.
pub fn close_tab(&mut self) { pub fn close_tab(&mut self) {
let active_tab_index = self.active_tab_index.unwrap(); let active_tab_index = self.active_tab_index.unwrap();
if self.tabs.len() > 1 { if self.tabs.len() > 1 {
@ -156,6 +176,8 @@ impl Screen {
.unwrap(); .unwrap();
} }
} }
/// Renders this [`Screen`], which amounts to rendering its active [`Tab`].
pub fn render(&mut self) { pub fn render(&mut self) {
if let Some(active_tab) = self.get_active_tab_mut() { if let Some(active_tab) = self.get_active_tab_mut() {
if active_tab.get_active_pane().is_some() { if active_tab.get_active_pane().is_some() {
@ -166,24 +188,31 @@ impl Screen {
}; };
} }
/// Returns a mutable reference to this [`Screen`]'s tabs.
pub fn get_tabs_mut(&mut self) -> &mut BTreeMap<usize, Tab> {
&mut self.tabs
}
/// Returns an immutable reference to this [`Screen`]'s active [`Tab`].
pub fn get_active_tab(&self) -> Option<&Tab> { pub fn get_active_tab(&self) -> Option<&Tab> {
match self.active_tab_index { match self.active_tab_index {
Some(tab) => self.tabs.get(&tab), Some(tab) => self.tabs.get(&tab),
None => None, None => None,
} }
} }
pub fn get_tabs_mut(&mut self) -> &mut BTreeMap<usize, Tab> {
&mut self.tabs /// Returns a mutable reference to this [`Screen`]'s active [`Tab`].
}
pub fn get_active_tab_mut(&mut self) -> Option<&mut Tab> { pub fn get_active_tab_mut(&mut self) -> Option<&mut Tab> {
let tab = match self.active_tab_index { match self.active_tab_index {
Some(tab) => self.get_tabs_mut().get_mut(&tab), Some(tab) => self.get_tabs_mut().get_mut(&tab),
None => None, None => None,
};
tab
} }
}
/// Creates a new [`Tab`] in this [`Screen`], applying the specified [`Layout`]
/// and switching to it.
pub fn apply_layout(&mut self, layout: Layout, new_pids: Vec<RawFd>) { pub fn apply_layout(&mut self, layout: Layout, new_pids: Vec<RawFd>) {
let tab_index = self.get_next_tab_index(); let tab_index = self.get_new_tab_index();
let mut tab = Tab::new( let mut tab = Tab::new(
tab_index, tab_index,
&self.full_screen_ws, &self.full_screen_ws,

View file

@ -1,3 +1,5 @@
//! Zellij program-wide constants.
pub const ZELLIJ_TMP_DIR: &str = "/tmp/zellij"; pub const ZELLIJ_TMP_DIR: &str = "/tmp/zellij";
pub const ZELLIJ_TMP_LOG_DIR: &str = "/tmp/zellij/zellij-log"; pub const ZELLIJ_TMP_LOG_DIR: &str = "/tmp/zellij/zellij-log";
pub const ZELLIJ_TMP_LOG_FILE: &str = "/tmp/zellij/zellij-log/log.txt"; pub const ZELLIJ_TMP_LOG_FILE: &str = "/tmp/zellij/zellij-log/log.txt";

View file

@ -1,3 +1,5 @@
//! Zellij logging utility functions.
use std::{ use std::{
fs, fs,
io::{self, prelude::*}, io::{self, prelude::*},

View file

@ -1,3 +1,5 @@
//! Zellij utilities.
pub mod consts; pub mod consts;
pub mod logging; pub mod logging;
pub mod shared; pub mod shared;

View file

@ -1,8 +1,10 @@
//! Some general utility functions.
use std::{iter, str::from_utf8}; use std::{iter, str::from_utf8};
use strip_ansi_escapes::strip; use strip_ansi_escapes::strip;
pub fn ansi_len(s: &str) -> usize { fn ansi_len(s: &str) -> usize {
from_utf8(&strip(s.as_bytes()).unwrap()) from_utf8(&strip(s.as_bytes()).unwrap())
.unwrap() .unwrap()
.chars() .chars()