use anyhow::Result;
use async_trait::async_trait;
use zellij_utils::pane_size::Size;
#[cfg(not(windows))]
use crate::os_input_output_unix::{
disable_mouse_support, enable_mouse_support, setup_ipc, AsyncSignalListener,
BlockingSignalIterator,
};
#[cfg(windows)]
use crate::os_input_output_windows::{
disable_mouse_support, enable_mouse_support, restore_console_mode, setup_ipc,
AsyncSignalListener, BlockingSignalIterator,
};
use std::io::prelude::*;
use std::io::IsTerminal;
use std::path::Path;
use std::sync::{Arc, Mutex};
use std::{io, thread, time};
use zellij_utils::{
data::Palette,
errors::ErrorContext,
ipc::{ClientToServerMsg, IpcReceiverWithContext, IpcSenderWithContext, ServerToClientMsg},
shared::default_palette,
};
const SIGWINCH_CB_THROTTLE_DURATION: time::Duration = time::Duration::from_millis(50);
pub(crate) const ENABLE_MOUSE_SUPPORT: &str =
"\u{1b}[?1000h\u{1b}[?1002h\u{1b}[?1003h\u{1b}[?1015h\u{1b}[?1006h";
pub(crate) const DISABLE_MOUSE_SUPPORT: &str =
"\u{1b}[?1006l\u{1b}[?1015l\u{1b}[?1003l\u{1b}[?1002l\u{1b}[?1000l";
#[async_trait]
pub trait AsyncStdin: Send {
async fn read(&mut self) -> io::Result<Vec<u8>>;
}
pub struct AsyncStdinReader {
stdin: tokio::io::Stdin,
buffer: Vec<u8>,
}
impl AsyncStdinReader {
pub fn new() -> Self {
Self {
stdin: tokio::io::stdin(),
buffer: vec![0u8; 10 * 1024],
}
}
}
#[async_trait]
impl AsyncStdin for AsyncStdinReader {
async fn read(&mut self) -> io::Result<Vec<u8>> {
use tokio::io::AsyncReadExt;
let n = self.stdin.read(&mut self.buffer).await?;
Ok(self.buffer[..n].to_vec())
}
}
pub enum SignalEvent {
Resize,
Quit,
}
#[async_trait]
pub trait AsyncSignals: Send {
async fn recv(&mut self) -> Option<SignalEvent>;
}
pub(crate) fn get_terminal_size() -> Size {
match crossterm::terminal::size() {
Ok((cols, rows)) => {
let rows = if rows != 0 { rows as usize } else { 24 };
let cols = if cols != 0 { cols as usize } else { 80 };
Size { rows, cols }
},
Err(_) => Size { rows: 24, cols: 80 },
}
}
#[derive(Clone)]
pub struct ClientOsInputOutput {
send_instructions_to_server: Arc<Mutex<Option<IpcSenderWithContext<ClientToServerMsg>>>>,
receive_instructions_from_server: Arc<Mutex<Option<IpcReceiverWithContext<ServerToClientMsg>>>>,
reading_from_stdin: Arc<Mutex<Option<Vec<u8>>>>,
session_name: Arc<Mutex<Option<String>>>,
}
impl std::fmt::Debug for ClientOsInputOutput {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ClientOsInputOutput").finish()
}
}
pub trait ClientOsApi: Send + Sync + std::fmt::Debug {
fn get_terminal_size(&self) -> Size;
fn set_raw_mode(&mut self);
fn unset_raw_mode(&self) -> Result<(), std::io::Error>;
fn get_stdout_writer(&self) -> Box<dyn io::Write>;
fn get_stdin_reader(&self) -> Box<dyn io::BufRead>;
fn stdin_is_terminal(&self) -> bool {
true
}
fn stdout_is_terminal(&self) -> bool {
true
}
fn update_session_name(&mut self, new_session_name: String);
fn read_from_stdin(&mut self) -> Result<Vec<u8>, &'static str>;
fn box_clone(&self) -> Box<dyn ClientOsApi>;
fn send_to_server(&self, msg: ClientToServerMsg);
fn recv_from_server(&self) -> Option<(ServerToClientMsg, ErrorContext)>;
fn handle_signals(
&self,
sigwinch_cb: Box<dyn Fn()>,
quit_cb: Box<dyn Fn()>,
resize_receiver: Option<std::sync::mpsc::Receiver<()>>,
);
fn connect_to_server(&self, path: &Path);
fn load_palette(&self) -> Palette;
fn enable_mouse(&self) -> Result<()>;
fn disable_mouse(&self) -> Result<()>;
fn restore_console_mode(&self) {}
fn env_variable(&self, _name: &str) -> Option<String> {
None
}
fn get_async_stdin_reader(&self) -> Box<dyn AsyncStdin> {
Box::new(AsyncStdinReader::new())
}
fn get_async_signal_listener(&self) -> io::Result<Box<dyn AsyncSignals>> {
Ok(Box::new(AsyncSignalListener::new()?))
}
}
impl ClientOsApi for ClientOsInputOutput {
fn get_terminal_size(&self) -> Size {
get_terminal_size()
}
fn set_raw_mode(&mut self) {
crossterm::terminal::enable_raw_mode().expect("could not enable raw mode");
}
fn unset_raw_mode(&self) -> Result<(), std::io::Error> {
crossterm::terminal::disable_raw_mode()
}
fn box_clone(&self) -> Box<dyn ClientOsApi> {
Box::new((*self).clone())
}
fn update_session_name(&mut self, new_session_name: String) {
*self.session_name.lock().unwrap() = Some(new_session_name);
}
fn read_from_stdin(&mut self) -> Result<Vec<u8>, &'static str> {
let session_name_at_calltime = { self.session_name.lock().unwrap().clone() };
let mut buffered_bytes = self.reading_from_stdin.lock().unwrap();
match buffered_bytes.take() {
Some(buffered_bytes) => Ok(buffered_bytes),
None => {
let stdin = std::io::stdin();
let mut stdin = stdin.lock();
let buffer = stdin.fill_buf().unwrap();
let length = buffer.len();
let read_bytes = Vec::from(buffer);
stdin.consume(length);
let session_name_after_reading_from_stdin =
{ self.session_name.lock().unwrap().clone() };
if session_name_at_calltime.is_some()
&& session_name_at_calltime != session_name_after_reading_from_stdin
{
*buffered_bytes = Some(read_bytes);
Err("Session ended")
} else {
Ok(read_bytes)
}
},
}
}
fn get_stdout_writer(&self) -> Box<dyn io::Write> {
let stdout = ::std::io::stdout();
Box::new(stdout)
}
fn get_stdin_reader(&self) -> Box<dyn io::BufRead> {
let stdin = ::std::io::stdin();
Box::new(stdin.lock())
}
fn stdin_is_terminal(&self) -> bool {
let stdin = ::std::io::stdin();
stdin.is_terminal()
}
fn stdout_is_terminal(&self) -> bool {
let stdout = ::std::io::stdout();
stdout.is_terminal()
}
fn send_to_server(&self, msg: ClientToServerMsg) {
match self.send_instructions_to_server.lock().unwrap().as_mut() {
Some(sender) => {
let _ = sender.send_client_msg(msg);
},
None => {
log::warn!("Server not ready, dropping message.");
},
}
}
fn recv_from_server(&self) -> Option<(ServerToClientMsg, ErrorContext)> {
self.receive_instructions_from_server
.lock()
.unwrap()
.as_mut()
.unwrap()
.recv_server_msg()
}
fn handle_signals(
&self,
sigwinch_cb: Box<dyn Fn()>,
quit_cb: Box<dyn Fn()>,
resize_receiver: Option<std::sync::mpsc::Receiver<()>>,
) {
let mut sigwinch_cb_timestamp = time::Instant::now();
let signals = BlockingSignalIterator::new(resize_receiver).unwrap();
for event in signals {
match event {
SignalEvent::Resize => {
if sigwinch_cb_timestamp.elapsed() < SIGWINCH_CB_THROTTLE_DURATION {
thread::sleep(SIGWINCH_CB_THROTTLE_DURATION);
}
sigwinch_cb_timestamp = time::Instant::now();
sigwinch_cb();
},
SignalEvent::Quit => {
quit_cb();
break;
},
}
}
}
fn connect_to_server(&self, path: &Path) {
let socket;
loop {
match zellij_utils::consts::ipc_connect(path) {
Ok(sock) => {
socket = sock;
break;
},
Err(_) => {
std::thread::sleep(std::time::Duration::from_millis(50));
},
}
}
let (sender, receiver) = setup_ipc(socket, path);
*self.send_instructions_to_server.lock().unwrap() = Some(sender);
*self.receive_instructions_from_server.lock().unwrap() = Some(receiver);
}
fn load_palette(&self) -> Palette {
default_palette()
}
fn enable_mouse(&self) -> Result<()> {
let mut stdout = self.get_stdout_writer();
enable_mouse_support(&mut *stdout)
}
fn disable_mouse(&self) -> Result<()> {
let mut stdout = self.get_stdout_writer();
disable_mouse_support(&mut *stdout)
}
#[cfg(windows)]
fn restore_console_mode(&self) {
restore_console_mode();
}
fn env_variable(&self, name: &str) -> Option<String> {
std::env::var(name).ok()
}
}
impl Clone for Box<dyn ClientOsApi> {
fn clone(&self) -> Box<dyn ClientOsApi> {
self.box_clone()
}
}
pub fn get_client_os_input() -> Result<ClientOsInputOutput, std::io::Error> {
let reading_from_stdin = Arc::new(Mutex::new(None));
Ok(ClientOsInputOutput {
send_instructions_to_server: Arc::new(Mutex::new(None)),
receive_instructions_from_server: Arc::new(Mutex::new(None)),
reading_from_stdin,
session_name: Arc::new(Mutex::new(None)),
})
}
pub fn get_cli_client_os_input() -> Result<ClientOsInputOutput, std::io::Error> {
let reading_from_stdin = Arc::new(Mutex::new(None));
Ok(ClientOsInputOutput {
send_instructions_to_server: Arc::new(Mutex::new(None)),
receive_instructions_from_server: Arc::new(Mutex::new(None)),
reading_from_stdin,
session_name: Arc::new(Mutex::new(None)),
})
}
pub const DEFAULT_STDIN_POLL_TIMEOUT_MS: u64 = 10;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn get_terminal_size_returns_nonzero_or_fallback() {
let size = get_terminal_size();
assert!(size.rows > 0, "rows should be positive");
assert!(size.cols > 0, "cols should be positive");
}
#[test]
fn get_terminal_size_fallback_values() {
let fallback = Size { rows: 24, cols: 80 };
assert_eq!(fallback.rows, 24);
assert_eq!(fallback.cols, 80);
}
#[test]
fn client_os_input_output_can_be_constructed() {
let os_input = get_client_os_input().expect("should construct ClientOsInputOutput");
let size = os_input.get_terminal_size();
assert!(size.rows > 0, "rows should be positive");
assert!(size.cols > 0, "cols should be positive");
}
#[test]
fn cli_client_os_input_can_be_constructed() {
let os_input = get_cli_client_os_input().expect("should construct CLI ClientOsInputOutput");
let size = os_input.get_terminal_size();
assert!(size.rows > 0, "rows should be positive");
assert!(size.cols > 0, "cols should be positive");
}
}