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
This commit is contained in:
Aram Drevekenin 2022-02-18 21:10:06 +01:00 committed by GitHub
parent 10a22c479f
commit 821e7cbc5a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
61 changed files with 6808 additions and 1250 deletions

View file

@ -75,4 +75,4 @@ jobs:
- name: Install cargo-make
run: test -x "${HOME}/.cargo/bin/cargo-make" || cargo install --debug cargo-make
- name: Check Lints
run: cargo make clippy -D clippy::all
run: cargo make clippy

View file

@ -24,6 +24,7 @@ pub const MOVE_FOCUS_IN_PANE_MODE: [u8; 1] = [112]; // p
pub const SPLIT_DOWN_IN_PANE_MODE: [u8; 1] = [100]; // d
pub const SPLIT_RIGHT_IN_PANE_MODE: [u8; 1] = [114]; // r
pub const TOGGLE_ACTIVE_TERMINAL_FULLSCREEN_IN_PANE_MODE: [u8; 1] = [102]; // f
pub const TOGGLE_FLOATING_PANES: [u8; 1] = [119]; // w
pub const CLOSE_PANE_IN_PANE_MODE: [u8; 1] = [120]; // x
pub const MOVE_FOCUS_DOWN_IN_PANE_MODE: [u8; 1] = [106]; // j
pub const MOVE_FOCUS_UP_IN_PANE_MODE: [u8; 1] = [107]; // k
@ -1664,6 +1665,53 @@ pub fn bracketed_paste() {
assert_snapshot!(last_snapshot);
}
#[test]
#[ignore]
pub fn toggle_floating_panes() {
let fake_win_size = Size {
cols: 120,
rows: 24,
};
let mut test_attempts = 10;
let last_snapshot = loop {
RemoteRunner::kill_running_sessions(fake_win_size);
let mut runner = RemoteRunner::new(fake_win_size).add_step(Step {
name: "Toggle floating panes",
instruction: |mut remote_terminal: RemoteTerminal| -> bool {
let mut step_is_complete = false;
if remote_terminal.status_bar_appears() && remote_terminal.cursor_position_is(3, 2)
{
remote_terminal.send_key(&PANE_MODE);
remote_terminal.send_key(&TOGGLE_FLOATING_PANES);
// back to normal mode after split
step_is_complete = true;
}
step_is_complete
},
});
runner.run_all_steps();
let last_snapshot = runner.take_snapshot_after(Step {
name: "Wait for new pane to appear",
instruction: |remote_terminal: RemoteTerminal| -> bool {
let mut step_is_complete = false;
if remote_terminal.cursor_position_is(33, 7) && remote_terminal.tip_appears() {
// cursor is in the newly opened second pane
step_is_complete = true;
}
step_is_complete
},
});
if runner.test_timed_out && test_attempts > 0 {
test_attempts -= 1;
continue;
} else {
break last_snapshot;
}
};
assert_snapshot!(last_snapshot);
}
#[test]
#[ignore]
pub fn focus_tab_with_layout() {

View file

@ -2,7 +2,7 @@ use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use zellij_tile::data::Palette;
use zellij_server::panes::TerminalPane;
use zellij_server::panes::{LinkHandler, TerminalPane};
use zellij_utils::pane_size::{Dimension, PaneGeom, Size};
use zellij_utils::{vte, zellij_tile};
@ -12,6 +12,9 @@ use std::net::TcpStream;
use std::path::Path;
use std::cell::RefCell;
use std::rc::Rc;
const ZELLIJ_EXECUTABLE_LOCATION: &str = "/usr/src/zellij/x86_64-unknown-linux-musl/release/zellij";
const ZELLIJ_LAYOUT_PATH: &str = "/usr/src/zellij/fixtures/layouts";
const CONNECTION_STRING: &str = "127.0.0.1:2222";
@ -141,16 +144,23 @@ fn read_from_channel(
let thread = std::thread::Builder::new()
.name("read_thread".into())
.spawn({
let pane_geom = *pane_geom;
let should_keep_running = should_keep_running.clone();
let channel = channel.clone();
let last_snapshot = last_snapshot.clone();
let cursor_coordinates = cursor_coordinates.clone();
let mut vte_parser = vte::Parser::new();
let mut terminal_output =
TerminalPane::new(0, *pane_geom, Palette::default(), 0, String::new()); // 0 is the pane index
let mut retries_left = 3;
move || {
let mut retries_left = 3;
let mut should_sleep = false;
let mut vte_parser = vte::Parser::new();
let mut terminal_output = TerminalPane::new(
0,
pane_geom,
Palette::default(),
0,
String::new(),
Rc::new(RefCell::new(LinkHandler::new())),
); // 0 is the pane index
loop {
if !should_keep_running.load(Ordering::SeqCst) {
break;

View file

@ -0,0 +1,29 @@
---
source: src/tests/e2e/cases.rs
expression: last_snapshot
---
Zellij (e2e-test)  Tab #1 
┌ Pane #1 ─────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│$ │
│ │
│ │
│ │
│ ┌ Pane #2 ─────────────────────────────────────────────────┐ │
│ │$ █ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ │
│ │
│ │
│ │
└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
Ctrl + <g> LOCK  <p> PANE  <t> TAB  <n> RESIZE  <h> MOVE  <s> SCROLL  <o> SESSION  <q> QUIT 
Tip: Alt + <n> => new pane. Alt + <[] or hjkl> => navigate. Alt + <+-> => resize pane.

View file

@ -199,6 +199,8 @@ impl InputHandler {
}
Action::CloseFocus
| Action::NewPane(_)
| Action::ToggleFloatingPanes
| Action::TogglePaneEmbedOrFloating
| Action::NewTab(_)
| Action::GoToNextTab
| Action::GoToPreviousTab

View file

@ -1,4 +1,5 @@
pub mod os_input_output;
pub mod output;
pub mod panes;
pub mod tab;
@ -29,7 +30,6 @@ use crate::{
os_input_output::ServerOsApi,
pty::{pty_thread_main, Pty, PtyInstruction},
screen::{screen_thread_main, ScreenInstruction},
tab::Output,
thread_bus::{Bus, ThreadSenders},
wasm_vm::{wasm_thread_main, PluginInstruction},
};
@ -63,7 +63,7 @@ pub enum ServerInstruction {
ClientId,
Option<PluginsConfig>,
),
Render(Option<Output>),
Render(Option<HashMap<ClientId, String>>),
UnblockInputThread,
ClientExit(ClientId),
RemoveClient(ClientId),
@ -492,14 +492,12 @@ pub fn start_server(mut os_input: Box<dyn ServerOsApi>, socket_path: PathBuf) {
.send_to_plugin(PluginInstruction::RemoveClient(client_id))
.unwrap();
}
ServerInstruction::Render(mut output) => {
ServerInstruction::Render(serialized_output) => {
let client_ids = session_state.read().unwrap().client_ids();
// Here the output is of the type Option<String> sent by screen thread.
// If `Some(_)`- unwrap it and forward it to the clients to render.
// If `None`- Send an exit instruction. This is the case when a user closes the last Tab/Pane.
if let Some(op) = &mut output {
for (client_id, client_render_instruction) in &mut op.client_render_instructions
{
if let Some(output) = &serialized_output {
for (client_id, client_render_instruction) in output.iter() {
os_input.send_to_client(
*client_id,
ServerToClientMsg::Render(client_render_instruction.clone()),

View file

@ -0,0 +1,571 @@
use std::collections::VecDeque;
use crate::panes::selection::Selection;
use crate::panes::Row;
use crate::{
panes::terminal_character::{AnsiCode, CharacterStyles},
panes::{LinkHandler, TerminalCharacter, EMPTY_TERMINAL_CHARACTER},
ClientId,
};
use std::cell::RefCell;
use std::fmt::Write;
use std::rc::Rc;
use std::{
collections::{HashMap, HashSet},
str,
};
use zellij_utils::pane_size::PaneGeom;
fn vte_goto_instruction(x_coords: usize, y_coords: usize, vte_output: &mut String) {
write!(
vte_output,
"\u{1b}[{};{}H\u{1b}[m",
y_coords + 1, // + 1 because VTE is 1 indexed
x_coords + 1,
)
.unwrap();
}
fn adjust_styles_for_possible_selection(
chunk_selection_and_background_color: Option<(Selection, AnsiCode)>,
character_styles: CharacterStyles,
chunk_y: usize,
chunk_width: usize,
) -> CharacterStyles {
chunk_selection_and_background_color
.and_then(|(selection, background_color)| {
if selection.contains(chunk_y, chunk_width) {
Some(character_styles.background(Some(background_color)))
} else {
None
}
})
.unwrap_or(character_styles)
}
fn write_changed_styles(
character_styles: &mut CharacterStyles,
current_character_styles: CharacterStyles,
chunk_changed_colors: Option<[Option<AnsiCode>; 256]>,
link_handler: Option<&std::cell::Ref<LinkHandler>>,
vte_output: &mut String,
) {
if let Some(new_styles) =
character_styles.update_and_return_diff(&current_character_styles, chunk_changed_colors)
{
// if let Some(osc8_link) = link_handler.as_ref().and_then(|l_h| l_h.borrow().output_osc8(new_styles.link_anchor)) {
if let Some(osc8_link) =
link_handler.and_then(|l_h| l_h.output_osc8(new_styles.link_anchor))
{
write!(vte_output, "{}{}", new_styles, osc8_link).unwrap();
} else {
write!(vte_output, "{}", new_styles).unwrap();
}
}
}
fn serialize_character_chunks(
character_chunks: Vec<CharacterChunk>,
link_handler: Option<&mut Rc<RefCell<LinkHandler>>>,
) -> String {
let mut vte_output = String::new(); // TODO: preallocate character_chunks.len()?
let link_handler = link_handler.map(|l_h| l_h.borrow());
for character_chunk in character_chunks {
let chunk_selection_and_background_color = character_chunk.selection_and_background_color();
let chunk_changed_colors = character_chunk.changed_colors();
let mut character_styles = CharacterStyles::new();
vte_goto_instruction(character_chunk.x, character_chunk.y, &mut vte_output);
let mut chunk_width = character_chunk.x;
for t_character in character_chunk.terminal_characters.iter() {
let current_character_styles = adjust_styles_for_possible_selection(
chunk_selection_and_background_color,
t_character.styles,
character_chunk.y,
chunk_width,
);
write_changed_styles(
&mut character_styles,
current_character_styles,
chunk_changed_colors,
link_handler.as_ref(),
&mut vte_output,
);
chunk_width += t_character.width;
vte_output.push(t_character.character);
}
character_styles.clear();
}
vte_output
}
type AbsoluteMiddleStart = usize;
type AbsoluteMiddleEnd = usize;
type PadLeftEndBy = usize;
type PadRightStartBy = usize;
fn adjust_middle_segment_for_wide_chars(
middle_start: usize,
middle_end: usize,
terminal_characters: &[TerminalCharacter],
) -> (
AbsoluteMiddleStart,
AbsoluteMiddleEnd,
PadLeftEndBy,
PadRightStartBy,
) {
let mut absolute_middle_start_index = None;
let mut absolute_middle_end_index = None;
let mut current_x = 0;
let mut pad_left_end_by = 0;
let mut pad_right_start_by = 0;
for (absolute_index, t_character) in terminal_characters.iter().enumerate() {
current_x += t_character.width;
if current_x >= middle_start && absolute_middle_start_index.is_none() {
if current_x > middle_start {
pad_left_end_by = current_x - middle_start;
absolute_middle_start_index = Some(absolute_index);
} else {
absolute_middle_start_index = Some(absolute_index + 1);
}
}
if current_x >= middle_end && absolute_middle_end_index.is_none() {
absolute_middle_end_index = Some(absolute_index + 1);
if current_x > middle_end {
pad_right_start_by = current_x - middle_end;
}
}
}
(
absolute_middle_start_index.unwrap(),
absolute_middle_end_index.unwrap(),
pad_left_end_by,
pad_right_start_by,
)
}
#[derive(Clone, Debug, Default)]
pub struct Output {
pre_vte_instructions: HashMap<ClientId, Vec<String>>,
post_vte_instructions: HashMap<ClientId, Vec<String>>,
client_character_chunks: HashMap<ClientId, Vec<CharacterChunk>>,
link_handler: Option<Rc<RefCell<LinkHandler>>>,
floating_panes_stack: Option<FloatingPanesStack>,
}
impl Output {
pub fn add_clients(
&mut self,
client_ids: &HashSet<ClientId>,
link_handler: Rc<RefCell<LinkHandler>>,
floating_panes_stack: Option<FloatingPanesStack>,
) {
self.link_handler = Some(link_handler);
self.floating_panes_stack = floating_panes_stack;
for client_id in client_ids {
self.client_character_chunks.insert(*client_id, vec![]);
}
}
pub fn add_character_chunks_to_client(
&mut self,
client_id: ClientId,
mut character_chunks: Vec<CharacterChunk>,
z_index: Option<usize>,
) {
if let Some(client_character_chunks) = self.client_character_chunks.get_mut(&client_id) {
if let Some(floating_panes_stack) = &self.floating_panes_stack {
let mut visible_character_chunks =
floating_panes_stack.visible_character_chunks(character_chunks, z_index);
client_character_chunks.append(&mut visible_character_chunks);
} else {
client_character_chunks.append(&mut character_chunks);
}
}
}
pub fn add_character_chunks_to_multiple_clients(
&mut self,
character_chunks: Vec<CharacterChunk>,
client_ids: impl Iterator<Item = ClientId>,
z_index: Option<usize>,
) {
for client_id in client_ids {
self.add_character_chunks_to_client(client_id, character_chunks.clone(), z_index);
// TODO: forgo clone by adding an all_clients thing?
}
}
pub fn add_post_vte_instruction_to_multiple_clients(
&mut self,
client_ids: impl Iterator<Item = ClientId>,
vte_instruction: &str,
) {
for client_id in client_ids {
let entry = self
.post_vte_instructions
.entry(client_id)
.or_insert_with(Vec::new);
entry.push(String::from(vte_instruction));
}
}
pub fn add_pre_vte_instruction_to_multiple_clients(
&mut self,
client_ids: impl Iterator<Item = ClientId>,
vte_instruction: &str,
) {
for client_id in client_ids {
let entry = self
.pre_vte_instructions
.entry(client_id)
.or_insert_with(Vec::new);
entry.push(String::from(vte_instruction));
}
}
pub fn add_post_vte_instruction_to_client(
&mut self,
client_id: ClientId,
vte_instruction: &str,
) {
let entry = self
.post_vte_instructions
.entry(client_id)
.or_insert_with(Vec::new);
entry.push(String::from(vte_instruction));
}
pub fn add_pre_vte_instruction_to_client(
&mut self,
client_id: ClientId,
vte_instruction: &str,
) {
let entry = self
.pre_vte_instructions
.entry(client_id)
.or_insert_with(Vec::new);
entry.push(String::from(vte_instruction));
}
pub fn serialize(&mut self) -> HashMap<ClientId, String> {
let mut serialized_render_instructions = HashMap::new();
for (client_id, client_character_chunks) in self.client_character_chunks.drain() {
let mut client_serialized_render_instructions = String::new();
// append pre-vte instructions for this client
if let Some(pre_vte_instructions_for_client) =
self.pre_vte_instructions.remove(&client_id)
{
for vte_instruction in pre_vte_instructions_for_client {
client_serialized_render_instructions.push_str(&vte_instruction);
}
}
// append the actual vte
client_serialized_render_instructions.push_str(&serialize_character_chunks(
client_character_chunks,
self.link_handler.as_mut(),
)); // TODO: less allocations?
// append post-vte instructions for this client
if let Some(post_vte_instructions_for_client) =
self.post_vte_instructions.remove(&client_id)
{
for vte_instruction in post_vte_instructions_for_client {
client_serialized_render_instructions.push_str(&vte_instruction);
}
}
serialized_render_instructions.insert(client_id, client_serialized_render_instructions);
}
serialized_render_instructions
}
}
// this struct represents the geometry of a group of floating panes
// we use it to filter out CharacterChunks who are behind these geometries
// and so would not be visible. If a chunk is partially covered, it is adjusted
// to include only the non-covered parts
#[derive(Debug, Clone, Default)]
pub struct FloatingPanesStack {
pub layers: Vec<PaneGeom>,
}
impl FloatingPanesStack {
pub fn visible_character_chunks(
&self,
mut character_chunks: Vec<CharacterChunk>,
z_index: Option<usize>,
) -> Vec<CharacterChunk> {
let z_index = z_index.unwrap_or(0);
let mut chunks_to_check: Vec<CharacterChunk> = character_chunks.drain(..).collect();
let mut visible_chunks = vec![];
'chunk_loop: loop {
match chunks_to_check.pop() {
Some(mut c_chunk) => {
let panes_to_check = self.layers.iter().skip(z_index);
for pane_geom in panes_to_check {
let new_chunk_to_check = self.remove_covered_parts(pane_geom, &mut c_chunk);
if let Some(new_chunk_to_check) = new_chunk_to_check {
// this happens when the pane covers the middle of the chunk, and so we
// end up with an extra chunk we need to check (eg. against panes above
// this one)
chunks_to_check.push(new_chunk_to_check);
}
if c_chunk.terminal_characters.is_empty() {
continue 'chunk_loop;
}
}
visible_chunks.push(c_chunk);
}
None => {
break 'chunk_loop;
}
}
}
visible_chunks
}
fn remove_covered_parts(
&self,
pane_geom: &PaneGeom,
c_chunk: &mut CharacterChunk,
) -> Option<CharacterChunk> {
let pane_top_edge = pane_geom.y;
let pane_left_edge = pane_geom.x;
let pane_bottom_edge = pane_geom.y + pane_geom.rows.as_usize().saturating_sub(1);
let pane_right_edge = pane_geom.x + pane_geom.cols.as_usize().saturating_sub(1);
let c_chunk_left_side = c_chunk.x;
let c_chunk_right_side = c_chunk.x + (c_chunk.width()).saturating_sub(1);
if pane_top_edge <= c_chunk.y && pane_bottom_edge >= c_chunk.y {
if pane_left_edge <= c_chunk_left_side && pane_right_edge >= c_chunk_right_side {
// pane covers chunk completely
drop(c_chunk.terminal_characters.drain(..));
return None;
} else if pane_right_edge > c_chunk_left_side
&& pane_right_edge < c_chunk_right_side
&& pane_left_edge <= c_chunk_left_side
{
// pane covers chunk partially to the left
let covered_part = c_chunk.drain_by_width(pane_right_edge + 1 - c_chunk_left_side);
drop(covered_part);
c_chunk.x = pane_right_edge + 1;
return None;
} else if pane_left_edge > c_chunk_left_side
&& pane_left_edge < c_chunk_right_side
&& pane_right_edge >= c_chunk_right_side
{
// pane covers chunk partially to the right
c_chunk.retain_by_width(pane_left_edge - c_chunk_left_side);
return None;
} else if pane_left_edge >= c_chunk_left_side && pane_right_edge <= c_chunk_right_side {
// pane covers chunk middle
let (left_chunk_characters, right_chunk_characters) = c_chunk.cut_middle_out(
pane_left_edge - c_chunk_left_side,
(pane_right_edge + 1) - c_chunk_left_side,
);
let left_chunk_x = c_chunk_left_side;
let right_chunk_x = pane_right_edge + 1;
let left_chunk =
CharacterChunk::new(left_chunk_characters, left_chunk_x, c_chunk.y);
c_chunk.x = right_chunk_x;
c_chunk.terminal_characters = right_chunk_characters;
return Some(left_chunk);
}
};
None
}
}
#[derive(Debug, Clone, Default)]
pub struct CharacterChunk {
pub terminal_characters: Vec<TerminalCharacter>,
pub x: usize,
pub y: usize,
pub changed_colors: Option<[Option<AnsiCode>; 256]>,
selection_and_background_color: Option<(Selection, AnsiCode)>,
}
impl CharacterChunk {
pub fn new(terminal_characters: Vec<TerminalCharacter>, x: usize, y: usize) -> Self {
CharacterChunk {
terminal_characters,
x,
y,
..Default::default()
}
}
pub fn add_selection_and_background(
&mut self,
selection: Selection,
background_color: AnsiCode,
offset_x: usize,
offset_y: usize,
) {
self.selection_and_background_color =
Some((selection.offset(offset_x, offset_y), background_color));
}
pub fn selection_and_background_color(&self) -> Option<(Selection, AnsiCode)> {
self.selection_and_background_color
}
pub fn add_changed_colors(&mut self, changed_colors: Option<[Option<AnsiCode>; 256]>) {
self.changed_colors = changed_colors;
}
pub fn changed_colors(&self) -> Option<[Option<AnsiCode>; 256]> {
self.changed_colors
}
pub fn width(&self) -> usize {
let mut width = 0;
for t_character in &self.terminal_characters {
width += t_character.width
}
width
}
pub fn drain_by_width(&mut self, x: usize) -> impl Iterator<Item = TerminalCharacter> {
let mut drained_part: VecDeque<TerminalCharacter> = VecDeque::new();
let mut drained_part_len = 0;
loop {
if self.terminal_characters.is_empty() {
break;
}
let next_character = self.terminal_characters.remove(0); // TODO: consider copying self.terminal_characters into a VecDeque to make this process faster?
if drained_part_len + next_character.width <= x {
drained_part.push_back(next_character);
drained_part_len += next_character.width;
} else {
if drained_part_len == x {
self.terminal_characters.insert(0, next_character); // put it back
} else if next_character.width > 1 {
for _ in 1..next_character.width {
self.terminal_characters.insert(0, EMPTY_TERMINAL_CHARACTER);
drained_part.push_back(EMPTY_TERMINAL_CHARACTER);
}
}
break;
}
}
drained_part.into_iter()
}
pub fn retain_by_width(&mut self, x: usize) {
let part_to_retain = self.drain_by_width(x);
self.terminal_characters = part_to_retain.collect();
}
pub fn cut_middle_out(
&mut self,
middle_start: usize,
middle_end: usize,
) -> (Vec<TerminalCharacter>, Vec<TerminalCharacter>) {
let (
absolute_middle_start_index,
absolute_middle_end_index,
pad_left_end_by,
pad_right_start_by,
) = adjust_middle_segment_for_wide_chars(
middle_start,
middle_end,
&self.terminal_characters,
);
let mut terminal_characters: Vec<TerminalCharacter> =
self.terminal_characters.drain(..).collect();
let mut characters_on_the_right: Vec<TerminalCharacter> = terminal_characters
.drain(absolute_middle_end_index..)
.collect();
let mut characters_on_the_left: Vec<TerminalCharacter> = terminal_characters
.drain(..absolute_middle_start_index)
.collect();
if pad_left_end_by > 0 {
characters_on_the_left.resize(pad_left_end_by, EMPTY_TERMINAL_CHARACTER);
}
if pad_right_start_by > 0 {
for _ in 0..pad_right_start_by {
characters_on_the_right.insert(0, EMPTY_TERMINAL_CHARACTER);
}
}
(characters_on_the_left, characters_on_the_right)
}
}
#[derive(Clone, Debug)]
pub struct OutputBuffer {
changed_lines: Vec<usize>, // line index
should_update_all_lines: bool,
}
impl Default for OutputBuffer {
fn default() -> Self {
OutputBuffer {
changed_lines: vec![],
should_update_all_lines: true, // first time we should do a full render
}
}
}
impl OutputBuffer {
pub fn update_line(&mut self, line_index: usize) {
if !self.should_update_all_lines {
self.changed_lines.push(line_index);
}
}
pub fn update_all_lines(&mut self) {
self.clear();
self.should_update_all_lines = true;
}
pub fn clear(&mut self) {
self.changed_lines.clear();
self.should_update_all_lines = false;
}
pub fn changed_chunks_in_viewport(
&self,
viewport: &[Row],
viewport_width: usize,
viewport_height: usize,
x_offset: usize,
y_offset: usize,
) -> Vec<CharacterChunk> {
if self.should_update_all_lines {
let mut changed_chunks = Vec::with_capacity(viewport.len());
for line_index in 0..viewport_height {
let terminal_characters =
self.extract_line_from_viewport(line_index, viewport, viewport_width);
let x = x_offset; // right now we only buffer full lines as this doesn't seem to have a huge impact on performance, but the infra is here if we want to change this
let y = line_index + y_offset;
changed_chunks.push(CharacterChunk::new(terminal_characters, x, y));
}
changed_chunks
} else {
let mut line_changes = self.changed_lines.to_vec();
line_changes.sort_unstable();
line_changes.dedup();
let mut changed_chunks = Vec::with_capacity(line_changes.len());
for line_index in line_changes {
let terminal_characters =
self.extract_line_from_viewport(line_index, viewport, viewport_width);
let x = x_offset;
let y = line_index + y_offset;
changed_chunks.push(CharacterChunk::new(terminal_characters, x, y));
}
changed_chunks
}
}
fn extract_characters_from_row(
&self,
row: &Row,
viewport_width: usize,
) -> Vec<TerminalCharacter> {
let mut terminal_characters: Vec<TerminalCharacter> = row.columns.iter().copied().collect();
// pad row
let row_width = row.width();
if row_width < viewport_width {
let mut padding = vec![EMPTY_TERMINAL_CHARACTER; viewport_width - row_width];
terminal_characters.append(&mut padding);
}
terminal_characters
}
fn extract_line_from_viewport(
&self,
line_index: usize,
viewport: &[Row],
viewport_width: usize,
) -> Vec<TerminalCharacter> {
match viewport.get(line_index) {
// TODO: iterator?
Some(row) => self.extract_characters_from_row(row, viewport_width),
None => {
vec![EMPTY_TERMINAL_CHARACTER; viewport_width]
}
}
}
}

View file

@ -0,0 +1,813 @@
use zellij_utils::{position::Position, zellij_tile};
use crate::tab::floating_pane_grid::FloatingPaneGrid;
use crate::tab::Pane;
use crate::{
os_input_output::ServerOsApi,
output::{FloatingPanesStack, Output},
panes::PaneId,
ui::pane_contents_and_ui::PaneContentsAndUi,
ClientId,
};
use std::cell::RefCell;
use std::collections::{BTreeMap, HashMap, HashSet};
use std::rc::Rc;
use zellij_tile::data::{ModeInfo, Palette};
use zellij_utils::pane_size::{Offset, PaneGeom, Size, Viewport};
macro_rules! resize_pty {
($pane:expr, $os_input:expr) => {
if let PaneId::Terminal(ref pid) = $pane.pid() {
// FIXME: This `set_terminal_size_using_fd` call would be best in
// `TerminalPane::reflow_lines`
$os_input.set_terminal_size_using_fd(
*pid,
$pane.get_content_columns() as u16,
$pane.get_content_rows() as u16,
);
}
};
}
pub struct FloatingPanes {
panes: BTreeMap<PaneId, Box<dyn Pane>>,
display_area: Rc<RefCell<Size>>,
viewport: Rc<RefCell<Viewport>>,
desired_pane_positions: HashMap<PaneId, PaneGeom>, // this represents the positions of panes the user moved with intention, rather than by resizing the terminal window
z_indices: Vec<PaneId>,
active_panes: HashMap<ClientId, PaneId>,
show_panes: bool,
pane_being_moved_with_mouse: Option<(PaneId, Position)>,
}
#[allow(clippy::borrowed_box)]
impl FloatingPanes {
pub fn new(display_area: Rc<RefCell<Size>>, viewport: Rc<RefCell<Viewport>>) -> Self {
FloatingPanes {
panes: BTreeMap::new(),
display_area,
viewport,
desired_pane_positions: HashMap::new(),
z_indices: vec![],
show_panes: false,
active_panes: HashMap::new(),
pane_being_moved_with_mouse: None,
}
}
pub fn stack(&self) -> FloatingPanesStack {
let layers = self
.z_indices
.iter()
.map(|pane_id| self.panes.get(pane_id).unwrap().position_and_size())
.collect();
FloatingPanesStack { layers }
}
pub fn pane_ids(&self) -> impl Iterator<Item = &PaneId> {
self.panes.keys()
}
pub fn add_pane(&mut self, pane_id: PaneId, pane: Box<dyn Pane>) {
self.desired_pane_positions
.insert(pane_id, pane.position_and_size());
self.panes.insert(pane_id, pane);
self.z_indices.push(pane_id);
}
pub fn remove_pane(&mut self, pane_id: PaneId) -> Option<Box<dyn Pane>> {
self.z_indices.retain(|p_id| *p_id != pane_id);
self.desired_pane_positions.remove(&pane_id);
self.panes.remove(&pane_id)
}
pub fn get(&self, pane_id: &PaneId) -> Option<&Box<dyn Pane>> {
self.panes.get(pane_id)
}
pub fn get_mut(&mut self, pane_id: &PaneId) -> Option<&mut Box<dyn Pane>> {
self.panes.get_mut(pane_id)
}
pub fn get_active_pane(&self, client_id: ClientId) -> Option<&Box<dyn Pane>> {
self.active_panes
.get(&client_id)
.and_then(|active_pane_id| self.panes.get(active_pane_id))
}
pub fn get_active_pane_mut(&mut self, client_id: ClientId) -> Option<&mut Box<dyn Pane>> {
self.active_panes
.get(&client_id)
.and_then(|active_pane_id| self.panes.get_mut(active_pane_id))
}
pub fn panes_are_visible(&self) -> bool {
self.show_panes
}
pub fn has_active_panes(&self) -> bool {
!self.active_panes.is_empty()
}
pub fn has_panes(&self) -> bool {
!self.panes.is_empty()
}
pub fn active_pane_id(&self, client_id: ClientId) -> Option<PaneId> {
self.active_panes.get(&client_id).copied()
}
pub fn toggle_show_panes(&mut self, should_show_floating_panes: bool) {
self.show_panes = should_show_floating_panes;
}
pub fn active_panes_contain(&self, client_id: &ClientId) -> bool {
self.active_panes.contains_key(client_id)
}
pub fn panes_contain(&self, pane_id: &PaneId) -> bool {
self.panes.contains_key(pane_id)
}
pub fn find_room_for_new_pane(&mut self) -> Option<PaneGeom> {
let display_area = *self.display_area.borrow();
let viewport = *self.viewport.borrow();
let floating_pane_grid = FloatingPaneGrid::new(
&mut self.panes,
&mut self.desired_pane_positions,
display_area,
viewport,
);
floating_pane_grid.find_room_for_new_pane()
}
pub fn first_floating_pane_id(&self) -> Option<PaneId> {
self.panes.keys().next().copied()
}
pub fn first_active_floating_pane_id(&self) -> Option<PaneId> {
self.active_panes.values().next().copied()
}
pub fn set_force_render(&mut self) {
for pane in self.panes.values_mut() {
pane.set_should_render(true);
pane.set_should_render_boundaries(true);
pane.render_full_viewport();
}
}
pub fn set_pane_frames(&mut self, os_api: &mut Box<dyn ServerOsApi>) {
for pane in self.panes.values_mut() {
// floating panes should always have a frame unless explicitly set otherwise
if !pane.borderless() {
pane.set_frame(true);
pane.set_content_offset(Offset::frame(1));
} else {
pane.set_content_offset(Offset::default());
}
resize_pty!(pane, os_api);
}
}
#[allow(clippy::too_many_arguments)]
pub fn render(
&mut self,
connected_clients_in_app: &Rc<RefCell<HashSet<ClientId>>>,
connected_clients: &HashSet<ClientId>,
mode_info: &HashMap<ClientId, ModeInfo>,
default_mode_info: &ModeInfo,
session_is_mirrored: bool,
output: &mut Output,
colors: Palette,
) {
let mut floating_panes: Vec<_> = self.panes.iter_mut().collect();
floating_panes.sort_by(|(a_id, _a_pane), (b_id, _b_pane)| {
self.z_indices
.iter()
.position(|id| id == *a_id)
.unwrap()
.cmp(&self.z_indices.iter().position(|id| id == *b_id).unwrap())
});
for (z_index, (kind, pane)) in floating_panes.iter_mut().enumerate() {
let mut active_panes = self.active_panes.clone();
let multiple_users_exist_in_session = { connected_clients_in_app.borrow().len() > 1 };
active_panes.retain(|c_id, _| connected_clients.contains(c_id));
let mut pane_contents_and_ui = PaneContentsAndUi::new(
pane,
output,
colors,
&active_panes,
multiple_users_exist_in_session,
Some(z_index + 1), // +1 because 0 is reserved for non-floating panes
);
for &client_id in connected_clients {
let client_mode = mode_info.get(&client_id).unwrap_or(default_mode_info).mode;
pane_contents_and_ui.render_pane_frame(client_id, client_mode, session_is_mirrored);
if let PaneId::Plugin(..) = kind {
pane_contents_and_ui.render_pane_contents_for_client(client_id);
}
// this is done for panes that don't have their own cursor (eg. panes of
// another user)
pane_contents_and_ui.render_fake_cursor_if_needed(client_id);
}
if let PaneId::Terminal(..) = kind {
pane_contents_and_ui
.render_pane_contents_to_multiple_clients(connected_clients.iter().copied());
}
}
}
pub fn resize(&mut self, new_screen_size: Size) {
let display_area = *self.display_area.borrow();
let viewport = *self.viewport.borrow();
let mut floating_pane_grid = FloatingPaneGrid::new(
&mut self.panes,
&mut self.desired_pane_positions,
display_area,
viewport,
);
floating_pane_grid.resize(new_screen_size);
self.set_force_render();
}
pub fn resize_active_pane_left(
&mut self,
client_id: ClientId,
os_api: &mut Box<dyn ServerOsApi>,
) -> bool {
// true => successfully resized
let display_area = *self.display_area.borrow();
let viewport = *self.viewport.borrow();
if let Some(active_floating_pane_id) = self.active_panes.get(&client_id) {
let mut floating_pane_grid = FloatingPaneGrid::new(
&mut self.panes,
&mut self.desired_pane_positions,
display_area,
viewport,
);
floating_pane_grid.resize_pane_left(active_floating_pane_id);
for pane in self.panes.values_mut() {
resize_pty!(pane, os_api);
}
self.set_force_render();
return true;
}
false
}
pub fn resize_active_pane_right(
&mut self,
client_id: ClientId,
os_api: &mut Box<dyn ServerOsApi>,
) -> bool {
// true => successfully resized
let display_area = *self.display_area.borrow();
let viewport = *self.viewport.borrow();
if let Some(active_floating_pane_id) = self.active_panes.get(&client_id) {
let mut floating_pane_grid = FloatingPaneGrid::new(
&mut self.panes,
&mut self.desired_pane_positions,
display_area,
viewport,
);
floating_pane_grid.resize_pane_right(&active_floating_pane_id);
for pane in self.panes.values_mut() {
resize_pty!(pane, os_api);
}
self.set_force_render();
return true;
}
false
}
pub fn resize_active_pane_down(
&mut self,
client_id: ClientId,
os_api: &mut Box<dyn ServerOsApi>,
) -> bool {
// true => successfully resized
let display_area = *self.display_area.borrow();
let viewport = *self.viewport.borrow();
if let Some(active_floating_pane_id) = self.active_panes.get(&client_id) {
let mut floating_pane_grid = FloatingPaneGrid::new(
&mut self.panes,
&mut self.desired_pane_positions,
display_area,
viewport,
);
floating_pane_grid.resize_pane_down(&active_floating_pane_id);
for pane in self.panes.values_mut() {
resize_pty!(pane, os_api);
}
self.set_force_render();
return true;
}
return false;
}
pub fn resize_active_pane_up(
&mut self,
client_id: ClientId,
os_api: &mut Box<dyn ServerOsApi>,
) -> bool {
// true => successfully resized
let display_area = *self.display_area.borrow();
let viewport = *self.viewport.borrow();
if let Some(active_floating_pane_id) = self.active_panes.get(&client_id) {
let mut floating_pane_grid = FloatingPaneGrid::new(
&mut self.panes,
&mut self.desired_pane_positions,
display_area,
viewport,
);
floating_pane_grid.resize_pane_up(&active_floating_pane_id);
for pane in self.panes.values_mut() {
resize_pty!(pane, os_api);
}
self.set_force_render();
return true;
}
return false;
}
pub fn resize_active_pane_increase(
&mut self,
client_id: ClientId,
os_api: &mut Box<dyn ServerOsApi>,
) -> bool {
// true => successfully resized
let display_area = *self.display_area.borrow();
let viewport = *self.viewport.borrow();
if let Some(active_floating_pane_id) = self.active_panes.get(&client_id) {
let mut floating_pane_grid = FloatingPaneGrid::new(
&mut self.panes,
&mut self.desired_pane_positions,
display_area,
viewport,
);
floating_pane_grid.resize_increase(&active_floating_pane_id);
for pane in self.panes.values_mut() {
resize_pty!(pane, os_api);
}
self.set_force_render();
return true;
}
return false;
}
pub fn resize_active_pane_decrease(
&mut self,
client_id: ClientId,
os_api: &mut Box<dyn ServerOsApi>,
) -> bool {
// true => successfully resized
let display_area = *self.display_area.borrow();
let viewport = *self.viewport.borrow();
if let Some(active_floating_pane_id) = self.active_panes.get(&client_id) {
let mut floating_pane_grid = FloatingPaneGrid::new(
&mut self.panes,
&mut self.desired_pane_positions,
display_area,
viewport,
);
floating_pane_grid.resize_decrease(&active_floating_pane_id);
for pane in self.panes.values_mut() {
resize_pty!(pane, os_api);
}
self.set_force_render();
return true;
}
return false;
}
pub fn move_focus_left(
&mut self,
client_id: ClientId,
connected_clients: &HashSet<ClientId>,
) -> bool {
// true => successfully moved
let display_area = *self.display_area.borrow();
let viewport = *self.viewport.borrow();
let active_pane_id = self.active_panes.get(&client_id).copied();
let updated_active_pane = if let Some(active_pane_id) = active_pane_id {
let floating_pane_grid = FloatingPaneGrid::new(
&mut self.panes,
&mut self.desired_pane_positions,
display_area,
viewport,
);
let next_index =
floating_pane_grid.next_selectable_pane_id_to_the_left(&active_pane_id);
match next_index {
Some(p) => {
// render previously active pane so that its frame does not remain actively
// colored
let previously_active_pane = self
.panes
.get_mut(self.active_panes.get(&client_id).unwrap())
.unwrap();
previously_active_pane.set_should_render(true);
// we render the full viewport to remove any ui elements that might have been
// there before (eg. another user's cursor)
previously_active_pane.render_full_viewport();
let next_active_pane = self.panes.get_mut(&p).unwrap();
next_active_pane.set_should_render(true);
// we render the full viewport to remove any ui elements that might have been
// there before (eg. another user's cursor)
next_active_pane.render_full_viewport();
// move all clients
let connected_clients: Vec<ClientId> =
connected_clients.iter().copied().collect();
for client_id in connected_clients {
self.focus_pane(p, client_id);
}
self.set_force_render();
return true;
}
None => Some(active_pane_id),
}
} else {
active_pane_id
};
match updated_active_pane {
Some(updated_active_pane) => {
let connected_clients: Vec<ClientId> = connected_clients.iter().copied().collect();
for client_id in connected_clients {
self.focus_pane(updated_active_pane, client_id);
}
self.set_force_render();
}
None => {
// TODO: can this happen?
self.active_panes.clear();
self.z_indices.clear();
}
}
false
}
pub fn move_focus_right(
&mut self,
client_id: ClientId,
connected_clients: &HashSet<ClientId>,
) -> bool {
// true => successfully moved
let display_area = *self.display_area.borrow();
let viewport = *self.viewport.borrow();
let active_pane_id = self.active_panes.get(&client_id).copied();
let updated_active_pane = if let Some(active_pane_id) = active_pane_id {
let floating_pane_grid = FloatingPaneGrid::new(
&mut self.panes,
&mut self.desired_pane_positions,
display_area,
viewport,
);
let next_index =
floating_pane_grid.next_selectable_pane_id_to_the_right(&active_pane_id);
match next_index {
Some(p) => {
// render previously active pane so that its frame does not remain actively
// colored
let previously_active_pane = self
.panes
.get_mut(self.active_panes.get(&client_id).unwrap())
.unwrap();
previously_active_pane.set_should_render(true);
// we render the full viewport to remove any ui elements that might have been
// there before (eg. another user's cursor)
previously_active_pane.render_full_viewport();
let next_active_pane = self.panes.get_mut(&p).unwrap();
next_active_pane.set_should_render(true);
// we render the full viewport to remove any ui elements that might have been
// there before (eg. another user's cursor)
next_active_pane.render_full_viewport();
// move all clients
let connected_clients: Vec<ClientId> =
connected_clients.iter().copied().collect();
for client_id in connected_clients {
self.focus_pane(p, client_id);
}
self.set_force_render();
return true;
}
None => Some(active_pane_id),
}
} else {
active_pane_id
};
match updated_active_pane {
Some(updated_active_pane) => {
let connected_clients: Vec<ClientId> = connected_clients.iter().copied().collect();
for client_id in connected_clients {
self.focus_pane(updated_active_pane, client_id);
}
self.set_force_render();
}
None => {
// TODO: can this happen?
self.active_panes.clear();
self.z_indices.clear();
}
}
false
}
pub fn move_focus_up(
&mut self,
client_id: ClientId,
connected_clients: &HashSet<ClientId>,
) -> bool {
// true => successfully moved
let display_area = *self.display_area.borrow();
let viewport = *self.viewport.borrow();
let active_pane_id = self.active_panes.get(&client_id).copied();
let updated_active_pane = if let Some(active_pane_id) = active_pane_id {
let floating_pane_grid = FloatingPaneGrid::new(
&mut self.panes,
&mut self.desired_pane_positions,
display_area,
viewport,
);
let next_index = floating_pane_grid.next_selectable_pane_id_above(&active_pane_id);
match next_index {
Some(p) => {
// render previously active pane so that its frame does not remain actively
// colored
let previously_active_pane = self
.panes
.get_mut(self.active_panes.get(&client_id).unwrap())
.unwrap();
previously_active_pane.set_should_render(true);
// we render the full viewport to remove any ui elements that might have been
// there before (eg. another user's cursor)
previously_active_pane.render_full_viewport();
let next_active_pane = self.panes.get_mut(&p).unwrap();
next_active_pane.set_should_render(true);
// we render the full viewport to remove any ui elements that might have been
// there before (eg. another user's cursor)
next_active_pane.render_full_viewport();
// move all clients
let connected_clients: Vec<ClientId> =
connected_clients.iter().copied().collect();
for client_id in connected_clients {
self.focus_pane(p, client_id);
}
self.set_force_render();
return true;
}
None => Some(active_pane_id),
}
} else {
active_pane_id
};
match updated_active_pane {
Some(updated_active_pane) => {
let connected_clients: Vec<ClientId> = connected_clients.iter().copied().collect();
for client_id in connected_clients {
self.focus_pane(updated_active_pane, client_id);
}
self.set_force_render();
}
None => {
// TODO: can this happen?
self.active_panes.clear();
self.z_indices.clear();
}
}
false
}
pub fn move_focus_down(
&mut self,
client_id: ClientId,
connected_clients: &HashSet<ClientId>,
) -> bool {
// true => successfully moved
let display_area = *self.display_area.borrow();
let viewport = *self.viewport.borrow();
let active_pane_id = self.active_panes.get(&client_id).copied();
let updated_active_pane = if let Some(active_pane_id) = active_pane_id {
let floating_pane_grid = FloatingPaneGrid::new(
&mut self.panes,
&mut self.desired_pane_positions,
display_area,
viewport,
);
let next_index = floating_pane_grid.next_selectable_pane_id_below(&active_pane_id);
match next_index {
Some(p) => {
// render previously active pane so that its frame does not remain actively
// colored
let previously_active_pane = self
.panes
.get_mut(self.active_panes.get(&client_id).unwrap())
.unwrap();
previously_active_pane.set_should_render(true);
// we render the full viewport to remove any ui elements that might have been
// there before (eg. another user's cursor)
previously_active_pane.render_full_viewport();
let next_active_pane = self.panes.get_mut(&p).unwrap();
next_active_pane.set_should_render(true);
// we render the full viewport to remove any ui elements that might have been
// there before (eg. another user's cursor)
next_active_pane.render_full_viewport();
// move all clients
let connected_clients: Vec<ClientId> =
connected_clients.iter().copied().collect();
for client_id in connected_clients {
self.focus_pane(p, client_id);
}
self.set_force_render();
return true;
}
None => Some(active_pane_id),
}
} else {
active_pane_id
};
match updated_active_pane {
Some(updated_active_pane) => {
let connected_clients: Vec<ClientId> = connected_clients.iter().copied().collect();
for client_id in connected_clients {
self.focus_pane(updated_active_pane, client_id);
}
self.set_force_render();
}
None => {
// TODO: can this happen?
self.active_panes.clear();
self.z_indices.clear();
}
}
false
}
pub fn move_active_pane_down(&mut self, client_id: ClientId) {
let display_area = *self.display_area.borrow();
let viewport = *self.viewport.borrow();
if let Some(active_pane_id) = self.active_panes.get(&client_id) {
let mut floating_pane_grid = FloatingPaneGrid::new(
&mut self.panes,
&mut self.desired_pane_positions,
display_area,
viewport,
);
floating_pane_grid.move_pane_down(&active_pane_id);
self.set_force_render();
}
}
pub fn move_active_pane_up(&mut self, client_id: ClientId) {
let display_area = *self.display_area.borrow();
let viewport = *self.viewport.borrow();
if let Some(active_pane_id) = self.active_panes.get(&client_id) {
let mut floating_pane_grid = FloatingPaneGrid::new(
&mut self.panes,
&mut self.desired_pane_positions,
display_area,
viewport,
);
floating_pane_grid.move_pane_up(&active_pane_id);
self.set_force_render();
}
}
pub fn move_active_pane_left(&mut self, client_id: ClientId) {
let display_area = *self.display_area.borrow();
let viewport = *self.viewport.borrow();
if let Some(active_pane_id) = self.active_panes.get(&client_id) {
let mut floating_pane_grid = FloatingPaneGrid::new(
&mut self.panes,
&mut self.desired_pane_positions,
display_area,
viewport,
);
floating_pane_grid.move_pane_left(&active_pane_id);
self.set_force_render();
}
}
pub fn move_active_pane_right(&mut self, client_id: ClientId) {
let display_area = *self.display_area.borrow();
let viewport = *self.viewport.borrow();
if let Some(active_pane_id) = self.active_panes.get(&client_id) {
let mut floating_pane_grid = FloatingPaneGrid::new(
&mut self.panes,
&mut self.desired_pane_positions,
display_area,
viewport,
);
floating_pane_grid.move_pane_right(&active_pane_id);
self.set_force_render();
}
}
pub fn move_clients_out_of_pane(&mut self, pane_id: PaneId) {
let active_panes: Vec<(ClientId, PaneId)> = self
.active_panes
.iter()
.map(|(cid, pid)| (*cid, *pid))
.collect();
let next_active_pane = self.panes.keys().next().copied();
for (client_id, active_pane_id) in active_panes {
if active_pane_id == pane_id {
match next_active_pane {
Some(next_active_pane) => {
self.active_panes.insert(client_id, next_active_pane);
self.focus_pane(next_active_pane, client_id);
}
None => {
self.defocus_pane(pane_id, client_id);
}
}
}
}
}
pub fn focus_pane(&mut self, pane_id: PaneId, client_id: ClientId) {
self.active_panes.insert(client_id, pane_id);
self.z_indices.retain(|p_id| *p_id != pane_id);
self.z_indices.push(pane_id);
self.set_force_render();
}
pub fn defocus_pane(&mut self, pane_id: PaneId, client_id: ClientId) {
self.z_indices.retain(|p_id| *p_id != pane_id);
self.active_panes.remove(&client_id);
self.set_force_render();
}
pub fn get_pane_id_at(&self, point: &Position, search_selectable: bool) -> Option<PaneId> {
if search_selectable {
// TODO: better - loop through z-indices and check each one if it contains the point
let mut selectable_panes: Vec<_> =
self.panes.iter().filter(|(_, p)| p.selectable()).collect();
selectable_panes.sort_by(|(a_id, _a_pane), (b_id, _b_pane)| {
self.z_indices
.iter()
.position(|id| id == *b_id)
.unwrap()
.cmp(&self.z_indices.iter().position(|id| id == *a_id).unwrap())
});
selectable_panes
.iter()
.find(|(_, p)| p.contains(point))
.map(|(&id, _)| id)
} else {
let mut panes: Vec<_> = self.panes.iter().collect();
panes.sort_by(|(a_id, _a_pane), (b_id, _b_pane)| {
self.z_indices
.iter()
.position(|id| id == *b_id)
.unwrap()
.cmp(&self.z_indices.iter().position(|id| id == *a_id).unwrap())
});
panes
.iter()
.find(|(_, p)| p.contains(point))
.map(|(&id, _)| id)
}
}
pub fn get_pane_at_mut(
&mut self,
position: &Position,
search_selectable: bool,
) -> Option<&mut Box<dyn Pane>> {
self.get_pane_id_at(position, search_selectable)
.and_then(|pane_id| self.panes.get_mut(&pane_id))
}
pub fn set_pane_being_moved_with_mouse(&mut self, pane_id: PaneId, position: Position) {
self.pane_being_moved_with_mouse = Some((pane_id, position));
}
pub fn pane_is_being_moved_with_mouse(&self) -> bool {
self.pane_being_moved_with_mouse.is_some()
}
pub fn move_pane_to_position(&mut self, click_position: &Position) -> bool {
// true => changed position
let display_area = *self.display_area.borrow();
let viewport = *self.viewport.borrow();
let (pane_id, previous_position) = self.pane_being_moved_with_mouse.unwrap();
if click_position == &previous_position {
return false;
}
let move_x_by = click_position.column() as isize - previous_position.column() as isize;
let move_y_by = click_position.line() as isize - previous_position.line() as isize;
let mut floating_pane_grid = FloatingPaneGrid::new(
&mut self.panes,
&mut self.desired_pane_positions,
display_area,
viewport,
);
floating_pane_grid.move_pane_by(pane_id, move_x_by, move_y_by);
self.set_pane_being_moved_with_mouse(pane_id, click_position.clone());
self.set_force_render();
true
}
pub fn move_pane_with_mouse(&mut self, position: Position, search_selectable: bool) -> bool {
// true => handled, false => not handled (eg. no pane at this position)
let show_panes = self.show_panes;
if self.pane_being_moved_with_mouse.is_some() {
self.move_pane_to_position(&position);
self.set_force_render();
return true;
} else if let Some(pane) = self.get_pane_at_mut(&position, search_selectable) {
let clicked_on_frame = pane.position_is_on_frame(&position);
if show_panes && clicked_on_frame {
let pid = pane.pid();
if self.pane_being_moved_with_mouse.is_none() {
self.set_pane_being_moved_with_mouse(pid, position.clone());
}
self.move_pane_to_position(&position);
self.set_force_render();
return true;
}
};
return false;
}
pub fn stop_moving_pane_with_mouse(&mut self, position: Position) {
if self.pane_being_moved_with_mouse.is_some() {
self.move_pane_to_position(&position);
self.set_force_render();
};
self.pane_being_moved_with_mouse = None;
}
}

View file

@ -1,3 +1,5 @@
use std::cell::RefCell;
use std::rc::Rc;
use unicode_width::UnicodeWidthChar;
use std::{
@ -20,6 +22,7 @@ use vte::{Params, Perform};
use zellij_tile::data::{Palette, PaletteColor};
use zellij_utils::{consts::VERSION, shared::version_number};
use crate::output::{CharacterChunk, OutputBuffer};
use crate::panes::alacritty_functions::{parse_number, xparse_color};
use crate::panes::link_handler::LinkHandler;
use crate::panes::selection::Selection;
@ -267,107 +270,6 @@ fn subtract_isize_from_usize(u: usize, i: isize) -> usize {
}
}
#[derive(Debug)]
pub struct CharacterChunk {
pub terminal_characters: Vec<TerminalCharacter>,
pub x: usize,
pub y: usize,
}
#[derive(Clone, Debug)]
pub struct OutputBuffer {
changed_lines: Vec<usize>, // line index
should_update_all_lines: bool,
}
impl Default for OutputBuffer {
fn default() -> Self {
OutputBuffer {
changed_lines: vec![],
should_update_all_lines: true, // first time we should do a full render
}
}
}
impl OutputBuffer {
pub fn update_line(&mut self, line_index: usize) {
if !self.should_update_all_lines {
self.changed_lines.push(line_index);
}
}
pub fn update_all_lines(&mut self) {
self.clear();
self.should_update_all_lines = true;
}
pub fn clear(&mut self) {
self.changed_lines.clear();
self.should_update_all_lines = false;
}
pub fn changed_chunks_in_viewport(
&self,
viewport: &[Row],
viewport_width: usize,
viewport_height: usize,
) -> Vec<CharacterChunk> {
if self.should_update_all_lines {
let mut changed_chunks = Vec::with_capacity(viewport.len());
for line_index in 0..viewport_height {
let terminal_characters =
self.extract_line_from_viewport(line_index, viewport, viewport_width);
changed_chunks.push(CharacterChunk {
x: 0,
y: line_index,
terminal_characters,
});
}
changed_chunks
} else {
let mut line_changes = self.changed_lines.to_vec();
line_changes.sort_unstable();
line_changes.dedup();
let mut changed_chunks = Vec::with_capacity(line_changes.len());
for line_index in line_changes {
let terminal_characters =
self.extract_line_from_viewport(line_index, viewport, viewport_width);
changed_chunks.push(CharacterChunk {
x: 0,
y: line_index,
terminal_characters,
});
}
changed_chunks
}
}
fn extract_characters_from_row(
&self,
row: &Row,
viewport_width: usize,
) -> Vec<TerminalCharacter> {
let mut terminal_characters: Vec<TerminalCharacter> = row.columns.iter().copied().collect();
// pad row
let row_width = row.width();
if row_width < viewport_width {
let mut padding = vec![EMPTY_TERMINAL_CHARACTER; viewport_width - row_width];
terminal_characters.append(&mut padding);
}
terminal_characters
}
fn extract_line_from_viewport(
&self,
line_index: usize,
viewport: &[Row],
viewport_width: usize,
) -> Vec<TerminalCharacter> {
match viewport.get(line_index) {
// TODO: iterator?
Some(row) => self.extract_characters_from_row(row, viewport_width),
None => {
vec![EMPTY_TERMINAL_CHARACTER; viewport_width]
}
}
}
}
#[derive(Clone)]
pub struct Grid {
lines_above: VecDeque<Row>,
@ -401,7 +303,7 @@ pub struct Grid {
pub selection: Selection,
pub title: Option<String>,
pub is_scrolled: bool,
pub link_handler: LinkHandler,
pub link_handler: Rc<RefCell<LinkHandler>>,
pub ring_bell: bool,
scrollback_buffer_lines: usize,
}
@ -420,7 +322,12 @@ impl Debug for Grid {
}
impl Grid {
pub fn new(rows: usize, columns: usize, colors: Palette) -> Self {
pub fn new(
rows: usize,
columns: usize,
colors: Palette,
link_handler: Rc<RefCell<LinkHandler>>,
) -> Self {
Grid {
lines_above: VecDeque::with_capacity(
// .get_or_init() is used instead of .get().unwrap() to prevent
@ -453,7 +360,7 @@ impl Grid {
title: None,
changed_colors: None,
is_scrolled: false,
link_handler: Default::default(),
link_handler,
ring_bell: false,
scrollback_buffer_lines: 0,
}
@ -717,7 +624,7 @@ impl Grid {
}
}
if let Some(trim_at) = trim_at {
line.columns.truncate(trim_at);
line.truncate(trim_at);
}
}
@ -865,10 +772,14 @@ impl Grid {
}
lines
}
pub fn read_changes(&mut self) -> Vec<CharacterChunk> {
let changes =
self.output_buffer
.changed_chunks_in_viewport(&self.viewport, self.width, self.height);
pub fn read_changes(&mut self, x_offset: usize, y_offset: usize) -> Vec<CharacterChunk> {
let changes = self.output_buffer.changed_chunks_in_viewport(
&self.viewport,
self.width,
self.height,
x_offset,
y_offset,
);
self.output_buffer.clear();
changes
}
@ -1056,7 +967,7 @@ impl Grid {
pub fn add_character(&mut self, terminal_character: TerminalCharacter) {
// TODO: try to separate adding characters from moving the cursors in this function
let character_width = terminal_character.width;
if self.cursor.x >= self.width {
if self.cursor.x + character_width > self.width {
if self.disable_linewrap {
return;
}
@ -1095,7 +1006,7 @@ impl Grid {
self.viewport.get(y).unwrap().absolute_character_index(x)
}
pub fn move_cursor_forward_until_edge(&mut self, count: usize) {
let count_to_move = std::cmp::min(count, self.width - (self.cursor.x));
let count_to_move = std::cmp::min(count, self.width - self.cursor.x);
self.cursor.x += count_to_move;
}
pub fn replace_characters_in_line_after_cursor(&mut self, replace_with: TerminalCharacter) {
@ -1613,7 +1524,8 @@ impl Perform for Grid {
if params.len() < 3 {
return;
}
self.cursor.pending_styles.link_anchor = self.link_handler.dispatch_osc8(params);
self.cursor.pending_styles.link_anchor =
self.link_handler.borrow_mut().dispatch_osc8(params);
}
// Get/set Foreground, Background, Cursor colors.
@ -2157,6 +2069,7 @@ impl Perform for Grid {
pub struct Row {
pub columns: VecDeque<TerminalCharacter>,
pub is_canonical: bool,
width: Option<usize>,
}
impl Debug for Row {
@ -2173,12 +2086,14 @@ impl Row {
Row {
columns: VecDeque::with_capacity(width),
is_canonical: false,
width: None,
}
}
pub fn from_columns(columns: VecDeque<TerminalCharacter>) -> Self {
Row {
columns,
is_canonical: false,
width: None,
}
}
pub fn from_rows(mut rows: Vec<Row>, width: usize) -> Self {
@ -2194,12 +2109,25 @@ impl Row {
}
pub fn with_character(mut self, terminal_character: TerminalCharacter) -> Self {
self.columns.push_back(terminal_character);
self.width = None;
self
}
pub fn canonical(mut self) -> Self {
self.is_canonical = true;
self
}
pub fn width_cached(&mut self) -> usize {
if self.width.is_some() {
self.width.unwrap()
} else {
let mut width = 0;
for terminal_character in &self.columns {
width += terminal_character.width;
}
self.width = Some(width);
width
}
}
pub fn width(&self) -> usize {
let mut width = 0;
for terminal_character in &self.columns {
@ -2239,15 +2167,18 @@ impl Row {
absolute_index
}
pub fn add_character_at(&mut self, terminal_character: TerminalCharacter, x: usize) {
match self.width().cmp(&x) {
match self.width_cached().cmp(&x) {
Ordering::Equal => {
self.columns.push_back(terminal_character);
// this is unwrapped because this always happens after self.width_cached()
*self.width.as_mut().unwrap() += terminal_character.width;
}
Ordering::Less => {
let width_offset = self.excess_width_until(x);
self.columns
.resize(x.saturating_sub(width_offset), EMPTY_TERMINAL_CHARACTER);
self.columns.push_back(terminal_character);
self.width = None;
}
Ordering::Greater => {
// wide-character-aware index, where each character is counted once
@ -2275,6 +2206,7 @@ impl Row {
}
_ => {}
}
self.width = None;
}
}
}
@ -2291,6 +2223,7 @@ impl Row {
self.columns.insert(insert_position, terminal_character);
}
}
self.width = None;
}
pub fn replace_character_at(&mut self, terminal_character: TerminalCharacter, x: usize) {
// this is much more performant than remove/insert
@ -2303,12 +2236,15 @@ impl Row {
self.columns.insert(x, terminal_character);
}
}
self.width = None;
}
pub fn replace_columns(&mut self, columns: VecDeque<TerminalCharacter>) {
self.columns = columns;
self.width = None;
}
pub fn push(&mut self, terminal_character: TerminalCharacter) {
self.columns.push_back(terminal_character);
self.width = None;
}
pub fn truncate(&mut self, x: usize) {
let width_offset = self.excess_width_until(x);
@ -2316,6 +2252,7 @@ impl Row {
if truncate_position < self.columns.len() {
self.columns.truncate(truncate_position);
}
self.width = None;
}
pub fn position_accounting_for_widechars(&self, x: usize) -> usize {
let mut position = x;
@ -2343,9 +2280,11 @@ impl Row {
self.columns
.truncate(from_position_accounting_for_widechars);
self.columns.append(&mut replace_with);
self.width = None;
}
pub fn append(&mut self, to_append: &mut VecDeque<TerminalCharacter>) {
self.columns.append(to_append);
self.width = None;
}
pub fn drain_until(&mut self, x: usize) -> VecDeque<TerminalCharacter> {
let mut drained_part: VecDeque<TerminalCharacter> = VecDeque::new();
@ -2359,6 +2298,7 @@ impl Row {
break;
}
}
self.width = None;
drained_part
}
pub fn replace_and_pad_beginning(&mut self, to: usize, terminal_character: TerminalCharacter) {
@ -2378,6 +2318,7 @@ impl Row {
drop(self.columns.drain(0..=to_position_accounting_for_widechars));
}
replace_with.append(&mut self.columns);
self.width = None;
self.columns = replace_with;
}
pub fn replace_beginning_with(&mut self, mut line_part: VecDeque<TerminalCharacter>) {
@ -2388,6 +2329,7 @@ impl Row {
drop(self.columns.drain(0..line_part.len()));
}
line_part.append(&mut self.columns);
self.width = None;
self.columns = line_part;
}
pub fn len(&self) -> usize {
@ -2399,6 +2341,7 @@ impl Row {
pub fn delete_and_return_character(&mut self, x: usize) -> Option<TerminalCharacter> {
let erase_position = self.absolute_character_index(x);
if erase_position < self.columns.len() {
self.width = None;
Some(self.columns.remove(erase_position).unwrap()) // TODO: just return the remove part?
} else {
None
@ -2426,6 +2369,7 @@ impl Row {
if parts.is_empty() {
parts.push(self.clone());
}
self.width = None;
parts
}
}

View file

@ -10,7 +10,7 @@ pub struct LinkHandler {
link_index: u16,
}
#[derive(Debug, Clone)]
struct Link {
pub struct Link {
id: Option<String>,
uri: String,
}
@ -49,8 +49,8 @@ impl LinkHandler {
}
}
pub fn output_osc8(&self, link_anchor: Option<LinkAnchor>) -> String {
link_anchor.map_or("".to_string(), |link| match link {
pub fn output_osc8(&self, link_anchor: Option<LinkAnchor>) -> Option<String> {
link_anchor.map(|link| match link {
LinkAnchor::Start(index) => {
let link = self.links.get(&index).unwrap();
let id = link
@ -93,7 +93,7 @@ mod tests {
}
let expected = format!("\u{1b}]8;id=test;http://test.com{}", TERMINATOR);
assert_eq!(link_handler.output_osc8(anchor), expected);
assert_eq!(link_handler.output_osc8(anchor).unwrap(), expected);
}
#[test]
@ -106,6 +106,6 @@ mod tests {
assert_eq!(anchor, Some(LinkAnchor::End));
let expected = format!("\u{1b}]8;;{}", TERMINATOR);
assert_eq!(link_handler.output_osc8(anchor), expected);
assert_eq!(link_handler.output_osc8(anchor).unwrap(), expected);
}
}

View file

@ -1,13 +1,16 @@
mod alacritty_functions;
mod grid;
mod link_handler;
mod floating_panes;
pub mod grid;
pub mod link_handler;
mod plugin_pane;
mod selection;
mod terminal_character;
pub mod selection;
pub mod terminal_character;
mod terminal_pane;
pub use alacritty_functions::*;
pub use floating_panes::*;
pub use grid::*;
pub use link_handler::*;
pub(crate) use plugin_pane::*;
pub use terminal_character::*;
pub(crate) use terminal_character::*;
pub use terminal_pane::*;

View file

@ -3,6 +3,7 @@ use std::sync::mpsc::channel;
use std::time::Instant;
use std::unimplemented;
use crate::output::CharacterChunk;
use crate::panes::PaneId;
use crate::pty::VteBytes;
use crate::tab::Pane;
@ -134,7 +135,10 @@ impl Pane for PluginPane {
fn set_selectable(&mut self, selectable: bool) {
self.selectable = selectable;
}
fn render(&mut self, client_id: Option<ClientId>) -> Option<String> {
fn render(
&mut self,
client_id: Option<ClientId>,
) -> Option<(Vec<CharacterChunk>, Option<String>)> {
// this is a bit of a hack but works in a pinch
client_id?;
let client_id = client_id.unwrap();
@ -207,7 +211,7 @@ impl Pane for PluginPane {
}
}
}
Some(vte_output)
Some((vec![], Some(vte_output))) // TODO: PluginPanes should have their own grid so that we can return the non-serialized TerminalCharacters and have them participate in the render buffer
} else {
None
}
@ -217,7 +221,7 @@ impl Pane for PluginPane {
_client_id: ClientId,
frame_params: FrameParams,
input_mode: InputMode,
) -> Option<String> {
) -> Option<(Vec<CharacterChunk>, Option<String>)> {
// FIXME: This is a hack that assumes all fixed-size panes are borderless. This
// will eventually need fixing!
if self.frame && !(self.geom.rows.is_fixed() || self.geom.cols.is_fixed()) {
@ -328,7 +332,6 @@ impl Pane for PluginPane {
unimplemented!();
}
fn start_selection(&mut self, start: &Position, client_id: ClientId) {
log::info!("plugin pane send left click plugin instruction");
self.send_plugin_instructions
.send(PluginInstruction::Update(
Some(self.pid),

View file

@ -4,7 +4,7 @@ use zellij_utils::position::Position;
// The selection is empty when start == end
// it includes the character at start, and everything before end.
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Copy)]
pub struct Selection {
pub start: Position,
pub end: Position,
@ -59,6 +59,16 @@ impl Selection {
end.line.0 == row && col < end.column.0
}
pub fn contains_row(&self, row: usize) -> bool {
let row = row as isize;
let (start, end) = if self.start <= self.end {
(self.start, self.end)
} else {
(self.end, self.start)
};
start.line.0 <= row && end.line.0 >= row
}
pub fn is_empty(&self) -> bool {
self.start == self.end
}
@ -99,6 +109,13 @@ impl Selection {
self.end.line.0 += lines as isize;
}
}
pub fn offset(mut self, offset_x: usize, offset_y: usize) -> Self {
self.start.line.0 += offset_y as isize;
self.end.line.0 += offset_y as isize;
self.start.column.0 += offset_x;
self.end.column.0 += offset_x;
self
}
/// Return an iterator over the line indices, up to max, that are not present in both self and other,
/// except for the indices of the first and last line of both self and s2, that are always included.

View file

@ -1,6 +1,7 @@
use std::convert::From;
use std::fmt::{self, Debug, Display, Formatter};
use std::ops::{Index, IndexMut};
use unicode_width::UnicodeWidthChar;
use zellij_utils::vte::ParamsIter;
@ -704,6 +705,16 @@ pub struct TerminalCharacter {
pub width: usize,
}
impl TerminalCharacter {
pub fn new(character: char) -> Self {
TerminalCharacter {
character,
styles: CharacterStyles::default(),
width: character.width().unwrap_or(0),
}
}
}
impl ::std::fmt::Debug for TerminalCharacter {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.character)

View file

@ -1,16 +1,17 @@
use crate::panes::AnsiCode;
use crate::output::CharacterChunk;
use crate::panes::{
grid::Grid,
terminal_character::{
CharacterStyles, CursorShape, TerminalCharacter, EMPTY_TERMINAL_CHARACTER,
},
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, Write};
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::{
@ -184,102 +185,42 @@ impl Pane for TerminalPane {
fn set_selectable(&mut self, selectable: bool) {
self.selectable = selectable;
}
fn render(&mut self, _client_id: Option<ClientId>) -> Option<String> {
// we don't use client_id because terminal panes render the same for all users
fn render(
&mut self,
_client_id: Option<ClientId>,
) -> Option<(Vec<CharacterChunk>, Option<String>)> {
if self.should_render() {
let mut vte_output = String::new();
let mut character_styles = CharacterStyles::new();
let mut raw_vte_output = String::new();
let content_x = self.get_content_x();
let content_y = self.get_content_y();
if self.grid.clear_viewport_before_rendering {
for line_index in 0..self.grid.height {
write!(
&mut vte_output,
"\u{1b}[{};{}H\u{1b}[m",
content_y + line_index + 1,
content_x + 1
)
.unwrap(); // goto row/col and reset styles
for _col_index in 0..self.grid.width {
vte_output.push(EMPTY_TERMINAL_CHARACTER.character);
}
}
self.grid.clear_viewport_before_rendering = false;
}
// here we clear the previous cursor locations by adding an empty style-less character
// in their location, this is done before the main rendering logic so that if there
// actually is another character there, it will be overwritten
for (y, x) in self.fake_cursor_locations.drain() {
// we need to make sure to update the line in the line buffer so that if there's
// another character there it'll override it and we won't create holes with our
// empty character
self.grid.update_line_for_rendering(y);
let x = content_x + x;
let y = content_y + y;
write!(
&mut vte_output,
"\u{1b}[{};{}H\u{1b}[m{}",
y + 1,
x + 1,
EMPTY_TERMINAL_CHARACTER.character
)
.unwrap();
}
let max_width = self.get_content_columns();
for character_chunk in self.grid.read_changes() {
let pane_x = self.get_content_x();
let pane_y = self.get_content_y();
let chunk_absolute_x = pane_x + character_chunk.x;
let chunk_absolute_y = pane_y + character_chunk.y;
let terminal_characters = character_chunk.terminal_characters;
write!(
&mut vte_output,
"\u{1b}[{};{}H\u{1b}[m",
chunk_absolute_y + 1,
chunk_absolute_x + 1
)
.unwrap(); // goto row/col and reset styles
let mut chunk_width = character_chunk.x;
for mut t_character in terminal_characters {
// adjust the background of currently selected characters
// doing it here is much easier than in grid
if self.grid.selection.contains(character_chunk.y, chunk_width) {
let color = match self.colors.bg {
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),
};
t_character.styles = t_character.styles.background(Some(color));
character_chunk.add_selection_and_background(
self.grid.selection,
background_color,
content_x,
content_y,
);
}
chunk_width += t_character.width;
if chunk_width > max_width {
break;
}
if let Some(new_styles) = character_styles
.update_and_return_diff(&t_character.styles, self.grid.changed_colors)
{
write!(
&mut vte_output,
"{}{}",
new_styles,
self.grid.link_handler.output_osc8(new_styles.link_anchor)
)
.unwrap();
}
vte_output.push(t_character.character);
}
character_styles.clear();
}
if self.grid.ring_bell {
let ring_bell = '\u{7}';
vte_output.push(ring_bell);
raw_vte_output.push(ring_bell);
self.grid.ring_bell = false;
}
self.set_should_render(false);
Some(vte_output)
Some((character_chunks, Some(raw_vte_output)))
} else {
None
}
@ -289,9 +230,8 @@ impl Pane for TerminalPane {
client_id: ClientId,
frame_params: FrameParams,
input_mode: InputMode,
) -> Option<String> {
) -> Option<(Vec<CharacterChunk>, Option<String>)> {
// TODO: remove the cursor stuff from here
let mut vte_output = None;
let pane_title = if self.pane_name.is_empty()
&& input_mode == InputMode::RenamePane
&& frame_params.is_main_client
@ -316,18 +256,24 @@ impl Pane for TerminalPane {
Some(last_frame) => {
if &frame != last_frame {
if !self.borderless {
vte_output = Some(frame.render());
}
let frame_output = frame.render();
self.frame.insert(client_id, frame);
Some(frame_output)
} else {
None
}
} else {
None
}
vte_output
}
None => {
if !self.borderless {
vte_output = Some(frame.render());
}
let frame_output = frame.render();
self.frame.insert(client_id, frame);
vte_output
Some(frame_output)
} else {
None
}
}
}
}
@ -452,7 +398,7 @@ impl Pane for TerminalPane {
self.grid.pending_messages_to_pty.drain(..).collect()
}
fn start_selection(&mut self, start: &Position, client_id: ClientId) {
fn start_selection(&mut self, start: &Position, _client_id: ClientId) {
self.grid.start_selection(start);
self.set_should_render(true);
}
@ -511,12 +457,14 @@ impl TerminalPane {
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(),

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,9 @@
use super::super::TerminalPane;
use crate::panes::LinkHandler;
use crate::tab::Pane;
use ::insta::assert_snapshot;
use std::cell::RefCell;
use std::rc::Rc;
use zellij_utils::pane_size::PaneGeom;
use zellij_utils::zellij_tile::data::Palette;
@ -15,7 +18,14 @@ pub fn scrolling_inside_a_pane() {
let pid = 1;
let palette = Palette::default();
let mut terminal_pane = TerminalPane::new(pid, fake_win_size, palette, 0, String::new()); // 0 is the pane index
let mut terminal_pane = TerminalPane::new(
pid,
fake_win_size,
palette,
0,
String::new(),
Rc::new(RefCell::new(LinkHandler::new())),
); // 0 is the pane index
let mut text_to_fill_pane = String::new();
for i in 0..30 {
writeln!(&mut text_to_fill_pane, "\rline {}", i + 1).unwrap();

View file

@ -226,6 +226,21 @@ fn route_action(
};
session.senders.send_to_pty(pty_instr).unwrap();
}
Action::TogglePaneEmbedOrFloating => {
session
.senders
.send_to_screen(ScreenInstruction::TogglePaneEmbedOrFloating(client_id))
.unwrap();
}
Action::ToggleFloatingPanes => {
session
.senders
.send_to_screen(ScreenInstruction::ToggleFloatingPanes(
client_id,
session.default_shell.clone(),
))
.unwrap();
}
Action::PaneNameInput(c) => {
session
.senders

View file

@ -8,12 +8,15 @@ use std::str;
use zellij_utils::input::options::Clipboard;
use zellij_utils::pane_size::Size;
use zellij_utils::{input::layout::Layout, position::Position, zellij_tile};
use zellij_utils::{
input::command::TerminalAction, input::layout::Layout, position::Position, zellij_tile,
};
use crate::{
output::Output,
panes::PaneId,
pty::{ClientOrTabIndex, PtyInstruction, VteBytes},
tab::{Output, Tab},
tab::Tab,
thread_bus::Bus,
ui::overlay::{Overlay, OverlayWindow, Overlayable},
wasm_vm::PluginInstruction,
@ -32,6 +35,8 @@ pub enum ScreenInstruction {
PtyBytes(RawFd, VteBytes),
Render,
NewPane(PaneId, ClientOrTabIndex),
TogglePaneEmbedOrFloating(ClientId),
ToggleFloatingPanes(ClientId, Option<TerminalAction>),
HorizontalSplit(PaneId, ClientId),
VerticalSplit(PaneId, ClientId),
WriteCharacter(Vec<u8>, ClientId),
@ -101,6 +106,10 @@ impl From<&ScreenInstruction> for ScreenContext {
ScreenInstruction::PtyBytes(..) => ScreenContext::HandlePtyBytes,
ScreenInstruction::Render => ScreenContext::Render,
ScreenInstruction::NewPane(..) => ScreenContext::NewPane,
ScreenInstruction::TogglePaneEmbedOrFloating(..) => {
ScreenContext::TogglePaneEmbedOrFloating
}
ScreenInstruction::ToggleFloatingPanes(..) => ScreenContext::ToggleFloatingPanes,
ScreenInstruction::HorizontalSplit(..) => ScreenContext::HorizontalSplit,
ScreenInstruction::VerticalSplit(..) => ScreenContext::VerticalSplit,
ScreenInstruction::WriteCharacter(..) => ScreenContext::WriteCharacter,
@ -364,7 +373,7 @@ impl Screen {
fn close_tab_at_index(&mut self, tab_index: usize) {
let mut tab_to_close = self.tabs.remove(&tab_index).unwrap();
let pane_ids = tab_to_close.get_pane_ids();
let pane_ids = tab_to_close.get_all_pane_ids();
// below we don't check the result of sending the CloseTab instruction to the pty thread
// because this might be happening when the app is closing, at which point the pty thread
// has already closed and this would result in an error
@ -429,9 +438,10 @@ impl Screen {
for tab_index in tabs_to_close {
self.close_tab_at_index(tab_index);
}
let serialized_output = output.serialize();
self.bus
.senders
.send_to_server(ServerInstruction::Render(Some(output)))
.send_to_server(ServerInstruction::Render(Some(serialized_output)))
.unwrap();
}
@ -748,6 +758,30 @@ pub(crate) fn screen_thread_main(
screen.render();
}
ScreenInstruction::TogglePaneEmbedOrFloating(client_id) => {
screen
.get_active_tab_mut(client_id)
.unwrap()
.toggle_pane_embed_or_floating(client_id);
screen
.bus
.senders
.send_to_server(ServerInstruction::UnblockInputThread)
.unwrap();
screen.render();
}
ScreenInstruction::ToggleFloatingPanes(client_id, default_shell) => {
screen
.get_active_tab_mut(client_id)
.unwrap()
.toggle_floating_panes(client_id, default_shell);
screen
.bus
.senders
.send_to_server(ServerInstruction::UnblockInputThread)
.unwrap();
screen.render();
}
ScreenInstruction::HorizontalSplit(pid, client_id) => {
screen
.get_active_tab_mut(client_id)
@ -1055,7 +1089,7 @@ pub(crate) fn screen_thread_main(
}
None => {
for tab in screen.tabs.values_mut() {
if tab.get_pane_ids().contains(&id) {
if tab.get_all_pane_ids().contains(&id) {
tab.close_pane(id);
break;
}

View file

@ -29,9 +29,9 @@ impl ClipboardProvider {
Clipboard::Primary => 'c',
Clipboard::System => 'c',
};
output.push_str_to_multiple_clients(
&format!("\u{1b}]52;{};{}\u{1b}\\", dest, base64::encode(content)),
output.add_pre_vte_instruction_to_multiple_clients(
client_ids,
&format!("\u{1b}]52;{};{}\u{1b}\\", dest, base64::encode(content)),
);
}
};

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,5 +1,5 @@
use super::pane_resizer::PaneResizer;
use crate::tab::is_inside_viewport;
use crate::tab::{is_inside_viewport, MIN_TERMINAL_HEIGHT, MIN_TERMINAL_WIDTH};
use crate::{panes::PaneId, tab::Pane};
use std::cmp::Reverse;
use std::collections::{HashMap, HashSet};
@ -14,26 +14,22 @@ use std::rc::Rc;
const RESIZE_PERCENT: f64 = 5.0;
const CURSOR_HEIGHT_WIDTH_RATIO: usize = 4; // this is not accurate and kind of a magic number, TODO: look into this
// FIXME: This should be replaced by `RESIZE_PERCENT` at some point
const MIN_TERMINAL_HEIGHT: usize = 5;
const MIN_TERMINAL_WIDTH: usize = 5;
type BorderAndPaneIds = (usize, Vec<PaneId>);
pub struct PaneGrid<'a> {
// panes: HashMap<&'a PaneId, &'a mut Box<dyn Pane>>,
pub struct TiledPaneGrid<'a> {
panes: Rc<RefCell<HashMap<PaneId, &'a mut Box<dyn Pane>>>>,
display_area: Size, // includes all panes (including eg. the status bar and tab bar in the default layout)
viewport: Viewport, // includes all non-UI panes
}
impl<'a> PaneGrid<'a> {
impl<'a> TiledPaneGrid<'a> {
pub fn new(
panes: impl IntoIterator<Item = (&'a PaneId, &'a mut Box<dyn Pane>)>,
display_area: Size,
viewport: Viewport,
) -> Self {
let panes: HashMap<_, _> = panes.into_iter().map(|(p_id, p)| (*p_id, p)).collect();
PaneGrid {
TiledPaneGrid {
panes: Rc::new(RefCell::new(panes)),
display_area,
viewport,
@ -1643,7 +1639,6 @@ impl<'a> PaneGrid<'a> {
pub fn fill_space_over_pane(&mut self, id: PaneId) -> bool {
// true => successfully filled space over pane
// false => didn't succeed, so didn't do anything
log::info!("fill_space_over_pane");
let (freed_width, freed_height) = {
let panes = self.panes.borrow_mut();
let pane_to_close = panes.get(&id).unwrap();

View file

@ -0,0 +1,26 @@
---
source: zellij-server/src/tab/./unit/tab_integration_tests.rs
expression: snapshot
---
00 (C): ┌ Pane #1 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
01 (C): │ │
02 (C): │ │
03 (C): │ │
04 (C): │ I am an embedded pane │
05 (C): │ │
06 (C): │ │
07 (C): │ │
08 (C): │ │
09 (C): │ │
10 (C): │ │
11 (C): │ │
12 (C): │ │
13 (C): │ │
14 (C): │ │
15 (C): │ │
16 (C): │ │
17 (C): │ │
18 (C): │ │
19 (C): └───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

View file

@ -0,0 +1,26 @@
---
source: zellij-server/src/tab/./unit/tab_integration_tests.rs
expression: snapshot
---
00 (C): ┌ Pane #1 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
01 (C): │ │
02 (C): │ │
03 (C): │ │
04 (C): │ │
05 (C): │ │
06 (C): │ ┌ Pane #2 ─────────────────────────────────────────────┐ │
07 (C): │ │ │ │
08 (C): │ │ │ │
09 (C): │ │ │ │
10 (C): │ │ I am scratch terminal │ │
11 (C): │ │ │ │
12 (C): │ │ │ │
13 (C): │ └──────────────────────────────────────────────────────┘ │
14 (C): │ │
15 (C): │ │
16 (C): │ │
17 (C): │ │
18 (C): │ │
19 (C): └───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

View file

@ -0,0 +1,26 @@
---
source: zellij-server/src/tab/./unit/tab_integration_tests.rs
expression: snapshot
---
00 (C): ┌ Pane #1 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
01 (C): │ │
02 (C): │ ┌ Pane #2 ─────────────── SCROLL: 0/1 ┐ ┌ Pane #2 ─────────────── SCROLL: 0/1 ┐ │
03 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │
04 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │
05 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │
06 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │
07 (C): │ └───────────────────────────────┌ Pane #2 ─────────────────────────────────────────────────┐────────────────────────┘ │
08 (C): │ │ │ │
09 (C): │ │ │ │
10 (C): │ │ │ │
11 (C): │ │ I am scratch terminal │ │
12 (C): │ ┌ Pane #2 ─────────────── SCROLL│ │────────── SCROLL: 0/1 ┐ │
13 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEE│ │
14 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEE│ │
15 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEE│ │
16 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE└──────────────────────────────────────────────────────────┘EEEEEEEEEEEEEEEEEEEEEEEE│ │
17 (C): │ └──────────────────────────────────────┘ └──────────────────────────────────────┘ │
18 (C): │ │
19 (C): └───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

View file

@ -0,0 +1,26 @@
---
source: zellij-server/src/tab/./unit/tab_integration_tests.rs
expression: snapshot
---
00 (C): ┌ Pane #1 ──────────────────────────────────────────────────┐┌ Pane #2 ─────────────────────────────────────────────────┐
01 (C): │ ││ │
02 (C): │ ││ │
03 (C): │ ││ │
04 (C): │ ││ I am scratch terminal │
05 (C): │ ││ │
06 (C): │ ││ │
07 (C): │ ││ │
08 (C): │ ││ │
09 (C): │ ││ │
10 (C): │ ││ │
11 (C): │ ││ │
12 (C): │ ││ │
13 (C): │ ││ │
14 (C): │ ││ │
15 (C): │ ││ │
16 (C): │ ││ │
17 (C): │ ││ │
18 (C): │ ││ │
19 (C): └───────────────────────────────────────────────────────────┘└──────────────────────────────────────────────────────────┘

View file

@ -0,0 +1,26 @@
---
source: zellij-server/src/tab/./unit/tab_integration_tests.rs
expression: snapshot
---
00 (C): ┌ Pane #1 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
01 (C): │ │
02 (C): │ ┌ Pane #2 ─────────────── SCROLL: 0/1 ┐ ┌ Pane #2 ─────────────── SCROLL: 0/1 ┐ │
03 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │
04 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │
05 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│─────────────────────────────────────│EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │
06 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │
07 (C): │ └──────────────────────────────────────┘ └──────────────────────────────────────┘ │
08 (C): │ │ │ │
09 (C): │ │ I am scratch terminal │ │
10 (C): │ │ │ │
11 (C): │ │ │ │
12 (C): │ ┌ Pane #2 ─────────────── SCROLL: 0/1 ┐ ┌ Pane #2 ─────────────── SCROLL: 0/1 ┐ │
13 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │
14 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│─────────────────────────────────────│EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │
15 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │
16 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │
17 (C): │ └──────────────────────────────────────┘ └──────────────────────────────────────┘ │
18 (C): │ │
19 (C): └───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

View file

@ -0,0 +1,26 @@
---
source: zellij-server/src/tab/./unit/tab_integration_tests.rs
expression: snapshot
---
00 (C): ┌ Pane #1 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
01 (C): │ │
02 (C): │ │
03 (C): │ │
04 (C): │ │
05 (C): │ ┌ Pane #2 ─────────────────────────────────────────────────┐ │
06 (C): │ │ │ │
07 (C): │ │ │ │
08 (C): │ │ │ │
09 (C): │ │ I am an embedded pane │ │
10 (C): │ │ │ │
11 (C): │ │ │ │
12 (C): │ │ │ │
13 (C): │ │ │ │
14 (C): │ └──────────────────────────────────────────────────────────┘ │
15 (C): │ │
16 (C): │ │
17 (C): │ │
18 (C): │ │
19 (C): └───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

View file

@ -0,0 +1,26 @@
---
source: zellij-server/src/tab/./unit/tab_integration_tests.rs
expression: snapshot
---
00 (C): ┌ Pane #1 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
01 (C): │ │
02 (C): │ │
03 (C): │ │
04 (C): │ │
05 (C): │ ┌ Pane #2 ─────────────────────────────────────────────────┐ │
06 (C): │ │ │ │
07 (C): │ │ │ │
08 (C): │ │ │ │
09 (C): │ │ I am scratch terminal │ │
10 (C): │ │ │ │
11 (C): │ │ │ │
12 (C): │ │ │ │
13 (C): │ │ │ │
14 (C): │ └──────────────────────────────────────────────────────────┘ │
15 (C): │ │
16 (C): │ │
17 (C): │ │
18 (C): │ │
19 (C): └───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

View file

@ -0,0 +1,26 @@
---
source: zellij-server/src/tab/./unit/tab_integration_tests.rs
expression: snapshot
---
00 (C): ┌ Pane #1 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
01 (C): │ │
02 (C): │ │
03 (C): │ │
04 (C): │ ┌ Pane #2 ─────────────────────────────────────────────────────┐ │
05 (C): │ │ │ │
06 (C): │ │ │ │
07 (C): │ │ │ │
08 (C): │ │ I am scratch terminal │ │
09 (C): │ │ │ │
10 (C): │ │ │ │
11 (C): │ │ │ │
12 (C): │ │ │ │
13 (C): │ │ │ │
14 (C): │ │ │ │
15 (C): │ └──────────────────────────────────────────────────────────────┘ │
16 (C): │ │
17 (C): │ │
18 (C): │ │
19 (C): └───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

View file

@ -0,0 +1,26 @@
---
source: zellij-server/src/tab/./unit/tab_integration_tests.rs
expression: snapshot
---
00 (C): ┌ Pane #1 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
01 (C): │ │
02 (C): │ ┌ Pane #2 ─────────────── SCROLL: 0/1 ┐ ┌ Pane #2 ─────────────── SCROLL: 0/1 ┐ │
03 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │
04 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │
05 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEE┌ Pane #2 ─────────────────────────────────────────────────┐EEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │
06 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │
07 (C): │ └───────────────────────────│ │────────────────────────────┘ │
08 (C): │ │ │ │
09 (C): │ │ I am scratch terminal │ │
10 (C): │ │ │ │
11 (C): │ │ │ │
12 (C): │ ┌ Pane #2 ─────────────── SC│ │────────────── SCROLL: 0/1 ┐ │
13 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │
14 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEE└──────────────────────────────────────────────────────────┘EEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │
15 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │
16 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │
17 (C): │ └──────────────────────────────────────┘ └──────────────────────────────────────┘ │
18 (C): │ │
19 (C): └───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

View file

@ -0,0 +1,26 @@
---
source: zellij-server/src/tab/./unit/tab_integration_tests.rs
expression: snapshot
---
00 (C): ┌ Pane #1 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
01 (C): │ │
02 (C): │ ┌ Pane #2 ─────────────── SCROLL: 0/1 ┐ ┌ Pane #2 ─────────────── SCROLL: 0/1 ┐ │
03 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │
04 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │
05 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEE┌ Pane #2 ─────────────────────────────────────────────────┐EEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │
06 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │
07 (C): │ └───────────────────────────│ │────────────────────────────┘ │
08 (C): │ │ │ │
09 (C): │ │ I am scratch terminal │ │
10 (C): │ │ │ │
11 (C): │ │ │ │
12 (C): │ ┌ Pane #2 ─────────────── SC│ ┌ Pane #2 ─────────────── SCROLL: 0/1 ┐ │
13 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │
14 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEE└────────────────────────────────────────────────│EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │
15 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │
16 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │
17 (C): │ └──────────────────────────────────────┘ └──────────────────────────────────────┘ │
18 (C): │ │
19 (C): └───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

View file

@ -0,0 +1,26 @@
---
source: zellij-server/src/tab/./unit/tab_integration_tests.rs
expression: snapshot
---
00 (C): ┌ Pane #1 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
01 (C): │ │
02 (C): │ ┌ Pane #2 ─────────────── SCROLL: 0/1 ┐ ┌ Pane #2 ─────────────── SCROLL: 0/1 ┐ │
03 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │
04 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │
05 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEE┌ Pane #2 ─────────────────────────────────────────────────┐EEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │
06 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │
07 (C): │ └───────────────────────────│ │────────────────────────────┘ │
08 (C): │ │ │ │
09 (C): │ │ I am scratch terminal │ │
10 (C): │ │ │ │
11 (C): │ │ │ │
12 (C): │ ┌ Pane #2 ─────────────── SC│ │────────────── SCROLL: 0/1 ┐ │
13 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │
14 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEE└──────────────────────────────────────────────────────────┘EEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │
15 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │
16 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │
17 (C): │ └──────────────────────────────────────┘ └──────────────────────────────────────┘ │
18 (C): │ │
19 (C): └───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

View file

@ -0,0 +1,26 @@
---
source: zellij-server/src/tab/./unit/tab_integration_tests.rs
expression: snapshot
---
00 (C): ┌ Pane #1 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
01 (C): │ │
02 (C): │ ┌ Pane #2 ─────────────── SCROLL: 0/1 ┐ ┌ Pane #2 ─────────────── SCROLL: 0/1 ┐ │
03 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │
04 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │
05 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEE┌ Pane #2 ───────────────────────────────────────│EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │
06 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │
07 (C): │ └───────────────────────────│ └──────────────────────────────────────┘ │
08 (C): │ │ │ │
09 (C): │ │ I am scratch terminal │ │
10 (C): │ │ │ │
11 (C): │ │ │ │
12 (C): │ ┌ Pane #2 ─────────────── SC│ │────────────── SCROLL: 0/1 ┐ │
13 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │
14 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEE└──────────────────────────────────────────────────────────┘EEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │
15 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │
16 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │
17 (C): │ └──────────────────────────────────────┘ └──────────────────────────────────────┘ │
18 (C): │ │
19 (C): └───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

View file

@ -0,0 +1,26 @@
---
source: zellij-server/src/tab/./unit/tab_integration_tests.rs
expression: snapshot
---
00 (C): ┌ Pane #1 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
01 (C): │ │
02 (C): │ ┌ Pane #2 ─────────────── SCROLL: 0/1 ┐ ┌ Pane #2 ─────────────── SCROLL: 0/1 ┐ │
03 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │
04 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │
05 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEE┌ Pane #2 ─────────────────────────────────────────────────┐EEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │
06 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │
07 (C): │ └───────────────────────────│ │────────────────────────────┘ │
08 (C): │ │ │ │
09 (C): │ │ I am scratch terminal │ │
10 (C): │ │ │ │
11 (C): │ │ │ │
12 (C): │ ┌ Pane #2 ─────────────── SC│ │────────────── SCROLL: 0/1 ┐ │
13 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │
14 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEE└──────────────────────────────────────────────────────────┘EEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │
15 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │
16 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │
17 (C): │ └──────────────────────────────────────┘ └──────────────────────────────────────┘ │
18 (C): │ │
19 (C): └───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

View file

@ -0,0 +1,26 @@
---
source: zellij-server/src/tab/./unit/tab_integration_tests.rs
expression: snapshot
---
00 (C): ┌ Pane #1 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
01 (C): │ │
02 (C): │ ┌ Pane #2 ─────────────── SCROLL: 0/1 ┐ ┌ Pane #2 ─────────────── SCROLL: 0/1 ┐ │
03 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │
04 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │
05 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEE┌ Pane #2 ─────────────────────────────────────────────────┐EEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │
06 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │
07 (C): │ └───────────────────────────│ │────────────────────────────┘ │
08 (C): │ │ │ │
09 (C): │ │ I am scratch terminal │ │
10 (C): │ │ │ │
11 (C): │ │ │ │
12 (C): │ ┌ Pane #2 ─────────────── SC│ │────────────── SCROLL: 0/1 ┐ │
13 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │
14 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEE└──────────────────────────────────────────────────────────┘EEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │
15 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │
16 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │
17 (C): │ └──────────────────────────────────────┘ └──────────────────────────────────────┘ │
18 (C): │ │
19 (C): └───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

View file

@ -0,0 +1,26 @@
---
source: zellij-server/src/tab/./unit/tab_integration_tests.rs
expression: snapshot
---
00 (C): ┌ Pane #1 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
01 (C): │ │
02 (C): │ │
03 (C): │ │
04 (C): │ │
05 (C): │ │
06 (C): │ │
07 (C): │ │
08 (C): │ │
09 (C): │ │
10 (C): │ │
11 (C): │ │
12 (C): │ │
13 (C): │ │
14 (C): │ │
15 (C): │ │
16 (C): │ │
17 (C): │ │
18 (C): │ │
19 (C): └───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

View file

@ -0,0 +1,26 @@
---
source: zellij-server/src/tab/./unit/tab_integration_tests.rs
expression: snapshot
---
00 (C): ┌ Pane #1 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
01 (C): │ │
02 (C): │ │
03 (C): │ │
04 (C): │ │
05 (C): │ ┌ Pane #2 ─────────────────────────────────────────────────┐ │
06 (C): │ │ │ │
07 (C): │ │ │ │
08 (C): │ │ │ │
09 (C): │ │ I am scratch terminal │ │
10 (C): │ │ │ │
11 (C): │ │ │ │
12 (C): │ │ │ │
13 (C): │ │ │ │
14 (C): │ └──────────────────────────────────────────────────────────┘ │
15 (C): │ │
16 (C): │ │
17 (C): │ │
18 (C): │ │
19 (C): └───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

View file

@ -0,0 +1,26 @@
---
source: zellij-server/src/tab/./unit/tab_integration_tests.rs
expression: snapshot
---
00 (C): ┌ Pane #1 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
01 (C): │ │
02 (C): │ │
03 (C): │ │
04 (C): │ │
05 (C): │ ┌ Pane #2 ─────────────────────────────────────────────────┐ │
06 (C): │ │ │ │
07 (C): │ │ │ │
08 (C): │ │ │ │
09 (C): │ │ I am scratch terminal │ │
10 (C): │ │ │ │
11 (C): │ │ │ │
12 (C): │ │ │ │
13 (C): │ │ │ │
14 (C): │ │ │ │
15 (C): │ │ │ │
16 (C): │ └──────────────────────────────────────────────────────────┘ │
17 (C): │ │
18 (C): │ │
19 (C): └───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

View file

@ -0,0 +1,26 @@
---
source: zellij-server/src/tab/./unit/tab_integration_tests.rs
expression: snapshot
---
00 (C): ┌ Pane #1 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
01 (C): │ │
02 (C): │ │
03 (C): │ │
04 (C): │ │
05 (C): │ ┌ Pane #2 ──────────────────────────────────────────────────────┐ │
06 (C): │ │ │ │
07 (C): │ │ │ │
08 (C): │ │ │ │
09 (C): │ │ I am scratch terminal │ │
10 (C): │ │ │ │
11 (C): │ │ │ │
12 (C): │ │ │ │
13 (C): │ │ │ │
14 (C): │ └───────────────────────────────────────────────────────────────┘ │
15 (C): │ │
16 (C): │ │
17 (C): │ │
18 (C): │ │
19 (C): └───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

View file

@ -0,0 +1,26 @@
---
source: zellij-server/src/tab/./unit/tab_integration_tests.rs
expression: snapshot
---
00 (C): ┌ Pane #1 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
01 (C): │ │
02 (C): │ │
03 (C): │ │
04 (C): │ │
05 (C): │ ┌ Pane #2 ──────────────────────────────────────────────────────┐ │
06 (C): │ │ │ │
07 (C): │ │ │ │
08 (C): │ │ │ │
09 (C): │ │ I am scratch terminal │ │
10 (C): │ │ │ │
11 (C): │ │ │ │
12 (C): │ │ │ │
13 (C): │ │ │ │
14 (C): │ └───────────────────────────────────────────────────────────────┘ │
15 (C): │ │
16 (C): │ │
17 (C): │ │
18 (C): │ │
19 (C): └───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

View file

@ -0,0 +1,26 @@
---
source: zellij-server/src/tab/./unit/tab_integration_tests.rs
expression: snapshot
---
00 (C): ┌ Pane #1 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
01 (C): │ │
02 (C): │ │
03 (C): │ ┌ Pane #2 ─────────────────────────────────────────────────┐ │
04 (C): │ │ │ │
05 (C): │ │ │ │
06 (C): │ │ │ │
07 (C): │ │ I am scratch terminal │ │
08 (C): │ │ │ │
09 (C): │ │ │ │
10 (C): │ │ │ │
11 (C): │ │ │ │
12 (C): │ │ │ │
13 (C): │ │ │ │
14 (C): │ └──────────────────────────────────────────────────────────┘ │
15 (C): │ │
16 (C): │ │
17 (C): │ │
18 (C): │ │
19 (C): └───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

View file

@ -0,0 +1,26 @@
---
source: zellij-server/src/tab/./unit/tab_integration_tests.rs
expression: snapshot
---
00 (C): ┌ Pane #1 ────────────────────┌ Pane #2 ─────────────────────────────────────────────────┐─────────┐
01 (C): │ │ │ │
02 (C): │ ┌ Pane #2 ─────────────── SCROLL: 0/1 ┐ ┌ Pane #2 ─────────────── SCROLL: 0/1 ┐
03 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│
04 (C): │ ┌ Pane #2 ─────────────── SCROLL: 0/1 ┐ I am scrat┌ Pane #2 ─────────────── SCROLL: 0/1 ┐
05 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│
06 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│
07 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│
08 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│
09 (C): └─└──────────────────────────────────────┘──────────────────└──────────────────────────────────────┘
10 (C):
11 (C):
12 (C):
13 (C):
14 (C):
15 (C):
16 (C):
17 (C):
18 (C):
19 (C):

View file

@ -0,0 +1,26 @@
---
source: zellij-server/src/tab/./unit/tab_integration_tests.rs
expression: snapshot
---
00 (C): ┌ Pane #1 ────────────────────┌ Pane #2 ─────────┐
01 (C): │ │ │
02 (C): │ ┌ Pane #┌ Pane #2 ─────────────── SCROLL: 0/1 ┐
03 (C): │ │EEEEEEE│EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│
04 (C): │ ┌ Pane #┌ Pane #2 ─────────────── SCROLL: 0/1 ┐
05 (C): │ │EEEEEEE│EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│
06 (C): │ │EEEEEEE│EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│
07 (C): │ │EEEEEEE│EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│
08 (C): │ │EEEEEEE│EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│
09 (C): └─└───────└──────────────────────────────────────┘
10 (C):
11 (C):
12 (C):
13 (C):
14 (C):
15 (C):
16 (C):
17 (C):
18 (C):
19 (C):

View file

@ -0,0 +1,26 @@
---
source: zellij-server/src/tab/./unit/tab_integration_tests.rs
expression: snapshot
---
00 (C): ┌ Pane #1 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
01 (C): │ │
02 (C): │ ┌ Pane #2 ─────────────── SCROLL: 0/1 ┐ ┌ Pane #2 ─────────────── SCROLL: 0/1 ┐ │
03 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │
04 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │
05 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│─────────────────────────────────────│EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │
06 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │
07 (C): │ └──────────────────────────────────────┘ └──────────────────────────────────────┘ │
08 (C): │ │ │ │
09 (C): │ │ I am scratch terminal │ │
10 (C): │ │ │ │
11 (C): │ │ │ │
12 (C): │ ┌ Pane #2 ─────────────── SCROLL: 0/1 ┐ ┌ Pane #2 ─────────────── SCROLL: 0/1 ┐ │
13 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │
14 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│─────────────────────────────────────│EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │
15 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │
16 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │
17 (C): │ └──────────────────────────────────────┘ └──────────────────────────────────────┘ │
18 (C): │ │
19 (C): └───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

View file

@ -0,0 +1,26 @@
---
source: zellij-server/src/tab/./unit/tab_integration_tests.rs
expression: snapshot
---
00 (C): ┌ Pane #1 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
01 (C): │ │
02 (C): │ │
03 (C): │ │
04 (C): │ │
05 (C): │ │
06 (C): │ │
07 (C): │ │
08 (C): │ │
09 (C): │ │
10 (C): │ │
11 (C): │ │
12 (C): │ │
13 (C): │ │
14 (C): │ │
15 (C): │ │
16 (C): │ │
17 (C): │ │
18 (C): │ │
19 (C): └───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

View file

@ -0,0 +1,26 @@
---
source: zellij-server/src/tab/./unit/tab_integration_tests.rs
expression: snapshot
---
00 (C): ┌ Pane #1 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
01 (C): │ │
02 (C): │ │
03 (C): │ │
04 (C): │ │
05 (C): │ ┌ Pane #2 ─────────────────────────────────────────────────┐ │
06 (C): │ │ │ │
07 (C): │ │ │ │
08 (C): │ │ │ │
09 (C): │ │ I am scratch terminal │ │
10 (C): │ │ │ │
11 (C): │ │ │ │
12 (C): │ │ │ │
13 (C): │ │ │ │
14 (C): │ └──────────────────────────────────────────────────────────┘ │
15 (C): │ │
16 (C): │ │
17 (C): │ │
18 (C): │ │
19 (C): └───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

File diff suppressed because it is too large Load diff

View file

@ -1,9 +1,10 @@
use zellij_utils::{pane_size::Viewport, zellij_tile};
use crate::output::CharacterChunk;
use crate::panes::terminal_character::{TerminalCharacter, EMPTY_TERMINAL_CHARACTER, RESET_STYLES};
use crate::tab::Pane;
use ansi_term::Colour::{Fixed, RGB};
use std::collections::HashMap;
use std::fmt::Write;
use zellij_tile::data::PaletteColor;
use zellij_utils::shared::colors;
@ -43,6 +44,19 @@ impl BoundarySymbol {
self.color = color;
*self
}
pub fn as_terminal_character(&self) -> TerminalCharacter {
if self.invisible {
EMPTY_TERMINAL_CHARACTER
} else {
let character = self.boundary_type.chars().next().unwrap();
TerminalCharacter {
character,
width: 1,
styles: RESET_STYLES
.foreground(self.color.map(|palette_color| palette_color.into())),
}
}
}
}
impl Display for BoundarySymbol {
@ -511,19 +525,16 @@ impl Boundaries {
}
}
}
pub fn vte_output(&self) -> String {
let mut vte_output = String::new();
pub fn render(&self) -> Vec<CharacterChunk> {
let mut character_chunks = vec![];
for (coordinates, boundary_character) in &self.boundary_characters {
write!(
&mut vte_output,
"\u{1b}[{};{}H\u{1b}[m{}",
coordinates.y + 1,
coordinates.x + 1,
boundary_character
)
.unwrap(); // goto row/col + boundary character
character_chunks.push(CharacterChunk::new(
vec![boundary_character.as_terminal_character()],
coordinates.x,
coordinates.y,
));
}
vte_output
character_chunks
}
fn rect_right_boundary_is_before_screen_edge(&self, rect: &dyn Pane) -> bool {
rect.x() + rect.cols() < self.viewport.cols

View file

@ -1,32 +1,64 @@
use crate::output::CharacterChunk;
use crate::panes::{AnsiCode, CharacterStyles, TerminalCharacter, EMPTY_TERMINAL_CHARACTER};
use crate::ui::boundaries::boundary_type;
use crate::ClientId;
use ansi_term::Colour::{Fixed, RGB};
use ansi_term::Style;
use zellij_utils::zellij_tile::prelude::{client_id_to_colors, Palette, PaletteColor};
use zellij_utils::{envs::get_session_name, pane_size::Viewport};
use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
use std::fmt::Write;
fn color_string(character: &str, color: Option<PaletteColor>) -> String {
match color {
Some(PaletteColor::Rgb((r, g, b))) => RGB(r, g, b).bold().paint(character).to_string(),
Some(PaletteColor::EightBit(color)) => Fixed(color).bold().paint(character).to_string(),
None => Style::new().bold().paint(character).to_string(),
fn foreground_color(characters: &str, color: Option<PaletteColor>) -> Vec<TerminalCharacter> {
let mut colored_string = Vec::with_capacity(characters.chars().count());
for character in characters.chars() {
let styles = match color {
Some(palette_color) => {
let mut styles = CharacterStyles::new();
styles.reset_all();
styles
.foreground(Some(AnsiCode::from(palette_color)))
.bold(Some(AnsiCode::On))
}
None => {
let mut styles = CharacterStyles::new();
styles.reset_all();
styles.bold(Some(AnsiCode::On))
}
};
let terminal_character = TerminalCharacter {
character,
styles,
width: character.width().unwrap_or(0),
};
colored_string.push(terminal_character);
}
colored_string
}
fn background_color(character: &str, color: Option<PaletteColor>) -> String {
match color {
Some(PaletteColor::Rgb((r, g, b))) => {
Style::new().on(RGB(r, g, b)).paint(character).to_string()
fn background_color(characters: &str, color: Option<PaletteColor>) -> Vec<TerminalCharacter> {
let mut colored_string = Vec::with_capacity(characters.chars().count());
for character in characters.chars() {
let styles = match color {
Some(palette_color) => {
let mut styles = CharacterStyles::new();
styles.reset_all();
styles
.background(Some(AnsiCode::from(palette_color)))
.bold(Some(AnsiCode::On))
}
Some(PaletteColor::EightBit(color)) => {
Style::new().on(Fixed(color)).paint(character).to_string()
None => {
let mut styles = CharacterStyles::new();
styles.reset_all();
styles
}
None => character.to_string(),
};
let terminal_character = TerminalCharacter {
character,
styles,
width: character.width().unwrap_or(0),
};
colored_string.push(terminal_character);
}
colored_string
}
pub struct FrameParams {
@ -70,11 +102,14 @@ impl PaneFrame {
other_cursors_exist_in_session: frame_params.other_cursors_exist_in_session,
}
}
fn client_cursor(&self, client_id: ClientId) -> String {
fn client_cursor(&self, client_id: ClientId) -> Vec<TerminalCharacter> {
let color = client_id_to_colors(client_id, self.colors);
background_color(" ", color.map(|c| c.0))
}
fn render_title_right_side(&self, max_length: usize) -> Option<(String, usize)> {
fn render_title_right_side(
&self,
max_length: usize,
) -> Option<(Vec<TerminalCharacter>, usize)> {
// string and length because of color
if self.scroll_position.0 > 0 || self.scroll_position.1 > 0 {
let prefix = " SCROLL: ";
@ -86,17 +121,17 @@ impl PaneFrame {
let prefix_len = prefix.chars().count();
if prefix_len + full_indication_len <= max_length {
Some((
color_string(&format!("{}{}", prefix, full_indication), self.color),
foreground_color(&format!("{}{}", prefix, full_indication), self.color),
prefix_len + full_indication_len,
))
} else if full_indication_len <= max_length {
Some((
color_string(&full_indication, self.color),
foreground_color(&full_indication, self.color),
full_indication_len,
))
} else if short_indication_len <= max_length {
Some((
color_string(&short_indication, self.color),
foreground_color(&short_indication, self.color),
short_indication_len,
))
} else {
@ -106,24 +141,24 @@ impl PaneFrame {
None
}
}
fn render_my_focus(&self, max_length: usize) -> Option<(String, usize)> {
let left_separator = color_string(boundary_type::VERTICAL_LEFT, self.color);
let right_separator = color_string(boundary_type::VERTICAL_RIGHT, self.color);
fn render_my_focus(&self, max_length: usize) -> Option<(Vec<TerminalCharacter>, usize)> {
let left_separator = foreground_color(boundary_type::VERTICAL_LEFT, self.color);
let right_separator = foreground_color(boundary_type::VERTICAL_RIGHT, self.color);
let full_indication_text = "MY FOCUS";
let full_indication = format!(
"{} {} {}",
left_separator,
color_string(full_indication_text, self.color),
right_separator
);
let mut full_indication = vec![];
full_indication.append(&mut left_separator.clone());
full_indication.push(EMPTY_TERMINAL_CHARACTER);
full_indication.append(&mut foreground_color(full_indication_text, self.color));
full_indication.push(EMPTY_TERMINAL_CHARACTER);
full_indication.append(&mut right_separator.clone());
let full_indication_len = full_indication_text.width() + 4; // 2 for separators 2 for padding
let short_indication_text = "ME";
let short_indication = format!(
"{} {} {}",
left_separator,
color_string(short_indication_text, self.color),
right_separator
);
let mut short_indication = vec![];
short_indication.append(&mut left_separator.clone());
short_indication.push(EMPTY_TERMINAL_CHARACTER);
short_indication.append(&mut foreground_color(short_indication_text, self.color));
short_indication.push(EMPTY_TERMINAL_CHARACTER);
short_indication.append(&mut right_separator.clone());
let short_indication_len = short_indication_text.width() + 4; // 2 for separators 2 for padding
if full_indication_len <= max_length {
Some((full_indication, full_indication_len))
@ -133,91 +168,110 @@ impl PaneFrame {
None
}
}
fn render_my_and_others_focus(&self, max_length: usize) -> Option<(String, usize)> {
let left_separator = color_string(boundary_type::VERTICAL_LEFT, self.color);
let right_separator = color_string(boundary_type::VERTICAL_RIGHT, self.color);
fn render_my_and_others_focus(
&self,
max_length: usize,
) -> Option<(Vec<TerminalCharacter>, usize)> {
let mut left_separator = foreground_color(boundary_type::VERTICAL_LEFT, self.color);
let mut right_separator = foreground_color(boundary_type::VERTICAL_RIGHT, self.color);
let full_indication_text = "MY FOCUS AND:";
let short_indication_text = "+";
let mut full_indication = color_string(full_indication_text, self.color);
let mut full_indication = foreground_color(full_indication_text, self.color);
let mut full_indication_len = full_indication_text.width();
let mut short_indication = color_string(short_indication_text, self.color);
let mut short_indication = foreground_color(short_indication_text, self.color);
let mut short_indication_len = short_indication_text.width();
for client_id in &self.other_focused_clients {
let text = format!(" {}", self.client_cursor(*client_id));
let mut text = self.client_cursor(*client_id);
full_indication_len += 2;
full_indication.push_str(&text);
full_indication.push(EMPTY_TERMINAL_CHARACTER);
full_indication.append(&mut text.clone());
short_indication_len += 2;
short_indication.push_str(&text);
short_indication.append(&mut text);
}
if full_indication_len + 4 <= max_length {
// 2 for separators, 2 for padding
Some((
format!("{} {} {}", left_separator, full_indication, right_separator),
full_indication_len + 4,
))
let mut ret = vec![];
ret.append(&mut left_separator);
ret.push(EMPTY_TERMINAL_CHARACTER);
ret.append(&mut full_indication);
ret.push(EMPTY_TERMINAL_CHARACTER);
ret.append(&mut right_separator);
Some((ret, full_indication_len + 4))
} else if short_indication_len + 4 <= max_length {
// 2 for separators, 2 for padding
Some((
format!(
"{} {} {}",
left_separator, short_indication, right_separator
),
short_indication_len + 4,
))
let mut ret = vec![];
ret.append(&mut left_separator);
ret.push(EMPTY_TERMINAL_CHARACTER);
ret.append(&mut short_indication);
ret.push(EMPTY_TERMINAL_CHARACTER);
ret.append(&mut right_separator);
Some((ret, short_indication_len + 4))
} else {
None
}
}
fn render_other_focused_users(&self, max_length: usize) -> Option<(String, usize)> {
let left_separator = color_string(boundary_type::VERTICAL_LEFT, self.color);
let right_separator = color_string(boundary_type::VERTICAL_RIGHT, self.color);
fn render_other_focused_users(
&self,
max_length: usize,
) -> Option<(Vec<TerminalCharacter>, usize)> {
let mut left_separator = foreground_color(boundary_type::VERTICAL_LEFT, self.color);
let mut right_separator = foreground_color(boundary_type::VERTICAL_RIGHT, self.color);
let full_indication_text = if self.other_focused_clients.len() == 1 {
"FOCUSED USER:"
} else {
"FOCUSED USERS:"
};
let middle_indication_text = "U:";
let mut full_indication = color_string(full_indication_text, self.color);
let mut full_indication = foreground_color(full_indication_text, self.color);
let mut full_indication_len = full_indication_text.width();
let mut middle_indication = color_string(middle_indication_text, self.color);
let mut middle_indication = foreground_color(middle_indication_text, self.color);
let mut middle_indication_len = middle_indication_text.width();
let mut short_indication = String::from("");
let mut short_indication = vec![];
let mut short_indication_len = 0;
for client_id in &self.other_focused_clients {
let text = format!(" {}", self.client_cursor(*client_id));
let mut text = self.client_cursor(*client_id);
full_indication_len += 2;
full_indication.push_str(&text);
full_indication.push(EMPTY_TERMINAL_CHARACTER);
full_indication.append(&mut text.clone());
middle_indication_len += 2;
middle_indication.push_str(&text);
middle_indication.push(EMPTY_TERMINAL_CHARACTER);
middle_indication.append(&mut text.clone());
short_indication_len += 2;
short_indication.push_str(&text);
short_indication.push(EMPTY_TERMINAL_CHARACTER);
short_indication.append(&mut text);
}
if full_indication_len + 4 <= max_length {
// 2 for separators, 2 for padding
Some((
format!("{} {} {}", left_separator, full_indication, right_separator),
full_indication_len + 4,
))
let mut ret = vec![];
ret.append(&mut left_separator);
ret.push(EMPTY_TERMINAL_CHARACTER);
ret.append(&mut full_indication);
ret.push(EMPTY_TERMINAL_CHARACTER);
ret.append(&mut right_separator);
Some((ret, full_indication_len + 4))
} else if middle_indication_len + 4 <= max_length {
// 2 for separators, 2 for padding
Some((
format!(
"{} {} {}",
left_separator, middle_indication, right_separator
),
middle_indication_len + 4,
))
let mut ret = vec![];
ret.append(&mut left_separator);
ret.push(EMPTY_TERMINAL_CHARACTER);
ret.append(&mut middle_indication);
ret.push(EMPTY_TERMINAL_CHARACTER);
ret.append(&mut right_separator);
Some((ret, middle_indication_len + 4))
} else if short_indication_len + 3 <= max_length {
// 2 for separators, 1 for padding
Some((
format!("{}{} {}", left_separator, short_indication, right_separator),
short_indication_len + 3,
))
let mut ret = vec![];
ret.append(&mut left_separator);
ret.push(EMPTY_TERMINAL_CHARACTER);
ret.append(&mut short_indication);
ret.push(EMPTY_TERMINAL_CHARACTER);
ret.append(&mut right_separator);
Some((ret, short_indication_len + 3))
} else {
None
}
}
fn render_title_middle(&self, max_length: usize) -> Option<(String, usize)> {
fn render_title_middle(&self, max_length: usize) -> Option<(Vec<TerminalCharacter>, usize)> {
// string and length because of color
if self.is_main_client
&& self.other_focused_clients.is_empty()
@ -237,7 +291,7 @@ impl PaneFrame {
None
}
}
fn render_title_left_side(&self, max_length: usize) -> Option<(String, usize)> {
fn render_title_left_side(&self, max_length: usize) -> Option<(Vec<TerminalCharacter>, usize)> {
let middle_truncated_sign = "[..]";
let middle_truncated_sign_long = "[...]";
let full_text = format!(" {} ", &self.title);
@ -245,7 +299,7 @@ impl PaneFrame {
None
} else if full_text.width() <= max_length {
Some((
color_string(&full_text, self.color),
foreground_color(&full_text, self.color),
full_text.chars().count(),
))
} else {
@ -269,35 +323,39 @@ impl PaneFrame {
}
}
let title_left_side =
if first_part.width() + middle_truncated_sign.width() + second_part.width()
let (title_left_side, title_length) = if first_part.width()
+ middle_truncated_sign.width()
+ second_part.width()
< max_length
{
// this means we lost 1 character when dividing the total length into halves
(
format!(
"{}{}{}",
first_part, middle_truncated_sign_long, second_part
),
first_part.width() + middle_truncated_sign_long.width() + second_part.width(),
)
} else {
format!("{}{}{}", first_part, middle_truncated_sign, second_part)
(
format!("{}{}{}", first_part, middle_truncated_sign, second_part),
first_part.width() + middle_truncated_sign.width() + second_part.width(),
)
};
Some((
color_string(&title_left_side, self.color),
title_left_side.chars().count(),
))
Some((foreground_color(&title_left_side, self.color), title_length))
}
}
fn three_part_title_line(
&self,
left_side: &str,
mut left_side: Vec<TerminalCharacter>,
left_side_len: &usize,
middle: &str,
mut middle: Vec<TerminalCharacter>,
middle_len: &usize,
right_side: &str,
mut right_side: Vec<TerminalCharacter>,
right_side_len: &usize,
) -> String {
) -> Vec<TerminalCharacter> {
let total_title_length = self.geom.cols.saturating_sub(2); // 2 for the left and right corners
let mut title_line = String::new();
let mut title_line = vec![];
let left_side_start_position = self.geom.x + 1;
let middle_start_position = self.geom.x + (total_title_length / 2) - (middle_len / 2) + 1;
let right_side_start_position =
@ -306,24 +364,23 @@ impl PaneFrame {
let mut col = self.geom.x;
loop {
if col == self.geom.x {
title_line.push_str(&color_string(boundary_type::TOP_LEFT, self.color));
title_line.append(&mut foreground_color(boundary_type::TOP_LEFT, self.color));
} else if col == self.geom.x + self.geom.cols - 1 {
title_line.push_str(&color_string(boundary_type::TOP_RIGHT, self.color));
title_line.append(&mut foreground_color(boundary_type::TOP_RIGHT, self.color));
} else if col == left_side_start_position {
title_line.push_str(left_side);
title_line.append(&mut left_side);
col += left_side_len;
continue;
} else if col == middle_start_position {
title_line.push_str(middle);
title_line.append(&mut middle);
col += middle_len;
continue;
} else if col == right_side_start_position {
title_line.push_str(right_side);
title_line.append(&mut right_side);
col += right_side_len;
continue;
} else {
title_line.push_str(&color_string(boundary_type::HORIZONTAL, self.color));
// TODO: BETTER
title_line.append(&mut foreground_color(boundary_type::HORIZONTAL, self.color));
}
if col == self.geom.x + self.geom.cols - 1 {
break;
@ -334,33 +391,32 @@ impl PaneFrame {
}
fn left_and_middle_title_line(
&self,
left_side: &str,
mut left_side: Vec<TerminalCharacter>,
left_side_len: &usize,
middle: &str,
mut middle: Vec<TerminalCharacter>,
middle_len: &usize,
) -> String {
) -> Vec<TerminalCharacter> {
let total_title_length = self.geom.cols.saturating_sub(2); // 2 for the left and right corners
let mut title_line = String::new();
let mut title_line = vec![];
let left_side_start_position = self.geom.x + 1;
let middle_start_position = self.geom.x + (total_title_length / 2) - (*middle_len / 2) + 1;
let mut col = self.geom.x;
loop {
if col == self.geom.x {
title_line.push_str(&color_string(boundary_type::TOP_LEFT, self.color));
title_line.append(&mut foreground_color(boundary_type::TOP_LEFT, self.color));
} else if col == self.geom.x + self.geom.cols - 1 {
title_line.push_str(&color_string(boundary_type::TOP_RIGHT, self.color));
title_line.append(&mut foreground_color(boundary_type::TOP_RIGHT, self.color));
} else if col == left_side_start_position {
title_line.push_str(left_side);
title_line.append(&mut left_side);
col += *left_side_len;
continue;
} else if col == middle_start_position {
title_line.push_str(middle);
title_line.append(&mut middle);
col += *middle_len;
continue;
} else {
title_line.push_str(&color_string(boundary_type::HORIZONTAL, self.color));
// TODO: BETTER
title_line.append(&mut foreground_color(boundary_type::HORIZONTAL, self.color));
}
if col == self.geom.x + self.geom.cols - 1 {
break;
@ -369,24 +425,27 @@ impl PaneFrame {
}
title_line
}
fn middle_only_title_line(&self, middle: &str, middle_len: &usize) -> String {
fn middle_only_title_line(
&self,
mut middle: Vec<TerminalCharacter>,
middle_len: &usize,
) -> Vec<TerminalCharacter> {
let total_title_length = self.geom.cols.saturating_sub(2); // 2 for the left and right corners
let mut title_line = String::new();
let mut title_line = vec![];
let middle_start_position = self.geom.x + (total_title_length / 2) - (*middle_len / 2) + 1;
let mut col = self.geom.x;
loop {
if col == self.geom.x {
title_line.push_str(&color_string(boundary_type::TOP_LEFT, self.color));
title_line.append(&mut foreground_color(boundary_type::TOP_LEFT, self.color));
} else if col == self.geom.x + self.geom.cols - 1 {
title_line.push_str(&color_string(boundary_type::TOP_RIGHT, self.color));
title_line.append(&mut foreground_color(boundary_type::TOP_RIGHT, self.color));
} else if col == middle_start_position {
title_line.push_str(middle);
title_line.append(&mut middle);
col += *middle_len;
continue;
} else {
title_line.push_str(&color_string(boundary_type::HORIZONTAL, self.color));
// TODO: BETTER
title_line.append(&mut foreground_color(boundary_type::HORIZONTAL, self.color));
}
if col == self.geom.x + self.geom.cols - 1 {
break;
@ -397,81 +456,86 @@ impl PaneFrame {
}
fn two_part_title_line(
&self,
left_side: &str,
mut left_side: Vec<TerminalCharacter>,
left_side_len: &usize,
right_side: &str,
mut right_side: Vec<TerminalCharacter>,
right_side_len: &usize,
) -> String {
let left_boundary = color_string(boundary_type::TOP_LEFT, self.color);
let right_boundary = color_string(boundary_type::TOP_RIGHT, self.color);
) -> Vec<TerminalCharacter> {
let mut left_boundary = foreground_color(boundary_type::TOP_LEFT, self.color);
let mut right_boundary = foreground_color(boundary_type::TOP_RIGHT, self.color);
let total_title_length = self.geom.cols.saturating_sub(2); // 2 for the left and right corners
let mut middle = String::new();
for _ in (left_side_len + right_side_len)..total_title_length {
middle.push_str(boundary_type::HORIZONTAL);
}
format!(
"{}{}{}{}{}",
left_boundary,
left_side,
color_string(&middle, self.color),
color_string(right_side, self.color),
&right_boundary
)
let mut ret = vec![];
ret.append(&mut left_boundary);
ret.append(&mut left_side);
ret.append(&mut foreground_color(&middle, self.color));
ret.append(&mut right_side);
ret.append(&mut right_boundary);
ret
}
fn left_only_title_line(&self, left_side: &str, left_side_len: &usize) -> String {
let left_boundary = color_string(boundary_type::TOP_LEFT, self.color);
let right_boundary = color_string(boundary_type::TOP_RIGHT, self.color);
fn left_only_title_line(
&self,
mut left_side: Vec<TerminalCharacter>,
left_side_len: &usize,
) -> Vec<TerminalCharacter> {
let mut left_boundary = foreground_color(boundary_type::TOP_LEFT, self.color);
let mut right_boundary = foreground_color(boundary_type::TOP_RIGHT, self.color);
let total_title_length = self.geom.cols.saturating_sub(2); // 2 for the left and right corners
let mut middle_padding = String::new();
for _ in *left_side_len..total_title_length {
middle_padding.push_str(boundary_type::HORIZONTAL);
}
format!(
"{}{}{}{}",
left_boundary,
left_side,
color_string(&middle_padding, self.color),
&right_boundary
)
let mut ret = vec![];
ret.append(&mut left_boundary);
ret.append(&mut left_side);
ret.append(&mut foreground_color(&middle_padding, self.color));
ret.append(&mut right_boundary);
ret
}
fn empty_title_line(&self) -> String {
let left_boundary = color_string(boundary_type::TOP_LEFT, self.color);
let right_boundary = color_string(boundary_type::TOP_RIGHT, self.color);
fn empty_title_line(&self) -> Vec<TerminalCharacter> {
let mut left_boundary = foreground_color(boundary_type::TOP_LEFT, self.color);
let mut right_boundary = foreground_color(boundary_type::TOP_RIGHT, self.color);
let total_title_length = self.geom.cols.saturating_sub(2); // 2 for the left and right corners
let mut middle_padding = String::new();
for _ in 0..total_title_length {
middle_padding.push_str(boundary_type::HORIZONTAL);
}
format!(
"{}{}{}",
left_boundary,
color_string(&middle_padding, self.color),
right_boundary
)
let mut ret = vec![];
ret.append(&mut left_boundary);
ret.append(&mut foreground_color(&middle_padding, self.color));
ret.append(&mut right_boundary);
ret
}
fn title_line_with_middle(&self, middle: &str, middle_len: &usize) -> String {
fn title_line_with_middle(
&self,
middle: Vec<TerminalCharacter>,
middle_len: &usize,
) -> Vec<TerminalCharacter> {
let total_title_length = self.geom.cols.saturating_sub(2); // 2 for the left and right corners
let length_of_each_side = total_title_length.saturating_sub(*middle_len + 2) / 2;
let mut left_side = self.render_title_left_side(length_of_each_side);
let mut right_side = self.render_title_right_side(length_of_each_side);
let left_side = self.render_title_left_side(length_of_each_side);
let right_side = self.render_title_right_side(length_of_each_side);
match (&mut left_side, &mut right_side) {
match (left_side, right_side) {
(Some((left_side, left_side_len)), Some((right_side, right_side_len))) => self
.three_part_title_line(
left_side,
left_side_len,
&left_side_len,
middle,
middle_len,
right_side,
right_side_len,
&right_side_len,
),
(Some((left_side, left_side_len)), None) => {
self.left_and_middle_title_line(left_side, left_side_len, middle, middle_len)
self.left_and_middle_title_line(left_side, &left_side_len, middle, middle_len)
}
_ => self.middle_only_title_line(middle, middle_len),
}
}
fn title_line_without_middle(&self) -> String {
fn title_line_without_middle(&self) -> Vec<TerminalCharacter> {
let total_title_length = self.geom.cols.saturating_sub(2); // 2 for the left and right corners
let left_side = self.render_title_left_side(total_title_length);
let right_side = left_side.as_ref().and_then(|(_left_side, left_side_len)| {
@ -480,96 +544,74 @@ impl PaneFrame {
});
match (left_side, right_side) {
(Some((left_side, left_side_len)), Some((right_side, right_side_len))) => {
self.two_part_title_line(&left_side, &left_side_len, &right_side, &right_side_len)
self.two_part_title_line(left_side, &left_side_len, right_side, &right_side_len)
}
(Some((left_side, left_side_len)), None) => {
self.left_only_title_line(&left_side, &left_side_len)
self.left_only_title_line(left_side, &left_side_len)
}
_ => self.empty_title_line(),
}
}
fn render_title(&self, vte_output: &mut String) {
fn render_title(&self) -> Vec<TerminalCharacter> {
let total_title_length = self.geom.cols.saturating_sub(2); // 2 for the left and right corners
if let Some((middle, middle_length)) = &self.render_title_middle(total_title_length) {
let title_text = self.title_line_with_middle(middle, middle_length);
write!(
vte_output,
"\u{1b}[{};{}H\u{1b}[m{}",
self.geom.y + 1, // +1 because goto is 1 indexed
self.geom.x + 1, // +1 because goto is 1 indexed
color_string(&title_text, self.color),
)
.unwrap(); // goto row/col + boundary character
} else {
let title_text = self.title_line_without_middle();
write!(
vte_output,
"\u{1b}[{};{}H\u{1b}[m{}",
self.geom.y + 1, // +1 because goto is 1 indexed
self.geom.x + 1, // +1 because goto is 1 indexed
color_string(&title_text, self.color),
)
.unwrap(); // goto row/col + boundary character
self.render_title_middle(total_title_length)
.map(|(middle, middle_length)| self.title_line_with_middle(middle, &middle_length))
.or_else(|| Some(self.title_line_without_middle()))
.unwrap()
}
}
pub fn render(&self) -> String {
let mut vte_output = String::new();
for row in self.geom.y..(self.geom.y + self.geom.rows) {
if row == self.geom.y {
pub fn render(&self) -> (Vec<CharacterChunk>, Option<String>) {
let mut character_chunks = vec![];
for row in 0..self.geom.rows {
if row == 0 {
// top row
self.render_title(&mut vte_output);
} else if row == self.geom.y + self.geom.rows - 1 {
let title = self.render_title();
let x = self.geom.x;
let y = self.geom.y + row;
character_chunks.push(CharacterChunk::new(title, x, y));
} else if row == self.geom.rows - 1 {
// bottom row
for col in self.geom.x..(self.geom.x + self.geom.cols) {
let boundary = if col == self.geom.x {
let mut bottom_row = vec![];
for col in 0..self.geom.cols {
let boundary = if col == 0 {
// bottom left corner
boundary_type::BOTTOM_LEFT
} else if col == self.geom.x + self.geom.cols - 1 {
} else if col == self.geom.cols - 1 {
// bottom right corner
boundary_type::BOTTOM_RIGHT
} else {
boundary_type::HORIZONTAL
};
let boundary_rendered = color_string(boundary, self.color);
write!(
&mut vte_output,
"\u{1b}[{};{}H\u{1b}[m{}",
row + 1,
col + 1,
boundary_rendered
)
.unwrap();
let mut boundary_character = foreground_color(boundary, self.color);
bottom_row.append(&mut boundary_character);
}
let x = self.geom.x;
let y = self.geom.y + row;
character_chunks.push(CharacterChunk::new(bottom_row, x, y));
} else {
write!(
&mut vte_output,
"\u{1b}[{};{}H\u{1b}[m{}",
row + 1, // +1 because goto is 1 indexed
self.geom.x + 1,
color_string(boundary_type::VERTICAL, self.color),
)
.unwrap(); // goto row/col + boundary character
write!(
&mut vte_output,
"\u{1b}[{};{}H\u{1b}[m{}",
row + 1, // +1 because goto is 1 indexed
self.geom.x + self.geom.cols,
color_string(boundary_type::VERTICAL, self.color),
)
.unwrap(); // goto row/col + boundary character
let boundary_character_left = foreground_color(boundary_type::VERTICAL, self.color);
let boundary_character_right =
foreground_color(boundary_type::VERTICAL, self.color);
let x = self.geom.x;
let y = self.geom.y + row;
character_chunks.push(CharacterChunk::new(boundary_character_left, x, y));
let x = (self.geom.x + self.geom.cols).saturating_sub(1);
let y = self.geom.y + row;
character_chunks.push(CharacterChunk::new(boundary_character_right, x, y));
}
}
if self.is_main_client {
write!(
&mut vte_output,
let vte_output = if self.is_main_client {
Some(format!(
"\u{1b}]0;Zellij ({}) - {}",
get_session_name().unwrap(),
self.title
)
.unwrap();
}
vte_output
))
} else {
None
};
(character_chunks, vte_output)
}
}

View file

@ -1,5 +1,6 @@
use crate::output::Output;
use crate::panes::PaneId;
use crate::tab::{Output, Pane};
use crate::tab::Pane;
use crate::ui::boundaries::Boundaries;
use crate::ui::pane_boundaries_frame::FrameParams;
use crate::ClientId;
@ -14,6 +15,7 @@ pub struct PaneContentsAndUi<'a> {
colors: Palette,
focused_clients: Vec<ClientId>,
multiple_users_exist_in_session: bool,
z_index: Option<usize>,
}
impl<'a> PaneContentsAndUi<'a> {
@ -23,6 +25,7 @@ impl<'a> PaneContentsAndUi<'a> {
colors: Palette,
active_panes: &HashMap<ClientId, PaneId>,
multiple_users_exist_in_session: bool,
z_index: Option<usize>,
) -> Self {
let focused_clients: Vec<ClientId> = active_panes
.iter()
@ -35,39 +38,50 @@ impl<'a> PaneContentsAndUi<'a> {
colors,
focused_clients,
multiple_users_exist_in_session,
z_index,
}
}
pub fn render_pane_contents_to_multiple_clients(
&mut self,
clients: impl Iterator<Item = ClientId>,
) {
if let Some(vte_output) = self.pane.render(None) {
// FIXME: Use Termion for cursor and style clearing?
self.output.push_str_to_multiple_clients(
if let Some((character_chunks, raw_vte_output)) = self.pane.render(None) {
let clients: Vec<ClientId> = clients.collect();
self.output.add_character_chunks_to_multiple_clients(
character_chunks,
clients.iter().copied(),
self.z_index,
);
if let Some(raw_vte_output) = raw_vte_output {
self.output.add_post_vte_instruction_to_multiple_clients(
clients.iter().copied(),
&format!(
"\u{1b}[{};{}H\u{1b}[m{}",
self.pane.y() + 1,
self.pane.x() + 1,
vte_output
raw_vte_output
),
clients,
);
}
}
}
pub fn render_pane_contents_for_client(&mut self, client_id: ClientId) {
if let Some(vte_output) = self.pane.render(Some(client_id)) {
// FIXME: Use Termion for cursor and style clearing?
self.output.push_to_client(
if let Some((character_chunks, raw_vte_output)) = self.pane.render(Some(client_id)) {
self.output
.add_character_chunks_to_client(client_id, character_chunks, self.z_index);
if let Some(raw_vte_output) = raw_vte_output {
self.output.add_post_vte_instruction_to_client(
client_id,
&format!(
"\u{1b}[{};{}H\u{1b}[m{}",
self.pane.y() + 1,
self.pane.x() + 1,
vte_output
raw_vte_output
),
);
}
}
}
pub fn render_fake_cursor_if_needed(&mut self, client_id: ClientId) {
let pane_focused_for_client_id = self.focused_clients.contains(&client_id);
let pane_focused_for_different_client = self
@ -84,7 +98,7 @@ impl<'a> PaneContentsAndUi<'a> {
.unwrap();
if let Some(colors) = client_id_to_colors(*fake_cursor_client_id, self.colors) {
if let Some(vte_output) = self.pane.render_fake_cursor(colors.0, colors.1) {
self.output.push_to_client(
self.output.add_post_vte_instruction_to_client(
client_id,
&format!(
"\u{1b}[{};{}H\u{1b}[m{}",
@ -139,17 +153,18 @@ impl<'a> PaneContentsAndUi<'a> {
other_cursors_exist_in_session: self.multiple_users_exist_in_session,
}
};
if let Some(vte_output) = self.pane.render_frame(client_id, frame_params, client_mode) {
// FIXME: Use Termion for cursor and style clearing?
self.output.push_to_client(
if let Some((frame_terminal_characters, vte_output)) =
self.pane.render_frame(client_id, frame_params, client_mode)
{
self.output.add_character_chunks_to_client(
client_id,
&format!(
"\u{1b}[{};{}H\u{1b}[m{}",
self.pane.y() + 1,
self.pane.x() + 1,
vte_output
),
frame_terminal_characters,
self.z_index,
);
if let Some(vte_output) = vte_output {
self.output
.add_post_vte_instruction_to_client(client_id, &vte_output);
}
}
}
pub fn render_pane_boundaries(

View file

@ -204,23 +204,8 @@ pub fn close_the_middle_tab() {
new_tab(&mut screen, 1);
new_tab(&mut screen, 2);
new_tab(&mut screen, 3);
dbg!(screen
.tabs
.values()
.map(|t| (t.index, t.position, t.name.clone(), t.get_pane_ids()))
.collect::<Vec<_>>());
screen.switch_tab_prev(1);
dbg!(screen
.tabs
.values()
.map(|t| (t.index, t.position, t.name.clone(), t.get_pane_ids()))
.collect::<Vec<_>>());
screen.close_tab(1);
dbg!(screen
.tabs
.values()
.map(|t| (t.index, t.position, t.name.clone(), t.get_pane_ids()))
.collect::<Vec<_>>());
assert_eq!(screen.tabs.len(), 2, "Two tabs left");
assert_eq!(

View file

@ -137,6 +137,10 @@ keybinds:
key: [Char: 'f',]
- action: [TogglePaneFrames, SwitchToMode: Normal,]
key: [Char: 'z',]
- action: [ToggleFloatingPanes, SwitchToMode: Normal,]
key: [Char: 'w']
- action: [TogglePaneEmbedOrFloating, SwitchToMode: Normal,]
key: [Char: 'e']
- action: [NewPane: ,]
key: [ Alt: 'n',]
- action: [MoveFocus: Left,]

View file

@ -0,0 +1,8 @@
---
template:
direction: Horizontal
parts:
- direction: Vertical
body: true
tabs:
- direction: Vertical

View file

@ -11,9 +11,11 @@ pub fn set_zellij(v: String) {
}
pub const SESSION_NAME_ENV_KEY: &str = "ZELLIJ_SESSION_NAME";
pub fn get_session_name() -> Result<String> {
Ok(var(SESSION_NAME_ENV_KEY)?)
}
pub fn set_session_name(v: String) {
set_var(SESSION_NAME_ENV_KEY, v);
}

View file

@ -207,6 +207,8 @@ pub enum ScreenContext {
HandlePtyBytes,
Render,
NewPane,
ToggleFloatingPanes,
TogglePaneEmbedOrFloating,
HorizontalSplit,
VerticalSplit,
WriteCharacter,

View file

@ -81,6 +81,10 @@ pub enum Action {
/// Open a new pane in the specified direction (relative to focus).
/// If no direction is specified, will try to use the biggest available space.
NewPane(Option<Direction>),
/// Embed focused pane in tab if floating or float focused pane if embedded
TogglePaneEmbedOrFloating,
/// Toggle the visibility of all floating panes (if any) in the current Tab
ToggleFloatingPanes,
/// Close the focus pane.
CloseFocus,
PaneNameInput(Vec<u8>),

View file

@ -33,7 +33,6 @@ pub fn get_mode_info(
],
InputMode::Pane => vec![
("←↓↑→".to_string(), "Move focus".to_string()),
("p".to_string(), "Next".to_string()),
("n".to_string(), "New".to_string()),
("d".to_string(), "Down split".to_string()),
("r".to_string(), "Right split".to_string()),
@ -41,6 +40,9 @@ pub fn get_mode_info(
("f".to_string(), "Fullscreen".to_string()),
("z".to_string(), "Frames".to_string()),
("c".to_string(), "Rename".to_string()),
("w".to_string(), "Floating Toggle".to_string()),
("e".to_string(), "Embed Pane".to_string()),
("p".to_string(), "Next".to_string()),
],
InputMode::Tab => vec![
("←↓↑→".to_string(), "Move focus".to_string()),

View file

@ -4,7 +4,7 @@ use crate::position::Position;
/// Contains the position and size of a [`Pane`], or more generally of any terminal, measured
/// in character rows and columns.
#[derive(Clone, Copy, Default, PartialEq, Debug, Serialize, Deserialize)]
#[derive(Clone, Copy, Default, PartialEq, Debug, Serialize, Deserialize, Eq)]
pub struct PaneGeom {
pub x: usize,
pub y: usize,
@ -34,7 +34,7 @@ pub struct Size {
pub cols: usize,
}
#[derive(Clone, Copy, PartialEq, Debug, Serialize, Deserialize)]
#[derive(Eq, Clone, Copy, PartialEq, Debug, Serialize, Deserialize)]
pub struct Dimension {
pub constraint: Constraint,
inner: usize,
@ -90,6 +90,8 @@ pub enum Constraint {
Percent(f64),
}
impl Eq for Constraint {}
impl PaneGeom {
pub fn contains(&self, point: &Position) -> bool {
let col = point.column.0 as usize;

View file

@ -20,6 +20,14 @@ impl Position {
column: Column(self.column.0.saturating_sub(column)),
}
}
pub fn line(&self) -> isize {
self.line.0
}
pub fn column(&self) -> usize {
self.column.0
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Deserialize, Serialize, PartialOrd)]