zellij/zellij-server/src/panes/terminal_pane.rs
Aram Drevekenin 821e7cbc5a
feat(ui): add floating panes (#1066)
* 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
2022-02-18 21:10:06 +01:00

528 lines
18 KiB
Rust
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;