zellij/zellij-utils/src/ipc.rs
Aram Drevekenin 79bf6ab868
feat(config): switch to kdl (#1759)
* chore(config): default kdl keybindings config

* tests

* work

* refactor(config): move stuff around

* work

* tab merge layout

* work

* work

* layouts working

* work

* layout tests

* work

* work

* feat(parsing): kdl layouts without config

* refactor(kdl): move stuff around

* work

* tests(layout): add cases and fix bugs

* work

* fix(kdl): various bugs

* chore(layouts): move all layouts to kdl

* feat(kdl): shared keybidns

* fix(layout): do not count fixed panes toward percentile

* fix(keybinds): missing keybinds and actions

* fix(config): adjust default tips

* refactor(config): move stuff around

* fix(tests): make e2e tests pass

* fix(kdl): add verbose parsing errors

* fix(kdl): focused tab

* fix(layout): corret default_tab_template behavior

* style(code): fix compile warnings

* feat(cli): send actions through the cli

* fix(cli): exit only when action is done

* fix(cli): open embedded pane from floating pane

* fix(cli): send actions to other sessions

* feat(cli): command alias

* feat(converter): convert old config

* feat(converter): convert old layout and theme files

* feat(kdl): pretty errors

* feat(client): convert old YAML files on startup

* fix: various bugs and styling issues

* fix: e2e tests

* fix(screen): propagate errors after merge

* style(clippy): lower clippy level

* fix(tests): own session_name variable

* style(fmt): rustfmt

* fix(cli): various action fixes

* style(fmt): rustfmt

* fix(themes): loading of theme files

* style(fmt): rustfmt

* fix(tests): theme fixtures

* fix(layouts): better errors on unknown nodes

* fix(kdl): clarify valid node terminator error

* fix(e2e): adjust close tab test

* fix(e2e): adjust close tab test again

* style(code): cleanup some comments
2022-10-05 07:44:00 +02:00

217 lines
6.8 KiB
Rust

//! IPC stuff for starting to split things into a client and server model.
use crate::{
cli::CliArgs,
data::{ClientId, InputMode, Style},
errors::{get_current_ctx, ErrorContext},
input::keybinds::Keybinds,
input::{actions::Action, layout::Layout, options::Options, plugins::PluginsConfig},
pane_size::{Size, SizeInPixels},
};
use interprocess::local_socket::LocalSocketStream;
use log::warn;
use nix::unistd::dup;
use serde::{Deserialize, Serialize};
use std::{
fmt::{Display, Error, Formatter},
io::{self, Write},
marker::PhantomData,
os::unix::io::{AsRawFd, FromRawFd},
};
type SessionId = u64;
#[derive(PartialEq, Eq, Serialize, Deserialize, Hash)]
pub struct Session {
// Unique ID for this session
id: SessionId,
// Identifier for the underlying IPC primitive (socket, pipe)
conn_name: String,
// User configured alias for the session
alias: String,
}
// How do we want to connect to a session?
#[derive(Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ClientType {
Reader,
Writer,
}
#[derive(Default, Serialize, Deserialize, Debug, Clone)]
pub struct ClientAttributes {
pub size: Size,
pub style: Style,
pub keybinds: Keybinds,
}
#[derive(Default, Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
pub struct PixelDimensions {
pub text_area_size: Option<SizeInPixels>,
pub character_cell_size: Option<SizeInPixels>,
}
impl PixelDimensions {
pub fn merge(&mut self, other: PixelDimensions) {
if let Some(text_area_size) = other.text_area_size {
self.text_area_size = Some(text_area_size);
}
if let Some(character_cell_size) = other.character_cell_size {
self.character_cell_size = Some(character_cell_size);
}
}
}
// Types of messages sent from the client to the server
#[allow(clippy::large_enum_variant)]
#[derive(Serialize, Deserialize, Debug, Clone)]
pub enum ClientToServerMsg {
/*// List which sessions are available
ListSessions,
// Create a new session
CreateSession,
// Attach to a running session
AttachToSession(SessionId, ClientType),
// Force detach
DetachSession(SessionId),
// Disconnect from the session we're connected to
DisconnectFromSession,*/
DetachSession(Vec<ClientId>),
TerminalPixelDimensions(PixelDimensions),
BackgroundColor(String),
ForegroundColor(String),
ColorRegisters(Vec<(usize, String)>),
TerminalResize(Size),
NewClient(
ClientAttributes,
Box<CliArgs>,
Box<Options>,
Box<Layout>,
Option<PluginsConfig>,
),
AttachClient(ClientAttributes, Options),
Action(Action, Option<ClientId>),
ClientExited,
KillSession,
ConnStatus,
ListClients,
}
// Types of messages sent from the server to the client
#[derive(Serialize, Deserialize, Debug, Clone)]
pub enum ServerToClientMsg {
/*// Info about a particular session
SessionInfo(Session),
// A list of sessions
SessionList(HashSet<Session>),*/
Render(String),
UnblockInputThread,
Exit(ExitReason),
SwitchToMode(InputMode),
Connected,
ActiveClients(Vec<ClientId>),
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub enum ExitReason {
Normal,
NormalDetached,
ForceDetached,
CannotAttach,
Error(String),
}
impl Display for ExitReason {
fn fmt(&self, f: &mut Formatter) -> Result<(), Error> {
match self {
Self::Normal => write!(f, "Bye from Zellij!"),
Self::NormalDetached => write!(f, "Session detached"),
Self::ForceDetached => write!(
f,
"Session was detached from this client (possibly because another client connected)"
),
Self::CannotAttach => write!(
f,
"Session attached to another client. Use --force flag to force connect."
),
Self::Error(e) => write!(f, "Error occurred in server:\n{}", e),
}
}
}
/// Sends messages on a stream socket, along with an [`ErrorContext`].
pub struct IpcSenderWithContext<T: Serialize> {
sender: io::BufWriter<LocalSocketStream>,
_phantom: PhantomData<T>,
}
impl<T: Serialize> IpcSenderWithContext<T> {
/// Returns a sender to the given [LocalSocketStream](interprocess::local_socket::LocalSocketStream).
pub fn new(sender: LocalSocketStream) -> Self {
Self {
sender: io::BufWriter::new(sender),
_phantom: PhantomData,
}
}
/// Sends an event, along with the current [`ErrorContext`], on this [`IpcSenderWithContext`]'s socket.
pub fn send(&mut self, msg: T) -> Result<(), &'static str> {
let err_ctx = get_current_ctx();
if rmp_serde::encode::write(&mut self.sender, &(msg, err_ctx)).is_err() {
Err("Failed to send message to client")
} else {
// TODO: unwrapping here can cause issues when the server disconnects which we don't mind
// do we need to handle errors here in other cases?
let _ = self.sender.flush();
Ok(())
}
}
/// Returns an [`IpcReceiverWithContext`] with the same socket as this sender.
pub fn get_receiver<F>(&self) -> IpcReceiverWithContext<F>
where
F: for<'de> Deserialize<'de> + Serialize,
{
let sock_fd = self.sender.get_ref().as_raw_fd();
let dup_sock = dup(sock_fd).unwrap();
let socket = unsafe { LocalSocketStream::from_raw_fd(dup_sock) };
IpcReceiverWithContext::new(socket)
}
}
/// Receives messages on a stream socket, along with an [`ErrorContext`].
pub struct IpcReceiverWithContext<T> {
receiver: io::BufReader<LocalSocketStream>,
_phantom: PhantomData<T>,
}
impl<T> IpcReceiverWithContext<T>
where
T: for<'de> Deserialize<'de> + Serialize,
{
/// Returns a receiver to the given [LocalSocketStream](interprocess::local_socket::LocalSocketStream).
pub fn new(receiver: LocalSocketStream) -> Self {
Self {
receiver: io::BufReader::new(receiver),
_phantom: PhantomData,
}
}
/// Receives an event, along with the current [`ErrorContext`], on this [`IpcReceiverWithContext`]'s socket.
pub fn recv(&mut self) -> Option<(T, ErrorContext)> {
match rmp_serde::decode::from_read(&mut self.receiver) {
Ok(msg) => Some(msg),
Err(e) => {
warn!("Error in IpcReceiver.recv(): {:?}", e);
None
},
}
}
/// Returns an [`IpcSenderWithContext`] with the same socket as this receiver.
pub fn get_sender<F: Serialize>(&self) -> IpcSenderWithContext<F> {
let sock_fd = self.receiver.get_ref().as_raw_fd();
let dup_sock = dup(sock_fd).unwrap();
let socket = unsafe { LocalSocketStream::from_raw_fd(dup_sock) };
IpcSenderWithContext::new(socket)
}
}