diff --git a/default-plugins/status-bar/src/first_line.rs b/default-plugins/status-bar/src/first_line.rs index 5dc4f08a..b3dea278 100644 --- a/default-plugins/status-bar/src/first_line.rs +++ b/default-plugins/status-bar/src/first_line.rs @@ -339,7 +339,7 @@ pub fn ctrl_keys(help: &ModeInfo, max_len: usize, separator: &str) -> LinePart { colored_elements, separator, ), - InputMode::Normal => key_indicators( + InputMode::Normal | InputMode::Prompt => key_indicators( max_len, &[ CtrlKeyShortcut::new(CtrlKeyMode::Unselected, CtrlKeyAction::Lock), diff --git a/zellij-server/src/lib.rs b/zellij-server/src/lib.rs index 036d41f0..4d94607f 100644 --- a/zellij-server/src/lib.rs +++ b/zellij-server/src/lib.rs @@ -49,11 +49,11 @@ use zellij_utils::{ setup::get_default_data_dir, }; -pub(crate) type ClientId = u16; +pub type ClientId = u16; /// Instructions related to server-side application #[derive(Debug, Clone)] -pub(crate) enum ServerInstruction { +pub enum ServerInstruction { NewClient( ClientAttributes, Box, diff --git a/zellij-server/src/route.rs b/zellij-server/src/route.rs index 45735491..ba70a3c8 100644 --- a/zellij-server/src/route.rs +++ b/zellij-server/src/route.rs @@ -335,6 +335,28 @@ fn route_action( .send_to_screen(ScreenInstruction::Copy(client_id)) .unwrap(); } + Action::Confirm => { + session + .senders + .send_to_screen(ScreenInstruction::ConfirmPrompt(client_id)) + .unwrap(); + } + Action::Deny => { + session + .senders + .send_to_screen(ScreenInstruction::DenyPrompt(client_id)) + .unwrap(); + } + #[allow(clippy::single_match)] + Action::SkipConfirm(action) => match *action { + Action::Quit => { + to_server + .send(ServerInstruction::ClientExit(client_id)) + .unwrap(); + should_break = true; + } + _ => {} + }, Action::NoOp => {} } should_break diff --git a/zellij-server/src/screen.rs b/zellij-server/src/screen.rs index c2c9d423..a6b871f9 100644 --- a/zellij-server/src/screen.rs +++ b/zellij-server/src/screen.rs @@ -12,6 +12,7 @@ use crate::{ pty::{ClientOrTabIndex, PtyInstruction, VteBytes}, tab::{Output, Tab}, thread_bus::Bus, + ui::overlay::{Overlay, OverlayWindow}, wasm_vm::PluginInstruction, ClientId, ServerInstruction, }; @@ -24,7 +25,7 @@ use zellij_utils::{ /// Instructions that can be sent to the [`Screen`]. #[derive(Debug, Clone)] -pub(crate) enum ScreenInstruction { +pub enum ScreenInstruction { PtyBytes(RawFd, VteBytes), Render, NewPane(PaneId, ClientOrTabIndex), @@ -84,6 +85,10 @@ pub(crate) enum ScreenInstruction { Copy(ClientId), AddClient(ClientId), RemoveClient(ClientId), + AddOverlay(Overlay, ClientId), + RemoveOverlay(ClientId), + ConfirmPrompt(ClientId), + DenyPrompt(ClientId), } impl From<&ScreenInstruction> for ScreenContext { @@ -154,6 +159,10 @@ impl From<&ScreenInstruction> for ScreenContext { ScreenInstruction::ToggleTab(..) => ScreenContext::ToggleTab, ScreenInstruction::AddClient(..) => ScreenContext::AddClient, ScreenInstruction::RemoveClient(..) => ScreenContext::RemoveClient, + ScreenInstruction::AddOverlay(..) => ScreenContext::AddOverlay, + ScreenInstruction::RemoveOverlay(..) => ScreenContext::RemoveOverlay, + ScreenInstruction::ConfirmPrompt(..) => ScreenContext::ConfirmPrompt, + ScreenInstruction::DenyPrompt(..) => ScreenContext::DenyPrompt, } } } @@ -169,6 +178,8 @@ pub(crate) struct Screen { tabs: BTreeMap, /// The full size of this [`Screen`]. size: Size, + /// The overlay that is drawn on top of [`Pane`]'s', [`Tab`]'s and the [`Screen`] + _overlay: OverlayWindow, /// The indices of this [`Screen`]'s active [`Tab`]s. active_tab_indices: BTreeMap, tab_history: BTreeMap>, @@ -193,6 +204,7 @@ impl Screen { colors: client_attributes.palette, active_tab_indices: BTreeMap::new(), tabs: BTreeMap::new(), + _overlay: OverlayWindow::default(), tab_history: BTreeMap::new(), mode_info, draw_pane_frames, @@ -1088,6 +1100,10 @@ pub(crate) fn screen_thread_main( screen.render(); } + ScreenInstruction::AddOverlay(_, _) => {} + ScreenInstruction::RemoveOverlay(_) => {} + ScreenInstruction::ConfirmPrompt(_) => {} + ScreenInstruction::DenyPrompt(_) => {} } } } diff --git a/zellij-server/src/ui/mod.rs b/zellij-server/src/ui/mod.rs index 6ebc4683..08c38183 100644 --- a/zellij-server/src/ui/mod.rs +++ b/zellij-server/src/ui/mod.rs @@ -1,3 +1,4 @@ pub mod boundaries; +pub mod overlay; pub mod pane_boundaries_frame; pub mod pane_resizer; diff --git a/zellij-server/src/ui/overlay/mod.rs b/zellij-server/src/ui/overlay/mod.rs new file mode 100644 index 00000000..3bac1afb --- /dev/null +++ b/zellij-server/src/ui/overlay/mod.rs @@ -0,0 +1,104 @@ +//! This module handles the overlay's over the [`Screen`] +//! +//! They consist of: +//! +//! prompt's: +//! +//! notification's: + +pub mod prompt; + +use crate::ServerInstruction; +use zellij_utils::pane_size::Size; + +#[derive(Clone, Debug)] +pub struct Overlay { + pub overlay_type: OverlayType, +} + +pub trait Overlayable { + /// Generates vte_output that can be passed into + /// the `render()` function + fn generate_overlay(&self, size: Size) -> String; +} + +#[derive(Clone, Debug)] +struct Padding { + rows: usize, + cols: usize, +} + +#[derive(Clone, Debug)] +pub enum OverlayType { + Prompt(prompt::Prompt), +} + +impl Overlayable for OverlayType { + fn generate_overlay(&self, size: Size) -> String { + match &self { + OverlayType::Prompt(prompt) => prompt.generate_overlay(size), + } + } +} + +/// Entrypoint from [`Screen`], which holds the context in which +/// the overlays are being rendered. +/// The most recent overlays draw over the previous overlays. +#[derive(Clone, Debug)] +pub struct OverlayWindow { + pub overlay_stack: Vec, +} + +impl Default for OverlayWindow { + fn default() -> Self { + Self { + overlay_stack: vec![], + } + } +} + +impl Overlayable for OverlayWindow { + fn generate_overlay(&self, size: Size) -> String { + let mut output = String::new(); + //let clear_display = "\u{1b}[2J"; + //output.push_str(&clear_display); + for overlay in &self.overlay_stack { + let vte_output = overlay.generate_overlay(size); + output.push_str(&vte_output); + } + output + } +} + +impl Overlay { + pub fn prompt_confirm(self) -> Option> { + match self.overlay_type { + OverlayType::Prompt(p) => p.confirm(), + } + } + pub fn prompt_deny(self) -> Option> { + match self.overlay_type { + OverlayType::Prompt(p) => p.deny(), + } + } +} + +impl Overlayable for Overlay { + fn generate_overlay(&self, size: Size) -> String { + self.overlay_type.generate_overlay(size) + } +} + +impl Overlay { + pub fn new(overlay_type: OverlayType) -> Self { + Self { overlay_type } + } + + fn pad_cols(output: &mut String, cols: usize) { + if let Some(padding) = cols.checked_sub(output.len()) { + for _ in 0..padding { + output.push(' '); + } + } + } +} diff --git a/zellij-server/src/ui/overlay/prompt.rs b/zellij-server/src/ui/overlay/prompt.rs new file mode 100644 index 00000000..f28e3c40 --- /dev/null +++ b/zellij-server/src/ui/overlay/prompt.rs @@ -0,0 +1,55 @@ +use zellij_utils::pane_size::Size; + +use super::{Overlay, OverlayType, Overlayable}; +use crate::{ClientId, ServerInstruction}; + +#[derive(Clone, Debug)] +pub struct Prompt { + pub message: String, + on_confirm: Option>, + on_deny: Option>, +} + +impl Prompt { + pub fn new( + message: String, + on_confirm: Option>, + on_deny: Option>, + ) -> Self { + Self { + message, + on_confirm, + on_deny, + } + } + pub fn confirm(self) -> Option> { + self.on_confirm + } + pub fn deny(self) -> Option> { + self.on_deny + } +} + +impl Overlayable for Prompt { + fn generate_overlay(&self, size: Size) -> String { + let mut output = String::new(); + let rows = size.rows; + let mut vte_output = self.message.clone(); + Overlay::pad_cols(&mut vte_output, size.cols); + for (x, h) in vte_output.chars().enumerate() { + output.push_str(&format!("\u{1b}[{};{}H\u{1b}[48;5;238m{}", rows, x + 1, h,)); + } + output + } +} + +pub fn _generate_quit_prompt(client_id: ClientId) -> Overlay { + let prompt = Prompt::new( + (" Do you want to quit zellij? [Y]es / [N]o").to_string(), + Some(Box::new(ServerInstruction::ClientExit(client_id))), + None, + ); + Overlay { + overlay_type: OverlayType::Prompt(prompt), + } +} diff --git a/zellij-tile/src/data.rs b/zellij-tile/src/data.rs index 872a78f9..11b94bef 100644 --- a/zellij-tile/src/data.rs +++ b/zellij-tile/src/data.rs @@ -85,6 +85,9 @@ pub enum InputMode { /// `Move` mode allows moving the different existing panes within a tab #[serde(alias = "move")] Move, + /// `Prompt` mode allows interacting with active prompts. + #[serde(alias = "prompt")] + Prompt, } impl Default for InputMode { @@ -129,6 +132,7 @@ impl FromStr for InputMode { "renametab" => Ok(InputMode::RenameTab), "session" => Ok(InputMode::Session), "move" => Ok(InputMode::Move), + "prompt" => Ok(InputMode::Prompt), e => Err(e.to_string().into()), } } diff --git a/zellij-utils/src/errors.rs b/zellij-utils/src/errors.rs index 7088c30c..aeeed203 100644 --- a/zellij-utils/src/errors.rs +++ b/zellij-utils/src/errors.rs @@ -267,6 +267,10 @@ pub enum ScreenContext { ToggleTab, AddClient, RemoveClient, + AddOverlay, + RemoveOverlay, + ConfirmPrompt, + DenyPrompt, } /// Stack call representations corresponding to the different types of [`PtyInstruction`]s. diff --git a/zellij-utils/src/input/actions.rs b/zellij-utils/src/input/actions.rs index bd43206c..c84ad285 100644 --- a/zellij-utils/src/input/actions.rs +++ b/zellij-utils/src/input/actions.rs @@ -105,6 +105,12 @@ pub enum Action { MouseRelease(Position), MouseHold(Position), Copy, + /// Confirm a prompt + Confirm, + /// Deny a prompt + Deny, + /// Confirm an action that invokes a prompt automatically + SkipConfirm(Box), } impl From for Action { diff --git a/zellij-utils/src/input/mod.rs b/zellij-utils/src/input/mod.rs index ca26f78f..a10ffa2c 100644 --- a/zellij-utils/src/input/mod.rs +++ b/zellij-utils/src/input/mod.rs @@ -22,7 +22,7 @@ pub fn get_mode_info( capabilities: PluginCapabilities, ) -> ModeInfo { let keybinds = match mode { - InputMode::Normal | InputMode::Locked => Vec::new(), + InputMode::Normal | InputMode::Locked | InputMode::Prompt => Vec::new(), InputMode::Resize => vec![ ("←↓↑→".to_string(), "Resize".to_string()), ("+-".to_string(), "Increase/Decrease size".to_string()),