feat(client): terminal synchronized output (#2798)
This commit is contained in:
parent
3e31a0e347
commit
ccc40a4a26
4 changed files with 84 additions and 3 deletions
|
|
@ -197,6 +197,11 @@ impl InputHandler {
|
||||||
self.os_input
|
self.os_input
|
||||||
.send_to_server(ClientToServerMsg::ColorRegisters(color_registers));
|
.send_to_server(ClientToServerMsg::ColorRegisters(color_registers));
|
||||||
},
|
},
|
||||||
|
AnsiStdinInstruction::SynchronizedOutput(enabled) => {
|
||||||
|
self.send_client_instructions
|
||||||
|
.send(ClientInstruction::SetSynchronizedOutput(enabled))
|
||||||
|
.unwrap();
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn handle_mouse_event(&mut self, mouse_event: &MouseEvent) {
|
fn handle_mouse_event(&mut self, mouse_event: &MouseEvent) {
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ use std::sync::{Arc, Mutex};
|
||||||
use std::thread;
|
use std::thread;
|
||||||
use zellij_utils::errors::FatalError;
|
use zellij_utils::errors::FatalError;
|
||||||
|
|
||||||
use crate::stdin_ansi_parser::{AnsiStdinInstruction, StdinAnsiParser};
|
use crate::stdin_ansi_parser::{AnsiStdinInstruction, StdinAnsiParser, SyncOutput};
|
||||||
use crate::{
|
use crate::{
|
||||||
command_is_executing::CommandIsExecuting, input_handler::input_loop,
|
command_is_executing::CommandIsExecuting, input_handler::input_loop,
|
||||||
os_input_output::ClientOsApi, stdin_handler::stdin_loop,
|
os_input_output::ClientOsApi, stdin_handler::stdin_loop,
|
||||||
|
|
@ -47,6 +47,7 @@ pub(crate) enum ClientInstruction {
|
||||||
DoneParsingStdinQuery,
|
DoneParsingStdinQuery,
|
||||||
Log(Vec<String>),
|
Log(Vec<String>),
|
||||||
SwitchSession(ConnectToSession),
|
SwitchSession(ConnectToSession),
|
||||||
|
SetSynchronizedOutput(Option<SyncOutput>),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<ServerToClientMsg> for ClientInstruction {
|
impl From<ServerToClientMsg> for ClientInstruction {
|
||||||
|
|
@ -82,6 +83,7 @@ impl From<&ClientInstruction> for ClientContext {
|
||||||
ClientInstruction::StartedParsingStdinQuery => ClientContext::StartedParsingStdinQuery,
|
ClientInstruction::StartedParsingStdinQuery => ClientContext::StartedParsingStdinQuery,
|
||||||
ClientInstruction::DoneParsingStdinQuery => ClientContext::DoneParsingStdinQuery,
|
ClientInstruction::DoneParsingStdinQuery => ClientContext::DoneParsingStdinQuery,
|
||||||
ClientInstruction::SwitchSession(..) => ClientContext::SwitchSession,
|
ClientInstruction::SwitchSession(..) => ClientContext::SwitchSession,
|
||||||
|
ClientInstruction::SetSynchronizedOutput(..) => ClientContext::SetSynchronisedOutput,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -381,6 +383,10 @@ pub fn start_client(
|
||||||
let mut exit_msg = String::new();
|
let mut exit_msg = String::new();
|
||||||
let mut loading = true;
|
let mut loading = true;
|
||||||
let mut pending_instructions = vec![];
|
let mut pending_instructions = vec![];
|
||||||
|
let mut synchronised_output = match os_input.env_variable("TERM").as_deref() {
|
||||||
|
Some("alacritty") => Some(SyncOutput::DCS),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
|
||||||
let mut stdout = os_input.get_stdout_writer();
|
let mut stdout = os_input.get_stdout_writer();
|
||||||
stdout
|
stdout
|
||||||
|
|
@ -439,9 +445,19 @@ pub fn start_client(
|
||||||
},
|
},
|
||||||
ClientInstruction::Render(output) => {
|
ClientInstruction::Render(output) => {
|
||||||
let mut stdout = os_input.get_stdout_writer();
|
let mut stdout = os_input.get_stdout_writer();
|
||||||
|
if let Some(sync) = synchronised_output {
|
||||||
|
stdout
|
||||||
|
.write_all(sync.start_seq())
|
||||||
|
.expect("cannot write to stdout");
|
||||||
|
}
|
||||||
stdout
|
stdout
|
||||||
.write_all(output.as_bytes())
|
.write_all(output.as_bytes())
|
||||||
.expect("cannot write to stdout");
|
.expect("cannot write to stdout");
|
||||||
|
if let Some(sync) = synchronised_output {
|
||||||
|
stdout
|
||||||
|
.write_all(sync.end_seq())
|
||||||
|
.expect("cannot write to stdout");
|
||||||
|
}
|
||||||
stdout.flush().expect("could not flush");
|
stdout.flush().expect("could not flush");
|
||||||
},
|
},
|
||||||
ClientInstruction::UnblockInputThread => {
|
ClientInstruction::UnblockInputThread => {
|
||||||
|
|
@ -462,6 +478,9 @@ pub fn start_client(
|
||||||
os_input.send_to_server(ClientToServerMsg::ClientExited);
|
os_input.send_to_server(ClientToServerMsg::ClientExited);
|
||||||
break;
|
break;
|
||||||
},
|
},
|
||||||
|
ClientInstruction::SetSynchronizedOutput(enabled) => {
|
||||||
|
synchronised_output = enabled;
|
||||||
|
},
|
||||||
_ => {},
|
_ => {},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,33 @@ use std::fs::{File, OpenOptions};
|
||||||
use std::io::{Read, Write};
|
use std::io::{Read, Write};
|
||||||
use zellij_utils::anyhow::Result;
|
use zellij_utils::anyhow::Result;
|
||||||
|
|
||||||
|
/// Describe the terminal implementation of synchronised output
|
||||||
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
||||||
|
pub enum SyncOutput {
|
||||||
|
DCS,
|
||||||
|
CSI,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SyncOutput {
|
||||||
|
pub fn start_seq(&self) -> &'static [u8] {
|
||||||
|
static CSI_BSU_SEQ: &'static [u8] = "\u{1b}[?2026h".as_bytes();
|
||||||
|
static DCS_BSU_SEQ: &'static [u8] = "\u{1b}P=1s\u{1b}".as_bytes();
|
||||||
|
match self {
|
||||||
|
SyncOutput::DCS => DCS_BSU_SEQ,
|
||||||
|
SyncOutput::CSI => CSI_BSU_SEQ,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn end_seq(&self) -> &'static [u8] {
|
||||||
|
static CSI_ESU_SEQ: &'static [u8] = "\u{1b}[?2026l".as_bytes();
|
||||||
|
static DCS_ESU_SEQ: &'static [u8] = "\u{1b}P=2s\u{1b}".as_bytes();
|
||||||
|
match self {
|
||||||
|
SyncOutput::DCS => DCS_ESU_SEQ,
|
||||||
|
SyncOutput::CSI => CSI_ESU_SEQ,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct StdinAnsiParser {
|
pub struct StdinAnsiParser {
|
||||||
raw_buffer: Vec<u8>,
|
raw_buffer: Vec<u8>,
|
||||||
|
|
@ -36,8 +63,10 @@ impl StdinAnsiParser {
|
||||||
// <ESC>[16t => get character cell size in pixels
|
// <ESC>[16t => get character cell size in pixels
|
||||||
// <ESC>]11;?<ESC>\ => get background color
|
// <ESC>]11;?<ESC>\ => get background color
|
||||||
// <ESC>]10;?<ESC>\ => get foreground color
|
// <ESC>]10;?<ESC>\ => get foreground color
|
||||||
let mut query_string =
|
// <ESC>[?2026$p => get synchronised output mode
|
||||||
String::from("\u{1b}[14t\u{1b}[16t\u{1b}]11;?\u{1b}\u{5c}\u{1b}]10;?\u{1b}\u{5c}");
|
let mut query_string = String::from(
|
||||||
|
"\u{1b}[14t\u{1b}[16t\u{1b}]11;?\u{1b}\u{5c}\u{1b}]10;?\u{1b}\u{5c}\u{1b}[?2026$p",
|
||||||
|
);
|
||||||
|
|
||||||
// query colors
|
// query colors
|
||||||
// eg. <ESC>]4;5;?<ESC>\ => query color register number 5
|
// eg. <ESC>]4;5;?<ESC>\ => query color register number 5
|
||||||
|
|
@ -130,6 +159,14 @@ impl StdinAnsiParser {
|
||||||
} else {
|
} else {
|
||||||
self.raw_buffer.clear();
|
self.raw_buffer.clear();
|
||||||
}
|
}
|
||||||
|
} else if byte == b'y' {
|
||||||
|
self.raw_buffer.push(byte);
|
||||||
|
if let Some(ansi_sequence) =
|
||||||
|
AnsiStdinInstruction::synchronized_output_from_bytes(&self.raw_buffer)
|
||||||
|
{
|
||||||
|
self.pending_events.push(ansi_sequence);
|
||||||
|
self.raw_buffer.clear();
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
self.raw_buffer.push(byte);
|
self.raw_buffer.push(byte);
|
||||||
}
|
}
|
||||||
|
|
@ -142,6 +179,7 @@ pub enum AnsiStdinInstruction {
|
||||||
BackgroundColor(String),
|
BackgroundColor(String),
|
||||||
ForegroundColor(String),
|
ForegroundColor(String),
|
||||||
ColorRegisters(Vec<(usize, String)>),
|
ColorRegisters(Vec<(usize, String)>),
|
||||||
|
SynchronizedOutput(Option<SyncOutput>),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AnsiStdinInstruction {
|
impl AnsiStdinInstruction {
|
||||||
|
|
@ -225,6 +263,24 @@ impl AnsiStdinInstruction {
|
||||||
}
|
}
|
||||||
Some(AnsiStdinInstruction::ColorRegisters(registers))
|
Some(AnsiStdinInstruction::ColorRegisters(registers))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn synchronized_output_from_bytes(bytes: &[u8]) -> Option<Self> {
|
||||||
|
lazy_static! {
|
||||||
|
static ref RE: Regex = Regex::new(r"^\u{1b}\[\?2026;([0|1|2|3|4])\$y$").unwrap();
|
||||||
|
}
|
||||||
|
let key_string = String::from_utf8_lossy(bytes);
|
||||||
|
if let Some(captures) = RE.captures_iter(&key_string).next() {
|
||||||
|
match captures[1].parse::<usize>().ok()? {
|
||||||
|
1 | 2 => Some(AnsiStdinInstruction::SynchronizedOutput(Some(
|
||||||
|
SyncOutput::CSI,
|
||||||
|
))),
|
||||||
|
0 | 4 => Some(AnsiStdinInstruction::SynchronizedOutput(None)),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn color_sequence_from_bytes(bytes: &[u8]) -> Result<(usize, String), &'static str> {
|
fn color_sequence_from_bytes(bytes: &[u8]) -> Result<(usize, String), &'static str> {
|
||||||
|
|
|
||||||
|
|
@ -407,6 +407,7 @@ pub enum ClientContext {
|
||||||
StartedParsingStdinQuery,
|
StartedParsingStdinQuery,
|
||||||
DoneParsingStdinQuery,
|
DoneParsingStdinQuery,
|
||||||
SwitchSession,
|
SwitchSession,
|
||||||
|
SetSynchronisedOutput,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Stack call representations corresponding to the different types of [`ServerInstruction`]s.
|
/// Stack call representations corresponding to the different types of [`ServerInstruction`]s.
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue