* basic functionality * close and reopen scratch terminal working * embed/float and resize whole tab for floating and static floating panes * move focus working * fix focus change in floating panes * move pane with mouse * floating z indices * tests and better resize algorithm * starting to work on performance * some performance experimentations * new render engine * reverse painters algorithm for floating panes * fix frame buffering * improve ux situation * handle multiple new panes on screen without overlap * adjust keybindings * adjust key hints * fix multiuser frame ui * fix various floating/multiuser bugs * remove stuff * wide characters under floating panes * fix wide character frame override * fix non-frame boundaries interactions with floating panes * fix selection character width * fix title frame wide char overflow * fix existing tests * add tests * refactor output out of tab * refactor floating panes out of tab * refactor tab * moar refactoring * refactorings and bring back terminal window title setting * add frame vte output * remove more unused stuff * remove even more unused stuff * you know the drill * refactor floating panes and remove more stuffs * refactor pane grids * remove unused output caching * refactor output * remove unused stuff * rustfmt * some formatting * rustfmt * reduce clippy to normal * remove comment * remove unused * fix closign pane * fix tests
528 lines
18 KiB
Rust
528 lines
18 KiB
Rust
use crate::output::CharacterChunk;
|
||
use crate::panes::{
|
||
grid::Grid,
|
||
terminal_character::{CursorShape, TerminalCharacter, EMPTY_TERMINAL_CHARACTER},
|
||
};
|
||
use crate::panes::{AnsiCode, LinkHandler};
|
||
use crate::pty::VteBytes;
|
||
use crate::tab::Pane;
|
||
use crate::ClientId;
|
||
use std::cell::RefCell;
|
||
use std::collections::{HashMap, HashSet};
|
||
use std::fmt::Debug;
|
||
use std::os::unix::io::RawFd;
|
||
use std::rc::Rc;
|
||
use std::time::{self, Instant};
|
||
use zellij_utils::pane_size::Offset;
|
||
use zellij_utils::{
|
||
pane_size::{Dimension, PaneGeom},
|
||
position::Position,
|
||
vte,
|
||
zellij_tile::data::{InputMode, Palette, PaletteColor},
|
||
};
|
||
|
||
pub const SELECTION_SCROLL_INTERVAL_MS: u64 = 10;
|
||
|
||
use crate::ui::pane_boundaries_frame::{FrameParams, PaneFrame};
|
||
|
||
#[derive(PartialEq, Eq, Ord, PartialOrd, Hash, Clone, Copy, Debug)]
|
||
pub enum PaneId {
|
||
Terminal(RawFd),
|
||
Plugin(u32), // FIXME: Drop the trait object, make this a wrapper for the struct?
|
||
}
|
||
|
||
// FIXME: This should hold an os_api handle so that terminal panes can set their own size via FD in
|
||
// their `reflow_lines()` method. Drop a Box<dyn ServerOsApi> in here somewhere.
|
||
pub struct TerminalPane {
|
||
pub grid: Grid,
|
||
pub pid: RawFd,
|
||
pub selectable: bool,
|
||
pub geom: PaneGeom,
|
||
pub geom_override: Option<PaneGeom>,
|
||
pub active_at: Instant,
|
||
pub colors: Palette,
|
||
vte_parser: vte::Parser,
|
||
selection_scrolled_at: time::Instant,
|
||
content_offset: Offset,
|
||
pane_title: String,
|
||
pane_name: String,
|
||
frame: HashMap<ClientId, PaneFrame>,
|
||
borderless: bool,
|
||
fake_cursor_locations: HashSet<(usize, usize)>, // (x, y) - these hold a record of previous fake cursors which we need to clear on render
|
||
}
|
||
|
||
impl Pane for TerminalPane {
|
||
fn x(&self) -> usize {
|
||
self.get_x()
|
||
}
|
||
fn y(&self) -> usize {
|
||
self.get_y()
|
||
}
|
||
fn rows(&self) -> usize {
|
||
self.get_rows()
|
||
}
|
||
fn cols(&self) -> usize {
|
||
self.get_columns()
|
||
}
|
||
fn get_content_x(&self) -> usize {
|
||
self.get_x() + self.content_offset.left
|
||
}
|
||
fn get_content_y(&self) -> usize {
|
||
self.get_y() + self.content_offset.top
|
||
}
|
||
fn get_content_columns(&self) -> usize {
|
||
// content columns might differ from the pane's columns if the pane has a frame
|
||
// in that case they would be 2 less
|
||
self.get_columns()
|
||
.saturating_sub(self.content_offset.left + self.content_offset.right)
|
||
}
|
||
fn get_content_rows(&self) -> usize {
|
||
// content rows might differ from the pane's rows if the pane has a frame
|
||
// in that case they would be 2 less
|
||
self.get_rows()
|
||
.saturating_sub(self.content_offset.top + self.content_offset.bottom)
|
||
}
|
||
fn reset_size_and_position_override(&mut self) {
|
||
self.geom_override = None;
|
||
self.reflow_lines();
|
||
}
|
||
fn set_geom(&mut self, position_and_size: PaneGeom) {
|
||
self.geom = position_and_size;
|
||
self.reflow_lines();
|
||
}
|
||
fn get_geom_override(&mut self, pane_geom: PaneGeom) {
|
||
self.geom_override = Some(pane_geom);
|
||
self.reflow_lines();
|
||
}
|
||
fn handle_pty_bytes(&mut self, bytes: VteBytes) {
|
||
for &byte in &bytes {
|
||
self.vte_parser.advance(&mut self.grid, byte);
|
||
}
|
||
self.set_should_render(true);
|
||
}
|
||
fn cursor_coordinates(&self) -> Option<(usize, usize)> {
|
||
// (x, y)
|
||
let Offset { top, left, .. } = self.content_offset;
|
||
self.grid
|
||
.cursor_coordinates()
|
||
.map(|(x, y)| (x + left, y + top))
|
||
}
|
||
fn adjust_input_to_terminal(&self, input_bytes: Vec<u8>) -> Vec<u8> {
|
||
// there are some cases in which the terminal state means that input sent to it
|
||
// needs to be adjusted.
|
||
// here we match against those cases - if need be, we adjust the input and if not
|
||
// we send back the original input
|
||
match input_bytes.as_slice() {
|
||
[27, 91, 68] => {
|
||
// left arrow
|
||
if self.grid.cursor_key_mode {
|
||
// please note that in the line below, there is an ANSI escape code (27) at the beginning of the string,
|
||
// some editors will not show this
|
||
return "OD".as_bytes().to_vec();
|
||
}
|
||
}
|
||
[27, 91, 67] => {
|
||
// right arrow
|
||
if self.grid.cursor_key_mode {
|
||
// please note that in the line below, there is an ANSI escape code (27) at the beginning of the string,
|
||
// some editors will not show this
|
||
return "OC".as_bytes().to_vec();
|
||
}
|
||
}
|
||
[27, 91, 65] => {
|
||
// up arrow
|
||
if self.grid.cursor_key_mode {
|
||
// please note that in the line below, there is an ANSI escape code (27) at the beginning of the string,
|
||
// some editors will not show this
|
||
return "OA".as_bytes().to_vec();
|
||
}
|
||
}
|
||
[27, 91, 66] => {
|
||
// down arrow
|
||
if self.grid.cursor_key_mode {
|
||
// please note that in the line below, there is an ANSI escape code (27) at the beginning of the string,
|
||
// some editors will not show this
|
||
return "OB".as_bytes().to_vec();
|
||
}
|
||
}
|
||
[27, 91, 50, 48, 48, 126] | [27, 91, 50, 48, 49, 126] => {
|
||
if !self.grid.bracketed_paste_mode {
|
||
// Zellij itself operates in bracketed paste mode, so the terminal sends these
|
||
// instructions (bracketed paste start and bracketed paste end respectively)
|
||
// when pasting input. We only need to make sure not to send them to terminal
|
||
// panes who do not work in this mode
|
||
return vec![];
|
||
}
|
||
}
|
||
_ => {}
|
||
};
|
||
input_bytes
|
||
}
|
||
fn position_and_size(&self) -> PaneGeom {
|
||
self.geom
|
||
}
|
||
fn current_geom(&self) -> PaneGeom {
|
||
self.geom_override.unwrap_or(self.geom)
|
||
}
|
||
fn geom_override(&self) -> Option<PaneGeom> {
|
||
self.geom_override
|
||
}
|
||
fn should_render(&self) -> bool {
|
||
self.grid.should_render
|
||
}
|
||
fn set_should_render(&mut self, should_render: bool) {
|
||
self.grid.should_render = should_render;
|
||
}
|
||
fn render_full_viewport(&mut self) {
|
||
// this marks the pane for a full re-render, rather than just rendering the
|
||
// diff as it usually does with the OutputBuffer
|
||
self.frame.clear();
|
||
self.grid.render_full_viewport();
|
||
}
|
||
fn selectable(&self) -> bool {
|
||
self.selectable
|
||
}
|
||
fn set_selectable(&mut self, selectable: bool) {
|
||
self.selectable = selectable;
|
||
}
|
||
fn render(
|
||
&mut self,
|
||
_client_id: Option<ClientId>,
|
||
) -> Option<(Vec<CharacterChunk>, Option<String>)> {
|
||
if self.should_render() {
|
||
let mut raw_vte_output = String::new();
|
||
let content_x = self.get_content_x();
|
||
let content_y = self.get_content_y();
|
||
|
||
let mut character_chunks = self.grid.read_changes(content_x, content_y);
|
||
for character_chunk in character_chunks.iter_mut() {
|
||
character_chunk.add_changed_colors(self.grid.changed_colors);
|
||
if self
|
||
.grid
|
||
.selection
|
||
.contains_row(character_chunk.y.saturating_sub(content_y))
|
||
{
|
||
let background_color = match self.colors.bg {
|
||
PaletteColor::Rgb(rgb) => AnsiCode::RgbCode(rgb),
|
||
PaletteColor::EightBit(col) => AnsiCode::ColorIndex(col),
|
||
};
|
||
character_chunk.add_selection_and_background(
|
||
self.grid.selection,
|
||
background_color,
|
||
content_x,
|
||
content_y,
|
||
);
|
||
}
|
||
}
|
||
if self.grid.ring_bell {
|
||
let ring_bell = '\u{7}';
|
||
raw_vte_output.push(ring_bell);
|
||
self.grid.ring_bell = false;
|
||
}
|
||
self.set_should_render(false);
|
||
Some((character_chunks, Some(raw_vte_output)))
|
||
} else {
|
||
None
|
||
}
|
||
}
|
||
fn render_frame(
|
||
&mut self,
|
||
client_id: ClientId,
|
||
frame_params: FrameParams,
|
||
input_mode: InputMode,
|
||
) -> Option<(Vec<CharacterChunk>, Option<String>)> {
|
||
// TODO: remove the cursor stuff from here
|
||
let pane_title = if self.pane_name.is_empty()
|
||
&& input_mode == InputMode::RenamePane
|
||
&& frame_params.is_main_client
|
||
{
|
||
String::from("Enter name...")
|
||
} else if self.pane_name.is_empty() {
|
||
self.grid
|
||
.title
|
||
.clone()
|
||
.unwrap_or_else(|| self.pane_title.clone())
|
||
} else {
|
||
self.pane_name.clone()
|
||
};
|
||
let frame = PaneFrame::new(
|
||
self.current_geom().into(),
|
||
self.grid.scrollback_position_and_length(),
|
||
pane_title,
|
||
frame_params,
|
||
);
|
||
match self.frame.get(&client_id) {
|
||
// TODO: use and_then or something?
|
||
Some(last_frame) => {
|
||
if &frame != last_frame {
|
||
if !self.borderless {
|
||
let frame_output = frame.render();
|
||
self.frame.insert(client_id, frame);
|
||
Some(frame_output)
|
||
} else {
|
||
None
|
||
}
|
||
} else {
|
||
None
|
||
}
|
||
}
|
||
None => {
|
||
if !self.borderless {
|
||
let frame_output = frame.render();
|
||
self.frame.insert(client_id, frame);
|
||
Some(frame_output)
|
||
} else {
|
||
None
|
||
}
|
||
}
|
||
}
|
||
}
|
||
fn render_fake_cursor(
|
||
&mut self,
|
||
cursor_color: PaletteColor,
|
||
text_color: PaletteColor,
|
||
) -> Option<String> {
|
||
let mut vte_output = None;
|
||
if let Some((cursor_x, cursor_y)) = self.cursor_coordinates() {
|
||
let mut character_under_cursor = self
|
||
.grid
|
||
.get_character_under_cursor()
|
||
.unwrap_or(EMPTY_TERMINAL_CHARACTER);
|
||
character_under_cursor.styles.background = Some(cursor_color.into());
|
||
character_under_cursor.styles.foreground = Some(text_color.into());
|
||
// we keep track of these so that we can clear them up later (see render function)
|
||
self.fake_cursor_locations.insert((cursor_y, cursor_x));
|
||
let mut fake_cursor = format!(
|
||
"\u{1b}[{};{}H\u{1b}[m{}", // goto row column and clear styles
|
||
self.get_content_y() + cursor_y + 1, // + 1 because goto is 1 indexed
|
||
self.get_content_x() + cursor_x + 1,
|
||
&character_under_cursor.styles,
|
||
);
|
||
fake_cursor.push(character_under_cursor.character);
|
||
vte_output = Some(fake_cursor);
|
||
}
|
||
vte_output
|
||
}
|
||
fn update_name(&mut self, name: &str) {
|
||
match name {
|
||
"\0" => {
|
||
self.pane_name = String::new();
|
||
}
|
||
"\u{007F}" | "\u{0008}" => {
|
||
//delete and backspace keys
|
||
self.pane_name.pop();
|
||
}
|
||
c => {
|
||
self.pane_name.push_str(c);
|
||
}
|
||
}
|
||
}
|
||
fn pid(&self) -> PaneId {
|
||
PaneId::Terminal(self.pid)
|
||
}
|
||
fn reduce_height(&mut self, percent: f64) {
|
||
if let Some(p) = self.geom.rows.as_percent() {
|
||
self.geom.rows = Dimension::percent(p - percent);
|
||
self.set_should_render(true);
|
||
}
|
||
}
|
||
fn increase_height(&mut self, percent: f64) {
|
||
if let Some(p) = self.geom.rows.as_percent() {
|
||
self.geom.rows = Dimension::percent(p + percent);
|
||
self.set_should_render(true);
|
||
}
|
||
}
|
||
fn reduce_width(&mut self, percent: f64) {
|
||
if let Some(p) = self.geom.cols.as_percent() {
|
||
self.geom.cols = Dimension::percent(p - percent);
|
||
self.set_should_render(true);
|
||
}
|
||
}
|
||
fn increase_width(&mut self, percent: f64) {
|
||
if let Some(p) = self.geom.cols.as_percent() {
|
||
self.geom.cols = Dimension::percent(p + percent);
|
||
self.set_should_render(true);
|
||
}
|
||
}
|
||
fn push_down(&mut self, count: usize) {
|
||
self.geom.y += count;
|
||
self.reflow_lines();
|
||
}
|
||
fn push_right(&mut self, count: usize) {
|
||
self.geom.x += count;
|
||
self.reflow_lines();
|
||
}
|
||
fn pull_left(&mut self, count: usize) {
|
||
self.geom.x -= count;
|
||
self.reflow_lines();
|
||
}
|
||
fn pull_up(&mut self, count: usize) {
|
||
self.geom.y -= count;
|
||
self.reflow_lines();
|
||
}
|
||
fn scroll_up(&mut self, count: usize, _client_id: ClientId) {
|
||
self.grid.move_viewport_up(count);
|
||
self.set_should_render(true);
|
||
}
|
||
fn scroll_down(&mut self, count: usize, _client_id: ClientId) {
|
||
self.grid.move_viewport_down(count);
|
||
self.set_should_render(true);
|
||
}
|
||
fn clear_scroll(&mut self) {
|
||
self.grid.reset_viewport();
|
||
self.set_should_render(true);
|
||
}
|
||
fn is_scrolled(&self) -> bool {
|
||
self.grid.is_scrolled
|
||
}
|
||
|
||
fn active_at(&self) -> Instant {
|
||
self.active_at
|
||
}
|
||
|
||
fn set_active_at(&mut self, time: Instant) {
|
||
self.active_at = time;
|
||
}
|
||
fn cursor_shape_csi(&self) -> String {
|
||
match self.grid.cursor_shape() {
|
||
CursorShape::Initial => "\u{1b}[0 q".to_string(),
|
||
CursorShape::Block => "\u{1b}[2 q".to_string(),
|
||
CursorShape::BlinkingBlock => "\u{1b}[1 q".to_string(),
|
||
CursorShape::Underline => "\u{1b}[4 q".to_string(),
|
||
CursorShape::BlinkingUnderline => "\u{1b}[3 q".to_string(),
|
||
CursorShape::Beam => "\u{1b}[6 q".to_string(),
|
||
CursorShape::BlinkingBeam => "\u{1b}[5 q".to_string(),
|
||
}
|
||
}
|
||
fn drain_messages_to_pty(&mut self) -> Vec<Vec<u8>> {
|
||
self.grid.pending_messages_to_pty.drain(..).collect()
|
||
}
|
||
|
||
fn start_selection(&mut self, start: &Position, _client_id: ClientId) {
|
||
self.grid.start_selection(start);
|
||
self.set_should_render(true);
|
||
}
|
||
|
||
fn update_selection(&mut self, to: &Position, _client_id: ClientId) {
|
||
let should_scroll = self.selection_scrolled_at.elapsed()
|
||
>= time::Duration::from_millis(SELECTION_SCROLL_INTERVAL_MS);
|
||
// TODO: check how far up/down mouse is relative to pane, to increase scroll lines?
|
||
if to.line.0 < 0 && should_scroll {
|
||
self.grid.scroll_up_one_line();
|
||
self.selection_scrolled_at = time::Instant::now();
|
||
} else if to.line.0 as usize >= self.grid.height && should_scroll {
|
||
self.grid.scroll_down_one_line();
|
||
self.selection_scrolled_at = time::Instant::now();
|
||
} else if to.line.0 >= 0 && (to.line.0 as usize) < self.grid.height {
|
||
self.grid.update_selection(to);
|
||
}
|
||
|
||
self.set_should_render(true);
|
||
}
|
||
|
||
fn end_selection(&mut self, end: Option<&Position>, _client_id: ClientId) {
|
||
self.grid.end_selection(end);
|
||
self.set_should_render(true);
|
||
}
|
||
|
||
fn reset_selection(&mut self) {
|
||
self.grid.reset_selection();
|
||
}
|
||
|
||
fn get_selected_text(&self) -> Option<String> {
|
||
self.grid.get_selected_text()
|
||
}
|
||
|
||
fn set_frame(&mut self, _frame: bool) {
|
||
self.frame.clear();
|
||
}
|
||
|
||
fn set_content_offset(&mut self, offset: Offset) {
|
||
self.content_offset = offset;
|
||
self.reflow_lines();
|
||
}
|
||
|
||
fn set_borderless(&mut self, borderless: bool) {
|
||
self.borderless = borderless;
|
||
}
|
||
fn borderless(&self) -> bool {
|
||
self.borderless
|
||
}
|
||
}
|
||
|
||
impl TerminalPane {
|
||
pub fn new(
|
||
pid: RawFd,
|
||
position_and_size: PaneGeom,
|
||
palette: Palette,
|
||
pane_index: usize,
|
||
pane_name: String,
|
||
link_handler: Rc<RefCell<LinkHandler>>,
|
||
) -> TerminalPane {
|
||
let initial_pane_title = format!("Pane #{}", pane_index);
|
||
let grid = Grid::new(
|
||
position_and_size.rows.as_usize(),
|
||
position_and_size.cols.as_usize(),
|
||
palette,
|
||
link_handler.clone(),
|
||
);
|
||
TerminalPane {
|
||
frame: HashMap::new(),
|
||
content_offset: Offset::default(),
|
||
pid,
|
||
grid,
|
||
selectable: true,
|
||
geom: position_and_size,
|
||
geom_override: None,
|
||
vte_parser: vte::Parser::new(),
|
||
active_at: Instant::now(),
|
||
colors: palette,
|
||
selection_scrolled_at: time::Instant::now(),
|
||
pane_title: initial_pane_title,
|
||
pane_name,
|
||
borderless: false,
|
||
fake_cursor_locations: HashSet::new(),
|
||
}
|
||
}
|
||
pub fn get_x(&self) -> usize {
|
||
match self.geom_override {
|
||
Some(position_and_size_override) => position_and_size_override.x,
|
||
None => self.geom.x,
|
||
}
|
||
}
|
||
pub fn get_y(&self) -> usize {
|
||
match self.geom_override {
|
||
Some(position_and_size_override) => position_and_size_override.y,
|
||
None => self.geom.y,
|
||
}
|
||
}
|
||
pub fn get_columns(&self) -> usize {
|
||
match self.geom_override {
|
||
Some(position_and_size_override) => position_and_size_override.cols.as_usize(),
|
||
None => self.geom.cols.as_usize(),
|
||
}
|
||
}
|
||
pub fn get_rows(&self) -> usize {
|
||
match self.geom_override {
|
||
Some(position_and_size_override) => position_and_size_override.rows.as_usize(),
|
||
None => self.geom.rows.as_usize(),
|
||
}
|
||
}
|
||
fn reflow_lines(&mut self) {
|
||
let rows = self.get_content_rows();
|
||
let cols = self.get_content_columns();
|
||
self.grid.change_size(rows, cols);
|
||
self.set_should_render(true);
|
||
}
|
||
pub fn read_buffer_as_lines(&self) -> Vec<Vec<TerminalCharacter>> {
|
||
self.grid.as_character_lines()
|
||
}
|
||
pub fn cursor_coordinates(&self) -> Option<(usize, usize)> {
|
||
// (x, y)
|
||
self.grid.cursor_coordinates()
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
#[path = "./unit/terminal_pane_tests.rs"]
|
||
mod grid_tests;
|