Skip to content

Commit

Permalink
feat(signals): support XTWINOPS 14 and 16 (and query the terminal for…
Browse files Browse the repository at this point in the history
… them on startup and SIGWINCH) (#1316)

* feat(signals): get pixel info from terminal emulator

* feat(signals): query for pixel info on sigwinch

* feat(signals): reply to csi 14t and csi 16t

* style(fmt): rustfmt

* style(comments): remove outdated
  • Loading branch information
imsnif authored Apr 12, 2022
1 parent 028885c commit 19adb29
Show file tree
Hide file tree
Showing 24 changed files with 1,099 additions and 27 deletions.
5 changes: 3 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/tests/e2e/remote_runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ fn read_from_channel(
0,
String::new(),
Rc::new(RefCell::new(LinkHandler::new())),
Rc::new(RefCell::new(None)),
); // 0 is the pane index
loop {
if !should_keep_running.load(Ordering::SeqCst) {
Expand Down
1 change: 1 addition & 0 deletions src/tests/fixtures/terminal_pixel_size_reports
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
;
47 changes: 44 additions & 3 deletions zellij-client/src/input_handler.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
//! Main input logic.

use zellij_utils::{
input::{
mouse::{MouseButton, MouseEvent},
Expand All @@ -10,7 +9,9 @@ use zellij_utils::{
};

use crate::{
os_input_output::ClientOsApi, ClientInstruction, CommandIsExecuting, InputInstruction,
os_input_output::ClientOsApi,
pixel_csi_parser::{PixelCsiParser, PixelDimensionsOrKeys},
ClientInstruction, CommandIsExecuting, InputInstruction,
};
use zellij_utils::{
channels::{Receiver, SenderWithContext, OPENCALLS},
Expand Down Expand Up @@ -70,6 +71,15 @@ impl InputHandler {
if self.options.mouse_mode.unwrap_or(true) {
self.os_input.enable_mouse();
}
// <ESC>[14t => get text area size in pixels, <ESC>[16t => get character cell size in pixels
let get_cell_pixel_info = "\u{1b}[14t\u{1b}[16t";
let _ = self
.os_input
.get_stdout_writer()
.write(get_cell_pixel_info.as_bytes())
.unwrap();
let mut pixel_csi_parser = PixelCsiParser::new();
pixel_csi_parser.increment_expected_csi_instructions(2);
loop {
if self.should_exit {
break;
Expand All @@ -79,7 +89,13 @@ impl InputHandler {
match input_event {
InputEvent::Key(key_event) => {
let key = cast_termwiz_key(key_event, &raw_bytes);
self.handle_key(&key, raw_bytes);
if pixel_csi_parser.expected_instructions() > 0 {
self.handle_possible_pixel_instruction(
pixel_csi_parser.parse(key, raw_bytes),
);
} else {
self.handle_key(&key, raw_bytes);
}
}
InputEvent::Mouse(mouse_event) => {
let mouse_event =
Expand All @@ -101,6 +117,14 @@ impl InputHandler {
Ok((InputInstruction::SwitchToMode(input_mode), _error_context)) => {
self.mode = input_mode;
}
Ok((InputInstruction::PossiblePixelRatioChange, _error_context)) => {
let _ = self
.os_input
.get_stdout_writer()
.write(get_cell_pixel_info.as_bytes())
.unwrap();
pixel_csi_parser.increment_expected_csi_instructions(2);
}
Err(err) => panic!("Encountered read error: {:?}", err),
}
}
Expand All @@ -114,6 +138,23 @@ impl InputHandler {
}
}
}
fn handle_possible_pixel_instruction(
&mut self,
pixel_instruction_or_keys: Option<PixelDimensionsOrKeys>,
) {
match pixel_instruction_or_keys {
Some(PixelDimensionsOrKeys::PixelDimensions(pixel_dimensions)) => {
self.os_input
.send_to_server(ClientToServerMsg::TerminalPixelDimensions(pixel_dimensions));
}
Some(PixelDimensionsOrKeys::Keys(keys)) => {
for (key, raw_bytes) in keys {
self.handle_key(&key, raw_bytes);
}
}
None => {}
}
}
fn handle_mouse_event(&mut self, mouse_event: &MouseEvent) {
match *mouse_event {
MouseEvent::Press(button, point) => match button {
Expand Down
5 changes: 5 additions & 0 deletions zellij-client/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ pub mod os_input_output;

mod command_is_executing;
mod input_handler;
mod pixel_csi_parser;
mod stdin_handler;

use log::info;
Expand Down Expand Up @@ -108,6 +109,7 @@ impl ClientInfo {
pub(crate) enum InputInstruction {
KeyEvent(InputEvent, Vec<u8>),
SwitchToMode(InputMode),
PossiblePixelRatioChange,
}

pub fn start_client(
Expand Down Expand Up @@ -237,6 +239,7 @@ pub fn start_client(
let _signal_thread = thread::Builder::new()
.name("signal_listener".to_string())
.spawn({
let send_input_instructions = send_input_instructions.clone();
let os_input = os_input.clone();
move || {
os_input.handle_signals(
Expand All @@ -246,6 +249,8 @@ pub fn start_client(
os_api.send_to_server(ClientToServerMsg::TerminalResize(
os_api.get_terminal_size_using_fd(0),
));
let _ = send_input_instructions
.send(InputInstruction::PossiblePixelRatioChange);
}
}),
Box::new({
Expand Down
146 changes: 146 additions & 0 deletions zellij-client/src/pixel_csi_parser.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
use zellij_utils::pane_size::SizeInPixels;

use zellij_utils::{ipc::PixelDimensions, lazy_static::lazy_static, regex::Regex};

use zellij_tile::data::Key;

pub struct PixelCsiParser {
expected_pixel_csi_instructions: usize,
current_buffer: Vec<(Key, Vec<u8>)>,
}

impl PixelCsiParser {
pub fn new() -> Self {
PixelCsiParser {
expected_pixel_csi_instructions: 0,
current_buffer: vec![],
}
}
pub fn increment_expected_csi_instructions(&mut self, by: usize) {
self.expected_pixel_csi_instructions += by;
}
pub fn decrement_expected_csi_instructions(&mut self, by: usize) {
self.expected_pixel_csi_instructions =
self.expected_pixel_csi_instructions.saturating_sub(by);
}
pub fn expected_instructions(&self) -> usize {
self.expected_pixel_csi_instructions
}
pub fn parse(&mut self, key: Key, raw_bytes: Vec<u8>) -> Option<PixelDimensionsOrKeys> {
if let Key::Char('t') = key {
self.current_buffer.push((key, raw_bytes));
match PixelDimensionsOrKeys::pixel_dimensions_from_keys(&self.current_buffer) {
Ok(pixel_instruction) => {
self.decrement_expected_csi_instructions(1);
self.current_buffer.clear();
Some(pixel_instruction)
}
Err(_) => {
self.expected_pixel_csi_instructions = 0;
Some(PixelDimensionsOrKeys::Keys(
self.current_buffer.drain(..).collect(),
))
}
}
} else if self.key_is_valid(key) {
self.current_buffer.push((key, raw_bytes));
None
} else {
self.current_buffer.push((key, raw_bytes));
self.expected_pixel_csi_instructions = 0;
Some(PixelDimensionsOrKeys::Keys(
self.current_buffer.drain(..).collect(),
))
}
}
fn key_is_valid(&self, key: Key) -> bool {
match key {
Key::Esc => {
// this is a UX improvement
// in case the user's terminal doesn't support one or more of these signals,
// if they spam ESC they need to be able to get back to normal mode and not "us
// waiting for pixel instructions" mode
if self
.current_buffer
.iter()
.find(|(key, _)| *key == Key::Esc)
.is_none()
{
true
} else {
false
}
}
Key::Char(';') | Key::Char('[') => true,
Key::Char(c) => {
if let '0'..='9' = c {
true
} else {
false
}
}
_ => false,
}
}
}

#[derive(Debug)]
pub enum PixelDimensionsOrKeys {
// TODO: rename to PixelDimensionsOrKeys
PixelDimensions(PixelDimensions),
Keys(Vec<(Key, Vec<u8>)>),
}

impl PixelDimensionsOrKeys {
pub fn pixel_dimensions_from_keys(keys: &Vec<(Key, Vec<u8>)>) -> Result<Self, &'static str> {
lazy_static! {
static ref RE: Regex = Regex::new(r"^\u{1b}\[(\d+);(\d+);(\d+)t$").unwrap();
}
let key_sequence: Vec<Option<char>> = keys
.iter()
.map(|(key, _)| match key {
Key::Char(c) => Some(*c),
Key::Esc => Some('\u{1b}'),
_ => None,
})
.collect();
if key_sequence.iter().all(|k| k.is_some()) {
let key_string: String = key_sequence.iter().map(|k| k.unwrap()).collect();
let captures = RE
.captures_iter(&key_string)
.next()
.ok_or("invalid_instruction")?;
let csi_index = captures[1].parse::<usize>();
let first_field = captures[2].parse::<usize>();
let second_field = captures[3].parse::<usize>();
if csi_index.is_err() || first_field.is_err() || second_field.is_err() {
return Err("invalid_instruction");
}
match csi_index {
Ok(4) => {
// text area size
Ok(PixelDimensionsOrKeys::PixelDimensions(PixelDimensions {
character_cell_size: None,
text_area_size: Some(SizeInPixels {
height: first_field.unwrap(),
width: second_field.unwrap(),
}),
}))
}
Ok(6) => {
// character cell size
Ok(PixelDimensionsOrKeys::PixelDimensions(PixelDimensions {
character_cell_size: Some(SizeInPixels {
height: first_field.unwrap(),
width: second_field.unwrap(),
}),
text_area_size: None,
}))
}
_ => Err("invalid sequence"),
}
} else {
Err("invalid sequence")
}
}
}
Loading

0 comments on commit 19adb29

Please sign in to comment.