isolate pty thread
This commit is contained in:
parent
03f8e7220c
commit
588cdaa008
11 changed files with 557 additions and 349 deletions
|
|
@ -2,7 +2,7 @@ use super::common::utils::consts::{ZELLIJ_CONFIG_DIR_ENV, ZELLIJ_CONFIG_FILE_ENV
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use structopt::StructOpt;
|
use structopt::StructOpt;
|
||||||
|
|
||||||
#[derive(StructOpt, Default, Debug)]
|
#[derive(StructOpt, Debug, Default, Clone)]
|
||||||
#[structopt(name = "zellij")]
|
#[structopt(name = "zellij")]
|
||||||
pub struct CliArgs {
|
pub struct CliArgs {
|
||||||
/// Send "split (direction h == horizontal / v == vertical)" to active zellij session
|
/// Send "split (direction h == horizontal / v == vertical)" to active zellij session
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
use crate::tab::Pane;
|
use crate::tab::Pane;
|
||||||
use ::nix::pty::Winsize;
|
use ::nix::pty::Winsize;
|
||||||
use ::std::os::unix::io::RawFd;
|
use ::std::os::unix::io::RawFd;
|
||||||
|
use ::vte::Perform;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
use std::fmt::Debug;
|
use std::fmt::Debug;
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
|
|
||||||
|
|
@ -10,7 +12,7 @@ use crate::panes::terminal_character::{
|
||||||
};
|
};
|
||||||
use crate::pty_bus::VteBytes;
|
use crate::pty_bus::VteBytes;
|
||||||
|
|
||||||
#[derive(PartialEq, Eq, Ord, PartialOrd, Hash, Clone, Copy, Debug)]
|
#[derive(PartialEq, Eq, Ord, PartialOrd, Hash, Clone, Copy, Debug, Serialize, Deserialize)]
|
||||||
pub enum PaneId {
|
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?
|
||||||
|
|
|
||||||
|
|
@ -69,7 +69,6 @@ pub struct Tab {
|
||||||
fullscreen_is_active: bool,
|
fullscreen_is_active: bool,
|
||||||
synchronize_is_active: bool,
|
synchronize_is_active: bool,
|
||||||
os_api: Box<dyn OsApi>,
|
os_api: Box<dyn OsApi>,
|
||||||
pub send_pty_instructions: SenderWithContext<PtyInstruction>,
|
|
||||||
pub send_plugin_instructions: SenderWithContext<PluginInstruction>,
|
pub send_plugin_instructions: SenderWithContext<PluginInstruction>,
|
||||||
pub send_app_instructions: SenderWithContext<AppInstruction>,
|
pub send_app_instructions: SenderWithContext<AppInstruction>,
|
||||||
should_clear_display_before_rendering: bool,
|
should_clear_display_before_rendering: bool,
|
||||||
|
|
@ -226,7 +225,6 @@ impl Tab {
|
||||||
name: String,
|
name: String,
|
||||||
full_screen_ws: &PositionAndSize,
|
full_screen_ws: &PositionAndSize,
|
||||||
mut os_api: Box<dyn OsApi>,
|
mut os_api: Box<dyn OsApi>,
|
||||||
send_pty_instructions: SenderWithContext<PtyInstruction>,
|
|
||||||
send_plugin_instructions: SenderWithContext<PluginInstruction>,
|
send_plugin_instructions: SenderWithContext<PluginInstruction>,
|
||||||
send_app_instructions: SenderWithContext<AppInstruction>,
|
send_app_instructions: SenderWithContext<AppInstruction>,
|
||||||
max_panes: Option<usize>,
|
max_panes: Option<usize>,
|
||||||
|
|
@ -261,7 +259,6 @@ impl Tab {
|
||||||
synchronize_is_active: false,
|
synchronize_is_active: false,
|
||||||
os_api,
|
os_api,
|
||||||
send_app_instructions,
|
send_app_instructions,
|
||||||
send_pty_instructions,
|
|
||||||
send_plugin_instructions,
|
send_plugin_instructions,
|
||||||
should_clear_display_before_rendering: false,
|
should_clear_display_before_rendering: false,
|
||||||
mode_info,
|
mode_info,
|
||||||
|
|
@ -354,8 +351,10 @@ impl Tab {
|
||||||
// this is a bit of a hack and happens because we don't have any central location that
|
// 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
|
// 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
|
// fixing this will require a bit of an architecture change
|
||||||
self.send_pty_instructions
|
self.send_app_instructions
|
||||||
.send(PtyInstruction::ClosePane(PaneId::Terminal(*unused_pid)))
|
.send(AppInstruction::ToPty(PtyInstruction::ClosePane(
|
||||||
|
PaneId::Terminal(*unused_pid),
|
||||||
|
)))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
self.active_terminal = self.panes.iter().map(|(id, _)| id.to_owned()).next();
|
self.active_terminal = self.panes.iter().map(|(id, _)| id.to_owned()).next();
|
||||||
|
|
@ -399,8 +398,8 @@ impl Tab {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
if terminal_id_to_split.is_none() {
|
if terminal_id_to_split.is_none() {
|
||||||
self.send_pty_instructions
|
self.send_app_instructions
|
||||||
.send(PtyInstruction::ClosePane(pid)) // we can't open this pane, close the pty
|
.send(AppInstruction::ToPty(PtyInstruction::ClosePane(pid))) // we can't open this pane, close the pty
|
||||||
.unwrap();
|
.unwrap();
|
||||||
return; // likely no terminal large enough to split
|
return; // likely no terminal large enough to split
|
||||||
}
|
}
|
||||||
|
|
@ -475,24 +474,25 @@ impl Tab {
|
||||||
self.panes.insert(pid, Box::new(new_terminal));
|
self.panes.insert(pid, Box::new(new_terminal));
|
||||||
self.active_terminal = Some(pid);
|
self.active_terminal = Some(pid);
|
||||||
}
|
}
|
||||||
} else if let PaneId::Terminal(term_pid) = pid {
|
} else {
|
||||||
// TODO: check minimum size of active terminal
|
// FIXME: This could use a second look
|
||||||
let active_pane_id = &self.get_active_pane_id().unwrap();
|
if let PaneId::Terminal(term_pid) = pid {
|
||||||
let active_pane = self.panes.get_mut(active_pane_id).unwrap();
|
// TODO: check minimum size of active terminal
|
||||||
if active_pane.rows() < MIN_TERMINAL_HEIGHT * 2 + 1 {
|
let active_pane_id = &self.get_active_pane_id().unwrap();
|
||||||
self.send_pty_instructions
|
let active_pane = self.panes.get_mut(active_pane_id).unwrap();
|
||||||
.send(PtyInstruction::ClosePane(pid)) // we can't open this pane, close the pty
|
if active_pane.rows() < MIN_TERMINAL_HEIGHT * 2 + 1 {
|
||||||
.unwrap();
|
self.send_app_instructions
|
||||||
return;
|
.send(AppInstruction::ToPty(PtyInstruction::ClosePane(pid))) // we can't open this pane, close the pty
|
||||||
}
|
.unwrap();
|
||||||
let terminal_ws = PositionAndSize {
|
return;
|
||||||
x: active_pane.x(),
|
}
|
||||||
y: active_pane.y(),
|
let terminal_ws = PositionAndSize {
|
||||||
rows: active_pane.rows(),
|
x: active_pane.x(),
|
||||||
columns: active_pane.columns(),
|
y: active_pane.y(),
|
||||||
..Default::default()
|
rows: active_pane.rows(),
|
||||||
};
|
columns: active_pane.columns(),
|
||||||
let (top_winsize, bottom_winsize) = split_horizontally_with_gap(&terminal_ws);
|
};
|
||||||
|
let (top_winsize, bottom_winsize) = split_horizontally_with_gap(&terminal_ws);
|
||||||
|
|
||||||
active_pane.change_pos_and_size(&top_winsize);
|
active_pane.change_pos_and_size(&top_winsize);
|
||||||
|
|
||||||
|
|
@ -532,26 +532,25 @@ impl Tab {
|
||||||
self.panes.insert(pid, Box::new(new_terminal));
|
self.panes.insert(pid, Box::new(new_terminal));
|
||||||
self.active_terminal = Some(pid);
|
self.active_terminal = Some(pid);
|
||||||
}
|
}
|
||||||
} else if let PaneId::Terminal(term_pid) = pid {
|
} else {
|
||||||
// TODO: check minimum size of active terminal
|
// FIXME: This could use a second look
|
||||||
let active_pane_id = &self.get_active_pane_id().unwrap();
|
if let PaneId::Terminal(term_pid) = pid {
|
||||||
let active_pane = self.panes.get_mut(active_pane_id).unwrap();
|
// TODO: check minimum size of active terminal
|
||||||
if active_pane.columns() < MIN_TERMINAL_WIDTH * 2 + 1 {
|
let active_pane_id = &self.get_active_pane_id().unwrap();
|
||||||
self.send_pty_instructions
|
let active_pane = self.panes.get_mut(active_pane_id).unwrap();
|
||||||
.send(PtyInstruction::ClosePane(pid)) // we can't open this pane, close the pty
|
if active_pane.columns() < MIN_TERMINAL_WIDTH * 2 + 1 {
|
||||||
.unwrap();
|
self.send_app_instructions
|
||||||
return;
|
.send(AppInstruction::ToPty(PtyInstruction::ClosePane(pid))) // we can't open this pane, close the pty
|
||||||
}
|
.unwrap();
|
||||||
let terminal_ws = PositionAndSize {
|
return;
|
||||||
x: active_pane.x(),
|
}
|
||||||
y: active_pane.y(),
|
let terminal_ws = PositionAndSize {
|
||||||
rows: active_pane.rows(),
|
x: active_pane.x(),
|
||||||
columns: active_pane.columns(),
|
y: active_pane.y(),
|
||||||
..Default::default()
|
rows: active_pane.rows(),
|
||||||
};
|
columns: active_pane.columns(),
|
||||||
let (left_winsize, right_winsize) = split_vertically_with_gap(&terminal_ws);
|
};
|
||||||
|
let (left_winsize, right_winsize) = split_vertically_with_gap(&terminal_ws);
|
||||||
active_pane.change_pos_and_size(&left_winsize);
|
|
||||||
|
|
||||||
let new_terminal = TerminalPane::new(term_pid, right_winsize);
|
let new_terminal = TerminalPane::new(term_pid, right_winsize);
|
||||||
self.os_api.set_terminal_size_using_fd(
|
self.os_api.set_terminal_size_using_fd(
|
||||||
|
|
@ -2105,8 +2104,8 @@ impl Tab {
|
||||||
if let Some(max_panes) = self.max_panes {
|
if let Some(max_panes) = self.max_panes {
|
||||||
let terminals = self.get_pane_ids();
|
let terminals = self.get_pane_ids();
|
||||||
for &pid in terminals.iter().skip(max_panes - 1) {
|
for &pid in terminals.iter().skip(max_panes - 1) {
|
||||||
self.send_pty_instructions
|
self.send_app_instructions
|
||||||
.send(PtyInstruction::ClosePane(pid))
|
.send(AppInstruction::ToPty(PtyInstruction::ClosePane(pid)))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
self.close_pane_without_rerender(pid);
|
self.close_pane_without_rerender(pid);
|
||||||
}
|
}
|
||||||
|
|
@ -2217,8 +2216,10 @@ impl Tab {
|
||||||
pub fn close_focused_pane(&mut self) {
|
pub fn close_focused_pane(&mut self) {
|
||||||
if let Some(active_pane_id) = self.get_active_pane_id() {
|
if let Some(active_pane_id) = self.get_active_pane_id() {
|
||||||
self.close_pane(active_pane_id);
|
self.close_pane(active_pane_id);
|
||||||
self.send_pty_instructions
|
self.send_app_instructions
|
||||||
.send(PtyInstruction::ClosePane(active_pane_id))
|
.send(AppInstruction::ToPty(PtyInstruction::ClosePane(
|
||||||
|
active_pane_id,
|
||||||
|
)))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
use super::{AppInstruction, ASYNCOPENCALLS, OPENCALLS};
|
use super::{AppInstruction, ASYNCOPENCALLS, OPENCALLS};
|
||||||
use crate::pty_bus::PtyInstruction;
|
use crate::pty_bus::PtyInstruction;
|
||||||
use crate::screen::ScreenInstruction;
|
use crate::screen::ScreenInstruction;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use std::fmt::{Display, Error, Formatter};
|
use std::fmt::{Display, Error, Formatter};
|
||||||
|
|
||||||
|
|
@ -79,7 +80,7 @@ pub fn get_current_ctx() -> ErrorContext {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A representation of the call stack.
|
/// A representation of the call stack.
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy, Serialize, Deserialize)]
|
||||||
pub struct ErrorContext {
|
pub struct ErrorContext {
|
||||||
calls: [ContextType; MAX_THREAD_CALL_STACK],
|
calls: [ContextType; MAX_THREAD_CALL_STACK],
|
||||||
}
|
}
|
||||||
|
|
@ -131,7 +132,7 @@ impl Display for ErrorContext {
|
||||||
/// Complex variants store a variant of a related enum, whose variants can be built from
|
/// Complex variants store a variant of a related enum, whose variants can be built from
|
||||||
/// the corresponding Zellij MSPC instruction enum variants ([`ScreenInstruction`],
|
/// the corresponding Zellij MSPC instruction enum variants ([`ScreenInstruction`],
|
||||||
/// [`PtyInstruction`], [`AppInstruction`], etc).
|
/// [`PtyInstruction`], [`AppInstruction`], etc).
|
||||||
#[derive(Copy, Clone, PartialEq)]
|
#[derive(Copy, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
pub enum ContextType {
|
pub enum ContextType {
|
||||||
/// A screen-related call.
|
/// A screen-related call.
|
||||||
Screen(ScreenContext),
|
Screen(ScreenContext),
|
||||||
|
|
@ -141,7 +142,7 @@ pub enum ContextType {
|
||||||
Plugin(PluginContext),
|
Plugin(PluginContext),
|
||||||
/// An app-related call.
|
/// An app-related call.
|
||||||
App(AppContext),
|
App(AppContext),
|
||||||
IpcServer,
|
IPCServer, // Fix: Create a separate ServerContext when sessions are introduced
|
||||||
StdinHandler,
|
StdinHandler,
|
||||||
AsyncTask,
|
AsyncTask,
|
||||||
/// An empty, placeholder call. This should be thought of as representing no call at all.
|
/// An empty, placeholder call. This should be thought of as representing no call at all.
|
||||||
|
|
@ -174,7 +175,7 @@ impl Display for ContextType {
|
||||||
|
|
||||||
// FIXME: Just deriving EnumDiscriminants from strum will remove the need for any of this!!!
|
// FIXME: Just deriving EnumDiscriminants from strum will remove the need for any of this!!!
|
||||||
/// Stack call representations corresponding to the different types of [`ScreenInstruction`]s.
|
/// Stack call representations corresponding to the different types of [`ScreenInstruction`]s.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
||||||
pub enum ScreenContext {
|
pub enum ScreenContext {
|
||||||
HandlePtyBytes,
|
HandlePtyBytes,
|
||||||
Render,
|
Render,
|
||||||
|
|
@ -267,7 +268,7 @@ impl From<&ScreenInstruction> for ScreenContext {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Stack call representations corresponding to the different types of [`PtyInstruction`]s.
|
/// Stack call representations corresponding to the different types of [`PtyInstruction`]s.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
||||||
pub enum PtyContext {
|
pub enum PtyContext {
|
||||||
SpawnTerminal,
|
SpawnTerminal,
|
||||||
SpawnTerminalVertically,
|
SpawnTerminalVertically,
|
||||||
|
|
@ -297,7 +298,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.
|
/// Stack call representations corresponding to the different types of [`PluginInstruction`]s.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
||||||
pub enum PluginContext {
|
pub enum PluginContext {
|
||||||
Load,
|
Load,
|
||||||
Update,
|
Update,
|
||||||
|
|
@ -319,10 +320,13 @@ impl From<&PluginInstruction> for PluginContext {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Stack call representations corresponding to the different types of [`AppInstruction`]s.
|
/// Stack call representations corresponding to the different types of [`AppInstruction`]s.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
||||||
pub enum AppContext {
|
pub enum AppContext {
|
||||||
Exit,
|
Exit,
|
||||||
Error,
|
Error,
|
||||||
|
ToPty,
|
||||||
|
ToPlugin,
|
||||||
|
ToScreen,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<&AppInstruction> for AppContext {
|
impl From<&AppInstruction> for AppContext {
|
||||||
|
|
@ -330,6 +334,9 @@ impl From<&AppInstruction> for AppContext {
|
||||||
match *app_instruction {
|
match *app_instruction {
|
||||||
AppInstruction::Exit => AppContext::Exit,
|
AppInstruction::Exit => AppContext::Exit,
|
||||||
AppInstruction::Error(_) => AppContext::Error,
|
AppInstruction::Error(_) => AppContext::Error,
|
||||||
|
AppInstruction::ToPty(_) => AppContext::ToPty,
|
||||||
|
AppInstruction::ToPlugin(_) => AppContext::ToPlugin,
|
||||||
|
AppInstruction::ToScreen(_) => AppContext::ToScreen,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,6 @@ struct InputHandler {
|
||||||
config: Config,
|
config: Config,
|
||||||
command_is_executing: CommandIsExecuting,
|
command_is_executing: CommandIsExecuting,
|
||||||
send_screen_instructions: SenderWithContext<ScreenInstruction>,
|
send_screen_instructions: SenderWithContext<ScreenInstruction>,
|
||||||
send_pty_instructions: SenderWithContext<PtyInstruction>,
|
|
||||||
send_plugin_instructions: SenderWithContext<PluginInstruction>,
|
send_plugin_instructions: SenderWithContext<PluginInstruction>,
|
||||||
send_app_instructions: SenderWithContext<AppInstruction>,
|
send_app_instructions: SenderWithContext<AppInstruction>,
|
||||||
should_exit: bool,
|
should_exit: bool,
|
||||||
|
|
@ -36,7 +35,6 @@ impl InputHandler {
|
||||||
command_is_executing: CommandIsExecuting,
|
command_is_executing: CommandIsExecuting,
|
||||||
config: Config,
|
config: Config,
|
||||||
send_screen_instructions: SenderWithContext<ScreenInstruction>,
|
send_screen_instructions: SenderWithContext<ScreenInstruction>,
|
||||||
send_pty_instructions: SenderWithContext<PtyInstruction>,
|
|
||||||
send_plugin_instructions: SenderWithContext<PluginInstruction>,
|
send_plugin_instructions: SenderWithContext<PluginInstruction>,
|
||||||
send_app_instructions: SenderWithContext<AppInstruction>,
|
send_app_instructions: SenderWithContext<AppInstruction>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
|
|
@ -46,7 +44,6 @@ impl InputHandler {
|
||||||
config,
|
config,
|
||||||
command_is_executing,
|
command_is_executing,
|
||||||
send_screen_instructions,
|
send_screen_instructions,
|
||||||
send_pty_instructions,
|
|
||||||
send_plugin_instructions,
|
send_plugin_instructions,
|
||||||
send_app_instructions,
|
send_app_instructions,
|
||||||
should_exit: false,
|
should_exit: false,
|
||||||
|
|
@ -58,25 +55,34 @@ impl InputHandler {
|
||||||
fn handle_input(&mut self) {
|
fn handle_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);
|
||||||
let alt_left_bracket = vec![27, 91];
|
self.send_app_instructions.update(err_ctx);
|
||||||
loop {
|
self.send_screen_instructions.update(err_ctx);
|
||||||
if self.should_exit {
|
if let Ok(keybinds) = get_default_keybinds() {
|
||||||
break;
|
'input_loop: loop {
|
||||||
}
|
//@@@ I think this should actually just iterate over stdin directly
|
||||||
let stdin_buffer = self.os_input.read_from_stdin();
|
let stdin_buffer = self.os_input.read_from_stdin();
|
||||||
for key_result in stdin_buffer.events_and_raw() {
|
for key_result in stdin_buffer.events_and_raw() {
|
||||||
match key_result {
|
match key_result {
|
||||||
Ok((event, raw_bytes)) => match event {
|
Ok((event, raw_bytes)) => match event {
|
||||||
termion::event::Event::Key(key) => {
|
termion::event::Event::Key(key) => {
|
||||||
let key = cast_termion_key(key);
|
let key = cast_termion_key(key);
|
||||||
self.handle_key(&key, raw_bytes);
|
// FIXME this explicit break is needed because the current test
|
||||||
}
|
// framework relies on it to not create dead threads that loop
|
||||||
termion::event::Event::Unsupported(unsupported_key) => {
|
// and eat up CPUs. Do not remove until the test framework has
|
||||||
// we have to do this because of a bug in termion
|
// been revised. Sorry about this (@categorille)
|
||||||
// this should be a key event and not an unsupported event
|
let mut should_break = false;
|
||||||
if unsupported_key == alt_left_bracket {
|
for action in key_to_actions(&key, raw_bytes, &self.mode, &keybinds)
|
||||||
let key = Key::Alt('[');
|
{
|
||||||
self.handle_key(&key, raw_bytes);
|
should_break |= self.dispatch_action(action);
|
||||||
|
}
|
||||||
|
if should_break {
|
||||||
|
break 'input_loop;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
termion::event::Event::Mouse(_)
|
||||||
|
| termion::event::Event::Unsupported(_) => {
|
||||||
|
// Mouse and unsupported events aren't implemented yet,
|
||||||
|
// use a NoOp untill then.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
termion::event::Event::Mouse(_) => {
|
termion::event::Event::Mouse(_) => {
|
||||||
|
|
@ -220,7 +226,9 @@ impl InputHandler {
|
||||||
None => PtyInstruction::SpawnTerminal(None),
|
None => PtyInstruction::SpawnTerminal(None),
|
||||||
};
|
};
|
||||||
self.command_is_executing.opening_new_pane();
|
self.command_is_executing.opening_new_pane();
|
||||||
self.send_pty_instructions.send(pty_instr).unwrap();
|
self.send_app_instructions
|
||||||
|
.send(AppInstruction::ToPty(pty_instr))
|
||||||
|
.unwrap();
|
||||||
self.command_is_executing.wait_until_new_pane_is_opened();
|
self.command_is_executing.wait_until_new_pane_is_opened();
|
||||||
}
|
}
|
||||||
Action::CloseFocus => {
|
Action::CloseFocus => {
|
||||||
|
|
@ -232,8 +240,8 @@ impl InputHandler {
|
||||||
}
|
}
|
||||||
Action::NewTab => {
|
Action::NewTab => {
|
||||||
self.command_is_executing.opening_new_pane();
|
self.command_is_executing.opening_new_pane();
|
||||||
self.send_pty_instructions
|
self.send_app_instructions
|
||||||
.send(PtyInstruction::NewTab)
|
.send(AppInstruction::ToPty(PtyInstruction::NewTab))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
self.command_is_executing.wait_until_new_pane_is_opened();
|
self.command_is_executing.wait_until_new_pane_is_opened();
|
||||||
}
|
}
|
||||||
|
|
@ -332,7 +340,6 @@ pub fn input_loop(
|
||||||
config: Config,
|
config: Config,
|
||||||
command_is_executing: CommandIsExecuting,
|
command_is_executing: CommandIsExecuting,
|
||||||
send_screen_instructions: SenderWithContext<ScreenInstruction>,
|
send_screen_instructions: SenderWithContext<ScreenInstruction>,
|
||||||
send_pty_instructions: SenderWithContext<PtyInstruction>,
|
|
||||||
send_plugin_instructions: SenderWithContext<PluginInstruction>,
|
send_plugin_instructions: SenderWithContext<PluginInstruction>,
|
||||||
send_app_instructions: SenderWithContext<AppInstruction>,
|
send_app_instructions: SenderWithContext<AppInstruction>,
|
||||||
) {
|
) {
|
||||||
|
|
@ -341,7 +348,6 @@ pub fn input_loop(
|
||||||
command_is_executing,
|
command_is_executing,
|
||||||
config,
|
config,
|
||||||
send_screen_instructions,
|
send_screen_instructions,
|
||||||
send_pty_instructions,
|
|
||||||
send_plugin_instructions,
|
send_plugin_instructions,
|
||||||
send_app_instructions,
|
send_app_instructions,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,9 @@ pub mod setup;
|
||||||
pub mod utils;
|
pub mod utils;
|
||||||
pub mod wasm_vm;
|
pub mod wasm_vm;
|
||||||
|
|
||||||
use std::cell::RefCell;
|
use std::io::Write;
|
||||||
use std::path::PathBuf;
|
use std::os::unix::net::UnixStream;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::mpsc;
|
use std::sync::mpsc;
|
||||||
use std::thread;
|
use std::thread;
|
||||||
use std::{collections::HashMap, fs};
|
use std::{collections::HashMap, fs};
|
||||||
|
|
@ -23,19 +24,12 @@ use std::{
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::cli::CliArgs;
|
use crate::cli::CliArgs;
|
||||||
use crate::common::input::config::Config;
|
use crate::server::start_server;
|
||||||
use crate::layout::Layout;
|
|
||||||
use crate::panes::PaneId;
|
|
||||||
use async_std::task_local;
|
|
||||||
use command_is_executing::CommandIsExecuting;
|
use command_is_executing::CommandIsExecuting;
|
||||||
use directories_next::ProjectDirs;
|
use errors::{AppContext, ContextType, ErrorContext, PluginContext, ScreenContext};
|
||||||
use errors::{
|
|
||||||
get_current_ctx, AppContext, ContextType, ErrorContext, PluginContext, PtyContext,
|
|
||||||
ScreenContext,
|
|
||||||
};
|
|
||||||
use input::handler::input_loop;
|
use input::handler::input_loop;
|
||||||
use os_input_output::OsApi;
|
use os_input_output::OsApi;
|
||||||
use pty_bus::{PtyBus, PtyInstruction};
|
use pty_bus::PtyInstruction;
|
||||||
use screen::{Screen, ScreenInstruction};
|
use screen::{Screen, ScreenInstruction};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use setup::install;
|
use setup::install;
|
||||||
|
|
@ -45,12 +39,36 @@ use wasmer::{ChainableNamedResolver, Instance, Module, Store, Value};
|
||||||
use wasmer_wasi::{Pipe, WasiState};
|
use wasmer_wasi::{Pipe, WasiState};
|
||||||
use zellij_tile::data::{EventType, InputMode, ModeInfo};
|
use zellij_tile::data::{EventType, InputMode, ModeInfo};
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
pub enum ApiCommand {
|
pub enum ApiCommand {
|
||||||
OpenFile(PathBuf),
|
OpenFile(PathBuf),
|
||||||
SplitHorizontally,
|
SplitHorizontally,
|
||||||
SplitVertically,
|
SplitVertically,
|
||||||
MoveFocus,
|
MoveFocus,
|
||||||
|
ToPty(PtyInstruction),
|
||||||
|
ToScreen(ScreenInstruction),
|
||||||
|
ClosePluginPane(u32),
|
||||||
|
Quit,
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: It would be good to add some more things to this over time
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct AppState {
|
||||||
|
pub input_mode: InputMode,
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: Make this a method on the big `Communication` struct, so that app_tx can be extracted
|
||||||
|
// from self instead of being explicitly passed here
|
||||||
|
pub fn update_state(
|
||||||
|
app_tx: &SenderWithContext<AppInstruction>,
|
||||||
|
update_fn: impl FnOnce(AppState) -> AppState,
|
||||||
|
) {
|
||||||
|
let (state_tx, state_rx) = mpsc::channel();
|
||||||
|
|
||||||
|
drop(app_tx.send(AppInstruction::GetState(state_tx)));
|
||||||
|
let state = state_rx.recv().unwrap();
|
||||||
|
|
||||||
|
drop(app_tx.send(AppInstruction::SetState(update_fn(state))))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An [MPSC](mpsc) asynchronous channel with added error context.
|
/// An [MPSC](mpsc) asynchronous channel with added error context.
|
||||||
|
|
@ -66,7 +84,7 @@ pub type SyncChannelWithContext<T> = (
|
||||||
|
|
||||||
/// Wrappers around the two standard [MPSC](mpsc) sender types, [`mpsc::Sender`] and [`mpsc::SyncSender`], with an additional [`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> {
|
pub enum SenderType<T: Clone> {
|
||||||
/// A wrapper around an [`mpsc::Sender`], adding an [`ErrorContext`].
|
/// A wrapper around an [`mpsc::Sender`], adding an [`ErrorContext`].
|
||||||
Sender(mpsc::Sender<(T, ErrorContext)>),
|
Sender(mpsc::Sender<(T, ErrorContext)>),
|
||||||
/// A wrapper around an [`mpsc::SyncSender`], adding an [`ErrorContext`].
|
/// A wrapper around an [`mpsc::SyncSender`], adding an [`ErrorContext`].
|
||||||
|
|
@ -81,8 +99,8 @@ pub struct SenderWithContext<T: Clone> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: Clone> SenderWithContext<T> {
|
impl<T: Clone> SenderWithContext<T> {
|
||||||
fn new(sender: SenderType<T>) -> Self {
|
pub fn new(err_ctx: ErrorContext, sender: SenderType<T>) -> Self {
|
||||||
Self { sender }
|
Self { err_ctx, sender }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sends an event, along with the current [`ErrorContext`], on this
|
/// Sends an event, along with the current [`ErrorContext`], on this
|
||||||
|
|
@ -116,6 +134,9 @@ task_local! {
|
||||||
pub enum AppInstruction {
|
pub enum AppInstruction {
|
||||||
Exit,
|
Exit,
|
||||||
Error(String),
|
Error(String),
|
||||||
|
ToPty(PtyInstruction),
|
||||||
|
ToScreen(ScreenInstruction),
|
||||||
|
ToPlugin(PluginInstruction),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Start Zellij with the specified [`OsApi`] and command-line arguments.
|
/// Start Zellij with the specified [`OsApi`] and command-line arguments.
|
||||||
|
|
@ -140,10 +161,6 @@ pub fn start(mut os_input: Box<dyn OsApi>, opts: CliArgs, config: Config) {
|
||||||
let send_screen_instructions =
|
let send_screen_instructions =
|
||||||
SenderWithContext::new(SenderType::Sender(send_screen_instructions));
|
SenderWithContext::new(SenderType::Sender(send_screen_instructions));
|
||||||
|
|
||||||
let (send_pty_instructions, receive_pty_instructions): ChannelWithContext<PtyInstruction> =
|
|
||||||
mpsc::channel();
|
|
||||||
let send_pty_instructions = SenderWithContext::new(SenderType::Sender(send_pty_instructions));
|
|
||||||
|
|
||||||
let (send_plugin_instructions, receive_plugin_instructions): ChannelWithContext<
|
let (send_plugin_instructions, receive_plugin_instructions): ChannelWithContext<
|
||||||
PluginInstruction,
|
PluginInstruction,
|
||||||
> = mpsc::channel();
|
> = mpsc::channel();
|
||||||
|
|
@ -155,31 +172,13 @@ pub fn start(mut os_input: Box<dyn OsApi>, opts: CliArgs, config: Config) {
|
||||||
let send_app_instructions =
|
let send_app_instructions =
|
||||||
SenderWithContext::new(SenderType::SyncSender(send_app_instructions));
|
SenderWithContext::new(SenderType::SyncSender(send_app_instructions));
|
||||||
|
|
||||||
let mut pty_bus = PtyBus::new(
|
let ipc_thread = start_server(
|
||||||
receive_pty_instructions,
|
|
||||||
send_screen_instructions.clone(),
|
|
||||||
send_plugin_instructions.clone(),
|
|
||||||
os_input.clone(),
|
os_input.clone(),
|
||||||
opts.debug,
|
opts.clone(),
|
||||||
|
command_is_executing.clone(),
|
||||||
|
send_app_instructions.clone(),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Determine and initialize the data directory
|
|
||||||
let project_dirs = ProjectDirs::from("org", "Zellij Contributors", "Zellij").unwrap();
|
|
||||||
let data_dir = opts
|
|
||||||
.data_dir
|
|
||||||
.unwrap_or_else(|| project_dirs.data_dir().to_path_buf());
|
|
||||||
install::populate_data_dir(&data_dir);
|
|
||||||
|
|
||||||
// Don't use default layouts in tests, but do everywhere else
|
|
||||||
#[cfg(not(test))]
|
|
||||||
let default_layout = Some(PathBuf::from("default"));
|
|
||||||
#[cfg(test)]
|
|
||||||
let default_layout = None;
|
|
||||||
let maybe_layout = opts
|
|
||||||
.layout
|
|
||||||
.map(|p| Layout::new(&p, &data_dir))
|
|
||||||
.or_else(|| default_layout.map(|p| Layout::from_defaults(&p, &data_dir)));
|
|
||||||
|
|
||||||
#[cfg(not(test))]
|
#[cfg(not(test))]
|
||||||
std::panic::set_hook({
|
std::panic::set_hook({
|
||||||
use crate::errors::handle_panic;
|
use crate::errors::handle_panic;
|
||||||
|
|
@ -189,72 +188,11 @@ pub fn start(mut os_input: Box<dyn OsApi>, opts: CliArgs, config: Config) {
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
let pty_thread = thread::Builder::new()
|
|
||||||
.name("pty".to_string())
|
|
||||||
.spawn({
|
|
||||||
let mut command_is_executing = command_is_executing.clone();
|
|
||||||
send_pty_instructions.send(PtyInstruction::NewTab).unwrap();
|
|
||||||
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);
|
|
||||||
command_is_executing.done_closing_pane();
|
|
||||||
}
|
|
||||||
PtyInstruction::CloseTab(ids) => {
|
|
||||||
pty_bus.close_tab(ids);
|
|
||||||
command_is_executing.done_closing_pane();
|
|
||||||
}
|
|
||||||
PtyInstruction::Quit => {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let screen_thread = thread::Builder::new()
|
let screen_thread = thread::Builder::new()
|
||||||
.name("screen".to_string())
|
.name("screen".to_string())
|
||||||
.spawn({
|
.spawn({
|
||||||
let mut command_is_executing = command_is_executing.clone();
|
let mut command_is_executing = command_is_executing.clone();
|
||||||
let os_input = os_input.clone();
|
let os_input = os_input.clone();
|
||||||
let send_pty_instructions = send_pty_instructions.clone();
|
|
||||||
let send_plugin_instructions = send_plugin_instructions.clone();
|
let send_plugin_instructions = send_plugin_instructions.clone();
|
||||||
let send_app_instructions = send_app_instructions.clone();
|
let send_app_instructions = send_app_instructions.clone();
|
||||||
let max_panes = opts.max_panes;
|
let max_panes = opts.max_panes;
|
||||||
|
|
@ -262,7 +200,6 @@ pub fn start(mut os_input: Box<dyn OsApi>, opts: CliArgs, config: Config) {
|
||||||
move || {
|
move || {
|
||||||
let mut screen = Screen::new(
|
let mut screen = Screen::new(
|
||||||
receive_screen_instructions,
|
receive_screen_instructions,
|
||||||
send_pty_instructions,
|
|
||||||
send_plugin_instructions,
|
send_plugin_instructions,
|
||||||
send_app_instructions,
|
send_app_instructions,
|
||||||
&full_screen_ws,
|
&full_screen_ws,
|
||||||
|
|
@ -281,6 +218,7 @@ pub fn start(mut os_input: Box<dyn OsApi>, opts: CliArgs, config: Config) {
|
||||||
.recv()
|
.recv()
|
||||||
.expect("failed to receive event on channel");
|
.expect("failed to receive event on channel");
|
||||||
err_ctx.add_call(ContextType::Screen(ScreenContext::from(&event)));
|
err_ctx.add_call(ContextType::Screen(ScreenContext::from(&event)));
|
||||||
|
screen.send_app_instructions.update(err_ctx);
|
||||||
match event {
|
match event {
|
||||||
ScreenInstruction::PtyBytes(pid, vte_bytes) => {
|
ScreenInstruction::PtyBytes(pid, vte_bytes) => {
|
||||||
let active_tab = screen.get_active_tab_mut().unwrap();
|
let active_tab = screen.get_active_tab_mut().unwrap();
|
||||||
|
|
@ -460,10 +398,8 @@ pub fn start(mut os_input: Box<dyn OsApi>, opts: CliArgs, config: Config) {
|
||||||
let wasm_thread = thread::Builder::new()
|
let wasm_thread = thread::Builder::new()
|
||||||
.name("wasm".to_string())
|
.name("wasm".to_string())
|
||||||
.spawn({
|
.spawn({
|
||||||
let send_pty_instructions = send_pty_instructions.clone();
|
let mut send_screen_instructions = send_screen_instructions.clone();
|
||||||
let send_screen_instructions = send_screen_instructions.clone();
|
let mut send_app_instructions = send_app_instructions.clone();
|
||||||
let send_app_instructions = send_app_instructions.clone();
|
|
||||||
let send_plugin_instructions = send_plugin_instructions.clone();
|
|
||||||
|
|
||||||
let store = Store::default();
|
let store = Store::default();
|
||||||
let mut plugin_id = 0;
|
let mut plugin_id = 0;
|
||||||
|
|
@ -473,6 +409,8 @@ pub fn start(mut os_input: Box<dyn OsApi>, opts: CliArgs, config: Config) {
|
||||||
.recv()
|
.recv()
|
||||||
.expect("failed to receive event on channel");
|
.expect("failed to receive event on channel");
|
||||||
err_ctx.add_call(ContextType::Plugin(PluginContext::from(&event)));
|
err_ctx.add_call(ContextType::Plugin(PluginContext::from(&event)));
|
||||||
|
send_screen_instructions.update(err_ctx);
|
||||||
|
send_app_instructions.update(err_ctx);
|
||||||
match event {
|
match event {
|
||||||
PluginInstruction::Load(pid_tx, path) => {
|
PluginInstruction::Load(pid_tx, path) => {
|
||||||
let plugin_dir = data_dir.join("plugins/");
|
let plugin_dir = data_dir.join("plugins/");
|
||||||
|
|
@ -505,7 +443,6 @@ pub fn start(mut os_input: Box<dyn OsApi>, opts: CliArgs, config: Config) {
|
||||||
|
|
||||||
let plugin_env = PluginEnv {
|
let plugin_env = PluginEnv {
|
||||||
plugin_id,
|
plugin_id,
|
||||||
send_pty_instructions: send_pty_instructions.clone(),
|
|
||||||
send_screen_instructions: send_screen_instructions.clone(),
|
send_screen_instructions: send_screen_instructions.clone(),
|
||||||
send_app_instructions: send_app_instructions.clone(),
|
send_app_instructions: send_app_instructions.clone(),
|
||||||
send_plugin_instructions: send_plugin_instructions.clone(),
|
send_plugin_instructions: send_plugin_instructions.clone(),
|
||||||
|
|
@ -556,83 +493,10 @@ pub fn start(mut os_input: Box<dyn OsApi>, opts: CliArgs, config: Config) {
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let _signal_thread = thread::Builder::new()
|
|
||||||
.name("signal_listener".to_string())
|
|
||||||
.spawn({
|
|
||||||
let os_input = os_input.clone();
|
|
||||||
let send_screen_instructions = send_screen_instructions.clone();
|
|
||||||
move || {
|
|
||||||
os_input.receive_sigwinch(Box::new(move || {
|
|
||||||
let _ = send_screen_instructions.send(ScreenInstruction::TerminalResize);
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
// TODO: currently we don't wait for this to quit
|
|
||||||
// because otherwise the app will hang. Need to fix this so it both
|
|
||||||
// listens to the ipc-bus and is able to quit cleanly
|
|
||||||
#[cfg(not(test))]
|
|
||||||
let _ipc_thread = thread::Builder::new()
|
|
||||||
.name("ipc_server".to_string())
|
|
||||||
.spawn({
|
|
||||||
use std::io::Read;
|
|
||||||
let send_pty_instructions = send_pty_instructions.clone();
|
|
||||||
let send_screen_instructions = send_screen_instructions.clone();
|
|
||||||
move || {
|
|
||||||
std::fs::remove_file(ZELLIJ_IPC_PIPE).ok();
|
|
||||||
let listener = std::os::unix::net::UnixListener::bind(ZELLIJ_IPC_PIPE)
|
|
||||||
.expect("could not listen on ipc socket");
|
|
||||||
let mut err_ctx = OPENCALLS.with(|ctx| *ctx.borrow());
|
|
||||||
err_ctx.add_call(ContextType::IpcServer);
|
|
||||||
|
|
||||||
for stream in listener.incoming() {
|
|
||||||
match stream {
|
|
||||||
Ok(mut stream) => {
|
|
||||||
let mut buffer = [0; 65535]; // TODO: more accurate
|
|
||||||
let _ = stream
|
|
||||||
.read(&mut buffer)
|
|
||||||
.expect("failed to parse ipc message");
|
|
||||||
let decoded: ApiCommand = bincode::deserialize(&buffer)
|
|
||||||
.expect("failed to deserialize ipc message");
|
|
||||||
match &decoded {
|
|
||||||
ApiCommand::OpenFile(file_name) => {
|
|
||||||
let path = PathBuf::from(file_name);
|
|
||||||
send_pty_instructions
|
|
||||||
.send(PtyInstruction::SpawnTerminal(Some(path)))
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
ApiCommand::SplitHorizontally => {
|
|
||||||
send_pty_instructions
|
|
||||||
.send(PtyInstruction::SpawnTerminalHorizontally(None))
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
ApiCommand::SplitVertically => {
|
|
||||||
send_pty_instructions
|
|
||||||
.send(PtyInstruction::SpawnTerminalVertically(None))
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
ApiCommand::MoveFocus => {
|
|
||||||
send_screen_instructions
|
|
||||||
.send(ScreenInstruction::FocusNextPane)
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
panic!("err {:?}", err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let _stdin_thread = thread::Builder::new()
|
let _stdin_thread = thread::Builder::new()
|
||||||
.name("stdin_handler".to_string())
|
.name("stdin_handler".to_string())
|
||||||
.spawn({
|
.spawn({
|
||||||
let send_screen_instructions = send_screen_instructions.clone();
|
let send_screen_instructions = send_screen_instructions.clone();
|
||||||
let send_pty_instructions = send_pty_instructions.clone();
|
|
||||||
let send_plugin_instructions = send_plugin_instructions.clone();
|
let send_plugin_instructions = send_plugin_instructions.clone();
|
||||||
let os_input = os_input.clone();
|
let os_input = os_input.clone();
|
||||||
let config = config;
|
let config = config;
|
||||||
|
|
@ -642,13 +506,14 @@ pub fn start(mut os_input: Box<dyn OsApi>, opts: CliArgs, config: Config) {
|
||||||
config,
|
config,
|
||||||
command_is_executing,
|
command_is_executing,
|
||||||
send_screen_instructions,
|
send_screen_instructions,
|
||||||
send_pty_instructions,
|
|
||||||
send_plugin_instructions,
|
send_plugin_instructions,
|
||||||
send_app_instructions,
|
send_app_instructions,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let mut server_stream = UnixStream::connect(ZELLIJ_IPC_PIPE).unwrap();
|
||||||
|
|
||||||
#[warn(clippy::never_loop)]
|
#[warn(clippy::never_loop)]
|
||||||
loop {
|
loop {
|
||||||
let (app_instruction, mut err_ctx) = receive_app_instructions
|
let (app_instruction, mut err_ctx) = receive_app_instructions
|
||||||
|
|
@ -656,15 +521,17 @@ pub fn start(mut os_input: Box<dyn OsApi>, opts: CliArgs, config: Config) {
|
||||||
.expect("failed to receive app instruction on channel");
|
.expect("failed to receive app instruction on channel");
|
||||||
|
|
||||||
err_ctx.add_call(ContextType::App(AppContext::from(&app_instruction)));
|
err_ctx.add_call(ContextType::App(AppContext::from(&app_instruction)));
|
||||||
|
send_screen_instructions.update(err_ctx);
|
||||||
match app_instruction {
|
match app_instruction {
|
||||||
AppInstruction::Exit => {
|
AppInstruction::Exit => {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
AppInstruction::Error(backtrace) => {
|
AppInstruction::Error(backtrace) => {
|
||||||
|
let api_command = bincode::serialize(&(err_ctx, ApiCommand::Quit)).unwrap();
|
||||||
|
server_stream.write_all(&api_command).unwrap();
|
||||||
|
let _ = ipc_thread.join();
|
||||||
let _ = send_screen_instructions.send(ScreenInstruction::Quit);
|
let _ = send_screen_instructions.send(ScreenInstruction::Quit);
|
||||||
let _ = screen_thread.join();
|
let _ = screen_thread.join();
|
||||||
let _ = send_pty_instructions.send(PtyInstruction::Quit);
|
|
||||||
let _ = pty_thread.join();
|
|
||||||
let _ = send_plugin_instructions.send(PluginInstruction::Quit);
|
let _ = send_plugin_instructions.send(PluginInstruction::Quit);
|
||||||
let _ = wasm_thread.join();
|
let _ = wasm_thread.join();
|
||||||
os_input.unset_raw_mode(0);
|
os_input.unset_raw_mode(0);
|
||||||
|
|
@ -680,11 +547,23 @@ pub fn start(mut os_input: Box<dyn OsApi>, opts: CliArgs, config: Config) {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
std::process::exit(1);
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
|
AppInstruction::ToScreen(instruction) => {
|
||||||
|
send_screen_instructions.send(instruction).unwrap();
|
||||||
|
}
|
||||||
|
AppInstruction::ToPlugin(instruction) => {
|
||||||
|
send_plugin_instructions.send(instruction).unwrap();
|
||||||
|
}
|
||||||
|
AppInstruction::ToPty(instruction) => {
|
||||||
|
let api_command =
|
||||||
|
bincode::serialize(&(err_ctx, ApiCommand::ToPty(instruction))).unwrap();
|
||||||
|
server_stream.write_all(&api_command).unwrap();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let _ = send_pty_instructions.send(PtyInstruction::Quit);
|
let api_command = bincode::serialize(&(err_ctx, ApiCommand::Quit)).unwrap();
|
||||||
pty_thread.join().unwrap();
|
server_stream.write_all(&api_command).unwrap();
|
||||||
|
let _ = ipc_thread.join().unwrap();
|
||||||
let _ = send_screen_instructions.send(ScreenInstruction::Quit);
|
let _ = send_screen_instructions.send(ScreenInstruction::Quit);
|
||||||
screen_thread.join().unwrap();
|
screen_thread.join().unwrap();
|
||||||
let _ = send_plugin_instructions.send(PluginInstruction::Quit);
|
let _ = send_plugin_instructions.send(PluginInstruction::Quit);
|
||||||
|
|
|
||||||
|
|
@ -6,16 +6,21 @@ use ::std::os::unix::io::RawFd;
|
||||||
use ::std::pin::*;
|
use ::std::pin::*;
|
||||||
use ::std::sync::mpsc::Receiver;
|
use ::std::sync::mpsc::Receiver;
|
||||||
use ::std::time::{Duration, Instant};
|
use ::std::time::{Duration, Instant};
|
||||||
|
use ::vte;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::io::Write;
|
||||||
|
use std::os::unix::net::UnixStream;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use super::{ScreenInstruction, SenderWithContext};
|
use super::{ScreenInstruction, OPENCALLS};
|
||||||
|
use crate::layout::Layout;
|
||||||
use crate::os_input_output::OsApi;
|
use crate::os_input_output::OsApi;
|
||||||
use crate::utils::logging::debug_to_file;
|
use crate::utils::{consts::ZELLIJ_IPC_PIPE, logging::debug_to_file};
|
||||||
use crate::{
|
use crate::{
|
||||||
errors::{get_current_ctx, ContextType, ErrorContext},
|
common::ApiCommand,
|
||||||
|
errors::{ContextType, ErrorContext},
|
||||||
panes::PaneId,
|
panes::PaneId,
|
||||||
};
|
};
|
||||||
use crate::{layout::Layout, wasm_vm::PluginInstruction};
|
|
||||||
|
|
||||||
pub struct ReadFromPid {
|
pub struct ReadFromPid {
|
||||||
pid: RawFd,
|
pid: RawFd,
|
||||||
|
|
@ -63,10 +68,114 @@ impl Stream for ReadFromPid {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type VteBytes = Vec<u8>;
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub enum VteEvent {
|
||||||
|
// TODO: try not to allocate Vecs
|
||||||
|
Print(char),
|
||||||
|
Execute(u8), // byte
|
||||||
|
Hook(Vec<i64>, Vec<u8>, bool, char), // params, intermediates, ignore, char
|
||||||
|
Put(u8), // byte
|
||||||
|
Unhook,
|
||||||
|
OscDispatch(Vec<Vec<u8>>, bool), // params, bell_terminated
|
||||||
|
CsiDispatch(Vec<i64>, Vec<u8>, bool, char), // params, intermediates, ignore, char
|
||||||
|
EscDispatch(Vec<u8>, bool, u8), // intermediates, ignore, byte
|
||||||
|
}
|
||||||
|
|
||||||
|
struct VteEventSender {
|
||||||
|
id: RawFd,
|
||||||
|
err_ctx: ErrorContext,
|
||||||
|
server_stream: UnixStream,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl VteEventSender {
|
||||||
|
pub fn new(id: RawFd, err_ctx: ErrorContext) -> Self {
|
||||||
|
VteEventSender {
|
||||||
|
id,
|
||||||
|
err_ctx,
|
||||||
|
server_stream: UnixStream::connect(ZELLIJ_IPC_PIPE).unwrap(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl vte::Perform for VteEventSender {
|
||||||
|
fn print(&mut self, c: char) {
|
||||||
|
let api_command = bincode::serialize(&(
|
||||||
|
self.err_ctx,
|
||||||
|
ApiCommand::ToScreen(ScreenInstruction::Pty(self.id, VteEvent::Print(c))),
|
||||||
|
))
|
||||||
|
.unwrap();
|
||||||
|
self.server_stream.write_all(&api_command).unwrap();
|
||||||
|
}
|
||||||
|
fn execute(&mut self, byte: u8) {
|
||||||
|
let api_command = bincode::serialize(&(
|
||||||
|
self.err_ctx,
|
||||||
|
ApiCommand::ToScreen(ScreenInstruction::Pty(self.id, VteEvent::Execute(byte))),
|
||||||
|
))
|
||||||
|
.unwrap();
|
||||||
|
self.server_stream.write_all(&api_command).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hook(&mut self, params: &[i64], intermediates: &[u8], ignore: bool, c: char) {
|
||||||
|
let params = params.iter().copied().collect();
|
||||||
|
let intermediates = intermediates.iter().copied().collect();
|
||||||
|
let instruction =
|
||||||
|
ScreenInstruction::Pty(self.id, VteEvent::Hook(params, intermediates, ignore, c));
|
||||||
|
let api_command =
|
||||||
|
bincode::serialize(&(self.err_ctx, ApiCommand::ToScreen(instruction))).unwrap();
|
||||||
|
self.server_stream.write_all(&api_command).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn put(&mut self, byte: u8) {
|
||||||
|
let api_command = bincode::serialize(&(
|
||||||
|
self.err_ctx,
|
||||||
|
ApiCommand::ToScreen(ScreenInstruction::Pty(self.id, VteEvent::Put(byte))),
|
||||||
|
))
|
||||||
|
.unwrap();
|
||||||
|
self.server_stream.write_all(&api_command).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unhook(&mut self) {
|
||||||
|
let api_command = bincode::serialize(&(
|
||||||
|
self.err_ctx,
|
||||||
|
ApiCommand::ToScreen(ScreenInstruction::Pty(self.id, VteEvent::Unhook)),
|
||||||
|
))
|
||||||
|
.unwrap();
|
||||||
|
self.server_stream.write_all(&api_command).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn osc_dispatch(&mut self, params: &[&[u8]], bell_terminated: bool) {
|
||||||
|
let params = params.iter().map(|p| p.to_vec()).collect();
|
||||||
|
let instruction =
|
||||||
|
ScreenInstruction::Pty(self.id, VteEvent::OscDispatch(params, bell_terminated));
|
||||||
|
let api_command =
|
||||||
|
bincode::serialize(&(self.err_ctx, ApiCommand::ToScreen(instruction))).unwrap();
|
||||||
|
self.server_stream.write_all(&api_command).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn csi_dispatch(&mut self, params: &[i64], intermediates: &[u8], ignore: bool, c: char) {
|
||||||
|
let params = params.iter().copied().collect();
|
||||||
|
let intermediates = intermediates.iter().copied().collect();
|
||||||
|
let instruction = ScreenInstruction::Pty(
|
||||||
|
self.id,
|
||||||
|
VteEvent::CsiDispatch(params, intermediates, ignore, c),
|
||||||
|
);
|
||||||
|
let api_command =
|
||||||
|
bincode::serialize(&(self.err_ctx, ApiCommand::ToScreen(instruction))).unwrap();
|
||||||
|
self.server_stream.write_all(&api_command).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn esc_dispatch(&mut self, intermediates: &[u8], ignore: bool, byte: u8) {
|
||||||
|
let intermediates = intermediates.iter().copied().collect();
|
||||||
|
let instruction =
|
||||||
|
ScreenInstruction::Pty(self.id, VteEvent::EscDispatch(intermediates, ignore, byte));
|
||||||
|
let api_command =
|
||||||
|
bincode::serialize(&(self.err_ctx, ApiCommand::ToScreen(instruction))).unwrap();
|
||||||
|
self.server_stream.write_all(&api_command).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Instructions related to PTYs (pseudoterminals).
|
/// Instructions related to PTYs (pseudoterminals).
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
pub enum PtyInstruction {
|
pub enum PtyInstruction {
|
||||||
SpawnTerminal(Option<PathBuf>),
|
SpawnTerminal(Option<PathBuf>),
|
||||||
SpawnTerminalVertically(Option<PathBuf>),
|
SpawnTerminalVertically(Option<PathBuf>),
|
||||||
|
|
@ -78,25 +187,22 @@ pub enum PtyInstruction {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct PtyBus {
|
pub struct PtyBus {
|
||||||
pub send_screen_instructions: SenderWithContext<ScreenInstruction>,
|
|
||||||
pub send_plugin_instructions: SenderWithContext<PluginInstruction>,
|
|
||||||
pub receive_pty_instructions: Receiver<(PtyInstruction, ErrorContext)>,
|
pub receive_pty_instructions: Receiver<(PtyInstruction, ErrorContext)>,
|
||||||
pub id_to_child_pid: HashMap<RawFd, RawFd>,
|
pub id_to_child_pid: HashMap<RawFd, RawFd>,
|
||||||
os_input: Box<dyn OsApi>,
|
os_input: Box<dyn OsApi>,
|
||||||
debug_to_file: bool,
|
debug_to_file: bool,
|
||||||
task_handles: HashMap<RawFd, JoinHandle<()>>,
|
task_handles: HashMap<RawFd, JoinHandle<()>>,
|
||||||
|
pub server_stream: UnixStream,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn stream_terminal_bytes(
|
fn stream_terminal_bytes(pid: RawFd, os_input: Box<dyn OsApi>, debug: bool) -> JoinHandle<()> {
|
||||||
pid: RawFd,
|
let mut err_ctx = OPENCALLS.with(|ctx| *ctx.borrow());
|
||||||
send_screen_instructions: SenderWithContext<ScreenInstruction>,
|
|
||||||
os_input: Box<dyn OsApi>,
|
|
||||||
debug: bool,
|
|
||||||
) -> JoinHandle<()> {
|
|
||||||
let mut err_ctx = get_current_ctx();
|
|
||||||
task::spawn({
|
task::spawn({
|
||||||
async move {
|
async move {
|
||||||
err_ctx.add_call(ContextType::AsyncTask);
|
err_ctx.add_call(ContextType::AsyncTask);
|
||||||
|
let mut server_stream = UnixStream::connect(ZELLIJ_IPC_PIPE).unwrap();
|
||||||
|
let mut vte_parser = vte::Parser::new();
|
||||||
|
let mut vte_event_sender = VteEventSender::new(pid, err_ctx);
|
||||||
let mut terminal_bytes = ReadFromPid::new(&pid, os_input);
|
let mut terminal_bytes = ReadFromPid::new(&pid, os_input);
|
||||||
|
|
||||||
let mut last_byte_receive_time: Option<Instant> = None;
|
let mut last_byte_receive_time: Option<Instant> = None;
|
||||||
|
|
@ -122,7 +228,12 @@ fn stream_terminal_bytes(
|
||||||
Some(receive_time) => {
|
Some(receive_time) => {
|
||||||
if receive_time.elapsed() > max_render_pause {
|
if receive_time.elapsed() > max_render_pause {
|
||||||
pending_render = false;
|
pending_render = false;
|
||||||
let _ = send_screen_instructions.send(ScreenInstruction::Render);
|
let api_command = bincode::serialize(&(
|
||||||
|
err_ctx,
|
||||||
|
ApiCommand::ToScreen(ScreenInstruction::Render),
|
||||||
|
))
|
||||||
|
.unwrap();
|
||||||
|
server_stream.write_all(&api_command).unwrap();
|
||||||
last_byte_receive_time = Some(Instant::now());
|
last_byte_receive_time = Some(Instant::now());
|
||||||
} else {
|
} else {
|
||||||
pending_render = true;
|
pending_render = true;
|
||||||
|
|
@ -136,22 +247,31 @@ fn stream_terminal_bytes(
|
||||||
} else {
|
} else {
|
||||||
if pending_render {
|
if pending_render {
|
||||||
pending_render = false;
|
pending_render = false;
|
||||||
let _ = send_screen_instructions.send(ScreenInstruction::Render);
|
let api_command = bincode::serialize(&(
|
||||||
|
err_ctx,
|
||||||
|
ApiCommand::ToScreen(ScreenInstruction::Render),
|
||||||
|
))
|
||||||
|
.unwrap();
|
||||||
|
server_stream.write_all(&api_command).unwrap();
|
||||||
}
|
}
|
||||||
last_byte_receive_time = None;
|
last_byte_receive_time = None;
|
||||||
task::sleep(::std::time::Duration::from_millis(10)).await;
|
task::sleep(::std::time::Duration::from_millis(10)).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
send_screen_instructions
|
let api_command =
|
||||||
.send(ScreenInstruction::Render)
|
bincode::serialize(&(err_ctx, ApiCommand::ToScreen(ScreenInstruction::Render)))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
server_stream.write_all(&api_command).unwrap();
|
||||||
#[cfg(not(test))]
|
#[cfg(not(test))]
|
||||||
// this is a little hacky, and is because the tests end the file as soon as
|
// 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
|
// 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
|
// a better solution would be to fix the test fakes, but this will do for now
|
||||||
send_screen_instructions
|
let api_command = bincode::serialize(&(
|
||||||
.send(ScreenInstruction::ClosePane(PaneId::Terminal(pid)))
|
err_ctx,
|
||||||
.unwrap();
|
ApiCommand::ToScreen(ScreenInstruction::ClosePane(PaneId::Terminal(pid))),
|
||||||
|
))
|
||||||
|
.unwrap();
|
||||||
|
server_stream.write_all(&api_command).unwrap();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -159,35 +279,29 @@ fn stream_terminal_bytes(
|
||||||
impl PtyBus {
|
impl PtyBus {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
receive_pty_instructions: Receiver<(PtyInstruction, ErrorContext)>,
|
receive_pty_instructions: Receiver<(PtyInstruction, ErrorContext)>,
|
||||||
send_screen_instructions: SenderWithContext<ScreenInstruction>,
|
|
||||||
send_plugin_instructions: SenderWithContext<PluginInstruction>,
|
|
||||||
os_input: Box<dyn OsApi>,
|
os_input: Box<dyn OsApi>,
|
||||||
|
server_stream: UnixStream,
|
||||||
debug_to_file: bool,
|
debug_to_file: bool,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
PtyBus {
|
PtyBus {
|
||||||
send_screen_instructions,
|
|
||||||
send_plugin_instructions,
|
|
||||||
receive_pty_instructions,
|
receive_pty_instructions,
|
||||||
os_input,
|
os_input,
|
||||||
id_to_child_pid: HashMap::new(),
|
id_to_child_pid: HashMap::new(),
|
||||||
debug_to_file,
|
debug_to_file,
|
||||||
task_handles: HashMap::new(),
|
task_handles: HashMap::new(),
|
||||||
|
server_stream,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn spawn_terminal(&mut self, file_to_open: Option<PathBuf>) -> RawFd {
|
pub fn spawn_terminal(&mut self, file_to_open: Option<PathBuf>) -> RawFd {
|
||||||
let (pid_primary, pid_secondary): (RawFd, RawFd) =
|
let (pid_primary, pid_secondary): (RawFd, RawFd) =
|
||||||
self.os_input.spawn_terminal(file_to_open);
|
self.os_input.spawn_terminal(file_to_open);
|
||||||
let task_handle = stream_terminal_bytes(
|
let task_handle =
|
||||||
pid_primary,
|
stream_terminal_bytes(pid_primary, self.os_input.clone(), self.debug_to_file);
|
||||||
self.send_screen_instructions.clone(),
|
|
||||||
self.os_input.clone(),
|
|
||||||
self.debug_to_file,
|
|
||||||
);
|
|
||||||
self.task_handles.insert(pid_primary, task_handle);
|
self.task_handles.insert(pid_primary, task_handle);
|
||||||
self.id_to_child_pid.insert(pid_primary, pid_secondary);
|
self.id_to_child_pid.insert(pid_primary, pid_secondary);
|
||||||
pid_primary
|
pid_primary
|
||||||
}
|
}
|
||||||
pub fn spawn_terminals_for_layout(&mut self, layout: Layout) {
|
pub fn spawn_terminals_for_layout(&mut self, layout: Layout, err_ctx: ErrorContext) {
|
||||||
let total_panes = layout.total_terminal_panes();
|
let total_panes = layout.total_terminal_panes();
|
||||||
let mut new_pane_pids = vec![];
|
let mut new_pane_pids = vec![];
|
||||||
for _ in 0..total_panes {
|
for _ in 0..total_panes {
|
||||||
|
|
@ -195,23 +309,21 @@ impl PtyBus {
|
||||||
self.id_to_child_pid.insert(pid_primary, pid_secondary);
|
self.id_to_child_pid.insert(pid_primary, pid_secondary);
|
||||||
new_pane_pids.push(pid_primary);
|
new_pane_pids.push(pid_primary);
|
||||||
}
|
}
|
||||||
self.send_screen_instructions
|
let api_command = bincode::serialize(&(
|
||||||
.send(ScreenInstruction::ApplyLayout((
|
err_ctx,
|
||||||
|
ApiCommand::ToScreen(ScreenInstruction::ApplyLayout((
|
||||||
layout,
|
layout,
|
||||||
new_pane_pids.clone(),
|
new_pane_pids.clone(),
|
||||||
)))
|
))),
|
||||||
.unwrap();
|
))
|
||||||
|
.unwrap();
|
||||||
|
self.server_stream.write_all(&api_command).unwrap();
|
||||||
for id in new_pane_pids {
|
for id in new_pane_pids {
|
||||||
let task_handle = stream_terminal_bytes(
|
let task_handle = stream_terminal_bytes(id, self.os_input.clone(), self.debug_to_file);
|
||||||
id,
|
|
||||||
self.send_screen_instructions.clone(),
|
|
||||||
self.os_input.clone(),
|
|
||||||
self.debug_to_file,
|
|
||||||
);
|
|
||||||
self.task_handles.insert(id, task_handle);
|
self.task_handles.insert(id, task_handle);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn close_pane(&mut self, id: PaneId) {
|
pub fn close_pane(&mut self, id: PaneId, err_ctx: ErrorContext) {
|
||||||
match id {
|
match id {
|
||||||
PaneId::Terminal(id) => {
|
PaneId::Terminal(id) => {
|
||||||
let child_pid = self.id_to_child_pid.remove(&id).unwrap();
|
let child_pid = self.id_to_child_pid.remove(&id).unwrap();
|
||||||
|
|
@ -221,15 +333,16 @@ impl PtyBus {
|
||||||
handle.cancel().await;
|
handle.cancel().await;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
PaneId::Plugin(pid) => drop(
|
PaneId::Plugin(pid) => {
|
||||||
self.send_plugin_instructions
|
let api_command =
|
||||||
.send(PluginInstruction::Unload(pid)),
|
bincode::serialize(&(err_ctx, ApiCommand::ClosePluginPane(pid))).unwrap();
|
||||||
),
|
self.server_stream.write_all(&api_command).unwrap();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn close_tab(&mut self, ids: Vec<PaneId>) {
|
pub fn close_tab(&mut self, ids: Vec<PaneId>, err_ctx: ErrorContext) {
|
||||||
ids.iter().for_each(|&id| {
|
ids.iter().for_each(|&id| {
|
||||||
self.close_pane(id);
|
self.close_pane(id, err_ctx);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -238,7 +351,7 @@ impl Drop for PtyBus {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
let child_ids: Vec<RawFd> = self.id_to_child_pid.keys().copied().collect();
|
let child_ids: Vec<RawFd> = self.id_to_child_pid.keys().copied().collect();
|
||||||
for id in child_ids {
|
for id in child_ids {
|
||||||
self.close_pane(PaneId::Terminal(id));
|
self.close_pane(PaneId::Terminal(id), ErrorContext::new());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
//! Things related to [`Screen`]s.
|
//! Things related to [`Screen`]s.
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use std::os::unix::io::RawFd;
|
use std::os::unix::io::RawFd;
|
||||||
use std::str;
|
use std::str;
|
||||||
|
|
@ -16,7 +17,7 @@ use crate::{layout::Layout, panes::PaneId};
|
||||||
use zellij_tile::data::{Event, InputMode, ModeInfo, Palette, TabInfo};
|
use zellij_tile::data::{Event, InputMode, ModeInfo, Palette, TabInfo};
|
||||||
|
|
||||||
/// Instructions that can be sent to the [`Screen`].
|
/// Instructions that can be sent to the [`Screen`].
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub enum ScreenInstruction {
|
pub enum ScreenInstruction {
|
||||||
PtyBytes(RawFd, VteBytes),
|
PtyBytes(RawFd, VteBytes),
|
||||||
Render,
|
Render,
|
||||||
|
|
@ -68,8 +69,6 @@ pub struct Screen {
|
||||||
max_panes: Option<usize>,
|
max_panes: Option<usize>,
|
||||||
/// A map between this [`Screen`]'s tabs and their ID/key.
|
/// 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>,
|
|
||||||
/// A [`PluginInstruction`] and [`ErrorContext`] sender.
|
/// A [`PluginInstruction`] and [`ErrorContext`] sender.
|
||||||
pub send_plugin_instructions: SenderWithContext<PluginInstruction>,
|
pub send_plugin_instructions: SenderWithContext<PluginInstruction>,
|
||||||
/// An [`AppInstruction`] and [`ErrorContext`] sender.
|
/// An [`AppInstruction`] and [`ErrorContext`] sender.
|
||||||
|
|
@ -91,7 +90,6 @@ impl Screen {
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub fn new(
|
pub fn new(
|
||||||
receive_screen_instructions: Receiver<(ScreenInstruction, ErrorContext)>,
|
receive_screen_instructions: Receiver<(ScreenInstruction, ErrorContext)>,
|
||||||
send_pty_instructions: SenderWithContext<PtyInstruction>,
|
|
||||||
send_plugin_instructions: SenderWithContext<PluginInstruction>,
|
send_plugin_instructions: SenderWithContext<PluginInstruction>,
|
||||||
send_app_instructions: SenderWithContext<AppInstruction>,
|
send_app_instructions: SenderWithContext<AppInstruction>,
|
||||||
full_screen_ws: &PositionAndSize,
|
full_screen_ws: &PositionAndSize,
|
||||||
|
|
@ -104,7 +102,6 @@ impl Screen {
|
||||||
Screen {
|
Screen {
|
||||||
receiver: receive_screen_instructions,
|
receiver: receive_screen_instructions,
|
||||||
max_panes,
|
max_panes,
|
||||||
send_pty_instructions,
|
|
||||||
send_plugin_instructions,
|
send_plugin_instructions,
|
||||||
send_app_instructions,
|
send_app_instructions,
|
||||||
full_screen_ws: *full_screen_ws,
|
full_screen_ws: *full_screen_ws,
|
||||||
|
|
@ -128,7 +125,6 @@ impl Screen {
|
||||||
String::new(),
|
String::new(),
|
||||||
&self.full_screen_ws,
|
&self.full_screen_ws,
|
||||||
self.os_api.clone(),
|
self.os_api.clone(),
|
||||||
self.send_pty_instructions.clone(),
|
|
||||||
self.send_plugin_instructions.clone(),
|
self.send_plugin_instructions.clone(),
|
||||||
self.send_app_instructions.clone(),
|
self.send_app_instructions.clone(),
|
||||||
self.max_panes,
|
self.max_panes,
|
||||||
|
|
@ -215,8 +211,8 @@ impl Screen {
|
||||||
// because this might be happening when the app is closing, at which point 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
|
// has already closed and this would result in an error
|
||||||
let _ = self
|
let _ = self
|
||||||
.send_pty_instructions
|
.send_app_instructions
|
||||||
.send(PtyInstruction::CloseTab(pane_ids));
|
.send(AppInstruction::ToPty(PtyInstruction::CloseTab(pane_ids)));
|
||||||
if self.tabs.is_empty() {
|
if self.tabs.is_empty() {
|
||||||
self.active_tab_index = None;
|
self.active_tab_index = None;
|
||||||
self.send_app_instructions
|
self.send_app_instructions
|
||||||
|
|
@ -285,7 +281,6 @@ impl Screen {
|
||||||
String::new(),
|
String::new(),
|
||||||
&self.full_screen_ws,
|
&self.full_screen_ws,
|
||||||
self.os_api.clone(),
|
self.os_api.clone(),
|
||||||
self.send_pty_instructions.clone(),
|
|
||||||
self.send_plugin_instructions.clone(),
|
self.send_plugin_instructions.clone(),
|
||||||
self.send_app_instructions.clone(),
|
self.send_app_instructions.clone(),
|
||||||
self.max_panes,
|
self.max_panes,
|
||||||
|
|
|
||||||
|
|
@ -29,9 +29,7 @@ pub struct PluginEnv {
|
||||||
pub plugin_id: u32,
|
pub plugin_id: u32,
|
||||||
// FIXME: This should be a big bundle of all of the channels
|
// FIXME: This should be a big bundle of all of the channels
|
||||||
pub send_screen_instructions: SenderWithContext<ScreenInstruction>,
|
pub send_screen_instructions: SenderWithContext<ScreenInstruction>,
|
||||||
pub send_app_instructions: SenderWithContext<AppInstruction>,
|
pub send_app_instructions: SenderWithContext<AppInstruction>, // FIXME: This should be a big bundle of all of the channels
|
||||||
pub send_pty_instructions: SenderWithContext<PtyInstruction>,
|
|
||||||
pub send_plugin_instructions: SenderWithContext<PluginInstruction>,
|
|
||||||
pub wasi_env: WasiEnv,
|
pub wasi_env: WasiEnv,
|
||||||
pub subscriptions: Arc<Mutex<HashSet<EventType>>>,
|
pub subscriptions: Arc<Mutex<HashSet<EventType>>>,
|
||||||
}
|
}
|
||||||
|
|
@ -74,6 +72,16 @@ fn host_unsubscribe(plugin_env: &PluginEnv) {
|
||||||
subscriptions.retain(|k| !old.contains(k));
|
subscriptions.retain(|k| !old.contains(k));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn host_open_file(plugin_env: &PluginEnv) {
|
||||||
|
let path = PathBuf::from(wasi_stdout(&plugin_env.wasi_env).lines().next().unwrap());
|
||||||
|
plugin_env
|
||||||
|
.send_app_instructions
|
||||||
|
.send(AppInstruction::ToPty(PtyInstruction::SpawnTerminal(Some(
|
||||||
|
path,
|
||||||
|
))))
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
fn host_set_selectable(plugin_env: &PluginEnv, selectable: i32) {
|
fn host_set_selectable(plugin_env: &PluginEnv, selectable: i32) {
|
||||||
let selectable = selectable != 0;
|
let selectable = selectable != 0;
|
||||||
plugin_env
|
plugin_env
|
||||||
|
|
|
||||||
22
src/main.rs
22
src/main.rs
|
|
@ -1,13 +1,11 @@
|
||||||
mod cli;
|
mod cli;
|
||||||
mod common;
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests;
|
|
||||||
// TODO mod server;
|
|
||||||
mod client;
|
mod client;
|
||||||
|
mod common;
|
||||||
|
mod server;
|
||||||
|
|
||||||
use crate::cli::CliArgs;
|
use crate::cli::CliArgs;
|
||||||
use crate::command_is_executing::CommandIsExecuting;
|
use crate::command_is_executing::CommandIsExecuting;
|
||||||
use crate::common::input::config::Config;
|
use crate::errors::ErrorContext;
|
||||||
use crate::os_input_output::get_os_input;
|
use crate::os_input_output::get_os_input;
|
||||||
use crate::utils::{
|
use crate::utils::{
|
||||||
consts::{ZELLIJ_IPC_PIPE, ZELLIJ_TMP_DIR, ZELLIJ_TMP_LOG_DIR},
|
consts::{ZELLIJ_IPC_PIPE, ZELLIJ_TMP_DIR, ZELLIJ_TMP_LOG_DIR},
|
||||||
|
|
@ -36,23 +34,29 @@ pub fn main() {
|
||||||
match split_dir {
|
match split_dir {
|
||||||
'h' => {
|
'h' => {
|
||||||
let mut stream = UnixStream::connect(ZELLIJ_IPC_PIPE).unwrap();
|
let mut stream = UnixStream::connect(ZELLIJ_IPC_PIPE).unwrap();
|
||||||
let api_command = bincode::serialize(&ApiCommand::SplitHorizontally).unwrap();
|
let api_command =
|
||||||
|
bincode::serialize(&(ErrorContext::new(), ApiCommand::SplitHorizontally))
|
||||||
|
.unwrap();
|
||||||
stream.write_all(&api_command).unwrap();
|
stream.write_all(&api_command).unwrap();
|
||||||
}
|
}
|
||||||
'v' => {
|
'v' => {
|
||||||
let mut stream = UnixStream::connect(ZELLIJ_IPC_PIPE).unwrap();
|
let mut stream = UnixStream::connect(ZELLIJ_IPC_PIPE).unwrap();
|
||||||
let api_command = bincode::serialize(&ApiCommand::SplitVertically).unwrap();
|
let api_command =
|
||||||
|
bincode::serialize(&(ErrorContext::new(), ApiCommand::SplitVertically))
|
||||||
|
.unwrap();
|
||||||
stream.write_all(&api_command).unwrap();
|
stream.write_all(&api_command).unwrap();
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
};
|
};
|
||||||
} else if opts.move_focus {
|
} else if opts.move_focus {
|
||||||
let mut stream = UnixStream::connect(ZELLIJ_IPC_PIPE).unwrap();
|
let mut stream = UnixStream::connect(ZELLIJ_IPC_PIPE).unwrap();
|
||||||
let api_command = bincode::serialize(&ApiCommand::MoveFocus).unwrap();
|
let api_command =
|
||||||
|
bincode::serialize(&(ErrorContext::new(), ApiCommand::MoveFocus)).unwrap();
|
||||||
stream.write_all(&api_command).unwrap();
|
stream.write_all(&api_command).unwrap();
|
||||||
} else if let Some(file_to_open) = opts.open_file {
|
} else if let Some(file_to_open) = opts.open_file {
|
||||||
let mut stream = UnixStream::connect(ZELLIJ_IPC_PIPE).unwrap();
|
let mut stream = UnixStream::connect(ZELLIJ_IPC_PIPE).unwrap();
|
||||||
let api_command = bincode::serialize(&ApiCommand::OpenFile(file_to_open)).unwrap();
|
let api_command =
|
||||||
|
bincode::serialize(&(ErrorContext::new(), ApiCommand::OpenFile(file_to_open))).unwrap();
|
||||||
stream.write_all(&api_command).unwrap();
|
stream.write_all(&api_command).unwrap();
|
||||||
} else if let Some(crate::cli::ConfigCli::GenerateCompletion { shell }) = opts.option {
|
} else if let Some(crate::cli::ConfigCli::GenerateCompletion { shell }) = opts.option {
|
||||||
let shell = match shell.as_ref() {
|
let shell = match shell.as_ref() {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,198 @@
|
||||||
use super::super::common::{screen};
|
use crate::cli::CliArgs;
|
||||||
|
use crate::command_is_executing::CommandIsExecuting;
|
||||||
|
use crate::common::{
|
||||||
|
ApiCommand, AppInstruction, ChannelWithContext, SenderType, SenderWithContext,
|
||||||
|
};
|
||||||
|
use crate::errors::{ContextType, ErrorContext, PtyContext};
|
||||||
|
use crate::layout::Layout;
|
||||||
|
use crate::os_input_output::OsApi;
|
||||||
|
use crate::panes::PaneId;
|
||||||
|
use crate::pty_bus::{PtyBus, PtyInstruction};
|
||||||
|
use crate::screen::ScreenInstruction;
|
||||||
|
use crate::utils::consts::ZELLIJ_IPC_PIPE;
|
||||||
|
use crate::wasm_vm::PluginInstruction;
|
||||||
|
use std::io::{Read, Write};
|
||||||
|
use std::os::unix::net::UnixStream;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::sync::mpsc::channel;
|
||||||
|
use std::thread;
|
||||||
|
|
||||||
pub fn start_server() {
|
pub fn start_server(
|
||||||
// TODO
|
os_input: Box<dyn OsApi>,
|
||||||
|
opts: CliArgs,
|
||||||
|
command_is_executing: CommandIsExecuting,
|
||||||
|
mut send_app_instructions: SenderWithContext<AppInstruction>,
|
||||||
|
) -> thread::JoinHandle<()> {
|
||||||
|
let (send_pty_instructions, receive_pty_instructions): ChannelWithContext<PtyInstruction> =
|
||||||
|
channel();
|
||||||
|
let mut send_pty_instructions = SenderWithContext::new(
|
||||||
|
ErrorContext::new(),
|
||||||
|
SenderType::Sender(send_pty_instructions),
|
||||||
|
);
|
||||||
|
|
||||||
|
std::fs::remove_file(ZELLIJ_IPC_PIPE).ok();
|
||||||
|
let listener = std::os::unix::net::UnixListener::bind(ZELLIJ_IPC_PIPE)
|
||||||
|
.expect("could not listen on ipc socket");
|
||||||
|
|
||||||
|
// Don't use default layouts in tests, but do everywhere else
|
||||||
|
#[cfg(not(test))]
|
||||||
|
let default_layout = Some(PathBuf::from("default"));
|
||||||
|
#[cfg(test)]
|
||||||
|
let default_layout = None;
|
||||||
|
let maybe_layout = opts.layout.or(default_layout).map(Layout::new);
|
||||||
|
|
||||||
|
let server_stream = UnixStream::connect(ZELLIJ_IPC_PIPE).unwrap();
|
||||||
|
let mut pty_bus = PtyBus::new(
|
||||||
|
receive_pty_instructions,
|
||||||
|
os_input.clone(),
|
||||||
|
server_stream,
|
||||||
|
opts.debug,
|
||||||
|
);
|
||||||
|
|
||||||
|
let pty_thread = thread::Builder::new()
|
||||||
|
.name("pty".to_string())
|
||||||
|
.spawn({
|
||||||
|
let mut command_is_executing = command_is_executing.clone();
|
||||||
|
send_pty_instructions.send(PtyInstruction::NewTab).unwrap();
|
||||||
|
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);
|
||||||
|
let api_command = bincode::serialize(&(
|
||||||
|
err_ctx,
|
||||||
|
ApiCommand::ToScreen(ScreenInstruction::NewPane(PaneId::Terminal(pid))),
|
||||||
|
))
|
||||||
|
.unwrap();
|
||||||
|
pty_bus.server_stream.write_all(&api_command).unwrap();
|
||||||
|
}
|
||||||
|
PtyInstruction::SpawnTerminalVertically(file_to_open) => {
|
||||||
|
let pid = pty_bus.spawn_terminal(file_to_open);
|
||||||
|
let api_command = bincode::serialize(&(
|
||||||
|
err_ctx,
|
||||||
|
ApiCommand::ToScreen(ScreenInstruction::VerticalSplit(
|
||||||
|
PaneId::Terminal(pid),
|
||||||
|
)),
|
||||||
|
))
|
||||||
|
.unwrap();
|
||||||
|
pty_bus.server_stream.write_all(&api_command).unwrap();
|
||||||
|
}
|
||||||
|
PtyInstruction::SpawnTerminalHorizontally(file_to_open) => {
|
||||||
|
let pid = pty_bus.spawn_terminal(file_to_open);
|
||||||
|
let api_command = bincode::serialize(&(
|
||||||
|
err_ctx,
|
||||||
|
ApiCommand::ToScreen(ScreenInstruction::HorizontalSplit(
|
||||||
|
PaneId::Terminal(pid),
|
||||||
|
)),
|
||||||
|
))
|
||||||
|
.unwrap();
|
||||||
|
pty_bus.server_stream.write_all(&api_command).unwrap();
|
||||||
|
}
|
||||||
|
PtyInstruction::NewTab => {
|
||||||
|
if let Some(layout) = maybe_layout.clone() {
|
||||||
|
pty_bus.spawn_terminals_for_layout(layout, err_ctx);
|
||||||
|
} else {
|
||||||
|
let pid = pty_bus.spawn_terminal(None);
|
||||||
|
let api_command = bincode::serialize(&(
|
||||||
|
err_ctx,
|
||||||
|
ApiCommand::ToScreen(ScreenInstruction::NewTab(pid)),
|
||||||
|
))
|
||||||
|
.unwrap();
|
||||||
|
pty_bus.server_stream.write_all(&api_command).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PtyInstruction::ClosePane(id) => {
|
||||||
|
pty_bus.close_pane(id, err_ctx);
|
||||||
|
command_is_executing.done_closing_pane();
|
||||||
|
}
|
||||||
|
PtyInstruction::CloseTab(ids) => {
|
||||||
|
pty_bus.close_tab(ids, err_ctx);
|
||||||
|
command_is_executing.done_closing_pane();
|
||||||
|
}
|
||||||
|
PtyInstruction::Quit => {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
thread::Builder::new()
|
||||||
|
.name("ipc_server".to_string())
|
||||||
|
.spawn({
|
||||||
|
move || {
|
||||||
|
for stream in listener.incoming() {
|
||||||
|
match stream {
|
||||||
|
Ok(mut stream) => {
|
||||||
|
let mut buffer = [0; 65535]; // TODO: more accurate
|
||||||
|
let bytes = stream
|
||||||
|
.read(&mut buffer)
|
||||||
|
.expect("failed to parse ipc message");
|
||||||
|
println!("{}\n\n", bytes);
|
||||||
|
let (mut err_ctx, decoded): (ErrorContext, ApiCommand) =
|
||||||
|
bincode::deserialize(&buffer[0..bytes])
|
||||||
|
.expect("failed to deserialize ipc message");
|
||||||
|
err_ctx.add_call(ContextType::IPCServer);
|
||||||
|
send_pty_instructions.update(err_ctx);
|
||||||
|
send_app_instructions.update(err_ctx);
|
||||||
|
|
||||||
|
match decoded {
|
||||||
|
ApiCommand::OpenFile(file_name) => {
|
||||||
|
let path = PathBuf::from(file_name);
|
||||||
|
send_pty_instructions
|
||||||
|
.send(PtyInstruction::SpawnTerminal(Some(path)))
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
ApiCommand::SplitHorizontally => {
|
||||||
|
send_pty_instructions
|
||||||
|
.send(PtyInstruction::SpawnTerminalHorizontally(None))
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
ApiCommand::SplitVertically => {
|
||||||
|
send_pty_instructions
|
||||||
|
.send(PtyInstruction::SpawnTerminalVertically(None))
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
ApiCommand::MoveFocus => {
|
||||||
|
send_app_instructions
|
||||||
|
.send(AppInstruction::ToScreen(
|
||||||
|
ScreenInstruction::MoveFocus,
|
||||||
|
))
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
ApiCommand::ToPty(instruction) => {
|
||||||
|
send_pty_instructions.send(instruction).unwrap();
|
||||||
|
}
|
||||||
|
ApiCommand::ToScreen(instruction) => {
|
||||||
|
send_app_instructions
|
||||||
|
.send(AppInstruction::ToScreen(instruction))
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
ApiCommand::ClosePluginPane(pid) => {
|
||||||
|
send_app_instructions
|
||||||
|
.send(AppInstruction::ToPlugin(PluginInstruction::Unload(
|
||||||
|
pid,
|
||||||
|
)))
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
ApiCommand::Quit => {
|
||||||
|
send_pty_instructions.send(PtyInstruction::Quit).unwrap();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
panic!("err {:?}", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = pty_thread.join();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.unwrap()
|
||||||
}
|
}
|
||||||
Loading…
Add table
Reference in a new issue