//! Things related to [`Screen`]s. use std::collections::BTreeMap; use std::os::unix::io::RawFd; use std::str; use std::sync::{Arc, RwLock}; use zellij_utils::zellij_tile; use crate::{ panes::PaneId, pty::{PtyInstruction, VteBytes}, tab::Tab, thread_bus::Bus, ui::layout::Layout, wasm_vm::PluginInstruction, ServerInstruction, SessionState, }; use zellij_tile::data::{Event, InputMode, ModeInfo, Palette, PluginCapabilities, TabInfo}; use zellij_utils::{ errors::{ContextType, ScreenContext}, input::options::Options, ipc::ClientAttributes, pane_size::PositionAndSize, }; /// Instructions that can be sent to the [`Screen`]. #[derive(Debug, Clone)] pub(crate) enum ScreenInstruction { PtyBytes(RawFd, VteBytes), Render, NewPane(PaneId), HorizontalSplit(PaneId), VerticalSplit(PaneId), WriteCharacter(Vec), ResizeLeft, ResizeRight, ResizeDown, ResizeUp, SwitchFocus, FocusNextPane, FocusPreviousPane, MoveFocusLeft, MoveFocusLeftOrPreviousTab, MoveFocusDown, MoveFocusUp, MoveFocusRight, MoveFocusRightOrNextTab, Exit, ScrollUp, ScrollDown, PageScrollUp, PageScrollDown, ClearScroll, CloseFocusedPane, ToggleActiveTerminalFullscreen, SetSelectable(PaneId, bool), SetFixedHeight(PaneId, usize), SetFixedWidth(PaneId, usize), SetInvisibleBorders(PaneId, bool), ClosePane(PaneId), ApplyLayout(Layout, Vec), NewTab(RawFd), SwitchTabNext, SwitchTabPrev, ToggleActiveSyncTab, CloseTab, GoToTab(u32), UpdateTabName(Vec), TerminalResize(PositionAndSize), ChangeMode(ModeInfo), } impl From<&ScreenInstruction> for ScreenContext { fn from(screen_instruction: &ScreenInstruction) -> Self { match *screen_instruction { ScreenInstruction::PtyBytes(..) => ScreenContext::HandlePtyBytes, ScreenInstruction::Render => ScreenContext::Render, ScreenInstruction::NewPane(_) => ScreenContext::NewPane, ScreenInstruction::HorizontalSplit(_) => ScreenContext::HorizontalSplit, ScreenInstruction::VerticalSplit(_) => ScreenContext::VerticalSplit, ScreenInstruction::WriteCharacter(_) => ScreenContext::WriteCharacter, ScreenInstruction::ResizeLeft => ScreenContext::ResizeLeft, ScreenInstruction::ResizeRight => ScreenContext::ResizeRight, ScreenInstruction::ResizeDown => ScreenContext::ResizeDown, ScreenInstruction::ResizeUp => ScreenContext::ResizeUp, ScreenInstruction::SwitchFocus => ScreenContext::SwitchFocus, ScreenInstruction::FocusNextPane => ScreenContext::FocusNextPane, ScreenInstruction::FocusPreviousPane => ScreenContext::FocusPreviousPane, ScreenInstruction::MoveFocusLeft => ScreenContext::MoveFocusLeft, ScreenInstruction::MoveFocusLeftOrPreviousTab => { ScreenContext::MoveFocusLeftOrPreviousTab } ScreenInstruction::MoveFocusDown => ScreenContext::MoveFocusDown, ScreenInstruction::MoveFocusUp => ScreenContext::MoveFocusUp, ScreenInstruction::MoveFocusRight => ScreenContext::MoveFocusRight, ScreenInstruction::MoveFocusRightOrNextTab => ScreenContext::MoveFocusRightOrNextTab, ScreenInstruction::Exit => ScreenContext::Exit, ScreenInstruction::ScrollUp => ScreenContext::ScrollUp, ScreenInstruction::ScrollDown => ScreenContext::ScrollDown, ScreenInstruction::PageScrollUp => ScreenContext::PageScrollUp, ScreenInstruction::PageScrollDown => ScreenContext::PageScrollDown, ScreenInstruction::ClearScroll => ScreenContext::ClearScroll, ScreenInstruction::CloseFocusedPane => ScreenContext::CloseFocusedPane, ScreenInstruction::ToggleActiveTerminalFullscreen => { ScreenContext::ToggleActiveTerminalFullscreen } ScreenInstruction::SetSelectable(..) => ScreenContext::SetSelectable, ScreenInstruction::SetInvisibleBorders(..) => ScreenContext::SetInvisibleBorders, ScreenInstruction::SetFixedHeight(..) => ScreenContext::SetFixedHeight, ScreenInstruction::SetFixedWidth(..) => ScreenContext::SetFixedWidth, ScreenInstruction::ClosePane(_) => ScreenContext::ClosePane, ScreenInstruction::ApplyLayout(..) => ScreenContext::ApplyLayout, ScreenInstruction::NewTab(_) => ScreenContext::NewTab, ScreenInstruction::SwitchTabNext => ScreenContext::SwitchTabNext, ScreenInstruction::SwitchTabPrev => ScreenContext::SwitchTabPrev, ScreenInstruction::CloseTab => ScreenContext::CloseTab, ScreenInstruction::GoToTab(_) => ScreenContext::GoToTab, ScreenInstruction::UpdateTabName(_) => ScreenContext::UpdateTabName, ScreenInstruction::TerminalResize(_) => ScreenContext::TerminalResize, ScreenInstruction::ChangeMode(_) => ScreenContext::ChangeMode, ScreenInstruction::ToggleActiveSyncTab => ScreenContext::ToggleActiveSyncTab, } } } /// A [`Screen`] holds multiple [`Tab`]s, each one holding multiple [`panes`](crate::client::panes). /// It only directly controls which tab is active, delegating the rest to the individual `Tab`. pub(crate) struct Screen { /// A Bus for sending and receiving messages with the other threads. pub bus: Bus, /// An optional maximal amount of panes allowed per [`Tab`] in this [`Screen`] instance. max_panes: Option, /// A map between this [`Screen`]'s tabs and their ID/key. tabs: BTreeMap, /// The full size of this [`Screen`]. position_and_size: PositionAndSize, /// The index of this [`Screen`]'s active [`Tab`]. active_tab_index: Option, mode_info: ModeInfo, input_mode: InputMode, colors: Palette, session_state: Arc>, } impl Screen { /// Creates and returns a new [`Screen`]. pub fn new( bus: Bus, client_attributes: &ClientAttributes, max_panes: Option, mode_info: ModeInfo, input_mode: InputMode, session_state: Arc>, ) -> Self { Screen { bus, max_panes, position_and_size: client_attributes.position_and_size, colors: client_attributes.palette, active_tab_index: None, tabs: BTreeMap::new(), mode_info, input_mode, session_state, } } /// Creates a new [`Tab`] in this [`Screen`], containing a single /// [pane](crate::client::panes) with PTY file descriptor `pane_id`. pub fn new_tab(&mut self, pane_id: RawFd) { let tab_index = self.get_new_tab_index(); let position = self.tabs.len(); let tab = Tab::new( tab_index, position, String::new(), &self.position_and_size, self.bus.os_input.as_ref().unwrap().clone(), self.bus.senders.clone(), self.max_panes, Some(PaneId::Terminal(pane_id)), self.mode_info.clone(), self.input_mode, self.colors, self.session_state.clone(), ); self.active_tab_index = Some(tab_index); self.tabs.insert(tab_index, tab); self.update_tabs(); self.render(); } /// Returns the index where a new [`Tab`] should be created in this [`Screen`]. /// Currently, this is right after the last currently existing tab, or `0` if /// no tabs exist in this screen yet. fn get_new_tab_index(&self) -> usize { if let Some(index) = self.tabs.keys().last() { *index + 1 } else { 0 } } /// Sets this [`Screen`]'s active [`Tab`] to the next tab. pub fn switch_tab_next(&mut self) { let active_tab_pos = self.get_active_tab().unwrap().position; let new_tab_pos = (active_tab_pos + 1) % self.tabs.len(); for tab in self.tabs.values_mut() { if tab.position == new_tab_pos { tab.set_force_render(); self.active_tab_index = Some(tab.index); break; } } self.update_tabs(); self.render(); } /// Sets this [`Screen`]'s active [`Tab`] to the previous tab. pub fn switch_tab_prev(&mut self) { let active_tab_pos = self.get_active_tab().unwrap().position; let new_tab_pos = if active_tab_pos == 0 { self.tabs.len() - 1 } else { active_tab_pos - 1 }; for tab in self.tabs.values_mut() { if tab.position == new_tab_pos { tab.set_force_render(); self.active_tab_index = Some(tab.index); break; } } self.update_tabs(); self.render(); } pub fn go_to_tab(&mut self, mut tab_index: usize) { tab_index -= 1; let active_tab_index = self.get_active_tab().unwrap().index; if let Some(t) = self.tabs.values_mut().find(|t| t.position == tab_index) { if t.index != active_tab_index { t.set_force_render(); self.active_tab_index = Some(t.index); self.update_tabs(); self.render(); } } } /// Closes this [`Screen`]'s active [`Tab`], exiting the application if it happens /// to be the last tab. pub fn close_tab(&mut self) { let active_tab_index = self.active_tab_index.unwrap(); if self.tabs.len() > 1 { self.switch_tab_prev(); } let active_tab = self.tabs.remove(&active_tab_index).unwrap(); let pane_ids = active_tab.get_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 self.bus .senders .send_to_pty(PtyInstruction::CloseTab(pane_ids)) .unwrap(); if self.tabs.is_empty() { self.active_tab_index = None; if *self.session_state.read().unwrap() == SessionState::Attached { self.bus .senders .send_to_server(ServerInstruction::Render(None)) .unwrap(); } } else { for t in self.tabs.values_mut() { if t.position > active_tab.position { t.position -= 1; } } self.update_tabs(); } } pub fn resize_to_screen(&mut self, new_screen_size: PositionAndSize) { self.position_and_size = new_screen_size; for (_, tab) in self.tabs.iter_mut() { tab.resize_whole_tab(new_screen_size); } let _ = self.get_active_tab_mut().map(|t| t.set_force_render()); self.render(); } /// Renders this [`Screen`], which amounts to rendering its active [`Tab`]. pub fn render(&mut self) { if *self.session_state.read().unwrap() != SessionState::Attached { return; } if let Some(active_tab) = self.get_active_tab_mut() { if active_tab.get_active_pane().is_some() { active_tab.render(); } else { self.close_tab(); } }; } /// Returns a mutable reference to this [`Screen`]'s tabs. pub fn get_tabs_mut(&mut self) -> &mut BTreeMap { &mut self.tabs } /// Returns an immutable reference to this [`Screen`]'s active [`Tab`]. pub fn get_active_tab(&self) -> Option<&Tab> { match self.active_tab_index { Some(tab) => self.tabs.get(&tab), None => None, } } /// Returns a mutable reference to this [`Screen`]'s active [`Tab`]. pub fn get_active_tab_mut(&mut self) -> Option<&mut Tab> { match self.active_tab_index { Some(tab) => self.get_tabs_mut().get_mut(&tab), None => None, } } /// Creates a new [`Tab`] in this [`Screen`], applying the specified [`Layout`] /// and switching to it. pub fn apply_layout(&mut self, layout: Layout, new_pids: Vec) { let tab_index = self.get_new_tab_index(); let position = self.tabs.len(); let mut tab = Tab::new( tab_index, position, String::new(), &self.position_and_size, self.bus.os_input.as_ref().unwrap().clone(), self.bus.senders.clone(), self.max_panes, None, self.mode_info.clone(), self.input_mode, self.colors, self.session_state.clone(), ); tab.apply_layout(layout, new_pids); self.active_tab_index = Some(tab_index); self.tabs.insert(tab_index, tab); self.update_tabs(); } pub fn update_tabs(&self) { let mut tab_data = vec![]; let active_tab_index = self.active_tab_index.unwrap(); for tab in self.tabs.values() { tab_data.push(TabInfo { position: tab.position, name: tab.name.clone(), active: active_tab_index == tab.index, is_sync_panes_active: tab.is_sync_panes_active(), }); } self.bus .senders .send_to_plugin(PluginInstruction::Update(None, Event::TabUpdate(tab_data))) .unwrap(); } pub fn update_active_tab_name(&mut self, buf: Vec) { let s = str::from_utf8(&buf).unwrap(); let active_tab = self.get_active_tab_mut().unwrap(); match s { "\0" => { active_tab.name = String::new(); } "\u{007F}" | "\u{0008}" => { //delete and backspace keys active_tab.name.pop(); } c => { active_tab.name.push_str(c); } } self.update_tabs(); } pub fn change_mode(&mut self, mode_info: ModeInfo) { self.colors = mode_info.palette; self.mode_info = mode_info; for tab in self.tabs.values_mut() { tab.mode_info = self.mode_info.clone(); } } } // The box is here in order to make the // NewClient enum smaller #[allow(clippy::boxed_local)] pub(crate) fn screen_thread_main( bus: Bus, max_panes: Option, client_attributes: ClientAttributes, config_options: Box, session_state: Arc>, ) { let capabilities = config_options.simplified_ui; let default_mode = config_options.default_mode.unwrap_or_default(); let mut screen = Screen::new( bus, &client_attributes, max_panes, ModeInfo { palette: client_attributes.palette, capabilities: PluginCapabilities { arrow_fonts: capabilities, }, mode: default_mode, ..ModeInfo::default() }, default_mode, session_state, ); loop { let (event, mut err_ctx) = screen .bus .recv() .expect("failed to receive event on channel"); err_ctx.add_call(ContextType::Screen((&event).into())); match event { ScreenInstruction::PtyBytes(pid, vte_bytes) => { let active_tab = screen.get_active_tab_mut().unwrap(); if active_tab.has_terminal_pid(pid) { // it's most likely that this event is directed at the active tab // look there first active_tab.handle_pty_bytes(pid, vte_bytes); } else { // if this event wasn't directed at the active tab, start looking // in other tabs let all_tabs = screen.get_tabs_mut(); for tab in all_tabs.values_mut() { if tab.has_terminal_pid(pid) { tab.handle_pty_bytes(pid, vte_bytes); break; } } } } ScreenInstruction::Render => { screen.render(); } ScreenInstruction::NewPane(pid) => { screen.get_active_tab_mut().unwrap().new_pane(pid); screen .bus .senders .send_to_server(ServerInstruction::UnblockInputThread) .unwrap(); } ScreenInstruction::HorizontalSplit(pid) => { screen.get_active_tab_mut().unwrap().horizontal_split(pid); screen .bus .senders .send_to_server(ServerInstruction::UnblockInputThread) .unwrap(); } ScreenInstruction::VerticalSplit(pid) => { screen.get_active_tab_mut().unwrap().vertical_split(pid); screen .bus .senders .send_to_server(ServerInstruction::UnblockInputThread) .unwrap(); } ScreenInstruction::WriteCharacter(bytes) => { let active_tab = screen.get_active_tab_mut().unwrap(); match active_tab.is_sync_panes_active() { true => active_tab.write_to_terminals_on_current_tab(bytes), false => active_tab.write_to_active_terminal(bytes), } } ScreenInstruction::ResizeLeft => { screen.get_active_tab_mut().unwrap().resize_left(); } ScreenInstruction::ResizeRight => { screen.get_active_tab_mut().unwrap().resize_right(); } ScreenInstruction::ResizeDown => { screen.get_active_tab_mut().unwrap().resize_down(); } ScreenInstruction::ResizeUp => { screen.get_active_tab_mut().unwrap().resize_up(); } ScreenInstruction::SwitchFocus => { screen.get_active_tab_mut().unwrap().move_focus(); } ScreenInstruction::FocusNextPane => { screen.get_active_tab_mut().unwrap().focus_next_pane(); } ScreenInstruction::FocusPreviousPane => { screen.get_active_tab_mut().unwrap().focus_previous_pane(); } ScreenInstruction::MoveFocusLeft => { screen.get_active_tab_mut().unwrap().move_focus_left(); } ScreenInstruction::MoveFocusLeftOrPreviousTab => { if !screen.get_active_tab_mut().unwrap().move_focus_left() { screen.switch_tab_prev(); } screen .bus .senders .send_to_server(ServerInstruction::UnblockInputThread) .unwrap(); } ScreenInstruction::MoveFocusDown => { screen.get_active_tab_mut().unwrap().move_focus_down(); } ScreenInstruction::MoveFocusRight => { screen.get_active_tab_mut().unwrap().move_focus_right(); } ScreenInstruction::MoveFocusRightOrNextTab => { if !screen.get_active_tab_mut().unwrap().move_focus_right() { screen.switch_tab_next(); } screen .bus .senders .send_to_server(ServerInstruction::UnblockInputThread) .unwrap(); } ScreenInstruction::MoveFocusUp => { screen.get_active_tab_mut().unwrap().move_focus_up(); } ScreenInstruction::ScrollUp => { screen .get_active_tab_mut() .unwrap() .scroll_active_terminal_up(); } ScreenInstruction::ScrollDown => { screen .get_active_tab_mut() .unwrap() .scroll_active_terminal_down(); } ScreenInstruction::PageScrollUp => { screen .get_active_tab_mut() .unwrap() .scroll_active_terminal_up_page(); } ScreenInstruction::PageScrollDown => { screen .get_active_tab_mut() .unwrap() .scroll_active_terminal_down_page(); } ScreenInstruction::ClearScroll => { screen .get_active_tab_mut() .unwrap() .clear_active_terminal_scroll(); } ScreenInstruction::CloseFocusedPane => { screen.get_active_tab_mut().unwrap().close_focused_pane(); screen.render(); } ScreenInstruction::SetSelectable(id, selectable) => { screen .get_active_tab_mut() .unwrap() .set_pane_selectable(id, selectable); } ScreenInstruction::SetFixedHeight(id, fixed_height) => { screen .get_active_tab_mut() .unwrap() .set_pane_fixed_height(id, fixed_height); } ScreenInstruction::SetFixedWidth(id, fixed_width) => { screen .get_active_tab_mut() .unwrap() .set_pane_fixed_width(id, fixed_width); } ScreenInstruction::SetInvisibleBorders(id, invisible_borders) => { screen .get_active_tab_mut() .unwrap() .set_pane_invisible_borders(id, invisible_borders); screen.render(); } ScreenInstruction::ClosePane(id) => { screen.get_active_tab_mut().unwrap().close_pane(id); screen.render(); } ScreenInstruction::ToggleActiveTerminalFullscreen => { screen .get_active_tab_mut() .unwrap() .toggle_active_pane_fullscreen(); } ScreenInstruction::NewTab(pane_id) => { screen.new_tab(pane_id); screen .bus .senders .send_to_server(ServerInstruction::UnblockInputThread) .unwrap(); } ScreenInstruction::SwitchTabNext => { screen.switch_tab_next(); screen .bus .senders .send_to_server(ServerInstruction::UnblockInputThread) .unwrap(); } ScreenInstruction::SwitchTabPrev => { screen.switch_tab_prev(); screen .bus .senders .send_to_server(ServerInstruction::UnblockInputThread) .unwrap(); } ScreenInstruction::CloseTab => { screen.close_tab(); screen .bus .senders .send_to_server(ServerInstruction::UnblockInputThread) .unwrap(); } ScreenInstruction::ApplyLayout(layout, new_pane_pids) => { screen.apply_layout(layout, new_pane_pids); screen .bus .senders .send_to_server(ServerInstruction::UnblockInputThread) .unwrap(); } ScreenInstruction::GoToTab(tab_index) => { screen.go_to_tab(tab_index as usize); screen .bus .senders .send_to_server(ServerInstruction::UnblockInputThread) .unwrap(); } ScreenInstruction::UpdateTabName(c) => { screen.update_active_tab_name(c); } ScreenInstruction::TerminalResize(new_size) => { screen.resize_to_screen(new_size); } ScreenInstruction::ChangeMode(mode_info) => { screen.change_mode(mode_info); } ScreenInstruction::ToggleActiveSyncTab => { screen .get_active_tab_mut() .unwrap() .toggle_sync_panes_is_active(); screen.update_tabs(); } ScreenInstruction::Exit => { break; } } } }