diff options
| author | Aram Drevekenin <aram@poor.dev> | 2026-06-10 16:50:55 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-06-10 16:50:55 +0200 |
| commit | b6a5ad0e5a93d9f584b3f7a0fc90f497a09cbdb0 (patch) | |
| tree | 38c77a381cfb937b428752a588c09a45c81434c9 /zellij-utils | |
| parent | feat: PWA support for the web client (#5184) (diff) | |
| download | sl0ppij-b6a5ad0e5a93d9f584b3f7a0fc90f497a09cbdb0.tar.gz sl0ppij-b6a5ad0e5a93d9f584b3f7a0fc90f497a09cbdb0.tar.xz sl0ppij-b6a5ad0e5a93d9f584b3f7a0fc90f497a09cbdb0.zip | |
Mobile layout (#5241)
* phase 2+3
* initial mobile plugin (phases 4 + 5 + 6)
* some bugfixes
* implement stage 7
* mobile interface improvements
* improve mobile pane/tab picker
* more ui improvements and fixes
* various mobile fixes
* some mobile fixes
* mobile keyboard triggering fix
* some cleanups
* initial plugin keyboard implementation
* improve keyboard hit zones
* improve keyboard
* pinch resize
* pinching fixes
* implement scrolling + panning
* initial keyboard resizing
* fix: mobile first render issues
* keyboard responsiveness
* fix some issues with the keyboard
* implement fit
* fix mobile scrolling
* add tilde cell to compact
* improve mobile keyboard
* fix starting mobile terminal font to natural keyboard size
* fix mobile resize blank screen flash
* shadow focus for mobile
* more keyboard fixes
* improve mobile status bar
* fix: render plugins to mobile clients
* allow opening new panes/tabs from the mobile plugin
* allow configuring
* fix dot slash
* improve mobile native soft keyboard interaction
* switch to native keyboard - initial implementation
* dirty hacks, but phone keyboard works now
* adjust keyboard help strip
* fix cursor in mobile
* fix keyboard and help line
* rename keyboard to modifier bar
* allow starting new session
* make clicking session name bring us to new session menu
* allow switching to desktop from the mobile ui
* mobile welcome screen initial implementation
* mobile welcome screen fixes
* various welcome screen fixes
* more mobile fixes
* moar mobile fixes
* mobile ui redesign
* fix: swallowed backspace bug
* switch session screen improvement
* touch up some interfaces
* refactor: move mobile state to own struct
* refactor: move fit resizing to the server
* remove dead flash code
* some cleanups
* refactor: clear fit on close
* refactor: improve fit api
* refactor: pane sync functions
* refactor: mouse
* refactor: break into modules
* some cleanups for the sessions screen
* fix scrolling and other stuff
* refactor: sessions screen
* refactor: panes screen
* refactor: new session screen
* refactor: menu screen
* refactor: render
* cleanups
* refactor: plugin structure
* remove unused welcome screen path
* Rename api
* refactor input.js
* cleanup useless comments
* fit/mobile refactoring
* take out the garbage
* remove more comments
* remove mobile flash on startup/attach
* fix: various mobile startup issues
* fix space bugs
* cleanup
* rustfmt
* fix windows build
* adjust modifier bar ui
* add pr
Diffstat (limited to 'zellij-utils')
43 files changed, 1598 insertions, 230 deletions
diff --git a/zellij-utils/assets/config/default.kdl b/zellij-utils/assets/config/default.kdl index 3ea42850..61aa4bb8 100644 --- a/zellij-utils/assets/config/default.kdl +++ b/zellij-utils/assets/config/default.kdl @@ -242,6 +242,7 @@ plugins { configuration location="zellij:configuration" plugin-manager location="zellij:plugin-manager" about location="zellij:about" + mobile location="zellij:mobile" } // Plugins to load in the background when a new session starts diff --git a/zellij-utils/assets/plugins/mobile.wasm b/zellij-utils/assets/plugins/mobile.wasm Binary files differnew file mode 100755 index 00000000..d31d600e --- /dev/null +++ b/zellij-utils/assets/plugins/mobile.wasm diff --git a/zellij-utils/assets/prost/api.action.rs b/zellij-utils/assets/prost/api.action.rs index 17bdc9c0..8d331b60 100644 --- a/zellij-utils/assets/prost/api.action.rs +++ b/zellij-utils/assets/prost/api.action.rs @@ -793,6 +793,10 @@ pub struct MouseEventPayload { pub line: i64, #[prost(int64, tag="11")] pub column: i64, + #[prost(bool, tag="12")] + pub wheel_left: bool, + #[prost(bool, tag="13")] + pub wheel_right: bool, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] diff --git a/zellij-utils/assets/prost/api.event.rs b/zellij-utils/assets/prost/api.event.rs index 38976384..eadb9394 100644 --- a/zellij-utils/assets/prost/api.event.rs +++ b/zellij-utils/assets/prost/api.event.rs @@ -9,7 +9,7 @@ pub struct EventNameList { pub struct Event { #[prost(enumeration="EventType", tag="1")] pub name: i32, - #[prost(oneof="event::Payload", tags="2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40")] + #[prost(oneof="event::Payload", tags="2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41")] pub payload: ::core::option::Option<event::Payload>, } /// Nested message and enum types in `Event`. @@ -95,6 +95,8 @@ pub mod event { CommandChangedPayload(super::CommandChangedPayload), #[prost(message, tag="40")] HostTerminalThemeChangedPayload(super::HostTerminalThemeChangedPayload), + #[prost(message, tag="41")] + SoftKeyboardVisibilityChangedPayload(super::SoftKeyboardVisibilityChangedPayload), } } #[allow(clippy::derive_partial_eq_without_eq)] @@ -105,6 +107,12 @@ pub struct HostTerminalThemeChangedPayload { } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] +pub struct SoftKeyboardVisibilityChangedPayload { + #[prost(bool, tag="1")] + pub visible: bool, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] pub struct CwdChangedPayload { #[prost(message, optional, tag="1")] pub pane_id: ::core::option::Option<PaneId>, @@ -703,6 +711,8 @@ pub struct PaneContents { pub lines_above_viewport: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, #[prost(string, repeated, tag="4")] pub lines_below_viewport: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, + #[prost(message, optional, tag="5")] + pub cursor: ::core::option::Option<super::action::Position>, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] @@ -839,6 +849,7 @@ pub enum EventType { InitialKeybinds = 44, CommandChanged = 45, HostTerminalThemeChanged = 46, + SoftKeyboardVisibilityChanged = 47, } impl EventType { /// String value of the enum field names used in the ProtoBuf definition. @@ -893,6 +904,7 @@ impl EventType { EventType::InitialKeybinds => "InitialKeybinds", EventType::CommandChanged => "CommandChanged", EventType::HostTerminalThemeChanged => "HostTerminalThemeChanged", + EventType::SoftKeyboardVisibilityChanged => "SoftKeyboardVisibilityChanged", } } /// Creates an enum from field names used in the ProtoBuf definition. @@ -944,6 +956,7 @@ impl EventType { "InitialKeybinds" => Some(Self::InitialKeybinds), "CommandChanged" => Some(Self::CommandChanged), "HostTerminalThemeChanged" => Some(Self::HostTerminalThemeChanged), + "SoftKeyboardVisibilityChanged" => Some(Self::SoftKeyboardVisibilityChanged), _ => None, } } @@ -1069,6 +1082,8 @@ pub enum MouseEventName { MouseHold = 4, MouseRelease = 5, MouseHover = 6, + MouseScrollLeft = 7, + MouseScrollRight = 8, } impl MouseEventName { /// String value of the enum field names used in the ProtoBuf definition. @@ -1084,6 +1099,8 @@ impl MouseEventName { MouseEventName::MouseHold => "MouseHold", MouseEventName::MouseRelease => "MouseRelease", MouseEventName::MouseHover => "MouseHover", + MouseEventName::MouseScrollLeft => "MouseScrollLeft", + MouseEventName::MouseScrollRight => "MouseScrollRight", } } /// Creates an enum from field names used in the ProtoBuf definition. @@ -1096,6 +1113,8 @@ impl MouseEventName { "MouseHold" => Some(Self::MouseHold), "MouseRelease" => Some(Self::MouseRelease), "MouseHover" => Some(Self::MouseHover), + "MouseScrollLeft" => Some(Self::MouseScrollLeft), + "MouseScrollRight" => Some(Self::MouseScrollRight), _ => None, } } diff --git a/zellij-utils/assets/prost/api.plugin_command.rs b/zellij-utils/assets/prost/api.plugin_command.rs index 1cb4e5d0..b43bd004 100644 --- a/zellij-utils/assets/prost/api.plugin_command.rs +++ b/zellij-utils/assets/prost/api.plugin_command.rs @@ -3,7 +3,7 @@ pub struct PluginCommand { #[prost(enumeration="CommandName", tag="1")] pub name: i32, - #[prost(oneof="plugin_command::Payload", tags="2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163")] + #[prost(oneof="plugin_command::Payload", tags="2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 167, 168, 169")] pub payload: ::core::option::Option<plugin_command::Payload>, } /// Nested message and enum types in `PluginCommand`. @@ -309,6 +309,16 @@ pub mod plugin_command { KillSessionsAndReplyPayload(super::KillSessionsPayload), #[prost(string, tag="163")] DeleteDeadSessionAndReplyPayload(::prost::alloc::string::String), + #[prost(message, tag="164")] + SetSoftKeyboardPayload(super::SetSoftKeyboardPayload), + #[prost(message, tag="165")] + SetTabFitPayload(super::SetTabFitPayload), + #[prost(message, tag="167")] + SetShadowFocusPayload(super::PaneId), + #[prost(message, tag="168")] + NewTabUnfocusedPayload(super::NewTabUnfocusedPayload), + #[prost(message, tag="169")] + NewTiledPaneInTabPayload(super::NewTiledPaneInTabPayload), } } #[allow(clippy::derive_partial_eq_without_eq)] @@ -321,6 +331,20 @@ pub struct NewTabPayload { } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] +pub struct NewTabUnfocusedPayload { + #[prost(string, optional, tag="1")] + pub name: ::core::option::Option<::prost::alloc::string::String>, + #[prost(string, optional, tag="2")] + pub cwd: ::core::option::Option<::prost::alloc::string::String>, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct NewTiledPaneInTabPayload { + #[prost(uint32, tag="1")] + pub tab_position: u32, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] pub struct DumpSessionLayoutPayload { #[prost(uint32, optional, tag="1")] pub tab_index: ::core::option::Option<u32>, @@ -1483,6 +1507,29 @@ pub mod new_tab_response { } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] +pub struct NewTabUnfocusedResponse { + #[prost(oneof="new_tab_unfocused_response::Result", tags="1, 2")] + pub result: ::core::option::Option<new_tab_unfocused_response::Result>, +} +/// Nested message and enum types in `NewTabUnfocusedResponse`. +pub mod new_tab_unfocused_response { + #[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Oneof)] + pub enum Result { + #[prost(uint64, tag="1")] + TabId(u64), + #[prost(bool, tag="2")] + None(bool), + } +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct NewTiledPaneInTabResponse { + #[prost(message, optional, tag="1")] + pub pane_id: ::core::option::Option<PaneId>, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] pub struct NewTabsResponse { /// Only includes successfully created tabs #[prost(uint64, repeated, tag="1")] @@ -1955,6 +2002,30 @@ pub struct DeleteAllDeadSessionsResponse { #[prost(string, optional, tag="1")] pub error: ::core::option::Option<::prost::alloc::string::String>, } +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct SetSoftKeyboardPayload { + #[prost(bool, tag="1")] + pub on: bool, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Size { + #[prost(uint32, tag="1")] + pub rows: u32, + #[prost(uint32, tag="2")] + pub cols: u32, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct SetTabFitPayload { + #[prost(uint32, tag="1")] + pub tab_id: u32, + #[prost(message, optional, tag="2")] + pub pane_id: ::core::option::Option<PaneId>, + #[prost(message, optional, tag="3")] + pub size: ::core::option::Option<Size>, +} #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] #[repr(i32)] pub enum CommandName { @@ -2152,6 +2223,12 @@ pub enum CommandName { KillSessionsAndReply = 212, DeleteDeadSessionAndReply = 213, DeleteAllDeadSessionsAndReply = 214, + SetSoftKeyboard = 215, + SetTabFit = 216, + SetShadowFocus = 219, + NewTabUnfocused = 220, + NewTiledPaneInTab = 221, + ExitMobileMode = 222, } impl CommandName { /// String value of the enum field names used in the ProtoBuf definition. @@ -2354,6 +2431,12 @@ impl CommandName { CommandName::KillSessionsAndReply => "KillSessionsAndReply", CommandName::DeleteDeadSessionAndReply => "DeleteDeadSessionAndReply", CommandName::DeleteAllDeadSessionsAndReply => "DeleteAllDeadSessionsAndReply", + CommandName::SetSoftKeyboard => "SetSoftKeyboard", + CommandName::SetTabFit => "SetTabFit", + CommandName::SetShadowFocus => "SetShadowFocus", + CommandName::NewTabUnfocused => "NewTabUnfocused", + CommandName::NewTiledPaneInTab => "NewTiledPaneInTab", + CommandName::ExitMobileMode => "ExitMobileMode", } } /// Creates an enum from field names used in the ProtoBuf definition. @@ -2553,6 +2636,12 @@ impl CommandName { "KillSessionsAndReply" => Some(Self::KillSessionsAndReply), "DeleteDeadSessionAndReply" => Some(Self::DeleteDeadSessionAndReply), "DeleteAllDeadSessionsAndReply" => Some(Self::DeleteAllDeadSessionsAndReply), + "SetSoftKeyboard" => Some(Self::SetSoftKeyboard), + "SetTabFit" => Some(Self::SetTabFit), + "SetShadowFocus" => Some(Self::SetShadowFocus), + "NewTabUnfocused" => Some(Self::NewTabUnfocused), + "NewTiledPaneInTab" => Some(Self::NewTiledPaneInTab), + "ExitMobileMode" => Some(Self::ExitMobileMode), _ => None, } } diff --git a/zellij-utils/assets/prost_ipc/client_server_contract.rs b/zellij-utils/assets/prost_ipc/client_server_contract.rs index 142609f7..4ef94968 100644 --- a/zellij-utils/assets/prost_ipc/client_server_contract.rs +++ b/zellij-utils/assets/prost_ipc/client_server_contract.rs @@ -119,7 +119,7 @@ pub struct RgbColor { #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct Action { - #[prost(oneof="action::ActionType", tags="1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139")] + #[prost(oneof="action::ActionType", tags="1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140")] pub action_type: ::core::option::Option<action::ActionType>, } /// Nested message and enum types in `Action`. @@ -407,6 +407,8 @@ pub mod action { SetLightTheme(super::SetLightThemeAction), #[prost(message, tag="139")] ToggleTheme(super::ToggleThemeAction), + #[prost(message, tag="140")] + ToggleMobileMode(super::ToggleMobileModeAction), } } // Action message definitions (all 92 variants) @@ -586,6 +588,10 @@ pub struct ToggleMouseModeAction { } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] +pub struct ToggleMobileModeAction { +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] pub struct PreviousSwapLayoutAction { } #[allow(clippy::derive_partial_eq_without_eq)] @@ -1706,6 +1712,10 @@ pub struct MouseEvent { pub ctrl: bool, #[prost(message, optional, tag="10")] pub position: ::core::option::Option<Position>, + #[prost(bool, tag="11")] + pub wheel_left: bool, + #[prost(bool, tag="12")] + pub wheel_right: bool, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] @@ -1949,6 +1959,12 @@ pub struct Options { pub theme_dark: ::core::option::Option<::prost::alloc::string::String>, #[prost(string, optional, tag="47")] pub theme_light: ::core::option::Option<::prost::alloc::string::String>, + #[prost(enumeration="MobileLayout", optional, tag="48")] + pub mobile_layout: ::core::option::Option<i32>, + #[prost(uint32, optional, tag="49")] + pub mobile_threshold_cols: ::core::option::Option<u32>, + #[prost(uint32, optional, tag="50")] + pub mobile_threshold_rows: ::core::option::Option<u32>, } /// Pane-targeting action messages #[allow(clippy::derive_partial_eq_without_eq)] @@ -2876,10 +2892,42 @@ impl WebSharing { } } } +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] +#[repr(i32)] +pub enum MobileLayout { + Unspecified = 0, + Web = 1, + Always = 2, + Never = 3, +} +impl MobileLayout { + /// String value of the enum field names used in the ProtoBuf definition. + /// + /// The values are not transformed in any way and thus are considered stable + /// (if the ProtoBuf definition does not change) and safe for programmatic use. + pub fn as_str_name(&self) -> &'static str { + match self { + MobileLayout::Unspecified => "MOBILE_LAYOUT_UNSPECIFIED", + MobileLayout::Web => "MOBILE_LAYOUT_WEB", + MobileLayout::Always => "MOBILE_LAYOUT_ALWAYS", + MobileLayout::Never => "MOBILE_LAYOUT_NEVER", + } + } + /// Creates an enum from field names used in the ProtoBuf definition. + pub fn from_str_name(value: &str) -> ::core::option::Option<Self> { + match value { + "MOBILE_LAYOUT_UNSPECIFIED" => Some(Self::Unspecified), + "MOBILE_LAYOUT_WEB" => Some(Self::Web), + "MOBILE_LAYOUT_ALWAYS" => Some(Self::Always), + "MOBILE_LAYOUT_NEVER" => Some(Self::Never), + _ => None, + } + } +} #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct ClientToServerMsg { - #[prost(oneof="client_to_server_msg::Message", tags="1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20")] + #[prost(oneof="client_to_server_msg::Message", tags="1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21")] pub message: ::core::option::Option<client_to_server_msg::Message>, } /// Nested message and enum types in `ClientToServerMsg`. @@ -2927,6 +2975,8 @@ pub mod client_to_server_msg { ForwardedReplyFromHost(super::ForwardedReplyFromHostMsg), #[prost(message, tag="20")] HostTerminalThemeChanged(super::HostTerminalThemeChangedMsg), + #[prost(message, tag="21")] + SoftKeyboardVisibilityChanged(super::SoftKeyboardVisibilityChangedMsg), } } #[allow(clippy::derive_partial_eq_without_eq)] @@ -2964,6 +3014,8 @@ pub struct ColorRegistersMsg { pub struct TerminalResizeMsg { #[prost(message, optional, tag="1")] pub new_size: ::core::option::Option<Size>, + #[prost(enumeration="ResizeCause", tag="2")] + pub cause: i32, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] @@ -3072,6 +3124,41 @@ pub struct HostTerminalThemeChangedMsg { #[prost(enumeration="HostTerminalThemeIndication", tag="1")] pub mode: i32, } +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct SoftKeyboardVisibilityChangedMsg { + #[prost(bool, tag="1")] + pub visible: bool, +} +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] +#[repr(i32)] +pub enum ResizeCause { + Viewport = 0, + RenderingPreference = 1, + SizeSettled = 2, +} +impl ResizeCause { + /// String value of the enum field names used in the ProtoBuf definition. + /// + /// The values are not transformed in any way and thus are considered stable + /// (if the ProtoBuf definition does not change) and safe for programmatic use. + pub fn as_str_name(&self) -> &'static str { + match self { + ResizeCause::Viewport => "RESIZE_CAUSE_VIEWPORT", + ResizeCause::RenderingPreference => "RESIZE_CAUSE_RENDERING_PREFERENCE", + ResizeCause::SizeSettled => "RESIZE_CAUSE_SIZE_SETTLED", + } + } + /// Creates an enum from field names used in the ProtoBuf definition. + pub fn from_str_name(value: &str) -> ::core::option::Option<Self> { + match value { + "RESIZE_CAUSE_VIEWPORT" => Some(Self::Viewport), + "RESIZE_CAUSE_RENDERING_PREFERENCE" => Some(Self::RenderingPreference), + "RESIZE_CAUSE_SIZE_SETTLED" => Some(Self::SizeSettled), + _ => None, + } + } +} #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] #[repr(i32)] pub enum HostTerminalThemeIndication { @@ -3101,7 +3188,7 @@ impl HostTerminalThemeIndication { #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct ServerToClientMsg { - #[prost(oneof="server_to_client_msg::Message", tags="1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16")] + #[prost(oneof="server_to_client_msg::Message", tags="1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17")] pub message: ::core::option::Option<server_to_client_msg::Message>, } /// Nested message and enum types in `ServerToClientMsg`. @@ -3141,6 +3228,8 @@ pub mod server_to_client_msg { SubscribedPaneClosed(super::SubscribedPaneClosedMsg), #[prost(message, tag="16")] ForwardQueryToHost(super::ForwardQueryToHostMsg), + #[prost(message, tag="17")] + SetSoftKeyboard(super::SetSoftKeyboardMsg), } } #[allow(clippy::derive_partial_eq_without_eq)] @@ -3248,3 +3337,9 @@ pub struct ForwardQueryToHostMsg { #[prost(bytes="vec", tag="2")] pub query_bytes: ::prost::alloc::vec::Vec<u8>, } +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct SetSoftKeyboardMsg { + #[prost(bool, tag="1")] + pub on: bool, +} diff --git a/zellij-utils/src/client_server_contract/client_to_server.proto b/zellij-utils/src/client_server_contract/client_to_server.proto index 834feccd..bc7c0c06 100644 --- a/zellij-utils/src/client_server_contract/client_to_server.proto +++ b/zellij-utils/src/client_server_contract/client_to_server.proto @@ -25,6 +25,7 @@ message ClientToServerMsg { DesktopNotificationResponseMsg desktop_notification_response = 18; ForwardedReplyFromHostMsg forwarded_reply_from_host = 19; HostTerminalThemeChangedMsg host_terminal_theme_changed = 20; + SoftKeyboardVisibilityChangedMsg soft_keyboard_visibility_changed = 21; } } @@ -50,6 +51,13 @@ message ColorRegistersMsg { message TerminalResizeMsg { Size new_size = 1; + ResizeCause cause = 2; +} + +enum ResizeCause { + RESIZE_CAUSE_VIEWPORT = 0; + RESIZE_CAUSE_RENDERING_PREFERENCE = 1; + RESIZE_CAUSE_SIZE_SETTLED = 2; } message FirstClientConnectedMsg { @@ -125,3 +133,7 @@ enum HostTerminalThemeIndication { HOST_TERMINAL_THEME_INDICATION_DARK = 0; HOST_TERMINAL_THEME_INDICATION_LIGHT = 1; } + +message SoftKeyboardVisibilityChangedMsg { + bool visible = 1; +} diff --git a/zellij-utils/src/client_server_contract/common_types.proto b/zellij-utils/src/client_server_contract/common_types.proto index b592eb8d..1dfc3994 100644 --- a/zellij-utils/src/client_server_contract/common_types.proto +++ b/zellij-utils/src/client_server_contract/common_types.proto @@ -277,6 +277,7 @@ message Action { SetDarkThemeAction set_dark_theme = 137; SetLightThemeAction set_light_theme = 138; ToggleThemeAction toggle_theme = 139; + ToggleMobileModeAction toggle_mobile_mode = 140; } } @@ -332,6 +333,7 @@ message CopyAction {} message ConfirmAction {} message DenyAction {} message ToggleMouseModeAction {} +message ToggleMobileModeAction {} message PreviousSwapLayoutAction {} message NextSwapLayoutAction {} message TabLayoutInfo { @@ -1012,6 +1014,8 @@ message MouseEvent { bool alt = 8; bool ctrl = 9; Position position = 10; + bool wheel_left = 11; + bool wheel_right = 12; } enum MouseEventType { @@ -1174,6 +1178,9 @@ message Options { optional bool mouse_click_through = 45; optional string theme_dark = 46; optional string theme_light = 47; + optional MobileLayout mobile_layout = 48; + optional uint32 mobile_threshold_cols = 49; + optional uint32 mobile_threshold_rows = 50; } enum OnForceClose { @@ -1201,6 +1208,13 @@ enum WebSharing { WEB_SHARING_DISABLED = 3; } +enum MobileLayout { + MOBILE_LAYOUT_UNSPECIFIED = 0; + MOBILE_LAYOUT_WEB = 1; + MOBILE_LAYOUT_ALWAYS = 2; + MOBILE_LAYOUT_NEVER = 3; +} + // Pane-targeting action messages message ScrollUpByPaneIdAction { PaneId pane_id = 1; } message ScrollDownByPaneIdAction { PaneId pane_id = 1; } diff --git a/zellij-utils/src/client_server_contract/server_to_client.proto b/zellij-utils/src/client_server_contract/server_to_client.proto index b2fc3bd6..a8c4779d 100644 --- a/zellij-utils/src/client_server_contract/server_to_client.proto +++ b/zellij-utils/src/client_server_contract/server_to_client.proto @@ -21,6 +21,7 @@ message ServerToClientMsg { PaneRenderUpdateMsg pane_render_update = 14; SubscribedPaneClosedMsg subscribed_pane_closed = 15; ForwardQueryToHostMsg forward_query_to_host = 16; + SetSoftKeyboardMsg set_soft_keyboard = 17; } } @@ -94,3 +95,7 @@ message ForwardQueryToHostMsg { uint32 token = 1; bytes query_bytes = 2; } + +message SetSoftKeyboardMsg { + bool on = 1; +} diff --git a/zellij-utils/src/consts.rs b/zellij-utils/src/consts.rs index 0e8b5112..2b3982b0 100644 --- a/zellij-utils/src/consts.rs +++ b/zellij-utils/src/consts.rs @@ -171,16 +171,12 @@ mod not_wasm { add_plugin!(assets, "multiple-select.wasm"); add_plugin!(assets, "layout-manager.wasm"); add_plugin!(assets, "link.wasm"); + add_plugin!(assets, "mobile.wasm"); assets }; } } -/// Check if a filesystem entry is an IPC socket. -/// -/// On Unix, this checks `FileTypeExt::is_socket()`. On non-Unix platforms, -/// this checks `is_file()` to detect marker files created by `ipc_bind()` -/// and `ipc_bind_async()` alongside kernel-level named pipes. #[cfg(unix)] pub fn is_ipc_socket(file_type: &std::fs::FileType) -> bool { use std::os::unix::fs::FileTypeExt; @@ -192,10 +188,6 @@ pub fn is_ipc_socket(file_type: &std::fs::FileType) -> bool { file_type.is_file() } -/// Connect to an IPC socket at the given path. -/// -/// On Unix, this uses Unix domain sockets via `GenericFilePath`. -/// On Windows, this uses named pipes via `GenericNamespaced`. #[cfg(unix)] pub fn ipc_connect(path: &std::path::Path) -> std::io::Result<interprocess::local_socket::Stream> { use interprocess::local_socket::{prelude::*, GenericFilePath, Stream as LocalSocketStream}; @@ -211,11 +203,6 @@ pub fn ipc_connect(path: &std::path::Path) -> std::io::Result<interprocess::loca LocalSocketStream::connect(ns_name) } -/// Create an IPC listener bound to the given path. -/// -/// On Unix, this uses Unix domain sockets via `GenericFilePath`. -/// On Windows, this uses named pipes via `GenericNamespaced` and creates -/// a marker file for session discovery. #[cfg(unix)] pub fn ipc_bind(path: &std::path::Path) -> std::io::Result<interprocess::local_socket::Listener> { use interprocess::local_socket::{prelude::*, GenericFilePath, ListenerOptions}; @@ -233,11 +220,6 @@ pub fn ipc_bind(path: &std::path::Path) -> std::io::Result<interprocess::local_s Ok(listener) } -/// Create an async (tokio) IPC listener bound to the given path. -/// -/// On Unix, this uses Unix domain sockets via `GenericFilePath`. -/// On Windows, this uses named pipes via `GenericNamespaced` and creates -/// a marker file for session discovery. #[cfg(unix)] pub fn ipc_bind_async( path: &std::path::Path, @@ -259,9 +241,6 @@ pub fn ipc_bind_async( Ok(listener) } -/// Connect to the reply pipe for a given IPC path (Windows only). -/// -/// Uses `path-reply` as the named pipe for the server→client direction. #[cfg(windows)] pub fn ipc_connect_reply( path: &std::path::Path, @@ -272,9 +251,6 @@ pub fn ipc_connect_reply( LocalSocketStream::connect(ns_name) } -/// Create an IPC listener for the reply pipe (Windows only). -/// -/// Binds to `path-reply` as the named pipe for the server→client direction. #[cfg(windows)] pub fn ipc_bind_reply( path: &std::path::Path, @@ -297,12 +273,7 @@ mod unix_only { use nix::unistd::Uid; use std::env::temp_dir; - // Maximum length of a Unix domain socket path (from sockaddr_un.sun_path). - // macOS (and other BSDs) use 104, Linux/Android/Solaris use 108. - // The not(target_os = "macos") fallback of 108 is used for all other Unix - // platforms — this is correct for Linux/Android/Solaris and only 4 bytes - // over for BSDs, which would cause a slightly late error rather than a - // missed one. + // Maximum sockaddr_un.sun_path length: 104 on macOS/BSD, 108 on Linux/Android/Solaris. #[cfg(target_os = "macos")] pub const ZELLIJ_SOCK_MAX_LENGTH: usize = 104; #[cfg(not(target_os = "macos"))] diff --git a/zellij-utils/src/data.rs b/zellij-utils/src/data.rs index 6f10a054..16ff560e 100644 --- a/zellij-utils/src/data.rs +++ b/zellij-utils/src/data.rs @@ -5,7 +5,7 @@ use crate::input::keybinds::Keybinds; use crate::input::layout::{ Layout, PercentOrFixed, Run, RunPlugin, RunPluginLocation, RunPluginOrAlias, }; -use crate::pane_size::PaneGeom; +use crate::pane_size::{PaneGeom, Size}; use crate::position::Position; use crate::shared::{colors as default_colors, eightbit_to_rgb}; use clap::ArgEnum; @@ -894,8 +894,10 @@ impl fmt::Display for ResizeStrategy { // left click) and the `ScrollUp` and `ScrollDown` events could probably be // merged into a single `Scroll(isize)` event. pub enum Mouse { - ScrollUp(usize), // number of lines - ScrollDown(usize), // number of lines + ScrollUp(usize), // number of lines + ScrollDown(usize), // number of lines + ScrollLeft(usize), + ScrollRight(usize), LeftClick(isize, usize), // line and column RightClick(isize, usize), // line and column Hold(isize, usize), // line and column @@ -1028,6 +1030,7 @@ pub enum Event { InitialKeybinds(KeybindsVec), /// The host terminal indicated its color palette theme mode (CSI 2031 / DSR 997). HostTerminalThemeChanged(HostTerminalThemeMode), + SoftKeyboardVisibilityChanged(bool), } #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] @@ -2432,6 +2435,7 @@ pub struct PaneContents { pub lines_below_viewport: Vec<String>, pub viewport: Vec<String>, pub selected_text: Option<SelectedText>, + pub cursor: Option<(usize, usize)>, } /// Extract text from a line between two column positions, accounting for wide characters @@ -2526,6 +2530,7 @@ impl PaneContents { selected_text: SelectedText::from_positions(selection_start, selection_end), lines_above_viewport, lines_below_viewport, + cursor: None, } } @@ -3366,6 +3371,13 @@ pub enum PluginCommand { name: Option<String>, cwd: Option<String>, }, + NewTabUnfocused { + name: Option<String>, + cwd: Option<String>, + }, + NewTiledPaneInTab { + tab_position: usize, + }, GoToNextTab, GoToPreviousTab, Resize(Resize), @@ -3610,6 +3622,13 @@ pub enum PluginCommand { KillSessionsAndReply(Vec<String>), // one or more session names; sends a response back DeleteDeadSessionAndReply(String), // session name; sends a response back DeleteAllDeadSessionsAndReply, // no payload; sends a response back + SetSoftKeyboard(bool), + SetTabFit { + tab_id: usize, + fit: Option<(PaneId, Size)>, + }, + SetShadowFocus(PaneId), + ExitMobileMode, } // Response type for plugin API methods that open a pane in a new tab @@ -3621,6 +3640,7 @@ pub struct OpenPaneInNewTabResponse { // Response types for plugin API methods that create tabs pub type NewTabResponse = Option<usize>; +pub type NewTabUnfocusedResponse = Option<usize>; pub type NewTabsResponse = Vec<usize>; pub type FocusOrCreateTabResponse = Option<usize>; pub type BreakPanesToNewTabResponse = Option<usize>; @@ -3641,6 +3661,7 @@ pub type OpenTerminalInPlaceResponse = Option<PaneId>; pub type OpenTerminalNearPluginResponse = Option<PaneId>; pub type OpenTerminalFloatingNearPluginResponse = Option<PaneId>; pub type OpenTerminalInPlaceOfPluginResponse = Option<PaneId>; +pub type NewTiledPaneInTabResponse = Option<PaneId>; pub type OpenCommandPaneResponse = Option<PaneId>; pub type OpenCommandPaneFloatingResponse = Option<PaneId>; diff --git a/zellij-utils/src/errors.rs b/zellij-utils/src/errors.rs index 6fa15141..8e4bc66c 100644 --- a/zellij-utils/src/errors.rs +++ b/zellij-utils/src/errors.rs @@ -331,6 +331,9 @@ pub enum ScreenContext { ToggleTab, AddClient, RemoveClient, + SuppressRenderUntilMobile, + MobileSizeSettled, + ForceMobileUngate, UpdateSearch, SearchDown, SearchUp, @@ -399,6 +402,7 @@ pub enum ScreenContext { PageScrollUpInPaneId, PageScrollDownInPaneId, TogglePaneIdFullscreen, + SetTabFit, TogglePaneEmbedOrEjectForPaneId, CloseTabWithIndex, BreakPanesToNewTab, @@ -462,6 +466,12 @@ pub enum ScreenContext { PluginSubscribedToAnsiPaneContents, UpdateBackgroundPluginSubscriptions, BroadcastModeUpdate, + EnterMobileMode, + ExitMobileMode, + ToggleMobileMode, + ReevaluateMobileMode, + SetSoftKeyboard, + SetShadowFocus, } /// Stack call representations corresponding to the different types of [`PtyInstruction`]s. @@ -516,6 +526,8 @@ pub enum PluginContext { NewTab, OverrideLayout, ApplyCachedEvents, + HoldMobileRender, + ReleaseMobileRender, ApplyCachedWorkerMessages, PostMessageToPluginWorker, PostMessageToPlugin, @@ -637,6 +649,7 @@ pub enum BackgroundJobContext { StopFlashPaneBell, FlashTabBell, StopFlashTabBell, + MobileGateTimeout, Exit, } diff --git a/zellij-utils/src/input/actions.rs b/zellij-utils/src/input/actions.rs index 7f6fabfe..cf5fc448 100644 --- a/zellij-utils/src/input/actions.rs +++ b/zellij-utils/src/input/actions.rs @@ -412,6 +412,7 @@ pub enum Action { option: SearchOption, }, ToggleMouseMode, + ToggleMobileMode, PreviousSwapLayout, NextSwapLayout, /// Override the layout of the active tab diff --git a/zellij-utils/src/input/mod.rs b/zellij-utils/src/input/mod.rs index 74091850..c103e580 100644 --- a/zellij-utils/src/input/mod.rs +++ b/zellij-utils/src/input/mod.rs @@ -163,112 +163,149 @@ mod not_wasm { let alt = modifiers.contains(KeyModifiers::ALT); let ctrl = modifiers.contains(KeyModifiers::CONTROL); - let (event_type, left, right, middle, wheel_up, wheel_down) = match event.kind { - MouseEventKind::Down(CButton::Left) => ( - mouse::MouseEventType::Press, - true, - false, - false, - false, - false, - ), - MouseEventKind::Down(CButton::Right) => ( - mouse::MouseEventType::Press, - false, - true, - false, - false, - false, - ), - MouseEventKind::Down(CButton::Middle) => ( - mouse::MouseEventType::Press, - false, - false, - true, - false, - false, - ), - MouseEventKind::Up(CButton::Left) => ( - mouse::MouseEventType::Release, - true, - false, - false, - false, - false, - ), - MouseEventKind::Up(CButton::Right) => ( - mouse::MouseEventType::Release, - false, - true, - false, - false, - false, - ), - MouseEventKind::Up(CButton::Middle) => ( - mouse::MouseEventType::Release, - false, - false, - true, - false, - false, - ), - MouseEventKind::Drag(CButton::Left) => ( - mouse::MouseEventType::Motion, - true, - false, - false, - false, - false, - ), - MouseEventKind::Drag(CButton::Right) => ( - mouse::MouseEventType::Motion, - false, - true, - false, - false, - false, - ), - MouseEventKind::Drag(CButton::Middle) => ( - mouse::MouseEventType::Motion, - false, - false, - true, - false, - false, - ), - MouseEventKind::Moved => ( - mouse::MouseEventType::Motion, - false, - false, - false, - false, - false, - ), - MouseEventKind::ScrollUp => ( - mouse::MouseEventType::Press, - false, - false, - false, - true, - false, - ), - MouseEventKind::ScrollDown => ( - mouse::MouseEventType::Press, - false, - false, - false, - false, - true, - ), - MouseEventKind::ScrollLeft | MouseEventKind::ScrollRight => ( - mouse::MouseEventType::Motion, - false, - false, - false, - false, - false, - ), - }; + let (event_type, left, right, middle, wheel_up, wheel_down, wheel_left, wheel_right) = + match event.kind { + MouseEventKind::Down(CButton::Left) => ( + mouse::MouseEventType::Press, + true, + false, + false, + false, + false, + false, + false, + ), + MouseEventKind::Down(CButton::Right) => ( + mouse::MouseEventType::Press, + false, + true, + false, + false, + false, + false, + false, + ), + MouseEventKind::Down(CButton::Middle) => ( + mouse::MouseEventType::Press, + false, + false, + true, + false, + false, + false, + false, + ), + MouseEventKind::Up(CButton::Left) => ( + mouse::MouseEventType::Release, + true, + false, + false, + false, + false, + false, + false, + ), + MouseEventKind::Up(CButton::Right) => ( + mouse::MouseEventType::Release, + false, + true, + false, + false, + false, + false, + false, + ), + MouseEventKind::Up(CButton::Middle) => ( + mouse::MouseEventType::Release, + false, + false, + true, + false, + false, + false, + false, + ), + MouseEventKind::Drag(CButton::Left) => ( + mouse::MouseEventType::Motion, + true, + false, + false, + false, + false, + false, + false, + ), + MouseEventKind::Drag(CButton::Right) => ( + mouse::MouseEventType::Motion, + false, + true, + false, + false, + false, + false, + false, + ), + MouseEventKind::Drag(CButton::Middle) => ( + mouse::MouseEventType::Motion, + false, + false, + true, + false, + false, + false, + false, + ), + MouseEventKind::Moved => ( + mouse::MouseEventType::Motion, + false, + false, + false, + false, + false, + false, + false, + ), + MouseEventKind::ScrollUp => ( + mouse::MouseEventType::Press, + false, + false, + false, + true, + false, + false, + false, + ), + MouseEventKind::ScrollDown => ( + mouse::MouseEventType::Press, + false, + false, + false, + false, + true, + false, + false, + ), + MouseEventKind::ScrollLeft => ( + mouse::MouseEventType::Press, + false, + false, + false, + false, + false, + true, + false, + ), + MouseEventKind::ScrollRight => ( + mouse::MouseEventType::Press, + false, + false, + false, + false, + false, + false, + true, + ), + }; mouse::MouseEvent { event_type, @@ -277,6 +314,8 @@ mod not_wasm { middle, wheel_up, wheel_down, + wheel_left, + wheel_right, shift, alt, ctrl, diff --git a/zellij-utils/src/input/mouse.rs b/zellij-utils/src/input/mouse.rs index a8fcb5a7..231dc589 100644 --- a/zellij-utils/src/input/mouse.rs +++ b/zellij-utils/src/input/mouse.rs @@ -16,6 +16,10 @@ pub struct MouseEvent { pub middle: bool, pub wheel_up: bool, pub wheel_down: bool, + #[serde(default)] + pub wheel_left: bool, + #[serde(default)] + pub wheel_right: bool, // Keyboard modifier flags can be encoded with events too. They // are not often passed on the wire (instead used for @@ -51,6 +55,8 @@ impl MouseEvent { middle: false, wheel_up: false, wheel_down: false, + wheel_left: false, + wheel_right: false, shift: false, alt: false, ctrl: false, @@ -66,6 +72,8 @@ impl MouseEvent { middle: false, wheel_up: false, wheel_down: false, + wheel_left: false, + wheel_right: false, shift: false, alt: false, ctrl: false, @@ -81,6 +89,8 @@ impl MouseEvent { middle: false, wheel_up: false, wheel_down: false, + wheel_left: false, + wheel_right: false, shift: false, alt: false, ctrl: false, @@ -96,6 +106,8 @@ impl MouseEvent { middle: false, wheel_up: false, wheel_down: false, + wheel_left: false, + wheel_right: false, shift: false, alt: false, ctrl: false, @@ -111,6 +123,8 @@ impl MouseEvent { middle: true, wheel_up: false, wheel_down: false, + wheel_left: false, + wheel_right: false, shift: false, alt: false, ctrl: false, @@ -126,6 +140,8 @@ impl MouseEvent { middle: true, wheel_up: false, wheel_down: false, + wheel_left: false, + wheel_right: false, shift: false, alt: false, ctrl: false, @@ -141,6 +157,8 @@ impl MouseEvent { middle: false, wheel_up: false, wheel_down: false, + wheel_left: false, + wheel_right: false, shift: false, alt: false, ctrl: false, @@ -156,6 +174,8 @@ impl MouseEvent { middle: false, wheel_up: false, wheel_down: false, + wheel_left: false, + wheel_right: false, shift: false, alt: false, ctrl: false, @@ -171,6 +191,8 @@ impl MouseEvent { middle: false, wheel_up: false, wheel_down: false, + wheel_left: false, + wheel_right: false, shift: false, alt: false, ctrl: false, @@ -186,6 +208,8 @@ impl MouseEvent { middle: false, wheel_up: false, wheel_down: false, + wheel_left: false, + wheel_right: false, shift: false, alt: false, ctrl: false, @@ -201,6 +225,8 @@ impl MouseEvent { middle: true, wheel_up: false, wheel_down: false, + wheel_left: false, + wheel_right: false, shift: false, alt: false, ctrl: false, @@ -216,6 +242,8 @@ impl MouseEvent { middle: false, wheel_up: false, wheel_down: false, + wheel_left: false, + wheel_right: false, shift: false, alt: true, ctrl: false, @@ -231,6 +259,8 @@ impl MouseEvent { middle: false, wheel_up: false, wheel_down: false, + wheel_left: false, + wheel_right: false, shift: false, alt: true, ctrl: false, @@ -246,6 +276,8 @@ impl MouseEvent { middle: false, wheel_up: false, wheel_down: false, + wheel_left: false, + wheel_right: false, shift: false, alt: false, ctrl: true, @@ -261,6 +293,8 @@ impl MouseEvent { middle: false, wheel_up: false, wheel_down: false, + wheel_left: false, + wheel_right: false, shift: false, alt: false, ctrl: true, @@ -276,6 +310,8 @@ impl MouseEvent { middle: false, wheel_up: false, wheel_down: false, + wheel_left: false, + wheel_right: false, shift: false, alt: false, ctrl: true, @@ -291,6 +327,8 @@ impl MouseEvent { middle: false, wheel_up: false, wheel_down: false, + wheel_left: false, + wheel_right: false, shift: false, alt: true, ctrl: false, @@ -306,6 +344,8 @@ impl MouseEvent { middle: false, wheel_up: true, wheel_down: false, + wheel_left: false, + wheel_right: false, shift: false, alt: false, ctrl: false, @@ -321,6 +361,8 @@ impl MouseEvent { middle: false, wheel_up: false, wheel_down: true, + wheel_left: false, + wheel_right: false, shift: false, alt: false, ctrl: false, @@ -336,6 +378,8 @@ impl MouseEvent { middle: false, wheel_up: true, wheel_down: false, + wheel_left: false, + wheel_right: false, shift: false, alt: false, ctrl: true, @@ -351,6 +395,8 @@ impl MouseEvent { middle: false, wheel_up: false, wheel_down: true, + wheel_left: false, + wheel_right: false, shift: false, alt: false, ctrl: true, @@ -358,6 +404,38 @@ impl MouseEvent { }; event } + pub fn new_scroll_left_event(position: Position) -> Self { + MouseEvent { + event_type: MouseEventType::Press, + left: false, + right: false, + middle: false, + wheel_up: false, + wheel_down: false, + wheel_left: true, + wheel_right: false, + shift: false, + alt: false, + ctrl: false, + position, + } + } + pub fn new_scroll_right_event(position: Position) -> Self { + MouseEvent { + event_type: MouseEventType::Press, + left: false, + right: false, + middle: false, + wheel_up: false, + wheel_down: false, + wheel_left: false, + wheel_right: true, + shift: false, + alt: false, + ctrl: false, + position, + } + } } impl Default for MouseEvent { diff --git a/zellij-utils/src/input/options.rs b/zellij-utils/src/input/options.rs index 854ef271..8f91cb47 100644 --- a/zellij-utils/src/input/options.rs +++ b/zellij-utils/src/input/options.rs @@ -16,6 +16,65 @@ pub enum OnForceClose { Detach, } +#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize, Serialize, ArgEnum)] +pub enum MobileLayoutConfiguration { + #[serde(alias = "web")] + Web, + #[serde(alias = "always")] + Always, + #[serde(alias = "never")] + Never, +} + +impl Default for MobileLayoutConfiguration { + fn default() -> Self { + Self::Web + } +} + +impl FromStr for MobileLayoutConfiguration { + type Err = String; + fn from_str(s: &str) -> Result<Self, Self::Err> { + match s { + "Web" | "web" => Ok(Self::Web), + "Always" | "always" => Ok(Self::Always), + "Never" | "never" => Ok(Self::Never), + _ => Err(format!("No such mobile_layout: {}", s)), + } + } +} + +impl MobileLayoutConfiguration { + pub fn should_route_to_mobile( + self, + is_web_client: bool, + viewport_cols: usize, + viewport_rows: usize, + threshold_cols: u16, + threshold_rows: u16, + ) -> bool { + let cols_reported = viewport_cols > 0; + let rows_reported = viewport_rows > 0; + let cols_match = + cols_reported && (threshold_cols == 0 || viewport_cols <= threshold_cols as usize); + let rows_match = + rows_reported && (threshold_rows == 0 || viewport_rows <= threshold_rows as usize); + let size_match = cols_match || rows_match; + match self { + MobileLayoutConfiguration::Always => size_match, + MobileLayoutConfiguration::Never => false, + MobileLayoutConfiguration::Web => is_web_client && size_match, + } + } + + pub fn may_route_web_client_to_mobile(self) -> bool { + matches!( + self, + MobileLayoutConfiguration::Web | MobileLayoutConfiguration::Always + ) + } +} + impl Default for OnForceClose { fn default() -> Self { Self::Detach @@ -278,6 +337,21 @@ pub struct Options { /// NOTE: This only applies to web clients at the moment. #[clap(long)] pub client_async_worker_tasks: Option<usize>, + + /// When a newly-attaching client should land in the mobile UI plugin (web, always, never) + #[clap(long, arg_enum, hide_possible_values = true, value_parser)] + #[serde(default)] + pub mobile_layout: Option<MobileLayoutConfiguration>, + + /// Column breakpoint for mobile_layout (0 to always match) + #[clap(long, value_parser)] + #[serde(default)] + pub mobile_threshold_cols: Option<u16>, + + /// Row breakpoint for mobile_layout (0 to always match) + #[clap(long, value_parser)] + #[serde(default)] + pub mobile_threshold_rows: Option<u16>, } #[derive(ArgEnum, Deserialize, Serialize, Debug, Clone, Copy, PartialEq)] @@ -384,6 +458,9 @@ impl Options { let client_async_worker_tasks = other .client_async_worker_tasks .or(self.client_async_worker_tasks); + let mobile_layout = other.mobile_layout.or(self.mobile_layout); + let mobile_threshold_cols = other.mobile_threshold_cols.or(self.mobile_threshold_cols); + let mobile_threshold_rows = other.mobile_threshold_rows.or(self.mobile_threshold_rows); Options { simplified_ui, @@ -433,6 +510,9 @@ impl Options { enforce_https_for_localhost, post_command_discovery_hook, client_async_worker_tasks, + mobile_layout, + mobile_threshold_cols, + mobile_threshold_rows, } } @@ -519,6 +599,9 @@ impl Options { let client_async_worker_tasks = other .client_async_worker_tasks .or(self.client_async_worker_tasks); + let mobile_layout = other.mobile_layout.or(self.mobile_layout); + let mobile_threshold_cols = other.mobile_threshold_cols.or(self.mobile_threshold_cols); + let mobile_threshold_rows = other.mobile_threshold_rows.or(self.mobile_threshold_rows); Options { simplified_ui, @@ -568,6 +651,9 @@ impl Options { enforce_https_for_localhost, post_command_discovery_hook, client_async_worker_tasks, + mobile_layout, + mobile_threshold_cols, + mobile_threshold_rows, } } @@ -579,3 +665,206 @@ impl Options { } } } + +#[cfg(test)] +mod tests { + use super::*; + + const SMALL: (usize, usize) = (40, 20); + const WIDE_SHORT: (usize, usize) = (200, 20); + const TALL_NARROW: (usize, usize) = (40, 200); + const LARGE: (usize, usize) = (200, 200); + + fn route( + layout: MobileLayoutConfiguration, + is_web: bool, + viewport: (usize, usize), + thresholds: (u16, u16), + ) -> bool { + layout.should_route_to_mobile(is_web, viewport.0, viewport.1, thresholds.0, thresholds.1) + } + + #[test] + fn never_never_routes_to_mobile() { + for &is_web in &[true, false] { + for &viewport in &[SMALL, WIDE_SHORT, TALL_NARROW, LARGE] { + for &thresholds in &[(60, 30), (0, 0), (0, 30), (60, 0)] { + assert!( + !route(MobileLayoutConfiguration::Never, is_web, viewport, thresholds), + "Never must never route (is_web={is_web} viewport={viewport:?} thresholds={thresholds:?})", + ); + } + } + } + } + + #[test] + fn web_requires_web_client() { + assert!(route(MobileLayoutConfiguration::Web, true, SMALL, (60, 30))); + assert!(!route( + MobileLayoutConfiguration::Web, + false, + SMALL, + (60, 30) + )); + } + + #[test] + fn web_respects_size_in_either_dimension() { + assert!(route( + MobileLayoutConfiguration::Web, + true, + TALL_NARROW, + (60, 30) + )); + assert!(route( + MobileLayoutConfiguration::Web, + true, + WIDE_SHORT, + (60, 30) + )); + assert!(!route( + MobileLayoutConfiguration::Web, + true, + LARGE, + (60, 30) + )); + } + + #[test] + fn always_routes_any_client_on_size_match() { + assert!(route( + MobileLayoutConfiguration::Always, + true, + SMALL, + (60, 30) + )); + assert!(route( + MobileLayoutConfiguration::Always, + false, + SMALL, + (60, 30) + )); + assert!(!route( + MobileLayoutConfiguration::Always, + true, + LARGE, + (60, 30) + )); + assert!(!route( + MobileLayoutConfiguration::Always, + false, + LARGE, + (60, 30) + )); + } + + #[test] + fn zero_threshold_makes_dimension_unconditional() { + assert!(route( + MobileLayoutConfiguration::Always, + false, + LARGE, + (0, 30) + )); + assert!(route( + MobileLayoutConfiguration::Always, + false, + LARGE, + (60, 0) + )); + assert!(route( + MobileLayoutConfiguration::Always, + false, + LARGE, + (0, 0) + )); + assert!(route(MobileLayoutConfiguration::Web, true, LARGE, (0, 0))); + assert!(!route(MobileLayoutConfiguration::Web, false, LARGE, (0, 0))); + } + + #[test] + fn zero_viewport_is_treated_as_unknown() { + assert!(!route( + MobileLayoutConfiguration::Always, + true, + (0, 0), + (60, 30) + )); + assert!(!route( + MobileLayoutConfiguration::Web, + true, + (0, 0), + (60, 30) + )); + assert!(!route( + MobileLayoutConfiguration::Always, + true, + (0, 0), + (0, 0) + )); + assert!(route( + MobileLayoutConfiguration::Always, + true, + (40, 0), + (60, 30) + )); + assert!(route( + MobileLayoutConfiguration::Always, + true, + (0, 20), + (60, 30) + )); + } + + #[test] + fn boundary_inclusive_at_threshold() { + assert!(route( + MobileLayoutConfiguration::Always, + false, + (60, 200), + (60, 30) + )); + assert!(route( + MobileLayoutConfiguration::Always, + false, + (200, 30), + (60, 30) + )); + assert!(!route( + MobileLayoutConfiguration::Always, + false, + (61, 31), + (60, 30) + )); + } + + #[test] + fn from_str_accepts_canonical_and_lowercase() { + assert_eq!( + "web".parse::<MobileLayoutConfiguration>(), + Ok(MobileLayoutConfiguration::Web) + ); + assert_eq!( + "Web".parse::<MobileLayoutConfiguration>(), + Ok(MobileLayoutConfiguration::Web) + ); + assert_eq!( + "always".parse::<MobileLayoutConfiguration>(), + Ok(MobileLayoutConfiguration::Always) + ); + assert_eq!( + "never".parse::<MobileLayoutConfiguration>(), + Ok(MobileLayoutConfiguration::Never) + ); + assert!("auto".parse::<MobileLayoutConfiguration>().is_err()); + } + + #[test] + fn default_is_web() { + assert_eq!( + MobileLayoutConfiguration::default(), + MobileLayoutConfiguration::Web + ); + } +} diff --git a/zellij-utils/src/input/plugins.rs b/zellij-utils/src/input/plugins.rs index 31e48b43..7f9bf427 100644 --- a/zellij-utils/src/input/plugins.rs +++ b/zellij-utils/src/input/plugins.rs @@ -69,6 +69,7 @@ impl PluginConfig { || tag == "multiple-select" || tag == "layout-manager" || tag == "link" + || tag == "mobile" { Some(PluginConfig { path: PathBuf::from(&tag), diff --git a/zellij-utils/src/input/web_client.rs b/zellij-utils/src/input/web_client.rs index de09efad..9607adc0 100644 --- a/zellij-utils/src/input/web_client.rs +++ b/zellij-utils/src/input/web_client.rs @@ -239,6 +239,7 @@ pub struct WebClientConfig { pub cursor_style: Option<CursorStyle>, pub mac_option_is_meta: bool, pub base_url: Option<String>, + pub font_size: Option<u16>, } impl Default for WebClientConfig { @@ -251,6 +252,7 @@ impl Default for WebClientConfig { cursor_style: None, mac_option_is_meta: true, // TODO: yes? no? base_url: None, + font_size: None, } } } @@ -289,6 +291,24 @@ impl WebClientConfig { web_client_config.base_url = Some(base_url.to_owned()); } + if let Some(font_size_node) = kdl_get_child!(kdl, "font_size") { + let raw = font_size_node + .entries() + .iter() + .find_map(|e| e.value().as_i64()); + if let Some(value) = raw { + if value > 0 && value <= u16::MAX as i64 { + web_client_config.font_size = Some(value as u16); + } else { + return Err(ConfigError::new_kdl_error( + format!("font_size must be between 1 and {}", u16::MAX), + font_size_node.span().offset(), + font_size_node.span().len(), + )); + } + } + } + Ok(web_client_config) } @@ -338,6 +358,12 @@ impl WebClientConfig { web_client_children.nodes_mut().push(base_url_node); } + if let Some(font_size) = self.font_size { + let mut font_size_node = KdlNode::new("font_size"); + font_size_node.push(KdlValue::Base10(font_size as i64)); + web_client_children.nodes_mut().push(font_size_node); + } + web_client_node.set_children(web_client_children); web_client_node } @@ -351,6 +377,7 @@ impl WebClientConfig { merged.cursor_style = other.cursor_style; merged.mac_option_is_meta = other.mac_option_is_meta; merged.base_url = other.base_url; + merged.font_size = other.font_size; merged } } diff --git a/zellij-utils/src/ipc.rs b/zellij-utils/src/ipc.rs index a837a0af..d5edbbb4 100644 --- a/zellij-utils/src/ipc.rs +++ b/zellij-utils/src/ipc.rs @@ -81,6 +81,14 @@ pub struct ColorRegister { pub color: String, } +#[derive(Default, Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)] +pub enum ResizeCause { + #[default] + Viewport, + RenderingPreference, + SizeSettled, +} + impl PixelDimensions { pub fn merge(&mut self, other: PixelDimensions) { if let Some(text_area_size) = other.text_area_size { @@ -113,6 +121,8 @@ pub enum ClientToServerMsg { }, TerminalResize { new_size: Size, + #[serde(default)] + cause: ResizeCause, }, FirstClientConnected { cli_assets: CliAssets, @@ -156,21 +166,16 @@ pub enum ClientToServerMsg { DesktopNotificationResponse { raw_bytes: Vec<u8>, }, - /// Raw reply bytes observed by the client inside a forwarding window - /// closed by the Primary-DA barrier. The server routes these bytes - /// verbatim to the pane whose app issued the whitelisted query - /// (tracked on the server by `token`). ForwardedReplyFromHost { token: u32, reply_bytes: Vec<u8>, }, - /// The host terminal reported a color-palette theme mode (DSR 997 reply - /// to `CSI ? 996 n`, or unsolicited notification while `CSI ? 2031 h` - /// is enabled). The server propagates this to the active configured - /// theme and to subscribed plugins/panes. HostTerminalThemeChanged { mode: HostTerminalThemeMode, }, + SoftKeyboardVisibilityChanged { + visible: bool, + }, } // Types of messages sent from the server to the client @@ -201,6 +206,9 @@ pub enum ServerToClientMsg { output: String, }, QueryTerminalSize, + SetSoftKeyboard { + on: bool, + }, StartWebServer, RenamedSession { name: String, @@ -215,10 +223,6 @@ pub enum ServerToClientMsg { SubscribedPaneClosed { pane_id: PaneId, }, - /// Instruct the client to write `query_bytes` followed by the - /// Primary-DA barrier to stdout, open a forwarding window keyed by - /// `token`, and reply with a `ForwardedReplyFromHost` once the - /// barrier closes or the window times out. ForwardQueryToHost { token: u32, query_bytes: Vec<u8>, diff --git a/zellij-utils/src/ipc/protobuf_conversion.rs b/zellij-utils/src/ipc/protobuf_conversion.rs index 8025d66f..d1bfb7d4 100644 --- a/zellij-utils/src/ipc/protobuf_conversion.rs +++ b/zellij-utils/src/ipc/protobuf_conversion.rs @@ -10,8 +10,9 @@ use crate::{ HostTerminalThemeIndication as ProtoHostTerminalThemeIndication, InputMode as ProtoInputMode, KeyMsg, KillSessionMsg, LayoutMetadata as ProtoLayoutMetadata, LogErrorMsg, LogMsg, PaneMetadata as ProtoPaneMetadata, PaneRenderUpdateMsg, - QueryTerminalSizeMsg, RenamedSessionMsg, RenderMsg, - ServerToClientMsg as ProtoServerToClientMsg, StartWebServerMsg, SubscribeToPaneRendersMsg, + QueryTerminalSizeMsg, RenamedSessionMsg, RenderMsg, ResizeCause as ProtoResizeCause, + ServerToClientMsg as ProtoServerToClientMsg, SetSoftKeyboardMsg, + SoftKeyboardVisibilityChangedMsg, StartWebServerMsg, SubscribeToPaneRendersMsg, SubscribedPaneClosedMsg, SwitchSessionMsg, TabMetadata as ProtoTabMetadata, TerminalPixelDimensionsMsg, TerminalResizeMsg, UnblockCliPipeInputMsg, UnblockInputThreadMsg, WebServerStartedMsg, @@ -19,7 +20,7 @@ use crate::{ data::{HostTerminalThemeMode, InputMode, PaneId}, errors::prelude::*, ipc::{ - ClientToServerMsg, ColorRegister, ExitReason, PaneReference, PixelDimensions, + ClientToServerMsg, ColorRegister, ExitReason, PaneReference, PixelDimensions, ResizeCause, ServerToClientMsg, }, }; @@ -51,9 +52,11 @@ impl From<ClientToServerMsg> for ProtoClientToServerMsg { color_registers: color_registers.into_iter().map(|cr| cr.into()).collect(), }) }, - ClientToServerMsg::TerminalResize { new_size } => { + ClientToServerMsg::TerminalResize { new_size, cause } => { + let cause_proto: ProtoResizeCause = cause.into(); client_to_server_msg::Message::TerminalResize(TerminalResizeMsg { new_size: Some(new_size.into()), + cause: cause_proto as i32, }) }, ClientToServerMsg::FirstClientConnected { @@ -146,6 +149,11 @@ impl From<ClientToServerMsg> for ProtoClientToServerMsg { }, ) }, + ClientToServerMsg::SoftKeyboardVisibilityChanged { visible } => { + client_to_server_msg::Message::SoftKeyboardVisibilityChanged( + SoftKeyboardVisibilityChangedMsg { visible }, + ) + }, }; ProtoClientToServerMsg { @@ -193,11 +201,15 @@ impl TryFrom<ProtoClientToServerMsg> for ClientToServerMsg { }) }, Some(client_to_server_msg::Message::TerminalResize(resize)) => { + let cause = ProtoResizeCause::from_i32(resize.cause) + .unwrap_or(ProtoResizeCause::Viewport) + .into(); Ok(ClientToServerMsg::TerminalResize { new_size: resize .new_size .ok_or_else(|| anyhow!("Missing new_size"))? .try_into()?, + cause, }) }, Some(client_to_server_msg::Message::FirstClientConnected(first_client)) => { @@ -287,6 +299,11 @@ impl TryFrom<ProtoClientToServerMsg> for ClientToServerMsg { mode: proto_mode.into(), }) }, + Some(client_to_server_msg::Message::SoftKeyboardVisibilityChanged(msg)) => { + Ok(ClientToServerMsg::SoftKeyboardVisibilityChanged { + visible: msg.visible, + }) + }, None => Err(anyhow!("Empty ClientToServerMsg message")), } } @@ -372,6 +389,9 @@ impl From<ServerToClientMsg> for ProtoServerToClientMsg { query_bytes, }) }, + ServerToClientMsg::SetSoftKeyboard { on } => { + server_to_client_msg::Message::SetSoftKeyboard(SetSoftKeyboardMsg { on }) + }, }; ProtoServerToClientMsg { @@ -484,6 +504,9 @@ impl TryFrom<ProtoServerToClientMsg> for ServerToClientMsg { query_bytes: msg.query_bytes, }) }, + Some(server_to_client_msg::Message::SetSoftKeyboard(msg)) => { + Ok(ServerToClientMsg::SetSoftKeyboard { on: msg.on }) + }, None => Err(anyhow!("Empty ServerToClientMsg message")), } } @@ -732,6 +755,17 @@ impl From<crate::input::options::Options> visual_bell: options.visual_bell, focus_follows_mouse: options.focus_follows_mouse, mouse_click_through: options.mouse_click_through, + mobile_layout: options.mobile_layout.map(|m| { + use crate::client_server_contract::client_server_contract::MobileLayout as ProtoMobileLayout; + use crate::input::options::MobileLayoutConfiguration; + match m { + MobileLayoutConfiguration::Web => ProtoMobileLayout::Web as i32, + MobileLayoutConfiguration::Always => ProtoMobileLayout::Always as i32, + MobileLayoutConfiguration::Never => ProtoMobileLayout::Never as i32, + } + }), + mobile_threshold_cols: options.mobile_threshold_cols.map(|v| v as u32), + mobile_threshold_rows: options.mobile_threshold_rows.map(|v| v as u32), } } } @@ -744,8 +778,8 @@ impl TryFrom<crate::client_server_contract::client_server_contract::Options> options: crate::client_server_contract::client_server_contract::Options, ) -> Result<Self> { use crate::client_server_contract::client_server_contract::{ - Clipboard as ProtoClipboard, OnForceClose as ProtoOnForceClose, - WebSharing as ProtoWebSharing, + Clipboard as ProtoClipboard, MobileLayout as ProtoMobileLayout, + OnForceClose as ProtoOnForceClose, WebSharing as ProtoWebSharing, }; Ok(Self { @@ -829,6 +863,23 @@ impl TryFrom<crate::client_server_contract::client_server_contract::Options> visual_bell: options.visual_bell, focus_follows_mouse: options.focus_follows_mouse, mouse_click_through: options.mouse_click_through, + mobile_layout: options + .mobile_layout + .map(|m| match ProtoMobileLayout::from_i32(m) { + Some(ProtoMobileLayout::Web) => { + Ok(crate::input::options::MobileLayoutConfiguration::Web) + }, + Some(ProtoMobileLayout::Always) => { + Ok(crate::input::options::MobileLayoutConfiguration::Always) + }, + Some(ProtoMobileLayout::Never) => { + Ok(crate::input::options::MobileLayoutConfiguration::Never) + }, + _ => Err(anyhow!("Invalid MobileLayout value: {}", m)), + }) + .transpose()?, + mobile_threshold_cols: options.mobile_threshold_cols.map(|v| v as u16), + mobile_threshold_rows: options.mobile_threshold_rows.map(|v| v as u16), }) } } @@ -1419,6 +1470,9 @@ impl From<crate::input::actions::Action> crate::input::actions::Action::ToggleMouseMode => { ActionType::ToggleMouseMode(ToggleMouseModeAction {}) }, + crate::input::actions::Action::ToggleMobileMode => ActionType::ToggleMobileMode( + crate::client_server_contract::client_server_contract::ToggleMobileModeAction {}, + ), crate::input::actions::Action::PreviousSwapLayout => { ActionType::PreviousSwapLayout(PreviousSwapLayoutAction {}) }, @@ -2284,6 +2338,7 @@ impl TryFrom<crate::client_server_contract::client_server_contract::Action> }) }, ActionType::ToggleMouseMode(_) => Ok(crate::input::actions::Action::ToggleMouseMode), + ActionType::ToggleMobileMode(_) => Ok(crate::input::actions::Action::ToggleMobileMode), ActionType::PreviousSwapLayout(_) => { Ok(crate::input::actions::Action::PreviousSwapLayout) }, @@ -3013,6 +3068,26 @@ impl TryFrom<ProtoExitReason> for ExitReason { } } +impl From<ResizeCause> for ProtoResizeCause { + fn from(cause: ResizeCause) -> Self { + match cause { + ResizeCause::Viewport => ProtoResizeCause::Viewport, + ResizeCause::RenderingPreference => ProtoResizeCause::RenderingPreference, + ResizeCause::SizeSettled => ProtoResizeCause::SizeSettled, + } + } +} + +impl From<ProtoResizeCause> for ResizeCause { + fn from(cause: ProtoResizeCause) -> Self { + match cause { + ProtoResizeCause::Viewport => ResizeCause::Viewport, + ProtoResizeCause::RenderingPreference => ResizeCause::RenderingPreference, + ProtoResizeCause::SizeSettled => ResizeCause::SizeSettled, + } + } +} + impl From<HostTerminalThemeMode> for ProtoHostTerminalThemeIndication { fn from(mode: HostTerminalThemeMode) -> Self { match mode { @@ -3580,6 +3655,8 @@ impl From<crate::input::mouse::MouseEvent> middle: event.middle, wheel_up: event.wheel_up, wheel_down: event.wheel_down, + wheel_left: event.wheel_left, + wheel_right: event.wheel_right, shift: event.shift, alt: event.alt, ctrl: event.ctrl, @@ -4167,6 +4244,8 @@ impl TryFrom<crate::client_server_contract::client_server_contract::MouseEvent> middle: event.middle, wheel_up: event.wheel_up, wheel_down: event.wheel_down, + wheel_left: event.wheel_left, + wheel_right: event.wheel_right, shift: event.shift, alt: event.alt, ctrl: event.ctrl, diff --git a/zellij-utils/src/ipc/tests/roundtrip_tests.rs b/zellij-utils/src/ipc/tests/roundtrip_tests.rs index 4847b679..280805ab 100644 --- a/zellij-utils/src/ipc/tests/roundtrip_tests.rs +++ b/zellij-utils/src/ipc/tests/roundtrip_tests.rs @@ -13,9 +13,10 @@ use crate::input::layout::{ TiledPaneLayout, }; use crate::input::mouse::{MouseEvent, MouseEventType}; -use crate::input::options::{Clipboard, OnForceClose, Options}; +use crate::input::options::{Clipboard, MobileLayoutConfiguration, OnForceClose, Options}; use crate::ipc::{ - ClientToServerMsg, ColorRegister, ExitReason, PaneReference, PixelDimensions, ServerToClientMsg, + ClientToServerMsg, ColorRegister, ExitReason, PaneReference, PixelDimensions, ResizeCause, + ServerToClientMsg, }; use crate::pane_size::{Size, SizeInPixels}; use crate::position::Position; @@ -24,12 +25,6 @@ use std::path::PathBuf; #[test] fn server_client_contract() { - // here we test all possible values of each of the nested types in the server/client contract - // in the context of its message. - // - // we do a "roundtrip" test, meaning we take each message, encode it to protobuf bytes and - // decode it back to its rust type and then assert its equality with the original type to make - // sure they are identical and did not lose any information test_client_messages(); test_server_messages(); } @@ -378,12 +373,18 @@ fn test_client_messages() { }); test_client_roundtrip!(ClientToServerMsg::TerminalResize { new_size: Size { cols: 80, rows: 24 }, + cause: ResizeCause::Viewport, }); test_client_roundtrip!(ClientToServerMsg::TerminalResize { new_size: Size { cols: 200, rows: 50 }, + cause: ResizeCause::Viewport, + }); + test_client_roundtrip!(ClientToServerMsg::TerminalResize { + new_size: Size { cols: 40, rows: 38 }, + cause: ResizeCause::RenderingPreference, }); test_client_roundtrip!(ClientToServerMsg::FirstClientConnected { cli_assets: CliAssets::default(), @@ -478,6 +479,9 @@ fn test_client_messages() { visual_bell: Some(true), focus_follows_mouse: Some(false), mouse_click_through: Some(false), + mobile_layout: Some(MobileLayoutConfiguration::Always), + mobile_threshold_cols: Some(72), + mobile_threshold_rows: Some(40), }), layout: None, terminal_window_size: Size { rows: 80, cols: 42 }, @@ -670,7 +674,6 @@ fn test_client_messages() { is_web_client: true, }); test_client_roundtrip!(ClientToServerMsg::AttachClient { - // cli_assets tested extensively ijn FirstClientConnected, we can skip it here cli_assets: CliAssets::default(), tab_position_to_focus: None, pane_to_focus: None, @@ -725,7 +728,6 @@ fn test_client_messages() { }); test_client_roundtrip!(ClientToServerMsg::Action { action: Action::Write { - // KeyWithModifier is tested extensively in the Key ClientToServerMsg key_with_modifier: Some(KeyWithModifier::new(BareKey::Char('a'))), bytes: "a".as_bytes().to_vec(), is_kitty_keyboard_protocol: true, @@ -744,7 +746,6 @@ fn test_client_messages() { }); test_client_roundtrip!(ClientToServerMsg::Action { action: Action::SwitchToMode { - // InputMode is tested extensively in the Options conversion tests above input_mode: InputMode::Locked, }, terminal_id: Some(1), @@ -753,7 +754,6 @@ fn test_client_messages() { }); test_client_roundtrip!(ClientToServerMsg::Action { action: Action::SwitchModeForAllClients { - // InputMode is tested extensively in the Options conversion tests above input_mode: InputMode::Locked, }, terminal_id: Some(1), @@ -834,7 +834,6 @@ fn test_client_messages() { }); test_client_roundtrip!(ClientToServerMsg::Action { action: Action::MoveFocus { - // Direction is tested extensively elsewhere direction: Direction::Up, }, terminal_id: Some(1), @@ -2235,7 +2234,6 @@ fn test_client_messages() { }); test_client_roundtrip!(ClientToServerMsg::Action { action: Action::Run { - // RunCommandAction serialization roundtrip is tested extensively upwards of here command: RunCommandAction { command: PathBuf::from("/path/to/command"), args: vec![], @@ -2260,7 +2258,6 @@ fn test_client_messages() { }); test_client_roundtrip!(ClientToServerMsg::Action { action: Action::LaunchOrFocusPlugin { - // RunPluginOrAlias serialization roundtrip is tested extensively upwards plugin: RunPluginOrAlias::RunPlugin(RunPlugin::default()), should_float: true, move_to_focused_tab: true, @@ -2275,7 +2272,6 @@ fn test_client_messages() { }); test_client_roundtrip!(ClientToServerMsg::Action { action: Action::LaunchOrFocusPlugin { - // RunPluginOrAlias serialization roundtrip is tested extensively upwards plugin: RunPluginOrAlias::Alias(PluginAlias::default()), should_float: false, move_to_focused_tab: false, @@ -2290,7 +2286,6 @@ fn test_client_messages() { }); test_client_roundtrip!(ClientToServerMsg::Action { action: Action::LaunchPlugin { - // RunPluginOrAlias serialization roundtrip is tested extensively upwards plugin: RunPluginOrAlias::RunPlugin(RunPlugin::default()), should_float: true, should_open_in_place: true, @@ -2305,7 +2300,6 @@ fn test_client_messages() { }); test_client_roundtrip!(ClientToServerMsg::Action { action: Action::LaunchPlugin { - // RunPluginOrAlias serialization roundtrip is tested extensively upwards plugin: RunPluginOrAlias::Alias(PluginAlias::default()), should_float: false, should_open_in_place: false, @@ -2327,6 +2321,8 @@ fn test_client_messages() { middle: true, wheel_up: true, wheel_down: true, + wheel_left: true, + wheel_right: true, shift: true, alt: true, ctrl: true, @@ -2346,6 +2342,8 @@ fn test_client_messages() { middle: true, wheel_up: true, wheel_down: true, + wheel_left: true, + wheel_right: true, shift: true, alt: true, ctrl: true, @@ -2365,6 +2363,8 @@ fn test_client_messages() { middle: true, wheel_up: true, wheel_down: true, + wheel_left: true, + wheel_right: true, shift: true, alt: true, ctrl: true, @@ -2530,7 +2530,6 @@ fn test_client_messages() { client_id: Some(100), is_cli_client: true, }); - // tab_id roundtrip tests - verify tab_id survives serialization test_client_roundtrip!(ClientToServerMsg::Action { action: Action::NewTiledPane { direction: Some(Direction::Right), @@ -3280,7 +3279,6 @@ fn test_client_messages() { scrollback: Some(0), ansi: true, }); - // Pane-targeting roundtrips test_client_roundtrip!(ClientToServerMsg::Action { action: Action::ScrollUpByPaneId { pane_id: PaneId::Terminal(1), @@ -3416,7 +3414,6 @@ fn test_client_messages() { client_id: Some(100), is_cli_client: true, }); - // ANSI flag roundtrip tests test_client_roundtrip!(ClientToServerMsg::Action { action: Action::DumpScreen { file_path: Some("/path".to_owned()), @@ -3500,7 +3497,6 @@ fn test_client_messages() { client_id: Some(100), is_cli_client: true, }); - // Tab-targeting roundtrips test_client_roundtrip!(ClientToServerMsg::Action { action: Action::UndoRenameTabByTabId { id: 5 }, terminal_id: Some(1), @@ -3720,7 +3716,6 @@ fn test_server_messages() { #[test] fn set_pane_color_wire_roundtrip() { - // Test actual byte-level encode/decode, not just struct conversion use prost::Message; let original = ClientToServerMsg::Action { @@ -3734,21 +3729,17 @@ fn set_pane_color_wire_roundtrip() { is_cli_client: true, }; - // Rust -> proto struct let proto: crate::client_server_contract::client_server_contract::ClientToServerMsg = original.clone().into(); - // proto struct -> bytes (what goes over the wire) let bytes = proto.encode_to_vec(); - // bytes -> proto struct (what the server does) let decoded_proto = crate::client_server_contract::client_server_contract::ClientToServerMsg::decode( &bytes[..], ) .expect("Failed to decode protobuf bytes"); - // proto struct -> Rust let roundtrip: ClientToServerMsg = decoded_proto .try_into() .expect("Failed to convert decoded protobuf back to Rust"); @@ -3782,14 +3773,12 @@ fn rename_active_pane_wire_roundtrip() { let proto: crate::client_server_contract::client_server_contract::ClientToServerMsg = original.clone().into(); - // proto struct → bytes → proto struct (the actual wire path) let mut buf = Vec::new(); proto.encode(&mut buf).expect("Failed to encode protobuf"); let decoded = crate::client_server_contract::client_server_contract::ClientToServerMsg::decode(&buf[..]) .expect("Failed to decode protobuf bytes"); - // proto struct → Rust let roundtrip: ClientToServerMsg = decoded .try_into() .expect("Failed to convert decoded protobuf back to Rust"); diff --git a/zellij-utils/src/ipc/tests/socket_tests.rs b/zellij-utils/src/ipc/tests/socket_tests.rs index d0e9ce65..b1f8c15d 100644 --- a/zellij-utils/src/ipc/tests/socket_tests.rs +++ b/zellij-utils/src/ipc/tests/socket_tests.rs @@ -1,5 +1,5 @@ use crate::ipc::{ - ClientToServerMsg, IpcReceiverWithContext, IpcSenderWithContext, ServerToClientMsg, + ClientToServerMsg, IpcReceiverWithContext, IpcSenderWithContext, ResizeCause, ServerToClientMsg, }; use crate::pane_size::Size; use interprocess::local_socket::{prelude::*, ListenerOptions}; @@ -162,9 +162,7 @@ fn bidirectional_communication_via_fd_duplication() { let stream = listener.incoming().next().unwrap().expect("accept failed"); let mut sender: IpcSenderWithContext<ServerToClientMsg> = IpcSenderWithContext::new(stream); - // Create a receiver from the same socket via dup() let mut receiver: IpcReceiverWithContext<ClientToServerMsg> = sender.get_receiver(); - sender .send_server_msg(ServerToClientMsg::Connected) .expect("send failed"); @@ -181,7 +179,6 @@ fn bidirectional_communication_via_fd_duplication() { let stream = connect_stream(&name); let mut sender: IpcSenderWithContext<ClientToServerMsg> = IpcSenderWithContext::new(stream); - // Create a receiver from the same socket via dup() let mut receiver: IpcReceiverWithContext<ServerToClientMsg> = sender.get_receiver(); let msg = receiver.recv_server_msg(); @@ -220,6 +217,7 @@ fn multiple_messages_in_sequence() { rows: 50, cols: 120, }, + cause: ResizeCause::Viewport, }) .expect("send 2 failed"); sender @@ -237,9 +235,10 @@ fn multiple_messages_in_sequence() { let (msg2, _) = receiver.recv_client_msg().expect("missing message 2"); match msg2 { - ClientToServerMsg::TerminalResize { new_size } => { + ClientToServerMsg::TerminalResize { new_size, cause } => { assert_eq!(new_size.rows, 50); assert_eq!(new_size.cols, 120); + assert_eq!(cause, ResizeCause::Viewport); }, other => panic!("expected TerminalResize, got: {:?}", other), } @@ -264,7 +263,6 @@ fn receiver_returns_none_on_closed_connection() { sender .send_client_msg(ClientToServerMsg::ConnStatus) .expect("send failed"); - // sender drops here, closing the connection } }); @@ -277,16 +275,10 @@ fn receiver_returns_none_on_closed_connection() { let msg = receiver.recv_client_msg(); assert!(msg.is_some(), "should receive the sent message"); - // After the sender is dropped, subsequent reads should return None let msg = receiver.recv_client_msg(); assert!(msg.is_none(), "should return None after connection closed"); } -// --- Session discovery tests --- -// These test the OS-specific mechanics used by get_sessions() in sessions.rs: -// FileTypeExt::is_socket() for identifying socket files, and the assert_socket -// probing pattern (connect, send ConnStatus, expect Connected). - #[cfg(unix)] #[test] fn is_socket_identifies_bound_unix_socket() { @@ -318,12 +310,9 @@ fn is_socket_rejects_regular_file() { #[test] fn session_probe_accepts_responding_socket() { - // Simulates the assert_socket() pattern from sessions.rs: - // A real Zellij server responds to ConnStatus with Connected. let (_guard, name) = new_ipc(); let listener = bind_listener(&name); - // Spawn a fake "server" that responds to ConnStatus with Connected let server = std::thread::spawn(move || { let stream = listener.incoming().next().unwrap().expect("accept failed"); let mut receiver: IpcReceiverWithContext<ClientToServerMsg> = @@ -338,7 +327,6 @@ fn session_probe_accepts_responding_socket() { .expect("send failed"); }); - // Client-side probing (mirrors assert_socket in sessions.rs) let stream = connect_stream(&name); let mut sender: IpcSenderWithContext<ClientToServerMsg> = IpcSenderWithContext::new(stream); sender @@ -357,15 +345,11 @@ fn session_probe_accepts_responding_socket() { #[test] fn session_probe_rejects_dead_socket() { - // Simulates discovering a stale socket file with no listener. - // get_sessions() filters these out via assert_socket() which tries to connect. let (_guard, name) = new_ipc(); - // Bind and immediately drop the listener to create a stale socket file { let _listener = bind_listener(&name); } - // Listener is dropped -- the socket file may still exist but nobody is listening let result = try_connect(&name); assert!( @@ -380,21 +364,17 @@ fn socket_directory_enumeration_finds_sockets() { use interprocess::local_socket::GenericFilePath; use std::os::unix::fs::FileTypeExt; - // Simulates the readdir + is_socket() filtering pattern from get_sessions(). let dir = tempfile::TempDir::new().expect("failed to create temp dir"); - // Create a socket let sock_path = dir.path().join("test-session"); let _listener = ListenerOptions::new() .name(sock_path.as_path().to_fs_name::<GenericFilePath>().unwrap()) .create_sync() .expect("bind failed"); - // Create a regular file (should be filtered out) let file_path = dir.path().join("not-a-session"); std::fs::write(&file_path, b"data").expect("write failed"); - // Enumerate the directory, filtering for sockets (same pattern as get_sessions) let entries: Vec<String> = std::fs::read_dir(dir.path()) .expect("read_dir failed") .filter_map(|entry| { diff --git a/zellij-utils/src/kdl/mod.rs b/zellij-utils/src/kdl/mod.rs index c7cc5366..0164ef92 100644 --- a/zellij-utils/src/kdl/mod.rs +++ b/zellij-utils/src/kdl/mod.rs @@ -81,6 +81,7 @@ macro_rules! parse_kdl_action_arguments { "Confirm" => Ok(Action::Confirm), "Deny" => Ok(Action::Deny), "ToggleMouseMode" => Ok(Action::ToggleMouseMode), + "ToggleMobileMode" => Ok(Action::ToggleMobileMode), "PreviousSwapLayout" => Ok(Action::PreviousSwapLayout), "NextSwapLayout" => Ok(Action::NextSwapLayout), "Clear" => Ok(Action::ClearScreen), @@ -1240,6 +1241,7 @@ impl Action { Some(node) }, Action::ToggleMouseMode => Some(KdlNode::new("ToggleMouseMode")), + Action::ToggleMobileMode => Some(KdlNode::new("ToggleMobileMode")), Action::PreviousSwapLayout => Some(KdlNode::new("PreviousSwapLayout")), Action::NextSwapLayout => Some(KdlNode::new("NextSwapLayout")), Action::BreakPane => Some(KdlNode::new("BreakPane")), @@ -1582,6 +1584,9 @@ impl TryFrom<(&KdlNode, &Options)> for Action { "ToggleMouseMode" => { parse_kdl_action_arguments!(action_name, action_arguments, kdl_action) }, + "ToggleMobileMode" => { + parse_kdl_action_arguments!(action_name, action_arguments, kdl_action) + }, "Detach" => parse_kdl_action_arguments!(action_name, action_arguments, kdl_action), "SwitchSession" => { let name = kdl_get_string_property_or_child_value!(kdl_action, "name") @@ -2824,6 +2829,39 @@ impl Options { let mouse_click_through = kdl_property_first_arg_as_bool_or_error!(kdl_options, "mouse_click_through") .map(|(v, _)| v); + let mobile_layout = + match kdl_property_first_arg_as_string_or_error!(kdl_options, "mobile_layout") { + Some((value, entry)) => { + use crate::input::options::MobileLayoutConfiguration; + match value.parse::<MobileLayoutConfiguration>() { + Ok(v) => Some(v), + Err(e) => return Err(kdl_parsing_error!(e, entry)), + } + }, + None => None, + }; + let mobile_threshold_cols = + match kdl_property_first_arg_as_i64_or_error!(kdl_options, "mobile_threshold_cols") { + Some((value, _)) if value >= 0 => Some(value as u16), + Some((value, entry)) => { + return Err(kdl_parsing_error!( + format!("mobile_threshold_cols must be >= 0, found '{}'", value), + entry + )); + }, + None => None, + }; + let mobile_threshold_rows = + match kdl_property_first_arg_as_i64_or_error!(kdl_options, "mobile_threshold_rows") { + Some((value, _)) if value >= 0 => Some(value as u16), + Some((value, entry)) => { + return Err(kdl_parsing_error!( + format!("mobile_threshold_rows must be >= 0, found '{}'", value), + entry + )); + }, + None => None, + }; Ok(Options { simplified_ui, @@ -2873,6 +2911,9 @@ impl Options { enforce_https_for_localhost, post_command_discovery_hook, client_async_worker_tasks, + mobile_layout, + mobile_threshold_cols, + mobile_threshold_rows, }) } pub fn from_string(stringified_keybindings: &String) -> Result<Self, ConfigError> { @@ -4218,6 +4259,94 @@ impl Options { None } } + fn mobile_layout_to_kdl(&self, add_comments: bool) -> Option<KdlNode> { + use crate::input::options::MobileLayoutConfiguration; + let comment_text = format!( + "{}\n{}\n{}\n{}\n{}\n{}\n{}", + " ", + "// When attached clients land in the mobile UI plugin.", + "// Options:", + "// - \"web\" (Default — web clients only, gated on mobile_threshold_cols/rows)", + "// - \"always\" (any client, gated on mobile_threshold_cols/rows)", + "// - \"never\"", + "// ", + ); + let create_node = |value: MobileLayoutConfiguration| -> KdlNode { + let mut node = KdlNode::new("mobile_layout"); + let s = match value { + MobileLayoutConfiguration::Web => "web", + MobileLayoutConfiguration::Always => "always", + MobileLayoutConfiguration::Never => "never", + }; + node.push(KdlValue::String(s.to_string())); + node + }; + if let Some(value) = self.mobile_layout { + let mut node = create_node(value); + if add_comments { + node.set_leading(format!("{}\n", comment_text)); + } + Some(node) + } else if add_comments { + let mut node = create_node(MobileLayoutConfiguration::Web); + node.set_leading(format!("{}\n// ", comment_text)); + Some(node) + } else { + None + } + } + fn mobile_threshold_cols_to_kdl(&self, add_comments: bool) -> Option<KdlNode> { + let comment_text = format!( + "{}\n{}\n{}", + " ", + "// Column breakpoint for mobile_layout (web/always). 0 = always match.", + "// Default: 60", + ); + let create_node = |value: u16| -> KdlNode { + let mut node = KdlNode::new("mobile_threshold_cols"); + node.push(KdlValue::Base10(value as i64)); + node + }; + if let Some(value) = self.mobile_threshold_cols { + let mut node = create_node(value); + if add_comments { + node.set_leading(format!("{}\n", comment_text)); + } + Some(node) + } else if add_comments { + let mut node = create_node(60); + node.set_leading(format!("{}\n// ", comment_text)); + Some(node) + } else { + None + } + } + fn mobile_threshold_rows_to_kdl(&self, add_comments: bool) -> Option<KdlNode> { + let comment_text = format!( + "{}\n{}\n{}", + " ", + "// Row breakpoint for mobile_layout (web/always). 0 = always match.", + "// Default: 30", + ); + let create_node = |value: u16| -> KdlNode { + let mut node = KdlNode::new("mobile_threshold_rows"); + node.push(KdlValue::Base10(value as i64)); + node + }; + if let Some(value) = self.mobile_threshold_rows { + let mut node = create_node(value); + if add_comments { + node.set_leading(format!("{}\n", comment_text)); + } + Some(node) + } else if add_comments { + let mut node = create_node(30); + node.set_leading(format!("{}\n// ", comment_text)); + Some(node) + } else { + None + } + } fn client_async_worker_tasks_to_kdl(&self, add_comments: bool) -> Option<KdlNode> { let comment_text = r#" // Number of async worker tasks to spawn per active client. @@ -4396,6 +4525,15 @@ impl Options { { nodes.push(client_async_worker_tasks); } + if let Some(mobile_layout) = self.mobile_layout_to_kdl(add_comments) { + nodes.push(mobile_layout); + } + if let Some(mobile_threshold_cols) = self.mobile_threshold_cols_to_kdl(add_comments) { + nodes.push(mobile_threshold_cols); + } + if let Some(mobile_threshold_rows) = self.mobile_threshold_rows_to_kdl(add_comments) { + nodes.push(mobile_threshold_rows); + } nodes } } @@ -7111,6 +7249,9 @@ fn config_options_to_string() { support_kitty_keyboard_protocol false web_server true web_sharing "disabled" + mobile_layout "always" + mobile_threshold_cols 72 + mobile_threshold_rows 0 "##; let document: KdlDocument = fake_config.parse().unwrap(); let deserialized = Options::from_kdl(&document).unwrap(); @@ -7158,6 +7299,9 @@ fn config_options_to_string_with_comments() { support_kitty_keyboard_protocol false web_server true web_sharing "disabled" + mobile_layout "always" + mobile_threshold_cols 72 + mobile_threshold_rows 0 "##; let document: KdlDocument = fake_config.parse().unwrap(); let deserialized = Options::from_kdl(&document).unwrap(); @@ -7192,6 +7336,37 @@ fn config_options_to_string_without_options() { } #[test] +fn mobile_layout_kdl_round_trip_for_every_variant() { + use crate::input::options::MobileLayoutConfiguration; + let cases = [ + ("web", MobileLayoutConfiguration::Web, (60, 30)), + ("always", MobileLayoutConfiguration::Always, (0, 0)), + ("never", MobileLayoutConfiguration::Never, (40, 0)), + ]; + for (value, expected, (cols, rows)) in cases { + let fake_config = format!( + r##" + mobile_layout "{value}" + mobile_threshold_cols {cols} + mobile_threshold_rows {rows} + "## + ); + let document: KdlDocument = fake_config.parse().unwrap(); + let parsed = Options::from_kdl(&document).unwrap(); + assert_eq!(parsed.mobile_layout, Some(expected), "case: {value}"); + assert_eq!(parsed.mobile_threshold_cols, Some(cols), "case: {value}"); + assert_eq!(parsed.mobile_threshold_rows, Some(rows), "case: {value}"); + + let mut serialized = Options::to_kdl(&parsed, false); + let mut fake_document = KdlDocument::new(); + fake_document.nodes_mut().append(&mut serialized); + let reparsed = + Options::from_kdl(&fake_document.to_string().parse::<KdlDocument>().unwrap()).unwrap(); + assert_eq!(parsed, reparsed, "round-trip mismatch for {value}"); + } +} + +#[test] fn config_options_to_string_with_some_options() { let fake_config = r##" default_layout "compact" diff --git a/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__bare_config_from_default_assets_to_string.snap b/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__bare_config_from_default_assets_to_string.snap index 6eda59b9..1ab39935 100644 --- a/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__bare_config_from_default_assets_to_string.snap +++ b/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__bare_config_from_default_assets_to_string.snap @@ -261,6 +261,7 @@ plugins { filepicker location="zellij:strider" { cwd "/" } + mobile location="zellij:mobile" plugin-manager location="zellij:plugin-manager" session-manager location="zellij:session-manager" status-bar location="zellij:status-bar" diff --git a/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__bare_config_from_default_assets_to_string_with_comments.snap b/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__bare_config_from_default_assets_to_string_with_comments.snap index c49fa8f9..5dbede33 100644 --- a/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__bare_config_from_default_assets_to_string_with_comments.snap +++ b/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__bare_config_from_default_assets_to_string_with_comments.snap @@ -264,6 +264,7 @@ plugins { filepicker location="zellij:strider" { cwd "/" } + mobile location="zellij:mobile" plugin-manager location="zellij:plugin-manager" session-manager location="zellij:session-manager" status-bar location="zellij:status-bar" @@ -582,4 +583,19 @@ web_client { // typically work best. Set to 0 to use the number of (physical) CPU cores. // Note: This only applies to web clients at the moment. // client_async_worker_tasks 4 - + +// When attached clients land in the mobile UI plugin. +// Options: +// - "web" (Default — web clients only, gated on mobile_threshold_cols/rows) +// - "always" (any client, gated on mobile_threshold_cols/rows) +// - "never" +// +// mobile_layout "web" + +// Column breakpoint for mobile_layout (web/always). 0 = always match. +// Default: 60 +// mobile_threshold_cols 60 + +// Row breakpoint for mobile_layout (web/always). 0 = always match. +// Default: 30 +// mobile_threshold_rows 30 diff --git a/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__config_options_to_string.snap b/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__config_options_to_string.snap index 6c78ea96..43c82f7e 100644 --- a/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__config_options_to_string.snap +++ b/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__config_options_to_string.snap @@ -31,3 +31,6 @@ disable_session_metadata true support_kitty_keyboard_protocol false web_server true web_sharing "disabled" +mobile_layout "always" +mobile_threshold_cols 72 +mobile_threshold_rows 0 diff --git a/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__config_options_to_string_with_comments.snap b/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__config_options_to_string_with_comments.snap index eda40849..347a2074 100644 --- a/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__config_options_to_string_with_comments.snap +++ b/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__config_options_to_string_with_comments.snap @@ -301,4 +301,19 @@ web_sharing "disabled" // typically work best. Set to 0 to use the number of (physical) CPU cores. // Note: This only applies to web clients at the moment. // client_async_worker_tasks 4 - + +// When attached clients land in the mobile UI plugin. +// Options: +// - "web" (Default — web clients only, gated on mobile_threshold_cols/rows) +// - "always" (any client, gated on mobile_threshold_cols/rows) +// - "never" +// +mobile_layout "always" + +// Column breakpoint for mobile_layout (web/always). 0 = always match. +// Default: 60 +mobile_threshold_cols 72 + +// Row breakpoint for mobile_layout (web/always). 0 = always match. +// Default: 30 +mobile_threshold_rows 0 diff --git a/zellij-utils/src/pane_size.rs b/zellij-utils/src/pane_size.rs index 4ba87326..454e73c5 100644 --- a/zellij-utils/src/pane_size.rs +++ b/zellij-utils/src/pane_size.rs @@ -68,7 +68,7 @@ pub struct Offset { pub left: usize, } -#[derive(Clone, Copy, Default, PartialEq, Debug, Serialize, Deserialize)] +#[derive(Clone, Copy, Default, PartialEq, Eq, Debug, Serialize, Deserialize)] pub struct Size { pub rows: usize, pub cols: usize, @@ -80,6 +80,14 @@ pub struct SizeInPixels { pub width: usize, } +#[derive(Clone, Copy, Default, PartialEq, Eq, Debug, Serialize, Deserialize)] +pub struct Insets { + pub top: usize, + pub bottom: usize, + pub left: usize, + pub right: usize, +} + #[derive(Eq, Clone, Copy, PartialEq, Debug, Serialize, Deserialize, Hash)] pub struct Dimension { pub constraint: Constraint, diff --git a/zellij-utils/src/plugin_api/action.proto b/zellij-utils/src/plugin_api/action.proto index 28a8337e..7eedf393 100644 --- a/zellij-utils/src/plugin_api/action.proto +++ b/zellij-utils/src/plugin_api/action.proto @@ -586,6 +586,8 @@ message MouseEventPayload { bool ctrl = 9; int64 line = 10; int64 column = 11; + bool wheel_left = 12; + bool wheel_right = 13; } message RunCommandAction { diff --git a/zellij-utils/src/plugin_api/action.rs b/zellij-utils/src/plugin_api/action.rs index cd3e1522..34b3adce 100644 --- a/zellij-utils/src/plugin_api/action.rs +++ b/zellij-utils/src/plugin_api/action.rs @@ -1943,7 +1943,8 @@ impl TryFrom<Action> for ProtobufAction { | Action::SaveSession | Action::ListTabs { .. } | Action::CurrentTabInfo { .. } - | Action::SetPaneColor { .. } => Err("Unsupported action"), + | Action::SetPaneColor { .. } + | Action::ToggleMobileMode => Err("Unsupported action"), } } } @@ -2097,6 +2098,8 @@ impl TryFrom<ProtobufMouseEventPayload> for MouseEvent { middle: protobuf_event.middle as bool, wheel_up: protobuf_event.wheel_up as bool, wheel_down: protobuf_event.wheel_down as bool, + wheel_left: protobuf_event.wheel_left as bool, + wheel_right: protobuf_event.wheel_right as bool, shift: protobuf_event.shift as bool, alt: protobuf_event.alt as bool, ctrl: protobuf_event.ctrl as bool, @@ -2119,6 +2122,8 @@ impl TryFrom<MouseEvent> for ProtobufMouseEventPayload { middle: event.middle as bool, wheel_up: event.wheel_up as bool, wheel_down: event.wheel_down as bool, + wheel_left: event.wheel_left as bool, + wheel_right: event.wheel_right as bool, shift: event.shift as bool, alt: event.alt as bool, ctrl: event.ctrl as bool, diff --git a/zellij-utils/src/plugin_api/event.proto b/zellij-utils/src/plugin_api/event.proto index 5334f434..2567c657 100644 --- a/zellij-utils/src/plugin_api/event.proto +++ b/zellij-utils/src/plugin_api/event.proto @@ -69,6 +69,7 @@ enum EventType { InitialKeybinds = 44; CommandChanged = 45; HostTerminalThemeChanged = 46; + SoftKeyboardVisibilityChanged = 47; } message EventNameList { @@ -117,6 +118,7 @@ message Event { InitialKeybindsPayload initial_keybinds_payload = 38; CommandChangedPayload command_changed_payload = 39; HostTerminalThemeChangedPayload host_terminal_theme_changed_payload = 40; + SoftKeyboardVisibilityChangedPayload soft_keyboard_visibility_changed_payload = 41; } } @@ -124,6 +126,10 @@ message HostTerminalThemeChangedPayload { HostTerminalThemeIndication mode = 1; } +message SoftKeyboardVisibilityChangedPayload { + bool visible = 1; +} + enum HostTerminalThemeIndication { Dark = 0; Light = 1; @@ -305,6 +311,8 @@ enum MouseEventName { MouseHold = 4; MouseRelease = 5; MouseHover = 6; + MouseScrollLeft = 7; + MouseScrollRight = 8; } message TabUpdatePayload { @@ -512,6 +520,7 @@ message PaneContents { optional SelectedText selected_text = 2; repeated string lines_above_viewport = 3; repeated string lines_below_viewport = 4; + optional action.Position cursor = 5; } message PaneScrollbackResponse { diff --git a/zellij-utils/src/plugin_api/event.rs b/zellij-utils/src/plugin_api/event.rs index af421fac..5ba106d0 100644 --- a/zellij-utils/src/plugin_api/event.rs +++ b/zellij-utils/src/plugin_api/event.rs @@ -26,6 +26,7 @@ pub use super::generated_api::api::{ PluginConfigurationChangedPayload as ProtobufPluginConfigurationChangedPayload, PluginInfo as ProtobufPluginInfo, ResurrectableSession as ProtobufResurrectableSession, SelectedText as ProtobufSelectedText, SessionManifest as ProtobufSessionManifest, + SoftKeyboardVisibilityChangedPayload as ProtobufSoftKeyboardVisibilityChangedPayload, SyntaxError as ProtobufSyntaxError, TabInfo as ProtobufTabInfo, TabMetadata as ProtobufTabMetadata, UserActionPayload as ProtobufUserActionPayload, WebServerStatusPayload as ProtobufWebServerStatusPayload, WebSharing as ProtobufWebSharing, @@ -588,6 +589,14 @@ impl TryFrom<ProtobufEvent> for Event { }, _ => Err("Malformed payload for HostTerminalThemeChanged Event"), }, + Some(ProtobufEventType::SoftKeyboardVisibilityChanged) => { + match protobuf_event.payload { + Some(ProtobufEventPayload::SoftKeyboardVisibilityChangedPayload(p)) => { + Ok(Event::SoftKeyboardVisibilityChanged(p.visible)) + }, + _ => Err("Malformed payload for SoftKeyboardVisibilityChanged Event"), + } + }, None => Err("Unknown Protobuf Event"), } } @@ -1129,6 +1138,15 @@ impl TryFrom<Event> for ProtobufEvent { payload: Some(event::Payload::HostTerminalThemeChangedPayload(payload)), }) }, + Event::SoftKeyboardVisibilityChanged(visible) => { + let payload = ProtobufSoftKeyboardVisibilityChangedPayload { visible }; + Ok(ProtobufEvent { + name: ProtobufEventType::SoftKeyboardVisibilityChanged as i32, + payload: Some(event::Payload::SoftKeyboardVisibilityChangedPayload( + payload, + )), + }) + }, Event::InitialKeybinds(keybinds) => { let mut protobuf_keybinds: Vec<ProtobufInputModeKeybinds> = vec![]; for (input_mode, input_mode_keybinds) in keybinds { @@ -1624,6 +1642,22 @@ impl TryFrom<MouseEventPayload> for Mouse { ), _ => Err("Malformed payload for mouse hover"), }, + Some(MouseEventName::MouseScrollLeft) => { + match mouse_event_payload.mouse_event_payload { + Some(mouse_event_payload::MouseEventPayload::LineCount(line_count)) => { + Ok(Mouse::ScrollLeft(line_count as usize)) + }, + _ => Err("Malformed payload for mouse scroll left"), + } + }, + Some(MouseEventName::MouseScrollRight) => { + match mouse_event_payload.mouse_event_payload { + Some(mouse_event_payload::MouseEventPayload::LineCount(line_count)) => { + Ok(Mouse::ScrollRight(line_count as usize)) + }, + _ => Err("Malformed payload for mouse scroll right"), + } + }, None => Err("Malformed payload for MouseEventName"), } } @@ -1690,6 +1724,18 @@ impl TryFrom<Mouse> for MouseEventPayload { }, )), }), + Mouse::ScrollLeft(cols) => Ok(MouseEventPayload { + mouse_event_name: MouseEventName::MouseScrollLeft as i32, + mouse_event_payload: Some(mouse_event_payload::MouseEventPayload::LineCount( + cols as u32, + )), + }), + Mouse::ScrollRight(cols) => Ok(MouseEventPayload { + mouse_event_name: MouseEventName::MouseScrollRight as i32, + mouse_event_payload: Some(mouse_event_payload::MouseEventPayload::LineCount( + cols as u32, + )), + }), } } } @@ -2078,6 +2124,9 @@ impl TryFrom<ProtobufEventType> for EventType { ProtobufEventType::HighlightClicked => EventType::HighlightClicked, ProtobufEventType::InitialKeybinds => EventType::InitialKeybinds, ProtobufEventType::HostTerminalThemeChanged => EventType::HostTerminalThemeChanged, + ProtobufEventType::SoftKeyboardVisibilityChanged => { + EventType::SoftKeyboardVisibilityChanged + }, }) } } @@ -2132,6 +2181,9 @@ impl TryFrom<EventType> for ProtobufEventType { EventType::HighlightClicked => ProtobufEventType::HighlightClicked, EventType::InitialKeybinds => ProtobufEventType::InitialKeybinds, EventType::HostTerminalThemeChanged => ProtobufEventType::HostTerminalThemeChanged, + EventType::SoftKeyboardVisibilityChanged => { + ProtobufEventType::SoftKeyboardVisibilityChanged + }, }) } } @@ -2986,12 +3038,16 @@ impl TryFrom<ProtobufPaneContents> for PaneContents { .selected_text .map(|st| st.try_into()) .transpose()?; + let cursor = protobuf_contents + .cursor + .map(|p| (p.column as usize, p.line as usize)); Ok(PaneContents { viewport: protobuf_contents.viewport, selected_text, lines_above_viewport: protobuf_contents.lines_above_viewport, lines_below_viewport: protobuf_contents.lines_below_viewport, + cursor, }) } } @@ -3003,12 +3059,17 @@ impl TryFrom<PaneContents> for ProtobufPaneContents { .selected_text .map(|st| st.try_into()) .transpose()?; + let cursor = pane_contents.cursor.map(|(x, y)| ProtobufPosition { + line: y as i64, + column: x as i64, + }); Ok(ProtobufPaneContents { viewport: pane_contents.viewport, selected_text, lines_above_viewport: pane_contents.lines_above_viewport, lines_below_viewport: pane_contents.lines_below_viewport, + cursor, }) } } @@ -3104,6 +3165,7 @@ fn serialize_pane_render_report_with_ansi_event_with_data() { selected_text: None, lines_above_viewport: vec![], lines_below_viewport: vec![], + cursor: None, }, ); let event = Event::PaneRenderReportWithAnsi(pane_contents_map); diff --git a/zellij-utils/src/plugin_api/plugin_command.proto b/zellij-utils/src/plugin_api/plugin_command.proto index 729159e3..c65bcbfd 100644 --- a/zellij-utils/src/plugin_api/plugin_command.proto +++ b/zellij-utils/src/plugin_api/plugin_command.proto @@ -207,6 +207,12 @@ enum CommandName { KillSessionsAndReply = 212; DeleteDeadSessionAndReply = 213; DeleteAllDeadSessionsAndReply = 214; + SetSoftKeyboard = 215; + SetTabFit = 216; + SetShadowFocus = 219; + NewTabUnfocused = 220; + NewTiledPaneInTab = 221; + ExitMobileMode = 222; } message PluginCommand { @@ -361,6 +367,11 @@ message PluginCommand { GetSessionListPayload get_session_list_payload = 161; KillSessionsPayload kill_sessions_and_reply_payload = 162; string delete_dead_session_and_reply_payload = 163; + SetSoftKeyboardPayload set_soft_keyboard_payload = 164; + SetTabFitPayload set_tab_fit_payload = 165; + PaneId set_shadow_focus_payload = 167; + NewTabUnfocusedPayload new_tab_unfocused_payload = 168; + NewTiledPaneInTabPayload new_tiled_pane_in_tab_payload = 169; } } @@ -369,6 +380,15 @@ message NewTabPayload { optional string cwd = 2; } +message NewTabUnfocusedPayload { + optional string name = 1; + optional string cwd = 2; +} + +message NewTiledPaneInTabPayload { + uint32 tab_position = 1; +} + message DumpSessionLayoutPayload { optional uint32 tab_index = 1; } @@ -1057,6 +1077,17 @@ message NewTabResponse { } } +message NewTabUnfocusedResponse { + oneof result { + uint64 tab_id = 1; + bool none = 2; + } +} + +message NewTiledPaneInTabResponse { + optional PaneId pane_id = 1; +} + message NewTabsResponse { repeated uint64 tab_ids = 1; // Only includes successfully created tabs } @@ -1328,3 +1359,18 @@ message DeleteDeadSessionResponse { message DeleteAllDeadSessionsResponse { optional string error = 1; // absent => success } + +message SetSoftKeyboardPayload { + bool on = 1; +} + +message Size { + uint32 rows = 1; + uint32 cols = 2; +} + +message SetTabFitPayload { + uint32 tab_id = 1; + PaneId pane_id = 2; + Size size = 3; +} diff --git a/zellij-utils/src/plugin_api/plugin_command.rs b/zellij-utils/src/plugin_api/plugin_command.rs index fa77739a..42b0774f 100644 --- a/zellij-utils/src/plugin_api/plugin_command.rs +++ b/zellij-utils/src/plugin_api/plugin_command.rs @@ -13,9 +13,9 @@ pub use super::generated_api::api::{ get_focused_pane_info_response, get_pane_cwd_response, get_pane_pid_response, get_pane_running_command_response, get_session_list_response, hide_floating_panes_response, highlight_style::Style as ProtobufHighlightStyleVariant, new_tab_response, - parse_layout_response, plugin_command::Payload, rename_layout_response, - save_layout_response, save_session_response, show_floating_panes_response, - BreakPanesToNewTabPayload, + new_tab_unfocused_response, parse_layout_response, plugin_command::Payload, + rename_layout_response, save_layout_response, save_session_response, + show_floating_panes_response, BreakPanesToNewTabPayload, BreakPanesToNewTabResponse as ProtobufBreakPanesToNewTabResponse, BreakPanesToTabWithIdPayload, BreakPanesToTabWithIdResponse as ProtobufBreakPanesToTabWithIdResponse, @@ -65,8 +65,10 @@ pub use super::generated_api::api::{ KillSessionsResponse as ProtobufKillSessionsResponse, ListTokensResponse, LoadNewPluginPayload, MessageToPluginPayload, MovePaneWithPaneIdInDirectionPayload, MovePaneWithPaneIdPayload, MovePayload, NewPluginArgs as ProtobufNewPluginArgs, - NewTabPayload, NewTabResponse as ProtobufNewTabResponse, + NewTabPayload, NewTabResponse as ProtobufNewTabResponse, NewTabUnfocusedPayload, + NewTabUnfocusedResponse as ProtobufNewTabUnfocusedResponse, NewTabsResponse as ProtobufNewTabsResponse, NewTabsWithLayoutInfoPayload, + NewTiledPaneInTabPayload, NewTiledPaneInTabResponse as ProtobufNewTiledPaneInTabResponse, OpenCommandPaneBackgroundResponse as ProtobufOpenCommandPaneBackgroundResponse, OpenCommandPaneFloatingNearPluginPayload, OpenCommandPaneFloatingNearPluginResponse as ProtobufOpenCommandPaneFloatingNearPluginResponse, @@ -119,13 +121,15 @@ pub use super::generated_api::api::{ ScrollToBottomInPaneIdPayload, ScrollToTopInPaneIdPayload, ScrollUpInPaneIdPayload, SessionListSnapshot as ProtobufSessionListSnapshot, SetFloatingPanePinnedPayload, SetPaneBorderlessPayload, SetPaneColorPayload, SetPaneRegexHighlightsPayload, - SetSelfMouseSelectionSupportPayload, SetTimeoutPayload, ShowCursorPayload, + SetSelfMouseSelectionSupportPayload, + SetSoftKeyboardPayload as ProtobufSetSoftKeyboardPayload, + SetTabFitPayload as ProtobufSetTabFitPayload, SetTimeoutPayload, ShowCursorPayload, ShowFloatingPanesPayload as ProtobufShowFloatingPanesPayload, ShowFloatingPanesResponse as ProtobufShowFloatingPanesResponse, ShowPaneWithIdPayload, - StackPanesPayload, SubscribePayload, SwitchSessionPayload, SwitchTabToIdPayload, - SwitchTabToPayload, TogglePaneBorderlessPayload, TogglePaneEmbedOrEjectForPaneIdPayload, - TogglePaneIdFullscreenPayload, UnsubscribePayload, WebRequestPayload, - WriteCharsToPaneIdPayload, WriteToPaneIdPayload, + Size as ProtobufSize, StackPanesPayload, SubscribePayload, SwitchSessionPayload, + SwitchTabToIdPayload, SwitchTabToPayload, TogglePaneBorderlessPayload, + TogglePaneEmbedOrEjectForPaneIdPayload, TogglePaneIdFullscreenPayload, UnsubscribePayload, + WebRequestPayload, WriteCharsToPaneIdPayload, WriteToPaneIdPayload, }, plugin_permission::PermissionType as ProtobufPermissionType, resize::ResizeAction as ProtobufResizeAction, @@ -141,11 +145,26 @@ use crate::data::{ }; use crate::input::actions::Action; use crate::input::layout::PercentOrFixed; +use crate::pane_size::Size; use std::collections::BTreeMap; use std::convert::TryFrom; use std::path::PathBuf; +fn size_from_protobuf(size: ProtobufSize) -> Size { + Size { + rows: size.rows as usize, + cols: size.cols as usize, + } +} + +fn size_to_protobuf(size: Size) -> ProtobufSize { + ProtobufSize { + rows: size.rows as u32, + cols: size.cols as u32, + } +} + impl Into<FloatingPaneCoordinates> for ProtobufFloatingPaneCoordinates { fn into(self) -> FloatingPaneCoordinates { FloatingPaneCoordinates { @@ -900,6 +919,27 @@ impl TryFrom<ProtobufPluginCommand> for PluginCommand { }), _ => Err("Mismatched payload for NewTab"), }, + Some(CommandName::NewTabUnfocused) => match protobuf_plugin_command.payload { + Some(Payload::NewTabUnfocusedPayload(payload)) => { + Ok(PluginCommand::NewTabUnfocused { + name: payload.name, + cwd: payload.cwd, + }) + }, + None => Ok(PluginCommand::NewTabUnfocused { + name: None, + cwd: None, + }), + _ => Err("Mismatched payload for NewTabUnfocused"), + }, + Some(CommandName::NewTiledPaneInTab) => match protobuf_plugin_command.payload { + Some(Payload::NewTiledPaneInTabPayload(payload)) => { + Ok(PluginCommand::NewTiledPaneInTab { + tab_position: payload.tab_position as usize, + }) + }, + _ => Err("Mismatched payload for NewTiledPaneInTab"), + }, Some(CommandName::GoToNextTab) => { if protobuf_plugin_command.payload.is_some() { return Err("GoToNextTab should not have a payload"); @@ -1448,6 +1488,38 @@ impl TryFrom<ProtobufPluginCommand> for PluginCommand { Some(CommandName::DeleteAllDeadSessionsAndReply) => { Ok(PluginCommand::DeleteAllDeadSessionsAndReply) }, + Some(CommandName::SetSoftKeyboard) => match protobuf_plugin_command.payload { + Some(Payload::SetSoftKeyboardPayload(payload)) => { + Ok(PluginCommand::SetSoftKeyboard(payload.on)) + }, + _ => Err("Mismatched payload for SetSoftKeyboard"), + }, + Some(CommandName::SetTabFit) => match protobuf_plugin_command.payload { + Some(Payload::SetTabFitPayload(payload)) => { + let fit = match (payload.pane_id, payload.size) { + (Some(pane_id), Some(size)) => { + Some((pane_id.try_into()?, size_from_protobuf(size))) + }, + (None, None) => None, + _ => return Err("Malformed set_tab_fit_payload payload"), + }; + Ok(PluginCommand::SetTabFit { + tab_id: payload.tab_id as usize, + fit, + }) + }, + _ => Err("Mismatched payload for SetTabFit"), + }, + Some(CommandName::ExitMobileMode) => match protobuf_plugin_command.payload { + Some(_) => Err("ExitMobileMode should have no payload, found a payload"), + None => Ok(PluginCommand::ExitMobileMode), + }, + Some(CommandName::SetShadowFocus) => match protobuf_plugin_command.payload { + Some(Payload::SetShadowFocusPayload(pane_id)) => { + Ok(PluginCommand::SetShadowFocus(pane_id.try_into()?)) + }, + _ => Err("Mismatched payload for SetShadowFocus"), + }, Some(CommandName::DumpSessionLayout) => match protobuf_plugin_command.payload { Some(Payload::DumpSessionLayoutPayload(payload)) => { Ok(PluginCommand::DumpSessionLayout { @@ -2890,6 +2962,21 @@ impl TryFrom<PluginCommand> for ProtobufPluginCommand { name: CommandName::NewTab as i32, payload: Some(Payload::NewTabPayload(NewTabPayload { name, cwd })), }), + PluginCommand::NewTabUnfocused { name, cwd } => Ok(ProtobufPluginCommand { + name: CommandName::NewTabUnfocused as i32, + payload: Some(Payload::NewTabUnfocusedPayload(NewTabUnfocusedPayload { + name, + cwd, + })), + }), + PluginCommand::NewTiledPaneInTab { tab_position } => Ok(ProtobufPluginCommand { + name: CommandName::NewTiledPaneInTab as i32, + payload: Some(Payload::NewTiledPaneInTabPayload( + NewTiledPaneInTabPayload { + tab_position: tab_position as u32, + }, + )), + }), PluginCommand::GoToNextTab => Ok(ProtobufPluginCommand { name: CommandName::GoToNextTab as i32, payload: None, @@ -3315,6 +3402,32 @@ impl TryFrom<PluginCommand> for ProtobufPluginCommand { name: CommandName::DeleteAllDeadSessionsAndReply as i32, payload: None, }), + PluginCommand::SetSoftKeyboard(on) => Ok(ProtobufPluginCommand { + name: CommandName::SetSoftKeyboard as i32, + payload: Some(Payload::SetSoftKeyboardPayload( + ProtobufSetSoftKeyboardPayload { on }, + )), + }), + PluginCommand::SetTabFit { tab_id, fit } => { + let (pane_id, size) = match fit { + Some((pane_id, size)) => { + (Some(pane_id.try_into()?), Some(size_to_protobuf(size))) + }, + None => (None, None), + }; + Ok(ProtobufPluginCommand { + name: CommandName::SetTabFit as i32, + payload: Some(Payload::SetTabFitPayload(ProtobufSetTabFitPayload { + tab_id: tab_id as u32, + pane_id, + size, + })), + }) + }, + PluginCommand::ExitMobileMode => Ok(ProtobufPluginCommand { + name: CommandName::ExitMobileMode as i32, + payload: None, + }), PluginCommand::DumpSessionLayout { tab_index } => Ok(ProtobufPluginCommand { name: CommandName::DumpSessionLayout as i32, payload: tab_index.map(|idx| { @@ -3437,6 +3550,10 @@ impl TryFrom<PluginCommand> for ProtobufPluginCommand { name: CommandName::SendSigintToPaneId as i32, payload: Some(Payload::SendSigintToPaneIdPayload(pane_id.try_into()?)), }), + PluginCommand::SetShadowFocus(pane_id) => Ok(ProtobufPluginCommand { + name: CommandName::SetShadowFocus as i32, + payload: Some(Payload::SetShadowFocusPayload(pane_id.try_into()?)), + }), PluginCommand::SendSigkillToPaneId(pane_id) => Ok(ProtobufPluginCommand { name: CommandName::SendSigkillToPaneId as i32, payload: Some(Payload::SendSigkillToPaneIdPayload(pane_id.try_into()?)), @@ -4433,7 +4550,8 @@ impl TryFrom<PluginCommand> for ProtobufPluginCommand { // Conversion implementations for tab creation response types use crate::data::{ BreakPanesToNewTabResponse, BreakPanesToTabWithIdResponse, BreakPanesToTabWithIndexResponse, - FocusOrCreateTabResponse, NewTabResponse, NewTabsResponse, OpenCommandPaneBackgroundResponse, + FocusOrCreateTabResponse, NewTabResponse, NewTabUnfocusedResponse, NewTabsResponse, + NewTiledPaneInTabResponse, OpenCommandPaneBackgroundResponse, OpenCommandPaneFloatingNearPluginResponse, OpenCommandPaneFloatingResponse, OpenCommandPaneInPlaceOfPaneIdResponse, OpenCommandPaneInPlaceOfPluginResponse, OpenCommandPaneInPlaceResponse, OpenCommandPaneNearPluginResponse, OpenCommandPaneResponse, @@ -4468,6 +4586,47 @@ impl From<NewTabResponse> for ProtobufNewTabResponse { } } +impl TryFrom<ProtobufNewTabUnfocusedResponse> for NewTabUnfocusedResponse { + type Error = &'static str; + fn try_from(protobuf: ProtobufNewTabUnfocusedResponse) -> Result<Self, Self::Error> { + match protobuf.result { + Some(new_tab_unfocused_response::Result::TabId(id)) => Ok(Some(id as usize)), + Some(new_tab_unfocused_response::Result::None(_)) | None => Ok(None), + } + } +} + +impl From<NewTabUnfocusedResponse> for ProtobufNewTabUnfocusedResponse { + fn from(response: NewTabUnfocusedResponse) -> Self { + match response { + Some(tab_id) => ProtobufNewTabUnfocusedResponse { + result: Some(new_tab_unfocused_response::Result::TabId(tab_id as u64)), + }, + None => ProtobufNewTabUnfocusedResponse { + result: Some(new_tab_unfocused_response::Result::None(true)), + }, + } + } +} + +impl TryFrom<ProtobufNewTiledPaneInTabResponse> for NewTiledPaneInTabResponse { + type Error = &'static str; + fn try_from(protobuf: ProtobufNewTiledPaneInTabResponse) -> Result<Self, Self::Error> { + match protobuf.pane_id { + Some(pane_id) => Ok(Some(pane_id.try_into()?)), + None => Ok(None), + } + } +} + +impl From<NewTiledPaneInTabResponse> for ProtobufNewTiledPaneInTabResponse { + fn from(response: NewTiledPaneInTabResponse) -> Self { + ProtobufNewTiledPaneInTabResponse { + pane_id: response.map(|p| p.try_into().unwrap()), + } + } +} + impl TryFrom<ProtobufNewTabsResponse> for NewTabsResponse { type Error = &'static str; fn try_from(protobuf: ProtobufNewTabsResponse) -> Result<Self, Self::Error> { @@ -5053,3 +5212,47 @@ impl From<OpenPluginPaneFloatingResponse> for ProtobufOpenPluginPaneFloatingResp } } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::data::{PaneId, PluginCommand}; + use crate::pane_size::Size; + + #[test] + fn set_tab_fit_set_protobuf_round_trip() { + let original = PluginCommand::SetTabFit { + tab_id: 7, + fit: Some((PaneId::Terminal(3), Size { rows: 24, cols: 80 })), + }; + let protobuf: ProtobufPluginCommand = original.try_into().expect("encode"); + let decoded: PluginCommand = protobuf.try_into().expect("decode"); + match decoded { + PluginCommand::SetTabFit { tab_id, fit } => { + assert_eq!(tab_id, 7); + assert_eq!( + fit, + Some((PaneId::Terminal(3), Size { rows: 24, cols: 80 })) + ); + }, + other => panic!("expected SetTabFit, got {:?}", other), + } + } + + #[test] + fn set_tab_fit_clear_protobuf_round_trip() { + let original = PluginCommand::SetTabFit { + tab_id: 7, + fit: None, + }; + let protobuf: ProtobufPluginCommand = original.try_into().expect("encode"); + let decoded: PluginCommand = protobuf.try_into().expect("decode"); + match decoded { + PluginCommand::SetTabFit { tab_id, fit } => { + assert_eq!(tab_id, 7); + assert_eq!(fit, None); + }, + other => panic!("expected SetTabFit, got {:?}", other), + } + } +} diff --git a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__cli_arguments_override_config_options.snap b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__cli_arguments_override_config_options.snap index a13669e5..0d51e89b 100644 --- a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__cli_arguments_override_config_options.snap +++ b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__cli_arguments_override_config_options.snap @@ -52,4 +52,7 @@ Options { enforce_https_for_localhost: None, post_command_discovery_hook: None, client_async_worker_tasks: None, + mobile_layout: None, + mobile_threshold_cols: None, + mobile_threshold_rows: None, } diff --git a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__cli_arguments_override_layout_options.snap b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__cli_arguments_override_layout_options.snap index 9a0e7eda..1b0c154a 100644 --- a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__cli_arguments_override_layout_options.snap +++ b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__cli_arguments_override_layout_options.snap @@ -52,4 +52,7 @@ Options { enforce_https_for_localhost: None, post_command_discovery_hook: None, client_async_worker_tasks: None, + mobile_layout: None, + mobile_threshold_cols: None, + mobile_threshold_rows: None, } diff --git a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__default_config_with_no_cli_arguments-3.snap b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__default_config_with_no_cli_arguments-3.snap index 52734174..08b89aac 100644 --- a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__default_config_with_no_cli_arguments-3.snap +++ b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__default_config_with_no_cli_arguments-3.snap @@ -50,4 +50,7 @@ Options { enforce_https_for_localhost: None, post_command_discovery_hook: None, client_async_worker_tasks: None, + mobile_layout: None, + mobile_threshold_cols: None, + mobile_threshold_rows: None, } diff --git a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__default_config_with_no_cli_arguments.snap b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__default_config_with_no_cli_arguments.snap index 2b17d5a3..a8a144a8 100644 --- a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__default_config_with_no_cli_arguments.snap +++ b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__default_config_with_no_cli_arguments.snap @@ -6028,6 +6028,9 @@ Config { enforce_https_for_localhost: None, post_command_discovery_hook: None, client_async_worker_tasks: None, + mobile_layout: None, + mobile_threshold_cols: None, + mobile_threshold_rows: None, }, themes: {}, plugins: PluginAliases { @@ -6082,6 +6085,18 @@ Config { "/", ), }, + "mobile": RunPlugin { + _allow_exec_host_cmd: false, + location: Zellij( + PluginTag( + "mobile", + ), + ), + configuration: PluginUserConfiguration( + {}, + ), + initial_cwd: None, + }, "plugin-manager": RunPlugin { _allow_exec_host_cmd: false, location: Zellij( @@ -6189,5 +6204,6 @@ Config { cursor_style: None, mac_option_is_meta: true, base_url: None, + font_size: None, }, } diff --git a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_env_vars_override_config_env_vars.snap b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_env_vars_override_config_env_vars.snap index 1f1e8268..4792ab0c 100644 --- a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_env_vars_override_config_env_vars.snap +++ b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_env_vars_override_config_env_vars.snap @@ -6028,6 +6028,9 @@ Config { enforce_https_for_localhost: None, post_command_discovery_hook: None, client_async_worker_tasks: None, + mobile_layout: None, + mobile_threshold_cols: None, + mobile_threshold_rows: None, }, themes: {}, plugins: PluginAliases { @@ -6082,6 +6085,18 @@ Config { "/", ), }, + "mobile": RunPlugin { + _allow_exec_host_cmd: false, + location: Zellij( + PluginTag( + "mobile", + ), + ), + configuration: PluginUserConfiguration( + {}, + ), + initial_cwd: None, + }, "plugin-manager": RunPlugin { _allow_exec_host_cmd: false, location: Zellij( @@ -6193,5 +6208,6 @@ Config { cursor_style: None, mac_option_is_meta: true, base_url: None, + font_size: None, }, } diff --git a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_keybinds_override_config_keybinds.snap b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_keybinds_override_config_keybinds.snap index 8ad32dc7..8f5baed6 100644 --- a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_keybinds_override_config_keybinds.snap +++ b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_keybinds_override_config_keybinds.snap @@ -137,6 +137,9 @@ Config { enforce_https_for_localhost: None, post_command_discovery_hook: None, client_async_worker_tasks: None, + mobile_layout: None, + mobile_threshold_cols: None, + mobile_threshold_rows: None, }, themes: {}, plugins: PluginAliases { @@ -191,6 +194,18 @@ Config { "/", ), }, + "mobile": RunPlugin { + _allow_exec_host_cmd: false, + location: Zellij( + PluginTag( + "mobile", + ), + ), + configuration: PluginUserConfiguration( + {}, + ), + initial_cwd: None, + }, "plugin-manager": RunPlugin { _allow_exec_host_cmd: false, location: Zellij( @@ -298,5 +313,6 @@ Config { cursor_style: None, mac_option_is_meta: true, base_url: None, + font_size: None, }, } diff --git a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_options_override_config_options.snap b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_options_override_config_options.snap index 74927ae6..40822e6e 100644 --- a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_options_override_config_options.snap +++ b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_options_override_config_options.snap @@ -52,4 +52,7 @@ Options { enforce_https_for_localhost: None, post_command_discovery_hook: None, client_async_worker_tasks: None, + mobile_layout: None, + mobile_threshold_cols: None, + mobile_threshold_rows: None, } diff --git a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_themes_override_config_themes.snap b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_themes_override_config_themes.snap index 28c7b32d..2cb1c9af 100644 --- a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_themes_override_config_themes.snap +++ b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_themes_override_config_themes.snap @@ -6028,6 +6028,9 @@ Config { enforce_https_for_localhost: None, post_command_discovery_hook: None, client_async_worker_tasks: None, + mobile_layout: None, + mobile_threshold_cols: None, + mobile_threshold_rows: None, }, themes: { "other-theme-from-config": Theme { @@ -7853,6 +7856,18 @@ Config { "/", ), }, + "mobile": RunPlugin { + _allow_exec_host_cmd: false, + location: Zellij( + PluginTag( + "mobile", + ), + ), + configuration: PluginUserConfiguration( + {}, + ), + initial_cwd: None, + }, "plugin-manager": RunPlugin { _allow_exec_host_cmd: false, location: Zellij( @@ -7960,5 +7975,6 @@ Config { cursor_style: None, mac_option_is_meta: true, base_url: None, + font_size: None, }, } diff --git a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_ui_config_overrides_config_ui_config.snap b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_ui_config_overrides_config_ui_config.snap index 43f29c48..431c8fc4 100644 --- a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_ui_config_overrides_config_ui_config.snap +++ b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_ui_config_overrides_config_ui_config.snap @@ -6028,6 +6028,9 @@ Config { enforce_https_for_localhost: None, post_command_discovery_hook: None, client_async_worker_tasks: None, + mobile_layout: None, + mobile_threshold_cols: None, + mobile_threshold_rows: None, }, themes: {}, plugins: PluginAliases { @@ -6082,6 +6085,18 @@ Config { "/", ), }, + "mobile": RunPlugin { + _allow_exec_host_cmd: false, + location: Zellij( + PluginTag( + "mobile", + ), + ), + configuration: PluginUserConfiguration( + {}, + ), + initial_cwd: None, + }, "plugin-manager": RunPlugin { _allow_exec_host_cmd: false, location: Zellij( @@ -6189,5 +6204,6 @@ Config { cursor_style: None, mac_option_is_meta: true, base_url: None, + font_size: None, }, } |
