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
*.new
.vscode
.vim

View File

@ -18,6 +18,9 @@ pub enum PaneId {
Terminal(RawFd),
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)]
pub struct PositionAndSize {
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::panes::{PaneId, PositionAndSize, TerminalPane};
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;
/*
* 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 MIN_TERMINAL_HEIGHT: usize = 2;
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 crate::pty_bus::PtyInstruction;
use crate::screen::ScreenInstruction;
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;
#[cfg(not(test))]
use super::SenderWithContext;
#[cfg(not(test))]
use std::panic::PanicInfo;
/// Custom panic handler/hook. Prints the [`ErrorContext`].
#[cfg(not(test))]
pub fn handle_panic(
info: &PanicInfo<'_>,
@ -66,18 +72,22 @@ pub fn handle_panic(
}
}
/// A representation of the call stack.
#[derive(Clone, Copy)]
pub struct ErrorContext {
calls: [ContextType; MAX_THREAD_CALL_STACK],
}
impl ErrorContext {
/// Returns a new, blank [`ErrorContext`] containing only [`Empty`](ContextType::Empty)
/// calls.
pub fn new() -> Self {
Self {
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) {
for ctx in self.calls.iter_mut() {
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)]
pub enum ContextType {
/// A screen-related call.
Screen(ScreenContext),
/// A PTY-related call.
Pty(PtyContext),
/// A plugin-related call.
Plugin(PluginContext),
/// An app-related call.
App(AppContext),
IPCServer,
StdinHandler,
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,
}
// TODO use the `colored` crate for color formatting
impl Display for ContextType {
fn fmt(&self, f: &mut Formatter) -> Result<(), Error> {
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)]
pub enum ScreenContext {
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)]
pub enum PtyContext {
SpawnTerminal,
@ -245,6 +268,7 @@ impl From<&PtyInstruction> for PtyContext {
use crate::wasm_vm::PluginInstruction;
/// Stack call representations corresponding to the different types of [`PluginInstruction`]s.
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum PluginContext {
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)]
pub enum AppContext {
GetState,

View File

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

View File

@ -1,7 +1,8 @@
//! Main input logic.
use super::actions::Action;
use super::keybinds::get_default_keybinds;
use crate::common::{update_state, AppInstruction, AppState, SenderWithContext, OPENCALLS};
/// Module for handling input
use crate::errors::ContextType;
use crate::os_input_output::OsApi;
use crate::pty_bus::PtyInstruction;
@ -16,8 +17,9 @@ use termion::input::TermReadEventsAndRaw;
use super::keybinds::key_to_actions;
/// 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 {
/// The current input mode
mode: InputMode,
os_input: Box<dyn OsApi>,
command_is_executing: CommandIsExecuting,
@ -28,6 +30,7 @@ struct InputHandler {
}
impl InputHandler {
/// Returns a new [`InputHandler`] with the attributes specified as arguments.
fn new(
os_input: Box<dyn OsApi>,
command_is_executing: CommandIsExecuting,
@ -47,10 +50,9 @@ impl InputHandler {
}
}
/// Main event loop. Interprets the terminal [`Event`](termion::event::Event)s
/// as [`Action`]s according to the current [`InputMode`], and dispatches those
/// actions.
fn get_input(&mut self) {
/// Main input event loop. Interprets the terminal [`Event`](termion::event::Event)s
/// as [`Action`]s according to the current [`InputMode`], and dispatches those actions.
fn handle_input(&mut self) {
let mut err_ctx = OPENCALLS.with(|ctx| *ctx.borrow());
err_ctx.add_call(ContextType::StdinHandler);
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 {
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
/// same as quitting zellij)
/// same as quitting Zellij).
fn exit(&mut self) {
self.send_app_instructions
.send(AppInstruction::Exit)
@ -228,28 +241,29 @@ impl InputHandler {
}
}
/// Dictates the input mode, which is 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
/// Describes the different input modes, which change the way that keystrokes will be interpreted.
#[derive(Debug, PartialEq, Eq, Hash, Copy, Clone, EnumIter, Serialize, Deserialize)]
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,
/// 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,
/// `Resize` mode allows resizing the different existing panes.
Resize,
/// `Pane` mode allows creating and closing panes, as well as moving between them.
Pane,
/// `Tab` mode allows creating and closing tabs, as well as moving between them.
Tab,
/// `Scroll` mode allows scrolling up and down within a pane.
Scroll,
Exiting,
}
/// Represents the help message that is printed in the status bar, indicating
/// the current [`InputMode`], whether that mode is persistent, and what the
/// keybinds for that mode are.
/// Represents the contents of the help message that is printed in the status bar,
/// which indicates the current [`InputMode`] and what the keybinds for that mode
/// are. Related to the default `status-bar` plugin.
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
pub struct Help {
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
pub fn get_help(mode: InputMode) -> Help {
let mut keybinds: Vec<(String, String)> = vec![];
match mode {
InputMode::Normal | InputMode::Command | InputMode::Exiting => {
InputMode::Normal | InputMode::Command => {
keybinds.push((format!("p"), format!("PANE")));
keybinds.push((format!("t"), format!("TAB")));
keybinds.push((format!("r"), format!("RESIZE")));
@ -299,8 +314,8 @@ pub fn get_help(mode: InputMode) -> Help {
Help { mode, keybinds }
}
/// Entry point to the module. Instantiates a new InputHandler and calls its
/// input loop.
/// Entry point to the module. Instantiates an [`InputHandler`] and starts
/// its [`InputHandler::handle_input()`] loop.
pub fn input_loop(
os_input: Box<dyn OsApi>,
command_is_executing: CommandIsExecuting,
@ -317,5 +332,5 @@ pub fn input_loop(
send_plugin_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::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)]);
}
InputMode::Exiting => {}
}
Ok(defaults)

View File

@ -1,3 +1,5 @@
//! The way terminal iput is handled.
pub mod actions;
pub mod handler;
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 std::collections::HashSet;

View File

@ -11,7 +11,7 @@ pub mod wasm_vm;
use std::io::Write;
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::{cell::RefCell, sync::mpsc::TrySendError};
use std::{collections::HashMap, fs};
@ -55,7 +55,7 @@ pub fn update_state(
app_tx: &SenderWithContext<AppInstruction>,
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)));
let state = state_rx.recv().unwrap();
@ -63,15 +63,28 @@ pub fn update_state(
drop(app_tx.send(AppInstruction::SetState(update_fn(state))))
}
pub type ChannelWithContext<T> = (Sender<(T, ErrorContext)>, Receiver<(T, ErrorContext)>);
pub type SyncChannelWithContext<T> = (SyncSender<(T, ErrorContext)>, Receiver<(T, ErrorContext)>);
/// 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)]
enum SenderType<T: Clone> {
Sender(Sender<(T, ErrorContext)>),
SyncSender(SyncSender<(T, ErrorContext)>),
/// 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> {
err_ctx: ErrorContext,
@ -83,13 +96,20 @@ impl<T: Clone> SenderWithContext<T> {
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 {
SenderType::Sender(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)>> {
if let SenderType::SyncSender(ref s) = self.sender {
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) {
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> 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)]
pub enum AppInstruction {
GetState(Sender<AppState>),
GetState(mpsc::Sender<AppState>),
SetState(AppState),
Exit,
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) {
let take_snapshot = "\u{1b}[?1049h";
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);
let (send_screen_instructions, receive_screen_instructions): ChannelWithContext<
ScreenInstruction,
> = channel();
> = mpsc::channel();
let err_ctx = OPENCALLS.with(|ctx| *ctx.borrow());
let mut send_screen_instructions =
SenderWithContext::new(err_ctx, SenderType::Sender(send_screen_instructions));
let (send_pty_instructions, receive_pty_instructions): ChannelWithContext<PtyInstruction> =
channel();
mpsc::channel();
let mut send_pty_instructions =
SenderWithContext::new(err_ctx, SenderType::Sender(send_pty_instructions));
let (send_plugin_instructions, receive_plugin_instructions): ChannelWithContext<
PluginInstruction,
> = channel();
> = mpsc::channel();
let send_plugin_instructions =
SenderWithContext::new(err_ctx, SenderType::Sender(send_plugin_instructions));
let (send_app_instructions, receive_app_instructions): SyncChannelWithContext<AppInstruction> =
sync_channel(0);
mpsc::sync_channel(0);
let 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::pty::{forkpty, Winsize};
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::unistd::{read, write, ForkResult, Pid};
use nix::unistd;
use nix::unistd::{ForkResult, Pid};
use std::io;
use std::io::prelude::*;
use std::io::{stdin, Write};
use std::os::unix::io::RawFd;
use std::path::PathBuf;
use std::process::{Child, Command};
@ -15,16 +16,16 @@ use std::sync::{Arc, Mutex};
use std::env;
fn into_raw_mode(pid: RawFd) {
let mut tio = tcgetattr(pid).expect("could not get terminal attribute");
cfmakeraw(&mut tio);
match tcsetattr(pid, SetArg::TCSANOW, &tio) {
let mut tio = termios::tcgetattr(pid).expect("could not get terminal attribute");
termios::cfmakeraw(&mut tio);
match termios::tcsetattr(pid, termios::SetArg::TCSANOW, &tio) {
Ok(_) => {}
Err(e) => panic!("error {:?}", e),
};
}
fn unset_raw_mode(pid: RawFd, orig_termios: Termios) {
match tcsetattr(pid, SetArg::TCSANOW, &orig_termios) {
fn unset_raw_mode(pid: RawFd, orig_termios: termios::Termios) {
match termios::tcsetattr(pid, termios::SetArg::TCSANOW, &orig_termios) {
Ok(_) => {}
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) };
}
/// Handle some signals for the child process. This will loop until the child
/// process exits.
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();
'handle_exit: loop {
// test whether the child process has exited
match child.try_wait() {
Ok(Some(_status)) => {
// if the child process has exited, break outside of the loop
// and exit this function
// TODO: handle errors?
break;
break 'handle_exit;
}
Ok(None) => {
::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) = {
match forkpty(None, Some(&orig_termios)) {
Ok(fork_pty_res) => {
@ -136,56 +157,75 @@ fn spawn_terminal(file_to_open: Option<PathBuf>, orig_termios: Termios) -> (RawF
#[derive(Clone)]
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 {
fn get_terminal_size_using_fd(&self, pid: RawFd) -> PositionAndSize;
fn set_terminal_size_using_fd(&mut self, pid: RawFd, cols: u16, rows: u16);
fn set_raw_mode(&mut self, pid: RawFd);
fn unset_raw_mode(&mut self, pid: RawFd);
/// Returns the size of the terminal associated to file descriptor `fd`.
fn get_terminal_size_using_fd(&self, fd: RawFd) -> PositionAndSize;
/// Sets the size of the terminal associated to file descriptor `fd`.
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 read_from_tty_stdout(&mut self, pid: RawFd, buf: &mut [u8]) -> Result<usize, nix::Error>;
fn write_to_tty_stdin(&mut self, pid: RawFd, buf: &mut [u8]) -> Result<usize, nix::Error>;
fn tcdrain(&mut self, pid: RawFd) -> Result<(), nix::Error>;
/// Read bytes from the standard output of the virtual terminal referred to by `fd`.
fn read_from_tty_stdout(&mut self, fd: RawFd, buf: &mut [u8]) -> Result<usize, 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>;
/// Returns the raw contents of standard input.
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>;
}
impl OsApi for OsInputOutput {
fn get_terminal_size_using_fd(&self, pid: RawFd) -> PositionAndSize {
get_terminal_size_using_fd(pid)
fn get_terminal_size_using_fd(&self, fd: RawFd) -> PositionAndSize {
get_terminal_size_using_fd(fd)
}
fn set_terminal_size_using_fd(&mut self, pid: RawFd, cols: u16, rows: u16) {
set_terminal_size_using_fd(pid, cols, rows);
fn set_terminal_size_using_fd(&mut self, fd: RawFd, cols: u16, rows: u16) {
set_terminal_size_using_fd(fd, cols, rows);
}
fn set_raw_mode(&mut self, pid: RawFd) {
into_raw_mode(pid);
fn set_raw_mode(&mut self, fd: RawFd) {
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();
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) {
let orig_termios = self.orig_termios.lock().unwrap();
spawn_terminal(file_to_open, orig_termios.clone())
}
fn read_from_tty_stdout(&mut self, pid: RawFd, buf: &mut [u8]) -> Result<usize, nix::Error> {
read(pid, buf)
fn read_from_tty_stdout(&mut self, fd: RawFd, buf: &mut [u8]) -> Result<usize, nix::Error> {
unistd::read(fd, buf)
}
fn write_to_tty_stdin(&mut self, pid: RawFd, buf: &mut [u8]) -> Result<usize, nix::Error> {
write(pid, buf)
fn write_to_tty_stdin(&mut self, fd: RawFd, buf: &mut [u8]) -> Result<usize, nix::Error> {
unistd::write(fd, buf)
}
fn tcdrain(&mut self, pid: RawFd) -> Result<(), nix::Error> {
tcdrain(pid)
fn tcdrain(&mut self, fd: RawFd) -> Result<(), nix::Error> {
termios::tcdrain(fd)
}
fn box_clone(&self) -> Box<dyn OsApi> {
Box::new((*self).clone())
}
fn read_from_stdin(&self) -> Vec<u8> {
let stdin = stdin();
let stdin = std::io::stdin();
let mut stdin = stdin.lock();
let buffer = stdin.fill_buf().unwrap();
let length = buffer.len();
@ -193,13 +233,13 @@ impl OsApi for OsInputOutput {
stdin.consume(length);
read_bytes
}
fn get_stdout_writer(&self) -> Box<dyn Write> {
fn get_stdout_writer(&self) -> Box<dyn io::Write> {
let stdout = ::std::io::stdout();
Box::new(stdout)
}
fn kill(&mut self, fd: RawFd) -> Result<(), nix::Error> {
kill(Pid::from_raw(fd), Some(Signal::SIGINT)).unwrap();
waitpid(Pid::from_raw(fd), None).unwrap();
fn kill(&mut self, pid: RawFd) -> Result<(), nix::Error> {
kill(Pid::from_raw(pid), Some(Signal::SIGINT)).unwrap();
waitpid(Pid::from_raw(pid), None).unwrap();
Ok(())
}
}
@ -211,7 +251,7 @@ impl Clone for Box<dyn OsApi> {
}
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));
OsInputOutput { orig_termios }
}

View File

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

View File

@ -1,3 +1,5 @@
//! Things related to [`Screen`]s.
use std::collections::BTreeMap;
use std::os::unix::io::RawFd;
use std::sync::mpsc::Receiver;
@ -10,15 +12,7 @@ use crate::tab::Tab;
use crate::{errors::ErrorContext, wasm_vm::PluginInstruction};
use crate::{layout::Layout, panes::PaneId};
/*
* 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
*
*/
/// Instructions that can be sent to the [`Screen`].
#[derive(Debug, Clone)]
pub enum ScreenInstruction {
Pty(RawFd, VteEvent),
@ -53,19 +47,31 @@ pub enum ScreenInstruction {
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 {
/// A [`ScreenInstruction`] and [`ErrorContext`] receiver.
pub receiver: Receiver<(ScreenInstruction, ErrorContext)>,
/// 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 [`PtyInstruction`] and [`ErrorContext`] sender.
pub send_pty_instructions: SenderWithContext<PtyInstruction>,
/// A [`PluginInstruction`] and [`ErrorContext`] sender.
pub send_plugin_instructions: SenderWithContext<PluginInstruction>,
/// An [`AppInstruction`] and [`ErrorContext`] sender.
pub send_app_instructions: SenderWithContext<AppInstruction>,
/// The full size of this [`Screen`].
full_screen_ws: PositionAndSize,
/// The index of this [`Screen`]'s active [`Tab`].
active_tab_index: Option<usize>,
/// The [`OsApi`] this [`Screen`] uses.
os_api: Box<dyn OsApi>,
}
impl Screen {
/// Creates and returns a new [`Screen`].
pub fn new(
receive_screen_instructions: Receiver<(ScreenInstruction, ErrorContext)>,
send_pty_instructions: SenderWithContext<PtyInstruction>,
@ -87,8 +93,11 @@ impl Screen {
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) {
let tab_index = self.get_next_tab_index();
let tab_index = self.get_new_tab_index();
let tab = Tab::new(
tab_index,
&self.full_screen_ws,
@ -103,13 +112,19 @@ impl Screen {
self.tabs.insert(tab_index, tab);
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() {
*index + 1
} else {
0
}
}
/// Sets this [`Screen`]'s active [`Tab`] to the next tab.
pub fn switch_tab_next(&mut self) {
let active_tab_id = self.get_active_tab().unwrap().index;
let tab_ids: Vec<usize> = self.tabs.keys().copied().collect();
@ -122,6 +137,8 @@ impl Screen {
}
self.render();
}
/// Sets this [`Screen`]'s active [`Tab`] to the previous tab.
pub fn switch_tab_prev(&mut self) {
let active_tab_id = self.get_active_tab().unwrap().index;
let tab_ids: Vec<usize> = self.tabs.keys().copied().collect();
@ -136,6 +153,9 @@ impl Screen {
}
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) {
let active_tab_index = self.active_tab_index.unwrap();
if self.tabs.len() > 1 {
@ -156,6 +176,8 @@ impl Screen {
.unwrap();
}
}
/// Renders this [`Screen`], which amounts to rendering its active [`Tab`].
pub fn render(&mut self) {
if let Some(active_tab) = self.get_active_tab_mut() {
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> {
match self.active_tab_index {
Some(tab) => self.tabs.get(&tab),
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> {
let tab = match self.active_tab_index {
match self.active_tab_index {
Some(tab) => self.get_tabs_mut().get_mut(&tab),
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>) {
let tab_index = self.get_next_tab_index();
let tab_index = self.get_new_tab_index();
let mut tab = Tab::new(
tab_index,
&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_LOG_DIR: &str = "/tmp/zellij/zellij-log";
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::{
fs,
io::{self, prelude::*},

View File

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

View File

@ -1,8 +1,10 @@
//! Some general utility functions.
use std::{iter, str::from_utf8};
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())
.unwrap()
.chars()