mirror of
https://github.com/zellij-org/zellij.git
synced 2026-10-11 22:50:07 +00:00
feat(nested-sessions): report a guest's input mode and key bindings to its host (#5589)
* refactor(plugin-api): share the keybinding protobuf conversions
The conversion between a keybinding table and its protobuf form was written
out four times: once each way for the ModeUpdate and InitialKeybinds events.
Collect them into `keybinds_to_protobuf` and `keybinds_from_protobuf`.
Decoding now drops a binding whose key is missing instead of unwrapping it,
so a malformed or foreign payload can no longer panic the conversion, and a
mode whose key fails to convert costs only that binding rather than every
binding in the mode.
* feat(nested-sessions): carry input mode and keybindings over the protocol
A session hosting a nested session has no way to learn what that session is
doing, so its plugins cannot show hints for the session the user is actually
typing into. Add the three messages that carry it:
- GuestModeUpdate, sent by a guest whenever its input mode changes, so a host
always knows which of the guest's bindings currently apply.
- RequestGuestKeybinds and GuestKeybindsUpdate, a query and its answer, for
the far larger keybinding table. Keeping the table behind a query means it
is fetched once by whoever wants it rather than pushed at every host.
The table travels as an encoded plugin-API payload in an opaque bytes field.
Describing keybindings natively here would mean restating the whole Action
schema in this contract, which the codegen compiles with only its own include
path. Reusing the existing payload also means a host and a guest built from
different Zellij versions exchange whatever both understand and silently drop
the rest, which is the behavior this wire needs.
An input mode is written under the name InputMode::from_str reads, so adding a
mode needs no change to this contract and an unknown mode is rejected rather
than misread.
Peers advertise the new HintReporting capability, so each side can tell
whether the other can take part before asking it anything.
* feat(plugin-api): expose nested session mode and keybinding events
Plugins running in a host session can see that a pane holds a nested guest
session, but they have no way to learn what that guest is doing. A status bar
in the host therefore keeps showing the host's own hints while the user is
descended into the guest, which is the wrong keybinding table for the keys the
user is actually pressing.
Add two events and one command covering that gap:
- Event::NestedSessionModeUpdate reports a guest's input mode as it changes.
- Event::NestedSessionKeybinds delivers a guest's full keybinding table.
- PluginCommand::RequestNestedSessionKeybinds asks a guest pane for that
table, which arrives asynchronously as the event above.
Both events carry the pane the guest runs in and the guest's session name, so
a plugin watching several nested panes can tell them apart. Keybindings are
requested rather than pushed because the table is large and most plugins never
need it.
* feat(nested-sessions): report guest input mode and keybindings to the host
Wire the nested session hint events end to end.
A guest announces the HintReporting capability during the handshake and, from
then on, emits a GuestModeUpdate frame whenever its input mode changes. The
mode is reported on every change rather than only while the host is descended,
so the host already holds the right mode the moment focus arrives and the
status bar does not lag behind the descent.
The host turns each such frame into Event::NestedSessionModeUpdate. It answers
PluginCommand::RequestNestedSessionKeybinds by writing a RequestGuestKeybinds
frame into the guest pane; the guest replies with its keybinding table plus its
current mode, and the host forwards the table as Event::NestedSessionKeybinds.
Requests to guests that did not advertise HintReporting are dropped rather than
written, and frames from panes whose guest is not currently live are ignored,
matching how the other guest-to-host frames are handled.
Both events and the command require the ReadApplicationState permission, the
same gate ModeUpdate uses.
* test(nested-sessions): cover guest mode and keybinding reporting
Unit tests over the contract check that every input mode survives the round
trip under its own name, that a mode name the reading side does not know is
rejected rather than guessed at, and that a keybinding table keeps its modes,
keys and actions across the wire. The undecodable and empty payload cases are
covered too, since a host and a guest may be built from different versions.
Integration tests drive two real sessions bridged through a pane: both sides
advertise hint reporting during the handshake, the guest answers a keybinding
request with a populated table plus its current mode, and the guest reports
each mode change while the user is descended into it.
* test(nested-sessions): drop unknown capabilities without losing known ones
A peer built from a newer Zellij can advertise capabilities this side has
never heard of, so pin down that they are ignored individually rather than
invalidating the whole advertisement.
* feat(nested-sessions): carry the guest's base mode alongside its input mode
A host plugin that only knows a guest's current mode cannot tell which of the
guest's bindings are inherited from its base mode. Zellij's own hint rendering
leans on that distinction: bindings the base mode already advertises are
inherited by every other mode through `shared_except` groups, so repeating them
in each mode is noise. Without the guest's base mode a host plugin either
suppresses the wrong bindings or suppresses none.
A guest with a non-Normal base mode, such as an unlock-first config, is where
this shows: assuming Normal there hides the wrong half of its bindings.
The base mode rides along on the existing mode message, so a guest that has not
switched modes yet still reports one, resolved the way the rest of Screen
resolves it (the configured default mode is the base). It stays optional on the
wire, so a name the reading side does not recognize costs only the
inherited-binding distinction rather than the whole message.
* feat(nested-sessions): report the guest's mode when the handshake completes
A guest only pushed its input mode when that mode changed, so a host that
had just picked up a nested session learned nothing about it until the user
happened to switch modes inside it. A host plugin has no other way to tell
which of its panes holds a session worth asking for keybindings, which left
it unable to act on the feature at all in the common case of a guest sitting
in its starting mode.
Send the report once on contact as well, and reuse the same client mode
lookup the keybinding answer already did.
* docs(changelog): note the nested session hint reporting events
* fix(nested-sessions): drop guest keybindings on control characters
Nothing a guest says is authenticated. Its frames travel in the terminal
stream of the pane it runs in, so any program with that pane can write them,
including a keybinding table claiming a binding on a raw control character.
A host that draws those bindings puts their keys on its own screen, where a
control character is a terminal escape in the host's output rather than a
key anyone could press.
Drop them where the guest's payload is decoded, so no host plugin inherits
the problem. Zellij spells the control keys that do exist as their own
BareKey variants, so a Char holding one describes no binding worth keeping.
* test(nested-sessions): expect hint reporting in the announce ack
The host now advertises HintReporting alongside NestedControl when it
acknowledges a guest, so the assertion on the exact capability list had to
follow.
* fix(plugin-api): drop only what has no protobuf form when writing keybindings
keybinds_to_protobuf failed the whole table on the first key or mode it could
not represent, so one unrepresentable binding cost the caller every binding:
a ModeUpdate that failed to convert entirely, and a nested session's answer
that reached its host as an empty table indistinguishable from a session with
no bindings at all.
Drop the individual mode, key or action instead, which is what the reading
half already does and what the writing half already did for actions. A table
now survives the trip between builds minus whatever they do not have in
common.
* fix(nested-sessions): keep a host's view of the guest fresh across a reconfigure
A reconfigure rewrites a session's keybindings and base mode without going
through change_mode, and keybindings are otherwise only ever sent in answer
to a request. A host that had already asked once therefore kept drawing the
table the guest had before its config was reloaded, with no way of noticing
it had gone stale, and would not have picked up the new base mode until the
user happened to switch modes inside the guest.
Report both from reconfigure, next to the shortcut broadcast that already
happens there. Answer a keybinding request from the requesting client's own
table too, since a reconfigure with write_to_disk unset rebinds only the
client that asked: the mode in the same reply already came from that client,
so the two halves now describe the same thing.
* fix: adjust protocol + API
* Update CHANGELOG.md
---------
Co-authored-by: Aram Drevekenin <[email protected]>
This commit is contained in:
co-authored by
Aram Drevekenin
parent
81f56e1aed
commit
0591ca5432
@@ -10,6 +10,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
|
||||
* feat: upstream Yazelix graphics and stacked-pane fixes, named swap-layout selection and configurable pane-border styles (https://github.com/zellij-org/zellij/pull/5630)
|
||||
* feat: distributions -> allow using Zellij as a library (https://github.com/zellij-org/zellij/pull/5630)
|
||||
* fix: do not reuse client ids (https://github.com/zellij-org/zellij/pull/5630)
|
||||
* feat: nested sessions input-mode + keybindings reporting (https://github.com/zellij-org/zellij/issues/5561)
|
||||
|
||||
## [0.45.1] - 2026-08-28
|
||||
* fix: nested-session detection over SSH (https://github.com/zellij-org/zellij/pull/5522)
|
||||
|
||||
@@ -75,6 +75,10 @@ impl ZellijPlugin for State {
|
||||
.get("subscribe_mode_update")
|
||||
.map(|v| v == "true")
|
||||
.unwrap_or(false);
|
||||
let should_subscribe_nested_session_events = configuration
|
||||
.get("subscribe_nested_session_events")
|
||||
.map(|v| v == "true")
|
||||
.unwrap_or(false);
|
||||
self.configuration = configuration;
|
||||
subscribe(&[
|
||||
EventType::InputReceived,
|
||||
@@ -94,6 +98,12 @@ impl ZellijPlugin for State {
|
||||
if should_subscribe_mode_update {
|
||||
subscribe(&[EventType::ModeUpdate]);
|
||||
}
|
||||
if should_subscribe_nested_session_events {
|
||||
subscribe(&[
|
||||
EventType::NestedSessionModeUpdate,
|
||||
EventType::NestedSessionEnded,
|
||||
]);
|
||||
}
|
||||
watch_filesystem();
|
||||
}
|
||||
|
||||
@@ -945,8 +955,53 @@ impl ZellijPlugin for State {
|
||||
{
|
||||
focus_last_pane()
|
||||
},
|
||||
BareKey::Char('m')
|
||||
if key.has_only_modifiers(&[KeyModifier::Ctrl, KeyModifier::Alt]) =>
|
||||
{
|
||||
let skip_nested_keybinds = self
|
||||
.configuration
|
||||
.get("skip_nested_keybinds_request")
|
||||
.map(|v| v == "true")
|
||||
.unwrap_or(false);
|
||||
if !skip_nested_keybinds {
|
||||
self.explicit_string_to_render =
|
||||
Some(match get_nested_session_keybinds(PaneId::Terminal(1)) {
|
||||
Ok(nested_session_keybinds) => format!(
|
||||
"Nested keybinds ok: path={:?}, mode={:?}, base_mode={:?}, generation={}, bindings={}",
|
||||
nested_session_keybinds.session_path,
|
||||
nested_session_keybinds.mode,
|
||||
nested_session_keybinds.base_mode,
|
||||
nested_session_keybinds.keybinds_generation,
|
||||
nested_session_keybinds
|
||||
.keybinds
|
||||
.iter()
|
||||
.map(|(_mode, bindings)| bindings.len())
|
||||
.sum::<usize>()
|
||||
),
|
||||
Err(e) => format!("Nested keybinds error: {:?}", e),
|
||||
});
|
||||
}
|
||||
},
|
||||
_ => {},
|
||||
},
|
||||
Event::NestedSessionModeUpdate {
|
||||
pane_id,
|
||||
session_path,
|
||||
mode,
|
||||
keybinds_generation,
|
||||
..
|
||||
} => {
|
||||
self.explicit_string_to_render = Some(format!(
|
||||
"Nested mode update: pane={:?}, path={:?}, mode={:?}, generation={}",
|
||||
pane_id, session_path, mode, keybinds_generation
|
||||
));
|
||||
},
|
||||
Event::NestedSessionEnded { pane_id, reason } => {
|
||||
self.explicit_string_to_render = Some(format!(
|
||||
"Nested session ended: pane={:?}, reason={:?}",
|
||||
pane_id, reason
|
||||
));
|
||||
},
|
||||
Event::CustomMessage(message, payload) => {
|
||||
if message == "pong" {
|
||||
self.received_payload = Some(payload.clone());
|
||||
|
||||
@@ -685,7 +685,10 @@ pub async fn run_remote_client_terminal_loop(
|
||||
if reannounce_scheduler.on_tick(std::time::Instant::now()) {
|
||||
let announce = nested_session::NestedSessionMessage::Announce {
|
||||
session_name: session_name.clone(),
|
||||
capabilities: vec![nested_session::NestedSessionCapability::NestedControl],
|
||||
capabilities: vec![
|
||||
nested_session::NestedSessionCapability::NestedControl,
|
||||
nested_session::NestedSessionCapability::HintReporting,
|
||||
],
|
||||
};
|
||||
let mut stdout = os_input.get_stdout_writer();
|
||||
if stdout
|
||||
@@ -882,7 +885,10 @@ pub fn start_remote_client(
|
||||
stdout.write_all(ENABLE_FOCUS_REPORTING.as_bytes()).unwrap();
|
||||
let announce = nested_session::NestedSessionMessage::Announce {
|
||||
session_name: remote_session_name.clone(),
|
||||
capabilities: vec![nested_session::NestedSessionCapability::NestedControl],
|
||||
capabilities: vec![
|
||||
nested_session::NestedSessionCapability::NestedControl,
|
||||
nested_session::NestedSessionCapability::HintReporting,
|
||||
],
|
||||
};
|
||||
stdout
|
||||
.write_all(&nested_session::encode_frame(&announce))
|
||||
@@ -1200,7 +1206,10 @@ pub fn start_client(
|
||||
stdout.write_all(ENABLE_FOCUS_REPORTING.as_bytes()).unwrap();
|
||||
let announce = nested_session::NestedSessionMessage::Announce {
|
||||
session_name: own_session_name.clone(),
|
||||
capabilities: vec![nested_session::NestedSessionCapability::NestedControl],
|
||||
capabilities: vec![
|
||||
nested_session::NestedSessionCapability::NestedControl,
|
||||
nested_session::NestedSessionCapability::HintReporting,
|
||||
],
|
||||
};
|
||||
stdout
|
||||
.write_all(&nested_session::encode_frame(&announce))
|
||||
|
||||
@@ -48,7 +48,10 @@ impl NestedReannounce {
|
||||
}
|
||||
let announce = NestedSessionMessage::Announce {
|
||||
session_name: session_name.clone(),
|
||||
capabilities: vec![NestedSessionCapability::NestedControl],
|
||||
capabilities: vec![
|
||||
NestedSessionCapability::NestedControl,
|
||||
NestedSessionCapability::HintReporting,
|
||||
],
|
||||
};
|
||||
let mut stdout = os_input.get_stdout_writer();
|
||||
if stdout
|
||||
|
||||
@@ -451,6 +451,7 @@ pub struct NestedDepthThreeHarness {
|
||||
pub middle_pane: FakePtyHandle,
|
||||
pub inner_pane: FakePtyHandle,
|
||||
outer_to_middle: FrameLog,
|
||||
middle_to_outer: FrameLog,
|
||||
middle_to_inner: FrameLog,
|
||||
inner_to_middle: FrameLog,
|
||||
_frozen: Arc<AtomicBool>,
|
||||
@@ -469,6 +470,7 @@ impl NestedDepthThreeHarness {
|
||||
let outer_to_middle_bridge = bridge_guest_into_pane_for_host(&middle_pane, frozen.clone());
|
||||
let middle = outer_to_middle_bridge.guest;
|
||||
let outer_to_middle = outer_to_middle_bridge.host_to_guest;
|
||||
let middle_to_outer = outer_to_middle_bridge.guest_to_host;
|
||||
|
||||
middle.wait_for_app_load();
|
||||
let inner_pane = middle.expect_pty_spawn();
|
||||
@@ -488,6 +490,7 @@ impl NestedDepthThreeHarness {
|
||||
middle_pane,
|
||||
inner_pane,
|
||||
outer_to_middle,
|
||||
middle_to_outer,
|
||||
middle_to_inner,
|
||||
inner_to_middle,
|
||||
_frozen: frozen,
|
||||
@@ -499,6 +502,10 @@ impl NestedDepthThreeHarness {
|
||||
&self.outer_to_middle
|
||||
}
|
||||
|
||||
pub fn middle_to_outer_frames(&self) -> &FrameLog {
|
||||
&self.middle_to_outer
|
||||
}
|
||||
|
||||
pub fn middle_to_inner_frames(&self) -> &FrameLog {
|
||||
&self.middle_to_inner
|
||||
}
|
||||
@@ -573,6 +580,10 @@ impl NestedDepthThreeHarness {
|
||||
self.outer_to_middle.mark()
|
||||
}
|
||||
|
||||
pub fn mark_middle_to_outer(&self) -> usize {
|
||||
self.middle_to_outer.mark()
|
||||
}
|
||||
|
||||
pub fn mark_middle_to_inner(&self) -> usize {
|
||||
self.middle_to_inner.mark()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
#![cfg(unix)]
|
||||
|
||||
use zellij_integration_tests::keys;
|
||||
use zellij_integration_tests::nested::{NestedDepthThreeHarness, NestedHarness};
|
||||
use zellij_utils::data::InputMode;
|
||||
use zellij_utils::nested_session::{self, NestedSessionCapability, NestedSessionMessage};
|
||||
use zellij_utils::pane_size::Size;
|
||||
|
||||
const TERMINAL_SIZE: Size = Size {
|
||||
cols: 121,
|
||||
rows: 30,
|
||||
};
|
||||
|
||||
const DEPTH_THREE_TERMINAL_SIZE: Size = Size {
|
||||
cols: 160,
|
||||
rows: 40,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn both_sides_advertise_hint_reporting_during_the_handshake() {
|
||||
let nested = NestedHarness::start(TERMINAL_SIZE);
|
||||
nested.guest_to_host().wait_for(
|
||||
"the guest to advertise that it can report its hints",
|
||||
|message| {
|
||||
matches!(
|
||||
message,
|
||||
NestedSessionMessage::Announce { capabilities, .. }
|
||||
if capabilities.contains(&NestedSessionCapability::HintReporting)
|
||||
)
|
||||
},
|
||||
);
|
||||
nested.host_to_guest().wait_for(
|
||||
"the host to advertise that it can consume the guest's hints",
|
||||
|message| {
|
||||
matches!(
|
||||
message,
|
||||
NestedSessionMessage::AnnounceAck { capabilities, .. }
|
||||
if capabilities.contains(&NestedSessionCapability::HintReporting)
|
||||
)
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_guest_reports_its_mode_as_soon_as_the_handshake_completes() {
|
||||
let nested = NestedHarness::start(TERMINAL_SIZE);
|
||||
nested.wait_for_guest_to_announce();
|
||||
nested.wait_for_host_to_acknowledge_guest();
|
||||
|
||||
nested.guest_to_host().wait_for(
|
||||
"the guest to report its starting mode without being asked",
|
||||
|message| {
|
||||
matches!(
|
||||
message,
|
||||
NestedSessionMessage::GuestModeUpdate {
|
||||
mode: InputMode::Normal,
|
||||
base_mode: Some(_),
|
||||
session_path,
|
||||
..
|
||||
} if session_path.len() == 1
|
||||
)
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_guest_answers_a_keybinding_request_with_its_table_and_current_mode() {
|
||||
let nested = NestedHarness::start(TERMINAL_SIZE);
|
||||
nested.wait_for_guest_to_announce();
|
||||
nested.wait_for_host_to_acknowledge_guest();
|
||||
|
||||
let since = nested.mark_guest_to_host();
|
||||
nested.guest.send_stdin(&nested_session::encode_frame(
|
||||
&NestedSessionMessage::RequestGuestKeybinds { request_id: 41 },
|
||||
));
|
||||
|
||||
nested.guest_to_host().wait_for_after(
|
||||
since,
|
||||
"the guest to answer request 41 with its table, mode and base mode",
|
||||
|message| {
|
||||
matches!(
|
||||
message,
|
||||
NestedSessionMessage::GuestKeybindsReply {
|
||||
request_id: 41,
|
||||
result: Ok(nested_session_keybinds),
|
||||
} if nested_session_keybinds.base_mode.is_some()
|
||||
&& nested_session_keybinds.session_path.len() == 1
|
||||
&& nested_session_keybinds
|
||||
.keybinds
|
||||
.iter()
|
||||
.any(|(_, bindings)| !bindings.is_empty())
|
||||
)
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_guest_reports_its_mode_as_the_user_switches_modes_inside_it() {
|
||||
let nested = NestedHarness::start(TERMINAL_SIZE);
|
||||
nested.wait_for_guest_to_announce();
|
||||
nested.wait_for_host_to_acknowledge_guest();
|
||||
nested.descend_into_guest_via_modal();
|
||||
nested.wait_for_host_to_descend_into_guest();
|
||||
|
||||
let since = nested.mark_guest_to_host();
|
||||
nested.host.send_stdin(&keys::ctrl('p'));
|
||||
nested.guest_to_host().wait_for_after(
|
||||
since,
|
||||
"the guest to report that it entered pane mode",
|
||||
|message| {
|
||||
matches!(
|
||||
message,
|
||||
NestedSessionMessage::GuestModeUpdate {
|
||||
mode: InputMode::Pane,
|
||||
..
|
||||
}
|
||||
)
|
||||
},
|
||||
);
|
||||
|
||||
let since = nested.mark_guest_to_host();
|
||||
nested.host.send_stdin(&keys::ESC);
|
||||
nested.guest_to_host().wait_for_after(
|
||||
since,
|
||||
"the guest to report that it left pane mode again",
|
||||
|message| {
|
||||
matches!(
|
||||
message,
|
||||
NestedSessionMessage::GuestModeUpdate {
|
||||
mode: InputMode::Normal,
|
||||
..
|
||||
}
|
||||
)
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_middle_session_reports_and_answers_for_the_session_its_keys_go_to() {
|
||||
let nested = NestedDepthThreeHarness::start_depth_three(DEPTH_THREE_TERMINAL_SIZE);
|
||||
nested.boot_and_descend_depth_three();
|
||||
|
||||
nested.middle_to_outer_frames().wait_for(
|
||||
"the middle session to report the inner session's mode once its keys go there",
|
||||
|message| {
|
||||
matches!(
|
||||
message,
|
||||
NestedSessionMessage::GuestModeUpdate { session_path, .. }
|
||||
if session_path.len() == 2
|
||||
)
|
||||
},
|
||||
);
|
||||
|
||||
let since = nested.mark_middle_to_outer();
|
||||
nested.outer.send_stdin(&keys::ctrl('p'));
|
||||
nested.middle_to_outer_frames().wait_for_after(
|
||||
since,
|
||||
"the middle session to pass the inner session's switch to pane mode up to the outer session",
|
||||
|message| {
|
||||
matches!(
|
||||
message,
|
||||
NestedSessionMessage::GuestModeUpdate {
|
||||
mode: InputMode::Pane,
|
||||
session_path,
|
||||
..
|
||||
} if session_path.len() == 2
|
||||
)
|
||||
},
|
||||
);
|
||||
|
||||
let since = nested.mark_middle_to_outer();
|
||||
nested.middle.send_stdin(&nested_session::encode_frame(
|
||||
&NestedSessionMessage::RequestGuestKeybinds { request_id: 77 },
|
||||
));
|
||||
nested.middle_to_outer_frames().wait_for_after(
|
||||
since,
|
||||
"the middle session to relay request 77 to the inner session and pass its answer up",
|
||||
|message| {
|
||||
matches!(
|
||||
message,
|
||||
NestedSessionMessage::GuestKeybindsReply {
|
||||
request_id: 77,
|
||||
result: Ok(nested_session_keybinds),
|
||||
} if nested_session_keybinds.session_path.len() == 2
|
||||
&& nested_session_keybinds.mode == InputMode::Pane
|
||||
&& nested_session_keybinds
|
||||
.keybinds
|
||||
.iter()
|
||||
.any(|(_, bindings)| !bindings.is_empty())
|
||||
)
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -13299,3 +13299,355 @@ pub fn cli_pipe_is_released_when_plugin_panics_while_handling_it() {
|
||||
otherwise the `zellij pipe` client stays blocked until the plugin is unloaded"
|
||||
);
|
||||
}
|
||||
|
||||
enum NestedKeybindsAnswer {
|
||||
Reply(zellij_utils::data::NestedSessionKeybindsResponse),
|
||||
NeverAnswer,
|
||||
}
|
||||
|
||||
struct NestedKeybindsScenario {
|
||||
rendered: Vec<(u32, String)>,
|
||||
keybinds_requests: usize,
|
||||
request_to_result: Option<std::time::Duration>,
|
||||
}
|
||||
|
||||
fn run_nested_keybinds_scenario(
|
||||
plugin_configurations: Vec<BTreeMap<String, String>>,
|
||||
answer: NestedKeybindsAnswer,
|
||||
events_to_send: Vec<Event>,
|
||||
result_marker: &'static str,
|
||||
) -> NestedKeybindsScenario {
|
||||
let temp_folder = tempdir().unwrap();
|
||||
let plugin_host_folder = PathBuf::from(temp_folder.path());
|
||||
let cache_path = plugin_host_folder.join("permissions_test.kdl");
|
||||
let (plugin_thread_sender, screen_receiver, teardown) =
|
||||
create_plugin_thread(Some(plugin_host_folder), None);
|
||||
let client_id = 1;
|
||||
let size = Size {
|
||||
cols: 121,
|
||||
rows: 20,
|
||||
};
|
||||
let rendered = Arc::new(Mutex::new(vec![]));
|
||||
let keybinds_requests = Arc::new(Mutex::new(0));
|
||||
let request_to_result = Arc::new(Mutex::new(None));
|
||||
let screen_thread = std::thread::Builder::new()
|
||||
.name("fake_screen_thread".to_string())
|
||||
.spawn({
|
||||
let rendered = rendered.clone();
|
||||
let keybinds_requests = keybinds_requests.clone();
|
||||
let request_to_result = request_to_result.clone();
|
||||
let cache_path = cache_path.clone();
|
||||
let plugin_thread_sender = plugin_thread_sender.clone();
|
||||
move || {
|
||||
let mut unanswered_channels = vec![];
|
||||
let mut request_received_at = None;
|
||||
loop {
|
||||
let (event, _err_ctx) = screen_receiver
|
||||
.recv()
|
||||
.expect("failed to receive event on channel");
|
||||
match event {
|
||||
ScreenInstruction::RequestPluginPermissions(
|
||||
plugin_id,
|
||||
plugin_permission,
|
||||
) => {
|
||||
let _ = plugin_thread_sender.send(
|
||||
PluginInstruction::PermissionRequestResult(
|
||||
plugin_id,
|
||||
Some(client_id),
|
||||
plugin_permission.permissions,
|
||||
PermissionStatus::Granted,
|
||||
Some(cache_path.clone()),
|
||||
),
|
||||
);
|
||||
},
|
||||
ScreenInstruction::GetNestedSessionKeybinds {
|
||||
response_channel, ..
|
||||
} => {
|
||||
*keybinds_requests.lock().unwrap() += 1;
|
||||
request_received_at = Some(std::time::Instant::now());
|
||||
match &answer {
|
||||
NestedKeybindsAnswer::Reply(response) => {
|
||||
let _ = response_channel.send(response.clone());
|
||||
},
|
||||
NestedKeybindsAnswer::NeverAnswer => {
|
||||
unanswered_channels.push(response_channel);
|
||||
},
|
||||
}
|
||||
},
|
||||
ScreenInstruction::PluginBytes(plugin_render_assets) => {
|
||||
for asset in plugin_render_assets {
|
||||
let bytes = String::from_utf8_lossy(&asset.bytes).to_string();
|
||||
if bytes.contains(result_marker) {
|
||||
if let Some(received_at) = request_received_at {
|
||||
request_to_result
|
||||
.lock()
|
||||
.unwrap()
|
||||
.get_or_insert(received_at.elapsed());
|
||||
}
|
||||
}
|
||||
rendered.lock().unwrap().push((asset.plugin_id, bytes));
|
||||
}
|
||||
},
|
||||
ScreenInstruction::Exit => {
|
||||
break;
|
||||
},
|
||||
_ => {},
|
||||
}
|
||||
}
|
||||
drop(unanswered_channels);
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id));
|
||||
for (index, configuration) in plugin_configurations.into_iter().enumerate() {
|
||||
let run_plugin = RunPluginOrAlias::RunPlugin(RunPlugin {
|
||||
_allow_exec_host_cmd: false,
|
||||
location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)),
|
||||
configuration: PluginUserConfiguration::new(configuration),
|
||||
..Default::default()
|
||||
});
|
||||
let _ = plugin_thread_sender.send(PluginInstruction::Load(
|
||||
Some(false),
|
||||
false,
|
||||
false,
|
||||
Some(format!("plugin_{}", index)),
|
||||
run_plugin,
|
||||
Some(1),
|
||||
None,
|
||||
client_id,
|
||||
size,
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
));
|
||||
std::thread::sleep(std::time::Duration::from_millis(500));
|
||||
}
|
||||
for event in events_to_send {
|
||||
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
|
||||
None,
|
||||
Some(client_id),
|
||||
event,
|
||||
)]));
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(2000));
|
||||
teardown();
|
||||
screen_thread.join().unwrap();
|
||||
|
||||
let rendered = rendered.lock().unwrap().clone();
|
||||
let keybinds_requests = *keybinds_requests.lock().unwrap();
|
||||
let request_to_result = *request_to_result.lock().unwrap();
|
||||
NestedKeybindsScenario {
|
||||
rendered,
|
||||
keybinds_requests,
|
||||
request_to_result,
|
||||
}
|
||||
}
|
||||
|
||||
fn nested_keybinds_key() -> Event {
|
||||
Event::Key(
|
||||
KeyWithModifier::new(BareKey::Char('m'))
|
||||
.with_ctrl_modifier()
|
||||
.with_alt_modifier(),
|
||||
)
|
||||
}
|
||||
|
||||
fn sample_nested_session_keybinds() -> zellij_utils::data::NestedSessionKeybinds {
|
||||
zellij_utils::data::NestedSessionKeybinds {
|
||||
session_path: vec!["middle".to_owned(), "inner".to_owned()],
|
||||
mode: InputMode::Pane,
|
||||
base_mode: Some(InputMode::Normal),
|
||||
keybinds: vec![
|
||||
(
|
||||
InputMode::Normal,
|
||||
vec![(
|
||||
KeyWithModifier::new(BareKey::Char('p')).with_ctrl_modifier(),
|
||||
vec![Action::SwitchToMode {
|
||||
input_mode: InputMode::Pane,
|
||||
}],
|
||||
)],
|
||||
),
|
||||
(
|
||||
InputMode::Pane,
|
||||
vec![
|
||||
(
|
||||
KeyWithModifier::new(BareKey::Char('x')),
|
||||
vec![Action::CloseFocus],
|
||||
),
|
||||
(
|
||||
KeyWithModifier::new(BareKey::Esc),
|
||||
vec![Action::SwitchToMode {
|
||||
input_mode: InputMode::Normal,
|
||||
}],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
keybinds_generation: 7,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
pub fn get_nested_session_keybinds_returns_the_answer_to_the_plugin() {
|
||||
let scenario = run_nested_keybinds_scenario(
|
||||
vec![BTreeMap::new()],
|
||||
NestedKeybindsAnswer::Reply(Ok(sample_nested_session_keybinds())),
|
||||
vec![nested_keybinds_key()],
|
||||
"Nested keybinds",
|
||||
);
|
||||
assert_eq!(scenario.keybinds_requests, 1);
|
||||
assert!(
|
||||
scenario.rendered.iter().any(|(_, bytes)| bytes.contains(
|
||||
"Nested keybinds ok: path=[\"middle\", \"inner\"], mode=Pane, base_mode=Some(Normal), generation=7, bindings=3"
|
||||
)),
|
||||
"expected the plugin to render the answer, rendered: {:?}",
|
||||
scenario.rendered
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
pub fn get_nested_session_keybinds_times_out_when_the_guest_never_answers() {
|
||||
let scenario = run_nested_keybinds_scenario(
|
||||
vec![BTreeMap::new()],
|
||||
NestedKeybindsAnswer::NeverAnswer,
|
||||
vec![nested_keybinds_key()],
|
||||
"Nested keybinds",
|
||||
);
|
||||
assert_eq!(scenario.keybinds_requests, 1);
|
||||
assert!(
|
||||
scenario
|
||||
.rendered
|
||||
.iter()
|
||||
.any(|(_, bytes)| bytes.contains("Nested keybinds error: Timeout")),
|
||||
"expected the plugin to receive a timeout, rendered: {:?}",
|
||||
scenario.rendered
|
||||
);
|
||||
let waited = scenario
|
||||
.request_to_result
|
||||
.expect("the plugin never rendered its result");
|
||||
assert!(
|
||||
waited >= std::time::Duration::from_millis(900),
|
||||
"the plugin gave up after {:?}, before the one second timeout",
|
||||
waited
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
pub fn get_nested_session_keybinds_passes_errors_to_the_plugin() {
|
||||
let scenario = run_nested_keybinds_scenario(
|
||||
vec![BTreeMap::new()],
|
||||
NestedKeybindsAnswer::Reply(Err(
|
||||
zellij_utils::data::NestedSessionKeybindsError::GuestGone,
|
||||
)),
|
||||
vec![nested_keybinds_key()],
|
||||
"Nested keybinds",
|
||||
);
|
||||
assert_eq!(scenario.keybinds_requests, 1);
|
||||
assert!(
|
||||
scenario
|
||||
.rendered
|
||||
.iter()
|
||||
.any(|(_, bytes)| bytes.contains("Nested keybinds error: GuestGone")),
|
||||
"expected the plugin to receive GuestGone, rendered: {:?}",
|
||||
scenario.rendered
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
pub fn get_nested_session_keybinds_answers_only_the_plugin_that_asked() {
|
||||
let asking_plugin = BTreeMap::new();
|
||||
let mut other_plugin = BTreeMap::new();
|
||||
other_plugin.insert("skip_nested_keybinds_request".to_owned(), "true".to_owned());
|
||||
other_plugin.insert(
|
||||
"subscribe_nested_session_events".to_owned(),
|
||||
"true".to_owned(),
|
||||
);
|
||||
let scenario = run_nested_keybinds_scenario(
|
||||
vec![asking_plugin, other_plugin],
|
||||
NestedKeybindsAnswer::Reply(Ok(sample_nested_session_keybinds())),
|
||||
vec![nested_keybinds_key()],
|
||||
"Nested keybinds",
|
||||
);
|
||||
assert_eq!(scenario.keybinds_requests, 1);
|
||||
let rendering_plugins: std::collections::HashSet<u32> = scenario
|
||||
.rendered
|
||||
.iter()
|
||||
.map(|(plugin_id, _)| *plugin_id)
|
||||
.collect();
|
||||
assert_eq!(
|
||||
rendering_plugins.len(),
|
||||
2,
|
||||
"expected both plugins to render, rendered: {:?}",
|
||||
scenario.rendered
|
||||
);
|
||||
let plugins_that_got_the_answer: std::collections::HashSet<u32> = scenario
|
||||
.rendered
|
||||
.iter()
|
||||
.filter(|(_, bytes)| bytes.contains("Nested keybinds ok"))
|
||||
.map(|(plugin_id, _)| *plugin_id)
|
||||
.collect();
|
||||
assert_eq!(
|
||||
plugins_that_got_the_answer.len(),
|
||||
1,
|
||||
"expected exactly one plugin to see the answer, rendered: {:?}",
|
||||
scenario.rendered
|
||||
);
|
||||
assert!(
|
||||
!scenario
|
||||
.rendered
|
||||
.iter()
|
||||
.any(|(_, bytes)| bytes.contains("Nested mode update")
|
||||
|| bytes.contains("NestedSession")),
|
||||
"no nested session event should reach the plugin that did not ask, rendered: {:?}",
|
||||
scenario.rendered
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
pub fn nested_session_events_reach_subscribed_plugins() {
|
||||
let mut subscribed = BTreeMap::new();
|
||||
subscribed.insert(
|
||||
"subscribe_nested_session_events".to_owned(),
|
||||
"true".to_owned(),
|
||||
);
|
||||
let scenario = run_nested_keybinds_scenario(
|
||||
vec![subscribed],
|
||||
NestedKeybindsAnswer::NeverAnswer,
|
||||
vec![
|
||||
Event::NestedSessionModeUpdate {
|
||||
pane_id: zellij_utils::data::PaneId::Terminal(1),
|
||||
session_path: vec!["middle".to_owned(), "inner".to_owned()],
|
||||
mode: InputMode::Locked,
|
||||
base_mode: Some(InputMode::Normal),
|
||||
keybinds_generation: 3,
|
||||
},
|
||||
Event::NestedSessionEnded {
|
||||
pane_id: zellij_utils::data::PaneId::Terminal(1),
|
||||
reason: zellij_utils::data::NestedSessionEndReason::Unresponsive,
|
||||
},
|
||||
],
|
||||
"Nested",
|
||||
);
|
||||
assert_eq!(scenario.keybinds_requests, 0);
|
||||
assert!(
|
||||
scenario.rendered.iter().any(|(_, bytes)| bytes.contains(
|
||||
"Nested mode update: pane=Terminal(1), path=[\"middle\", \"inner\"], mode=Locked, generation=3"
|
||||
)),
|
||||
"expected the mode update to be rendered, rendered: {:?}",
|
||||
scenario.rendered
|
||||
);
|
||||
assert!(
|
||||
scenario.rendered.iter().any(|(_, bytes)| bytes
|
||||
.contains("Nested session ended: pane=Terminal(1), reason=Unresponsive")),
|
||||
"expected the ended event to be rendered, rendered: {:?}",
|
||||
scenario.rendered
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2171,6 +2171,8 @@ fn check_event_permission(
|
||||
| Event::SoftKeyboardVisibilityChanged(..)
|
||||
| Event::HintText(..)
|
||||
| Event::ActivePaneScroll(..)
|
||||
| Event::NestedSessionModeUpdate { .. }
|
||||
| Event::NestedSessionEnded { .. }
|
||||
| Event::InputReceived => PermissionType::ReadApplicationState,
|
||||
Event::WebServerStatus(..) => PermissionType::StartWebServer,
|
||||
Event::PaneRenderReport(..) => PermissionType::ReadPaneContents,
|
||||
|
||||
@@ -59,8 +59,9 @@ use prost::Message;
|
||||
use zellij_utils::{
|
||||
consts::{VERSION, ZELLIJ_SESSION_INFO_CACHE_DIR, ZELLIJ_SOCK_DIR, ZELLIJ_TMP_DIR},
|
||||
data::{
|
||||
CommandOrPlugin, CommandToRun, Direction, EventType, FileToOpen, InputMode, PluginCommand,
|
||||
PluginIds, PluginMessage, Resize, ResizeStrategy,
|
||||
CommandOrPlugin, CommandToRun, Direction, EventType, FileToOpen, InputMode,
|
||||
NestedSessionKeybindsError, PluginCommand, PluginIds, PluginMessage, Resize,
|
||||
ResizeStrategy,
|
||||
},
|
||||
errors::prelude::*,
|
||||
input::{
|
||||
@@ -72,7 +73,8 @@ use zellij_utils::{
|
||||
plugin_api::{
|
||||
event::{
|
||||
layout_parsing_error::ErrorType as ProtobufLayoutParsingErrorType,
|
||||
ProtobufLayoutParsingError, ProtobufPaneScrollbackResponse, ProtobufSyntaxError,
|
||||
nested_session_keybinds_response_to_protobuf, ProtobufLayoutParsingError,
|
||||
ProtobufPaneScrollbackResponse, ProtobufSyntaxError,
|
||||
},
|
||||
plugin_command::{
|
||||
dump_layout_response, dump_session_layout_response, hide_floating_panes_response,
|
||||
@@ -353,6 +355,9 @@ fn host_run_plugin_command(mut caller: Caller<'_, PluginEnv>) {
|
||||
PluginCommand::ToggleFocusFullscreen => toggle_focus_fullscreen(env),
|
||||
PluginCommand::ToggleFocusNoUiFullscreen => toggle_focus_no_ui_fullscreen(env),
|
||||
PluginCommand::FocusHostSession => focus_host_session(env),
|
||||
PluginCommand::GetNestedSessionKeybinds(pane_id) => {
|
||||
get_nested_session_keybinds(env, pane_id.into())
|
||||
},
|
||||
PluginCommand::TogglePaneFrames => toggle_pane_frames(env),
|
||||
PluginCommand::SetPaneFrameStyle(pane_frame_style) => {
|
||||
set_pane_frame_style(env, pane_frame_style)
|
||||
@@ -3219,6 +3224,45 @@ fn focus_host_session(env: &PluginEnv) {
|
||||
.non_fatal();
|
||||
}
|
||||
|
||||
fn get_nested_session_keybinds(env: &PluginEnv, pane_id: PaneId) {
|
||||
use crossbeam::channel::RecvTimeoutError;
|
||||
let (response_sender, response_receiver) = crossbeam::channel::bounded(1);
|
||||
env.senders
|
||||
.send_to_screen(ScreenInstruction::GetNestedSessionKeybinds {
|
||||
pane_id,
|
||||
response_channel: response_sender,
|
||||
})
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"failed to request nested session keybindings from plugin {}",
|
||||
env.name()
|
||||
)
|
||||
})
|
||||
.non_fatal();
|
||||
let response = match response_receiver
|
||||
.recv_timeout(zellij_utils::nested_session::KEYBINDS_REQUEST_TIMEOUT)
|
||||
{
|
||||
Ok(response) => response,
|
||||
Err(RecvTimeoutError::Timeout) | Err(RecvTimeoutError::Disconnected) => {
|
||||
log::warn!(
|
||||
"nested session keybindings for pane {:?} did not arrive in time for plugin {}",
|
||||
pane_id,
|
||||
env.plugin_id
|
||||
);
|
||||
Err(NestedSessionKeybindsError::Timeout)
|
||||
},
|
||||
};
|
||||
let protobuf_response = nested_session_keybinds_response_to_protobuf(response).or_else(|_| {
|
||||
nested_session_keybinds_response_to_protobuf(Err(NestedSessionKeybindsError::NotSupported))
|
||||
});
|
||||
match protobuf_response {
|
||||
Ok(protobuf_response) => {
|
||||
let _ = wasi_write_object(env, &protobuf_response.encode_to_vec());
|
||||
},
|
||||
Err(e) => log::error!("failed to encode nested session keybindings: {}", e),
|
||||
}
|
||||
}
|
||||
|
||||
fn toggle_pane_frames(env: &PluginEnv) {
|
||||
let error_msg = || format!("failed to toggle full screen in plugin {}", env.name());
|
||||
let action = Action::TogglePaneFrames;
|
||||
@@ -5684,6 +5728,7 @@ fn check_command_permission(
|
||||
| PluginCommand::CurrentSessionLastSavedTime
|
||||
| PluginCommand::GetPaneInfo(..)
|
||||
| PluginCommand::GetTabInfo(..)
|
||||
| PluginCommand::GetNestedSessionKeybinds(..)
|
||||
| PluginCommand::GetSessionList => PermissionType::ReadApplicationState,
|
||||
PluginCommand::RebindKeys { .. } | PluginCommand::Reconfigure(..) => {
|
||||
PermissionType::Reconfigure
|
||||
|
||||
@@ -2885,6 +2885,14 @@ pub(crate) fn route_thread_main(
|
||||
retry_queue
|
||||
);
|
||||
},
|
||||
Some(message @ zellij_utils::nested_session::NestedSessionMessage::RequestGuestKeybinds { .. }) => {
|
||||
let _ = send_to_screen_or_retry_queue!(
|
||||
senders,
|
||||
ScreenInstruction::NestedSessionMessageFromHost { client_id, message },
|
||||
instruction,
|
||||
retry_queue
|
||||
);
|
||||
},
|
||||
Some(message @ zellij_utils::nested_session::NestedSessionMessage::AncestryUpdate { .. }) => {
|
||||
let _ = send_to_screen_or_retry_queue!(
|
||||
senders,
|
||||
|
||||
+510
-9
@@ -42,10 +42,11 @@ use log::{debug, warn};
|
||||
use zellij_utils::data::{
|
||||
BorderStyle, BorderStyleOverride, CommandOrPlugin, Direction, EventType,
|
||||
FloatingPaneCoordinates, GetFocusedPaneInfoResponse, HostTerminalThemeMode, KeyWithModifier,
|
||||
LayoutInfo, LayoutWithError, ListPanesResponse, ListTabsResponse, NewPanePlacement,
|
||||
PaneContents, PaneInfo, PaneListEntry, PaneManifest, PaneRenderReport, PaneScrollbackResponse,
|
||||
PluginPermission, RegexHighlight, Resize, ResizeStrategy, SessionInfo, Styling, TabInfo,
|
||||
ThemeHue, WebSharing,
|
||||
KeybindsVec, LayoutInfo, LayoutWithError, ListPanesResponse, ListTabsResponse,
|
||||
NestedSessionEndReason, NestedSessionKeybinds, NestedSessionKeybindsError,
|
||||
NestedSessionKeybindsResponse, NewPanePlacement, PaneContents, PaneInfo, PaneListEntry,
|
||||
PaneManifest, PaneRenderReport, PaneScrollbackResponse, PluginPermission, RegexHighlight,
|
||||
Resize, ResizeStrategy, SessionInfo, Styling, TabInfo, ThemeHue, WebSharing,
|
||||
};
|
||||
use zellij_utils::errors::prelude::*;
|
||||
use zellij_utils::input::actions::Action;
|
||||
@@ -574,6 +575,10 @@ pub enum ScreenInstruction {
|
||||
client_id: ClientId,
|
||||
message: NestedSessionMessage,
|
||||
},
|
||||
GetNestedSessionKeybinds {
|
||||
pane_id: PaneId,
|
||||
response_channel: crossbeam::channel::Sender<NestedSessionKeybindsResponse>,
|
||||
},
|
||||
GuestModalChoice {
|
||||
client_id: ClientId,
|
||||
pane_id: PaneId,
|
||||
@@ -1122,6 +1127,9 @@ impl From<&ScreenInstruction> for ScreenContext {
|
||||
ScreenInstruction::NestedSessionMessageFromHost { .. } => {
|
||||
ScreenContext::NestedSessionMessageFromHost
|
||||
},
|
||||
ScreenInstruction::GetNestedSessionKeybinds { .. } => {
|
||||
ScreenContext::GetNestedSessionKeybinds
|
||||
},
|
||||
ScreenInstruction::GuestModalChoice { .. } => ScreenContext::GuestModalChoice,
|
||||
ScreenInstruction::ForwardedReplyFromHost { .. } => {
|
||||
ScreenContext::ForwardedReplyFromHost
|
||||
@@ -1525,6 +1533,30 @@ impl NestedGuestChoice {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(crate) struct GuestModeReport {
|
||||
mode: InputMode,
|
||||
base_mode: Option<InputMode>,
|
||||
session_path: Vec<String>,
|
||||
keybinds_generation: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) enum KeybindsReplyTo {
|
||||
Plugin(crossbeam::channel::Sender<NestedSessionKeybindsResponse>),
|
||||
Host {
|
||||
client_id: ClientId,
|
||||
request_id: u64,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct PendingKeybindsRequest {
|
||||
pane_id: PaneId,
|
||||
reply_to: KeybindsReplyTo,
|
||||
deadline: Instant,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum GuestModalOutcome {
|
||||
Zoom,
|
||||
@@ -1638,6 +1670,16 @@ pub(crate) struct Screen {
|
||||
dimmed_clients: HashSet<ClientId>,
|
||||
nested_guest_choices: HashMap<(ClientId, PaneId), NestedGuestChoice>,
|
||||
guest_ascend_keys: HashMap<PaneId, Vec<KeyWithModifier>>,
|
||||
guest_capabilities: HashMap<PaneId, Vec<NestedSessionCapability>>,
|
||||
guest_last_mode: HashMap<PaneId, GuestModeReport>,
|
||||
pending_keybinds_requests: HashMap<u64, PendingKeybindsRequest>,
|
||||
next_keybinds_request_id: u64,
|
||||
host_capabilities: Vec<NestedSessionCapability>,
|
||||
held_keybinds_requests: HashMap<ClientId, VecDeque<u64>>,
|
||||
own_keybinds_generation: u64,
|
||||
upward_generation: u64,
|
||||
upward_identity: Option<(Option<PaneId>, Vec<String>, u64)>,
|
||||
last_reported_upward: Option<GuestModeReport>,
|
||||
host_descend_keys: Vec<KeyWithModifier>,
|
||||
host_descended: bool,
|
||||
host_terminal_theme_mode: Option<HostTerminalThemeMode>,
|
||||
@@ -1850,6 +1892,16 @@ impl Screen {
|
||||
dimmed_clients: HashSet::new(),
|
||||
nested_guest_choices: HashMap::new(),
|
||||
guest_ascend_keys: HashMap::new(),
|
||||
guest_capabilities: HashMap::new(),
|
||||
guest_last_mode: HashMap::new(),
|
||||
pending_keybinds_requests: HashMap::new(),
|
||||
next_keybinds_request_id: 0,
|
||||
host_capabilities: vec![],
|
||||
held_keybinds_requests: HashMap::new(),
|
||||
own_keybinds_generation: 0,
|
||||
upward_generation: 0,
|
||||
upward_identity: None,
|
||||
last_reported_upward: None,
|
||||
host_descend_keys: vec![],
|
||||
host_descended: false,
|
||||
host_terminal_theme_mode: None,
|
||||
@@ -3138,8 +3190,11 @@ impl Screen {
|
||||
message: NestedSessionMessage,
|
||||
) {
|
||||
match message {
|
||||
NestedSessionMessage::Announce { session_name, .. } => {
|
||||
self.handle_nested_guest_announce(pane_id, session_name);
|
||||
NestedSessionMessage::Announce {
|
||||
session_name,
|
||||
capabilities,
|
||||
} => {
|
||||
self.handle_nested_guest_announce(pane_id, session_name, capabilities);
|
||||
},
|
||||
NestedSessionMessage::Pong => {
|
||||
self.nested_guest_tracker.on_pong(pane_id, Instant::now());
|
||||
@@ -3168,6 +3223,7 @@ impl Screen {
|
||||
),
|
||||
);
|
||||
}
|
||||
self.report_upward();
|
||||
let _ = self.render(None);
|
||||
},
|
||||
Some(direction) => {
|
||||
@@ -3209,6 +3265,60 @@ impl Screen {
|
||||
self.guest_ascend_keys.insert(pane_id, ascend_keys);
|
||||
self.refresh_nested_ascend_keys_for_pane(pane_id);
|
||||
},
|
||||
NestedSessionMessage::GuestModeUpdate {
|
||||
mode,
|
||||
base_mode,
|
||||
session_path,
|
||||
keybinds_generation,
|
||||
} => {
|
||||
if !self.nested_guest_tracker.is_tracked(pane_id) {
|
||||
log::debug!(
|
||||
"ignoring nested mode update from non-live guest pane {:?}",
|
||||
pane_id
|
||||
);
|
||||
return;
|
||||
}
|
||||
let report = GuestModeReport {
|
||||
mode,
|
||||
base_mode,
|
||||
session_path,
|
||||
keybinds_generation,
|
||||
};
|
||||
self.guest_last_mode.insert(pane_id, report.clone());
|
||||
let _ = self
|
||||
.bus
|
||||
.senders
|
||||
.send_to_plugin(PluginInstruction::Update(vec![(
|
||||
None,
|
||||
None,
|
||||
Event::NestedSessionModeUpdate {
|
||||
pane_id: pane_id.into(),
|
||||
session_path: report.session_path,
|
||||
mode: report.mode,
|
||||
base_mode: report.base_mode,
|
||||
keybinds_generation: report.keybinds_generation,
|
||||
},
|
||||
)]));
|
||||
self.report_upward();
|
||||
},
|
||||
NestedSessionMessage::GuestKeybindsReply { request_id, result } => {
|
||||
let belongs_to_pane = self
|
||||
.pending_keybinds_requests
|
||||
.get(&request_id)
|
||||
.map(|pending| pending.pane_id == pane_id)
|
||||
.unwrap_or(false);
|
||||
if !belongs_to_pane {
|
||||
log::debug!(
|
||||
"dropping nested keybinding reply {} from pane {:?} with no matching request",
|
||||
request_id,
|
||||
pane_id
|
||||
);
|
||||
return;
|
||||
}
|
||||
if let Some(pending) = self.pending_keybinds_requests.remove(&request_id) {
|
||||
self.deliver_keybinds_reply(pending, result);
|
||||
}
|
||||
},
|
||||
NestedSessionMessage::ToggleHostFullscreen { fullscreen } => {
|
||||
if !self.nested_guest_tracker.is_tracked(pane_id) {
|
||||
log::debug!(
|
||||
@@ -3239,7 +3349,316 @@ impl Screen {
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_nested_guest_announce(&mut self, pane_id: PaneId, guest_session_name: String) {
|
||||
pub fn get_nested_session_keybinds(&mut self, pane_id: PaneId, reply_to: KeybindsReplyTo) {
|
||||
let now = Instant::now();
|
||||
self.pending_keybinds_requests
|
||||
.retain(|_, pending| pending.deadline > now);
|
||||
let terminal_id = match pane_id {
|
||||
PaneId::Terminal(terminal_id) => terminal_id,
|
||||
PaneId::Plugin(_) => {
|
||||
return self.answer_keybinds_request(
|
||||
reply_to,
|
||||
pane_id,
|
||||
Err(NestedSessionKeybindsError::NotANestedSession),
|
||||
);
|
||||
},
|
||||
};
|
||||
if self.nested_guest_tracker.is_dormant(pane_id) {
|
||||
return self.answer_keybinds_request(
|
||||
reply_to,
|
||||
pane_id,
|
||||
Err(NestedSessionKeybindsError::GuestUnresponsive),
|
||||
);
|
||||
}
|
||||
if !self.nested_guest_tracker.is_tracked(pane_id) {
|
||||
return self.answer_keybinds_request(
|
||||
reply_to,
|
||||
pane_id,
|
||||
Err(NestedSessionKeybindsError::NotANestedSession),
|
||||
);
|
||||
}
|
||||
let guest_supports_hint_reporting = self
|
||||
.guest_capabilities
|
||||
.get(&pane_id)
|
||||
.map(|capabilities| capabilities.contains(&NestedSessionCapability::HintReporting))
|
||||
.unwrap_or(false);
|
||||
if !guest_supports_hint_reporting {
|
||||
return self.answer_keybinds_request(
|
||||
reply_to,
|
||||
pane_id,
|
||||
Err(NestedSessionKeybindsError::NotSupported),
|
||||
);
|
||||
}
|
||||
self.next_keybinds_request_id = self.next_keybinds_request_id.wrapping_add(1);
|
||||
let request_id = self.next_keybinds_request_id;
|
||||
self.pending_keybinds_requests.insert(
|
||||
request_id,
|
||||
PendingKeybindsRequest {
|
||||
pane_id,
|
||||
reply_to,
|
||||
deadline: now + nested_session::KEYBINDS_REQUEST_TIMEOUT,
|
||||
},
|
||||
);
|
||||
let _ = self
|
||||
.bus
|
||||
.senders
|
||||
.send_to_pty_writer(PtyWriteInstruction::Write(
|
||||
nested_session::encode_frame(&NestedSessionMessage::RequestGuestKeybinds {
|
||||
request_id,
|
||||
}),
|
||||
terminal_id,
|
||||
None,
|
||||
));
|
||||
}
|
||||
|
||||
fn answer_keybinds_request(
|
||||
&mut self,
|
||||
reply_to: KeybindsReplyTo,
|
||||
pane_id: PaneId,
|
||||
result: NestedSessionKeybindsResponse,
|
||||
) {
|
||||
self.deliver_keybinds_reply(
|
||||
PendingKeybindsRequest {
|
||||
pane_id,
|
||||
reply_to,
|
||||
deadline: Instant::now(),
|
||||
},
|
||||
result,
|
||||
);
|
||||
}
|
||||
|
||||
fn deliver_keybinds_reply(
|
||||
&mut self,
|
||||
pending: PendingKeybindsRequest,
|
||||
result: NestedSessionKeybindsResponse,
|
||||
) {
|
||||
match pending.reply_to {
|
||||
KeybindsReplyTo::Plugin(response_channel) => {
|
||||
let _ = response_channel.send(result);
|
||||
},
|
||||
KeybindsReplyTo::Host {
|
||||
client_id,
|
||||
request_id,
|
||||
} => {
|
||||
let relayed_from_current_source =
|
||||
self.refresh_upward_source() == Some(pending.pane_id);
|
||||
let result = result.map(|mut nested_session_keybinds| {
|
||||
nested_session_keybinds
|
||||
.session_path
|
||||
.insert(0, self.session_name.clone());
|
||||
nested_session_keybinds.keybinds_generation = if relayed_from_current_source {
|
||||
self.upward_generation
|
||||
} else {
|
||||
0
|
||||
};
|
||||
nested_session_keybinds
|
||||
});
|
||||
self.send_keybinds_reply_to_host(client_id, request_id, result);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn send_keybinds_reply_to_host(
|
||||
&mut self,
|
||||
client_id: ClientId,
|
||||
request_id: u64,
|
||||
result: NestedSessionKeybindsResponse,
|
||||
) {
|
||||
if self.nested_via_client_id != Some(client_id) {
|
||||
return;
|
||||
}
|
||||
let payload = nested_session::encode_keybinds_reply_payload(request_id, result);
|
||||
let _ = self
|
||||
.bus
|
||||
.senders
|
||||
.send_to_server(ServerInstruction::EmitNestedSessionFrameToClient(
|
||||
client_id, payload,
|
||||
));
|
||||
}
|
||||
|
||||
fn fail_pending_keybinds_requests_for_pane(
|
||||
&mut self,
|
||||
pane_id: PaneId,
|
||||
error: NestedSessionKeybindsError,
|
||||
) {
|
||||
let request_ids: Vec<u64> = self
|
||||
.pending_keybinds_requests
|
||||
.iter()
|
||||
.filter(|(_, pending)| pending.pane_id == pane_id)
|
||||
.map(|(request_id, _)| *request_id)
|
||||
.collect();
|
||||
for request_id in request_ids {
|
||||
if let Some(pending) = self.pending_keybinds_requests.remove(&request_id) {
|
||||
self.deliver_keybinds_reply(pending, Err(error));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn notify_plugins_nested_session_ended(
|
||||
&mut self,
|
||||
pane_id: PaneId,
|
||||
reason: NestedSessionEndReason,
|
||||
) {
|
||||
let _ = self
|
||||
.bus
|
||||
.senders
|
||||
.send_to_plugin(PluginInstruction::Update(vec![(
|
||||
None,
|
||||
None,
|
||||
Event::NestedSessionEnded {
|
||||
pane_id: pane_id.into(),
|
||||
reason,
|
||||
},
|
||||
)]));
|
||||
}
|
||||
|
||||
fn answer_keybinds_request_from_host(&mut self, client_id: ClientId, request_id: u64) {
|
||||
if !self
|
||||
.host_capabilities
|
||||
.contains(&NestedSessionCapability::HintReporting)
|
||||
{
|
||||
return;
|
||||
}
|
||||
match self.refresh_upward_source() {
|
||||
Some(guest_pane_id) => {
|
||||
self.get_nested_session_keybinds(
|
||||
guest_pane_id,
|
||||
KeybindsReplyTo::Host {
|
||||
client_id,
|
||||
request_id,
|
||||
},
|
||||
);
|
||||
},
|
||||
None => {
|
||||
let (mode, base_mode) = self.own_mode_for_host(client_id);
|
||||
let result = Ok(NestedSessionKeybinds {
|
||||
session_path: vec![self.session_name.clone()],
|
||||
mode,
|
||||
base_mode,
|
||||
keybinds: self.keybinds_for_client(client_id),
|
||||
keybinds_generation: self.upward_generation,
|
||||
});
|
||||
self.send_keybinds_reply_to_host(client_id, request_id, result);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn hold_keybinds_request_until_acknowledged(&mut self, client_id: ClientId, request_id: u64) {
|
||||
let held = self.held_keybinds_requests.entry(client_id).or_default();
|
||||
held.push_back(request_id);
|
||||
while held.len() > nested_session::MAX_HELD_KEYBINDS_REQUESTS {
|
||||
held.pop_front();
|
||||
}
|
||||
}
|
||||
|
||||
fn release_held_keybinds_requests(&mut self, acknowledged_client_id: ClientId) {
|
||||
let held = self
|
||||
.held_keybinds_requests
|
||||
.remove(&acknowledged_client_id)
|
||||
.unwrap_or_default();
|
||||
self.held_keybinds_requests.clear();
|
||||
for request_id in held {
|
||||
self.answer_keybinds_request_from_host(acknowledged_client_id, request_id);
|
||||
}
|
||||
}
|
||||
|
||||
fn upward_source_pane(&self) -> Option<PaneId> {
|
||||
let client_id = self.nested_via_client_id?;
|
||||
let active_pane_id = self.get_active_pane_id(&client_id)?;
|
||||
if self.should_route_keys_to_pane(client_id, active_pane_id)
|
||||
&& self.guest_last_mode.contains_key(&active_pane_id)
|
||||
{
|
||||
Some(active_pane_id)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn own_mode_for_host(&self, client_id: ClientId) -> (InputMode, Option<InputMode>) {
|
||||
let (mode, base_mode) = self.current_mode_for_client(client_id);
|
||||
let base_mode = base_mode
|
||||
.or(self.default_mode_info.base_mode)
|
||||
.unwrap_or(self.default_mode_info.mode);
|
||||
(mode, Some(base_mode))
|
||||
}
|
||||
|
||||
fn refresh_upward_source(&mut self) -> Option<PaneId> {
|
||||
self.current_upward_report()
|
||||
.map(|(source, _)| source)
|
||||
.flatten()
|
||||
}
|
||||
|
||||
fn current_upward_report(&mut self) -> Option<(Option<PaneId>, GuestModeReport)> {
|
||||
let client_id = self.nested_via_client_id?;
|
||||
let source = self.upward_source_pane();
|
||||
let (mode, base_mode, inner_path, inner_generation) =
|
||||
match source.and_then(|pane_id| self.guest_last_mode.get(&pane_id)) {
|
||||
Some(guest_report) => (
|
||||
guest_report.mode,
|
||||
guest_report.base_mode,
|
||||
guest_report.session_path.clone(),
|
||||
guest_report.keybinds_generation,
|
||||
),
|
||||
None => {
|
||||
let (mode, base_mode) = self.own_mode_for_host(client_id);
|
||||
(mode, base_mode, vec![], self.own_keybinds_generation)
|
||||
},
|
||||
};
|
||||
let identity = (source, inner_path.clone(), inner_generation);
|
||||
if self.upward_identity.as_ref() != Some(&identity) {
|
||||
self.upward_identity = Some(identity);
|
||||
self.upward_generation = self.upward_generation.wrapping_add(1);
|
||||
}
|
||||
let mut session_path = vec![self.session_name.clone()];
|
||||
session_path.extend(inner_path);
|
||||
Some((
|
||||
source,
|
||||
GuestModeReport {
|
||||
mode,
|
||||
base_mode,
|
||||
session_path,
|
||||
keybinds_generation: self.upward_generation,
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
fn report_upward(&mut self) {
|
||||
let Some(client_id) = self.nested_via_client_id else {
|
||||
return;
|
||||
};
|
||||
if !self
|
||||
.host_capabilities
|
||||
.contains(&NestedSessionCapability::HintReporting)
|
||||
{
|
||||
return;
|
||||
}
|
||||
let Some((_, report)) = self.current_upward_report() else {
|
||||
return;
|
||||
};
|
||||
if self.last_reported_upward.as_ref() == Some(&report) {
|
||||
return;
|
||||
}
|
||||
self.last_reported_upward = Some(report.clone());
|
||||
let payload = nested_session::encode_payload(&NestedSessionMessage::GuestModeUpdate {
|
||||
mode: report.mode,
|
||||
base_mode: report.base_mode,
|
||||
session_path: report.session_path,
|
||||
keybinds_generation: report.keybinds_generation,
|
||||
});
|
||||
let _ = self
|
||||
.bus
|
||||
.senders
|
||||
.send_to_server(ServerInstruction::EmitNestedSessionFrameToClient(
|
||||
client_id, payload,
|
||||
));
|
||||
}
|
||||
|
||||
fn handle_nested_guest_announce(
|
||||
&mut self,
|
||||
pane_id: PaneId,
|
||||
guest_session_name: String,
|
||||
guest_capabilities: Vec<NestedSessionCapability>,
|
||||
) {
|
||||
use crate::nested_guest::AnnounceKind;
|
||||
let terminal_id = match pane_id {
|
||||
PaneId::Terminal(terminal_id) => terminal_id,
|
||||
@@ -3252,6 +3671,7 @@ impl Screen {
|
||||
if is_fresh {
|
||||
self.nested_guest_choices
|
||||
.retain(|(_, choice_pane_id), _| *choice_pane_id != pane_id);
|
||||
self.guest_last_mode.remove(&pane_id);
|
||||
}
|
||||
let connected_client_ids: Vec<ClientId> =
|
||||
self.connected_clients.borrow().keys().copied().collect();
|
||||
@@ -3277,11 +3697,15 @@ impl Screen {
|
||||
return;
|
||||
},
|
||||
}
|
||||
self.guest_capabilities.insert(pane_id, guest_capabilities);
|
||||
let mut ancestry = self.nested_ancestry.clone();
|
||||
ancestry.push(self.session_name.clone());
|
||||
let announce_ack = NestedSessionMessage::AnnounceAck {
|
||||
ancestry,
|
||||
capabilities: vec![NestedSessionCapability::NestedControl],
|
||||
capabilities: vec![
|
||||
NestedSessionCapability::NestedControl,
|
||||
NestedSessionCapability::HintReporting,
|
||||
],
|
||||
descend_keys: self.own_descend_shortcut(),
|
||||
};
|
||||
let _ = self
|
||||
@@ -3302,6 +3726,9 @@ impl Screen {
|
||||
self.revive_nested_guest(pane_id);
|
||||
}
|
||||
let _ = self.render(None);
|
||||
if announce_kind != AnnounceKind::Refresh {
|
||||
let _ = self.log_and_report_session_state();
|
||||
}
|
||||
log::info!(
|
||||
"nested session handshake ({:?}): guest session {:?} announced in pane {:?}, sent announce_ack",
|
||||
announce_kind,
|
||||
@@ -3311,6 +3738,12 @@ impl Screen {
|
||||
}
|
||||
|
||||
fn suspend_nested_guest(&mut self, pane_id: PaneId) {
|
||||
self.guest_last_mode.remove(&pane_id);
|
||||
self.fail_pending_keybinds_requests_for_pane(
|
||||
pane_id,
|
||||
NestedSessionKeybindsError::GuestUnresponsive,
|
||||
);
|
||||
self.notify_plugins_nested_session_ended(pane_id, NestedSessionEndReason::Unresponsive);
|
||||
let _ = self
|
||||
.bus
|
||||
.senders
|
||||
@@ -3337,6 +3770,8 @@ impl Screen {
|
||||
client_id, pane_id, pane_id, false, None, false,
|
||||
));
|
||||
}
|
||||
self.report_upward();
|
||||
let _ = self.log_and_report_session_state();
|
||||
}
|
||||
|
||||
fn revive_nested_guest(&mut self, pane_id: PaneId) {
|
||||
@@ -3477,8 +3912,18 @@ impl Screen {
|
||||
}
|
||||
|
||||
pub fn clear_nested_guest(&mut self, pane_id: PaneId) {
|
||||
let was_nested_guest = self.nested_guest_tracker.is_known(pane_id);
|
||||
self.nested_guest_tracker.remove(pane_id);
|
||||
self.guest_ascend_keys.remove(&pane_id);
|
||||
self.guest_capabilities.remove(&pane_id);
|
||||
self.guest_last_mode.remove(&pane_id);
|
||||
self.fail_pending_keybinds_requests_for_pane(
|
||||
pane_id,
|
||||
NestedSessionKeybindsError::GuestGone,
|
||||
);
|
||||
if was_nested_guest {
|
||||
self.notify_plugins_nested_session_ended(pane_id, NestedSessionEndReason::Exited);
|
||||
}
|
||||
let _ = self
|
||||
.bus
|
||||
.senders
|
||||
@@ -3518,6 +3963,10 @@ impl Screen {
|
||||
client_id, pane_id, pane_id, false, None, false,
|
||||
));
|
||||
}
|
||||
if was_nested_guest {
|
||||
self.report_upward();
|
||||
let _ = self.log_and_report_session_state();
|
||||
}
|
||||
}
|
||||
|
||||
fn show_guest_modals_for_new_client(&mut self, client_id: ClientId) {
|
||||
@@ -3649,6 +4098,9 @@ impl Screen {
|
||||
entered_from_direction,
|
||||
true,
|
||||
));
|
||||
if self.nested_via_client_id == Some(client_id) {
|
||||
self.report_upward();
|
||||
}
|
||||
}
|
||||
|
||||
fn set_client_dimmed(
|
||||
@@ -4001,8 +4453,8 @@ impl Screen {
|
||||
match message {
|
||||
NestedSessionMessage::AnnounceAck {
|
||||
ancestry,
|
||||
capabilities,
|
||||
descend_keys,
|
||||
..
|
||||
} => {
|
||||
log::info!(
|
||||
"nested session handshake complete: this session is nested inside ancestry {:?}",
|
||||
@@ -4021,6 +4473,10 @@ impl Screen {
|
||||
let _ = self.bus.senders.send_to_server(
|
||||
ServerInstruction::EmitNestedSessionFrameToClient(client_id, payload),
|
||||
);
|
||||
self.host_capabilities = capabilities;
|
||||
self.last_reported_upward = None;
|
||||
self.release_held_keybinds_requests(client_id);
|
||||
self.report_upward();
|
||||
},
|
||||
NestedSessionMessage::ShortcutUpdate { descend_keys, .. } => {
|
||||
if self.host_descend_keys != descend_keys {
|
||||
@@ -4028,6 +4484,13 @@ impl Screen {
|
||||
self.update_all_clients_nesting_mode_info();
|
||||
}
|
||||
},
|
||||
NestedSessionMessage::RequestGuestKeybinds { request_id } => {
|
||||
if self.nested_via_client_id == Some(client_id) {
|
||||
self.answer_keybinds_request_from_host(client_id, request_id);
|
||||
} else {
|
||||
self.hold_keybinds_request_until_acknowledged(client_id, request_id);
|
||||
}
|
||||
},
|
||||
NestedSessionMessage::FullscreenState { fullscreen } => {
|
||||
self.host_fullscreen = fullscreen;
|
||||
self.own_fullscreen_requested = fullscreen;
|
||||
@@ -5271,7 +5734,13 @@ impl Screen {
|
||||
self.recompute_tab_size(prev_tab_id)
|
||||
.with_context(err_context)?;
|
||||
}
|
||||
self.held_keybinds_requests.remove(&client_id);
|
||||
self.pending_keybinds_requests.retain(|_, pending| {
|
||||
!matches!(pending.reply_to, KeybindsReplyTo::Host { client_id: host_client_id, .. } if host_client_id == client_id)
|
||||
});
|
||||
if self.nested_via_client_id == Some(client_id) {
|
||||
self.host_capabilities = vec![];
|
||||
self.last_reported_upward = None;
|
||||
self.nested_via_client_id = None;
|
||||
self.nested_ancestry = vec![];
|
||||
self.host_descended = false;
|
||||
@@ -6184,8 +6653,29 @@ impl Screen {
|
||||
.send_to_plugin(PluginInstruction::Update(bg_updates))
|
||||
.context("failed to update background plugins with mode info")?;
|
||||
}
|
||||
if self.nested_via_client_id == Some(client_id) {
|
||||
self.report_upward();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn current_mode_for_client(&self, client_id: ClientId) -> (InputMode, Option<InputMode>) {
|
||||
self.mode_info
|
||||
.get(&client_id)
|
||||
.map(|mode_info| (mode_info.mode, mode_info.base_mode))
|
||||
.unwrap_or((
|
||||
self.default_mode_info.mode,
|
||||
self.default_mode_info.base_mode,
|
||||
))
|
||||
}
|
||||
|
||||
fn keybinds_for_client(&self, client_id: ClientId) -> KeybindsVec {
|
||||
self.mode_info
|
||||
.get(&client_id)
|
||||
.map(|mode_info| mode_info.keybinds.clone())
|
||||
.unwrap_or_else(|| self.default_mode_info.keybinds.clone())
|
||||
}
|
||||
|
||||
// Keep the client's mode in sync with the pane it just focused: entering a scrolled
|
||||
// pane switches to Scroll so its position is navigable, leaving it returns to the
|
||||
// default mode. Only the default<->Scroll pair is touched (Normal by default, Locked
|
||||
@@ -7106,6 +7596,8 @@ impl Screen {
|
||||
tab.update_input_modes()?;
|
||||
}
|
||||
self.broadcast_nested_shortcuts();
|
||||
self.own_keybinds_generation = self.own_keybinds_generation.wrapping_add(1);
|
||||
self.report_upward();
|
||||
Ok(())
|
||||
}
|
||||
pub fn update_host_terminal_theme_mode(&mut self, mode: HostTerminalThemeMode) -> Result<()> {
|
||||
@@ -10250,6 +10742,15 @@ pub(crate) fn screen_thread_main(
|
||||
ScreenInstruction::NestedSessionMessageFromHost { client_id, message } => {
|
||||
screen.handle_nested_session_message_from_host(client_id, message);
|
||||
},
|
||||
ScreenInstruction::GetNestedSessionKeybinds {
|
||||
pane_id,
|
||||
response_channel,
|
||||
} => {
|
||||
screen.get_nested_session_keybinds(
|
||||
pane_id,
|
||||
KeybindsReplyTo::Plugin(response_channel),
|
||||
);
|
||||
},
|
||||
ScreenInstruction::GuestModalChoice {
|
||||
client_id,
|
||||
pane_id,
|
||||
|
||||
@@ -8244,6 +8244,9 @@ pub fn pane_info_for_pane(
|
||||
let (default_fg, default_bg) = pane.get_pane_default_colors();
|
||||
pane_info.default_fg = default_fg;
|
||||
pane_info.default_bg = default_bg;
|
||||
if pane.is_nested_guest() {
|
||||
pane_info.nested_session_name = pane.guest_session_name();
|
||||
}
|
||||
|
||||
match pane_id {
|
||||
PaneId::Terminal(terminal_id) => {
|
||||
|
||||
@@ -11328,7 +11328,8 @@ pub fn nested_guest_announce_gets_announce_ack_with_ancestry() {
|
||||
zellij_utils::nested_session::NestedSessionMessage::AnnounceAck {
|
||||
ancestry: vec!["zellij-test".to_owned()],
|
||||
capabilities: vec![
|
||||
zellij_utils::nested_session::NestedSessionCapability::NestedControl
|
||||
zellij_utils::nested_session::NestedSessionCapability::NestedControl,
|
||||
zellij_utils::nested_session::NestedSessionCapability::HintReporting
|
||||
],
|
||||
descend_keys: vec![],
|
||||
}
|
||||
@@ -14174,3 +14175,598 @@ pub fn reported_pixel_dimensions_are_applied_to_existing_ptys() {
|
||||
resizes_after_reply
|
||||
);
|
||||
}
|
||||
|
||||
mod nested_hint_reporting {
|
||||
use super::*;
|
||||
use crate::screen::KeybindsReplyTo;
|
||||
use zellij_utils::data::{
|
||||
BareKey, KeyWithModifier, NestedSessionEndReason, NestedSessionKeybinds,
|
||||
NestedSessionKeybindsError, NestedSessionKeybindsResponse,
|
||||
};
|
||||
use zellij_utils::nested_session::{
|
||||
decode_payload, NestedSessionCapability, NestedSessionMessage,
|
||||
};
|
||||
|
||||
struct Harness {
|
||||
screen: Screen,
|
||||
server_receiver: Receiver<(ServerInstruction, ErrorContext)>,
|
||||
pty_writer_receiver: Receiver<(PtyWriteInstruction, ErrorContext)>,
|
||||
plugin_receiver: Receiver<(PluginInstruction, ErrorContext)>,
|
||||
}
|
||||
|
||||
impl Harness {
|
||||
fn new() -> Self {
|
||||
let size = Size {
|
||||
cols: 121,
|
||||
rows: 20,
|
||||
};
|
||||
let (mut screen, _tty_stdin_bytes, server_receiver) =
|
||||
create_new_screen_with_capture(size, true, true, true, true);
|
||||
let (to_pty_writer, pty_writer_receiver): ChannelWithContext<PtyWriteInstruction> =
|
||||
channels::unbounded();
|
||||
screen.bus.senders.to_pty_writer = Some(SenderWithContext::new(to_pty_writer));
|
||||
let (to_plugin, plugin_receiver): ChannelWithContext<PluginInstruction> =
|
||||
channels::unbounded();
|
||||
screen.bus.senders.to_plugin = Some(SenderWithContext::new(to_plugin));
|
||||
new_tab(&mut screen, 1, 0);
|
||||
Harness {
|
||||
screen,
|
||||
server_receiver,
|
||||
pty_writer_receiver,
|
||||
plugin_receiver,
|
||||
}
|
||||
}
|
||||
|
||||
fn announce_guest(&mut self, capabilities: Vec<NestedSessionCapability>) {
|
||||
self.screen.handle_nested_session_message_from_pane(
|
||||
PaneId::Terminal(1),
|
||||
NestedSessionMessage::Announce {
|
||||
session_name: "inner".to_owned(),
|
||||
capabilities,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
fn announce_hint_reporting_guest(&mut self) {
|
||||
self.announce_guest(vec![
|
||||
NestedSessionCapability::NestedControl,
|
||||
NestedSessionCapability::HintReporting,
|
||||
]);
|
||||
}
|
||||
|
||||
fn acknowledge_from_host(&mut self, capabilities: Vec<NestedSessionCapability>) {
|
||||
self.screen.handle_nested_session_message_from_host(
|
||||
1,
|
||||
NestedSessionMessage::AnnounceAck {
|
||||
ancestry: vec!["outer".to_owned()],
|
||||
capabilities,
|
||||
descend_keys: vec![],
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
fn acknowledge_from_hint_reporting_host(&mut self) {
|
||||
self.acknowledge_from_host(vec![
|
||||
NestedSessionCapability::NestedControl,
|
||||
NestedSessionCapability::HintReporting,
|
||||
]);
|
||||
}
|
||||
|
||||
fn ask_as_plugin(&mut self, pane_id: PaneId) -> Receiver<NestedSessionKeybindsResponse> {
|
||||
let (sender, receiver) = crossbeam::channel::bounded(1);
|
||||
self.screen
|
||||
.get_nested_session_keybinds(pane_id, KeybindsReplyTo::Plugin(sender));
|
||||
receiver
|
||||
}
|
||||
|
||||
fn frames_written_to_guest(&self) -> Vec<NestedSessionMessage> {
|
||||
self.pty_writer_receiver
|
||||
.try_iter()
|
||||
.filter_map(|(instruction, _)| match instruction {
|
||||
PtyWriteInstruction::Write(bytes, 1, None) => decode_nested_frame(&bytes),
|
||||
_ => None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn frames_sent_to_host(&self) -> Vec<NestedSessionMessage> {
|
||||
self.server_receiver
|
||||
.try_iter()
|
||||
.filter_map(|(instruction, _)| match instruction {
|
||||
ServerInstruction::EmitNestedSessionFrameToClient(1, payload) => {
|
||||
decode_payload(&payload)
|
||||
},
|
||||
_ => None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn events_sent_to_plugins(&self) -> Vec<Event> {
|
||||
self.plugin_receiver
|
||||
.try_iter()
|
||||
.filter_map(|(instruction, _)| match instruction {
|
||||
PluginInstruction::Update(updates) => Some(updates),
|
||||
_ => None,
|
||||
})
|
||||
.flatten()
|
||||
.map(|(_, _, event)| event)
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn request_id_written_to_guest(&self) -> u64 {
|
||||
self.frames_written_to_guest()
|
||||
.into_iter()
|
||||
.find_map(|frame| match frame {
|
||||
NestedSessionMessage::RequestGuestKeybinds { request_id } => Some(request_id),
|
||||
_ => None,
|
||||
})
|
||||
.expect("a keybinding request written to the guest pane")
|
||||
}
|
||||
|
||||
fn descend_into_guest(&mut self) {
|
||||
self.screen.focus_guest_session(1);
|
||||
}
|
||||
|
||||
fn make_guest_unresponsive(&mut self) {
|
||||
let far_future = std::time::Instant::now() + std::time::Duration::from_secs(3600);
|
||||
let _ = self
|
||||
.screen
|
||||
.nested_guest_tracker
|
||||
.on_tick(PaneId::Terminal(1), far_future);
|
||||
self.screen.suspend_nested_guest(PaneId::Terminal(1));
|
||||
}
|
||||
}
|
||||
|
||||
fn inner_keybinds() -> NestedSessionKeybinds {
|
||||
NestedSessionKeybinds {
|
||||
session_path: vec!["inner".to_owned()],
|
||||
mode: InputMode::Pane,
|
||||
base_mode: Some(InputMode::Normal),
|
||||
keybinds: vec![(
|
||||
InputMode::Normal,
|
||||
vec![(
|
||||
KeyWithModifier::new(BareKey::Char('p')).with_ctrl_modifier(),
|
||||
vec![Action::SwitchToMode {
|
||||
input_mode: InputMode::Pane,
|
||||
}],
|
||||
)],
|
||||
)],
|
||||
keybinds_generation: 4,
|
||||
}
|
||||
}
|
||||
|
||||
fn mode_updates(frames: &[NestedSessionMessage]) -> Vec<(InputMode, Vec<String>, u64)> {
|
||||
frames
|
||||
.iter()
|
||||
.filter_map(|frame| match frame {
|
||||
NestedSessionMessage::GuestModeUpdate {
|
||||
mode,
|
||||
session_path,
|
||||
keybinds_generation,
|
||||
..
|
||||
} => Some((*mode, session_path.clone(), *keybinds_generation)),
|
||||
_ => None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn keybinds_replies(
|
||||
frames: &[NestedSessionMessage],
|
||||
) -> Vec<(u64, NestedSessionKeybindsResponse)> {
|
||||
frames
|
||||
.iter()
|
||||
.filter_map(|frame| match frame {
|
||||
NestedSessionMessage::GuestKeybindsReply { request_id, result } => {
|
||||
Some((*request_id, result.clone()))
|
||||
},
|
||||
_ => None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_plugin_pane_is_not_a_nested_session() {
|
||||
let mut harness = Harness::new();
|
||||
let receiver = harness.ask_as_plugin(PaneId::Plugin(1));
|
||||
assert_eq!(
|
||||
receiver.try_recv(),
|
||||
Ok(Err(NestedSessionKeybindsError::NotANestedSession))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_pane_without_a_guest_is_not_a_nested_session() {
|
||||
let mut harness = Harness::new();
|
||||
let receiver = harness.ask_as_plugin(PaneId::Terminal(1));
|
||||
assert_eq!(
|
||||
receiver.try_recv(),
|
||||
Ok(Err(NestedSessionKeybindsError::NotANestedSession))
|
||||
);
|
||||
assert!(harness.frames_written_to_guest().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_guest_without_hint_reporting_is_not_supported() {
|
||||
let mut harness = Harness::new();
|
||||
harness.announce_guest(vec![NestedSessionCapability::NestedControl]);
|
||||
let _ = harness.frames_written_to_guest();
|
||||
let receiver = harness.ask_as_plugin(PaneId::Terminal(1));
|
||||
assert_eq!(
|
||||
receiver.try_recv(),
|
||||
Ok(Err(NestedSessionKeybindsError::NotSupported))
|
||||
);
|
||||
assert!(harness.frames_written_to_guest().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_unresponsive_guest_is_reported_as_such() {
|
||||
let mut harness = Harness::new();
|
||||
harness.announce_hint_reporting_guest();
|
||||
harness.make_guest_unresponsive();
|
||||
let receiver = harness.ask_as_plugin(PaneId::Terminal(1));
|
||||
assert_eq!(
|
||||
receiver.try_recv(),
|
||||
Ok(Err(NestedSessionKeybindsError::GuestUnresponsive))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_reply_reaches_the_plugin_that_asked() {
|
||||
let mut harness = Harness::new();
|
||||
harness.announce_hint_reporting_guest();
|
||||
let _ = harness.frames_written_to_guest();
|
||||
let receiver = harness.ask_as_plugin(PaneId::Terminal(1));
|
||||
let request_id = harness.request_id_written_to_guest();
|
||||
assert!(receiver.try_recv().is_err());
|
||||
harness.screen.handle_nested_session_message_from_pane(
|
||||
PaneId::Terminal(1),
|
||||
NestedSessionMessage::GuestKeybindsReply {
|
||||
request_id,
|
||||
result: Ok(inner_keybinds()),
|
||||
},
|
||||
);
|
||||
assert_eq!(receiver.try_recv(), Ok(Ok(inner_keybinds())));
|
||||
assert!(!harness
|
||||
.events_sent_to_plugins()
|
||||
.iter()
|
||||
.any(|event| matches!(event, Event::NestedSessionModeUpdate { .. })));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_reply_with_an_unknown_id_or_from_another_pane_is_dropped() {
|
||||
let mut harness = Harness::new();
|
||||
harness.announce_hint_reporting_guest();
|
||||
let _ = harness.frames_written_to_guest();
|
||||
let receiver = harness.ask_as_plugin(PaneId::Terminal(1));
|
||||
let request_id = harness.request_id_written_to_guest();
|
||||
harness.screen.handle_nested_session_message_from_pane(
|
||||
PaneId::Terminal(1),
|
||||
NestedSessionMessage::GuestKeybindsReply {
|
||||
request_id: request_id + 100,
|
||||
result: Ok(inner_keybinds()),
|
||||
},
|
||||
);
|
||||
harness.screen.handle_nested_session_message_from_pane(
|
||||
PaneId::Terminal(2),
|
||||
NestedSessionMessage::GuestKeybindsReply {
|
||||
request_id,
|
||||
result: Ok(inner_keybinds()),
|
||||
},
|
||||
);
|
||||
assert!(receiver.try_recv().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_pending_request_fails_when_the_guest_exits() {
|
||||
let mut harness = Harness::new();
|
||||
harness.announce_hint_reporting_guest();
|
||||
let _ = harness.events_sent_to_plugins();
|
||||
let receiver = harness.ask_as_plugin(PaneId::Terminal(1));
|
||||
harness.screen.clear_nested_guest(PaneId::Terminal(1));
|
||||
assert_eq!(
|
||||
receiver.try_recv(),
|
||||
Ok(Err(NestedSessionKeybindsError::GuestGone))
|
||||
);
|
||||
assert!(harness
|
||||
.events_sent_to_plugins()
|
||||
.contains(&Event::NestedSessionEnded {
|
||||
pane_id: zellij_utils::data::PaneId::Terminal(1),
|
||||
reason: NestedSessionEndReason::Exited,
|
||||
}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_pending_request_fails_when_the_guest_stops_responding() {
|
||||
let mut harness = Harness::new();
|
||||
harness.announce_hint_reporting_guest();
|
||||
let _ = harness.events_sent_to_plugins();
|
||||
let receiver = harness.ask_as_plugin(PaneId::Terminal(1));
|
||||
harness.make_guest_unresponsive();
|
||||
assert_eq!(
|
||||
receiver.try_recv(),
|
||||
Ok(Err(NestedSessionKeybindsError::GuestUnresponsive))
|
||||
);
|
||||
assert!(harness
|
||||
.events_sent_to_plugins()
|
||||
.contains(&Event::NestedSessionEnded {
|
||||
pane_id: zellij_utils::data::PaneId::Terminal(1),
|
||||
reason: NestedSessionEndReason::Unresponsive,
|
||||
}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn closing_a_pane_without_a_guest_reports_no_ended_session() {
|
||||
let mut harness = Harness::new();
|
||||
let _ = harness.events_sent_to_plugins();
|
||||
harness.screen.clear_nested_guest(PaneId::Terminal(1));
|
||||
assert!(!harness
|
||||
.events_sent_to_plugins()
|
||||
.iter()
|
||||
.any(|event| matches!(event, Event::NestedSessionEnded { .. })));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pane_info_names_the_nested_session_while_it_runs() {
|
||||
let mut harness = Harness::new();
|
||||
assert_eq!(
|
||||
harness
|
||||
.screen
|
||||
.get_pane_info(PaneId::Terminal(1))
|
||||
.and_then(|pane_info| pane_info.nested_session_name),
|
||||
None
|
||||
);
|
||||
harness.announce_hint_reporting_guest();
|
||||
assert_eq!(
|
||||
harness
|
||||
.screen
|
||||
.get_pane_info(PaneId::Terminal(1))
|
||||
.and_then(|pane_info| pane_info.nested_session_name),
|
||||
Some("inner".to_owned())
|
||||
);
|
||||
harness.screen.clear_nested_guest(PaneId::Terminal(1));
|
||||
assert_eq!(
|
||||
harness
|
||||
.screen
|
||||
.get_pane_info(PaneId::Terminal(1))
|
||||
.and_then(|pane_info| pane_info.nested_session_name),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_guest_mode_update_reaches_plugins_with_its_path_and_generation() {
|
||||
let mut harness = Harness::new();
|
||||
harness.announce_hint_reporting_guest();
|
||||
let _ = harness.events_sent_to_plugins();
|
||||
harness.screen.handle_nested_session_message_from_pane(
|
||||
PaneId::Terminal(1),
|
||||
NestedSessionMessage::GuestModeUpdate {
|
||||
mode: InputMode::Locked,
|
||||
base_mode: Some(InputMode::Normal),
|
||||
session_path: vec!["inner".to_owned()],
|
||||
keybinds_generation: 2,
|
||||
},
|
||||
);
|
||||
assert!(harness
|
||||
.events_sent_to_plugins()
|
||||
.contains(&Event::NestedSessionModeUpdate {
|
||||
pane_id: zellij_utils::data::PaneId::Terminal(1),
|
||||
session_path: vec!["inner".to_owned()],
|
||||
mode: InputMode::Locked,
|
||||
base_mode: Some(InputMode::Normal),
|
||||
keybinds_generation: 2,
|
||||
}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_request_before_the_handshake_is_held_until_it_completes() {
|
||||
let mut harness = Harness::new();
|
||||
harness.screen.handle_nested_session_message_from_host(
|
||||
1,
|
||||
NestedSessionMessage::RequestGuestKeybinds { request_id: 5 },
|
||||
);
|
||||
assert!(keybinds_replies(&harness.frames_sent_to_host()).is_empty());
|
||||
harness.acknowledge_from_hint_reporting_host();
|
||||
let frames = harness.frames_sent_to_host();
|
||||
let replies = keybinds_replies(&frames);
|
||||
assert_eq!(replies.len(), 1);
|
||||
let (request_id, result) = &replies[0];
|
||||
assert_eq!(*request_id, 5);
|
||||
let nested_session_keybinds = result.as_ref().expect("the guest's own keybindings");
|
||||
assert_eq!(
|
||||
nested_session_keybinds.session_path,
|
||||
vec!["zellij-test".to_owned()]
|
||||
);
|
||||
assert!(nested_session_keybinds.base_mode.is_some());
|
||||
assert_eq!(mode_updates(&frames).len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn held_requests_are_dropped_when_the_client_disconnects() {
|
||||
let mut harness = Harness::new();
|
||||
harness.screen.handle_nested_session_message_from_host(
|
||||
1,
|
||||
NestedSessionMessage::RequestGuestKeybinds { request_id: 5 },
|
||||
);
|
||||
assert!(harness.screen.held_keybinds_requests.contains_key(&1));
|
||||
harness.screen.remove_client(1).expect("TEST");
|
||||
assert!(harness.screen.held_keybinds_requests.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn held_requests_are_dropped_when_another_connection_completes_the_handshake() {
|
||||
let mut harness = Harness::new();
|
||||
harness.screen.handle_nested_session_message_from_host(
|
||||
2,
|
||||
NestedSessionMessage::RequestGuestKeybinds { request_id: 5 },
|
||||
);
|
||||
harness.acknowledge_from_hint_reporting_host();
|
||||
assert!(keybinds_replies(&harness.frames_sent_to_host()).is_empty());
|
||||
assert!(harness.screen.held_keybinds_requests.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nothing_is_reported_to_a_host_without_hint_reporting() {
|
||||
let mut harness = Harness::new();
|
||||
harness.screen.handle_nested_session_message_from_host(
|
||||
1,
|
||||
NestedSessionMessage::RequestGuestKeybinds { request_id: 5 },
|
||||
);
|
||||
harness.acknowledge_from_host(vec![NestedSessionCapability::NestedControl]);
|
||||
harness
|
||||
.screen
|
||||
.change_mode(InputMode::Pane, None, 1)
|
||||
.unwrap();
|
||||
let frames = harness.frames_sent_to_host();
|
||||
assert!(mode_updates(&frames).is_empty());
|
||||
assert!(keybinds_replies(&frames).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mode_changes_are_reported_to_the_host_once_each() {
|
||||
let mut harness = Harness::new();
|
||||
harness.acknowledge_from_hint_reporting_host();
|
||||
let initial = mode_updates(&harness.frames_sent_to_host());
|
||||
assert_eq!(initial.len(), 1);
|
||||
harness
|
||||
.screen
|
||||
.change_mode(InputMode::Pane, None, 1)
|
||||
.unwrap();
|
||||
harness
|
||||
.screen
|
||||
.change_mode(InputMode::Pane, None, 1)
|
||||
.unwrap();
|
||||
let updates = mode_updates(&harness.frames_sent_to_host());
|
||||
assert_eq!(
|
||||
updates,
|
||||
vec![(
|
||||
InputMode::Pane,
|
||||
vec!["zellij-test".to_owned()],
|
||||
initial[0].2
|
||||
)]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_new_keybinding_table_changes_the_reported_generation() {
|
||||
let mut harness = Harness::new();
|
||||
harness.acknowledge_from_hint_reporting_host();
|
||||
let initial = mode_updates(&harness.frames_sent_to_host());
|
||||
harness.screen.own_keybinds_generation += 1;
|
||||
harness.screen.report_upward();
|
||||
let updates = mode_updates(&harness.frames_sent_to_host());
|
||||
assert_eq!(updates.len(), 1);
|
||||
assert_ne!(updates[0].2, initial[0].2);
|
||||
assert!(keybinds_replies(&harness.frames_sent_to_host()).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_middle_session_reports_the_guest_its_keys_go_to() {
|
||||
let mut harness = Harness::new();
|
||||
harness.acknowledge_from_hint_reporting_host();
|
||||
harness.announce_hint_reporting_guest();
|
||||
harness.screen.handle_nested_session_message_from_pane(
|
||||
PaneId::Terminal(1),
|
||||
NestedSessionMessage::GuestModeUpdate {
|
||||
mode: InputMode::Locked,
|
||||
base_mode: Some(InputMode::Locked),
|
||||
session_path: vec!["inner".to_owned()],
|
||||
keybinds_generation: 1,
|
||||
},
|
||||
);
|
||||
let before_descending = mode_updates(&harness.frames_sent_to_host());
|
||||
assert!(before_descending
|
||||
.iter()
|
||||
.all(|(_, session_path, _)| session_path == &vec!["zellij-test".to_owned()]));
|
||||
|
||||
harness.descend_into_guest();
|
||||
let after_descending = mode_updates(&harness.frames_sent_to_host());
|
||||
assert_eq!(after_descending.len(), 1);
|
||||
assert_eq!(after_descending[0].0, InputMode::Locked);
|
||||
assert_eq!(
|
||||
after_descending[0].1,
|
||||
vec!["zellij-test".to_owned(), "inner".to_owned()]
|
||||
);
|
||||
|
||||
harness.screen.clear_nested_guest(PaneId::Terminal(1));
|
||||
let after_exit = mode_updates(&harness.frames_sent_to_host());
|
||||
assert_eq!(after_exit.len(), 1);
|
||||
assert_eq!(after_exit[0].1, vec!["zellij-test".to_owned()]);
|
||||
assert_ne!(after_exit[0].2, after_descending[0].2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_middle_session_relays_a_request_to_the_guest_its_keys_go_to() {
|
||||
let mut harness = Harness::new();
|
||||
harness.acknowledge_from_hint_reporting_host();
|
||||
harness.announce_hint_reporting_guest();
|
||||
harness.screen.handle_nested_session_message_from_pane(
|
||||
PaneId::Terminal(1),
|
||||
NestedSessionMessage::GuestModeUpdate {
|
||||
mode: InputMode::Pane,
|
||||
base_mode: Some(InputMode::Normal),
|
||||
session_path: vec!["inner".to_owned()],
|
||||
keybinds_generation: 4,
|
||||
},
|
||||
);
|
||||
harness.descend_into_guest();
|
||||
let reported_generation = mode_updates(&harness.frames_sent_to_host())
|
||||
.last()
|
||||
.map(|(_, _, generation)| *generation)
|
||||
.expect("a mode update after descending");
|
||||
let _ = harness.frames_written_to_guest();
|
||||
|
||||
harness.screen.handle_nested_session_message_from_host(
|
||||
1,
|
||||
NestedSessionMessage::RequestGuestKeybinds { request_id: 9 },
|
||||
);
|
||||
let relayed_request_id = harness.request_id_written_to_guest();
|
||||
harness.screen.handle_nested_session_message_from_pane(
|
||||
PaneId::Terminal(1),
|
||||
NestedSessionMessage::GuestKeybindsReply {
|
||||
request_id: relayed_request_id,
|
||||
result: Ok(inner_keybinds()),
|
||||
},
|
||||
);
|
||||
let replies = keybinds_replies(&harness.frames_sent_to_host());
|
||||
let mut expected = inner_keybinds();
|
||||
expected.session_path = vec!["zellij-test".to_owned(), "inner".to_owned()];
|
||||
expected.keybinds_generation = reported_generation;
|
||||
assert_eq!(replies, vec![(9, Ok(expected))]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_middle_session_passes_errors_from_below_up_unchanged() {
|
||||
let mut harness = Harness::new();
|
||||
harness.acknowledge_from_hint_reporting_host();
|
||||
harness.announce_hint_reporting_guest();
|
||||
harness.screen.handle_nested_session_message_from_pane(
|
||||
PaneId::Terminal(1),
|
||||
NestedSessionMessage::GuestModeUpdate {
|
||||
mode: InputMode::Normal,
|
||||
base_mode: Some(InputMode::Normal),
|
||||
session_path: vec!["inner".to_owned()],
|
||||
keybinds_generation: 0,
|
||||
},
|
||||
);
|
||||
harness.descend_into_guest();
|
||||
let _ = harness.frames_written_to_guest();
|
||||
let _ = harness.frames_sent_to_host();
|
||||
harness.screen.handle_nested_session_message_from_host(
|
||||
1,
|
||||
NestedSessionMessage::RequestGuestKeybinds { request_id: 11 },
|
||||
);
|
||||
let relayed_request_id = harness.request_id_written_to_guest();
|
||||
harness.screen.handle_nested_session_message_from_pane(
|
||||
PaneId::Terminal(1),
|
||||
NestedSessionMessage::GuestKeybindsReply {
|
||||
request_id: relayed_request_id,
|
||||
result: Err(NestedSessionKeybindsError::TooLarge),
|
||||
},
|
||||
);
|
||||
assert_eq!(
|
||||
keybinds_replies(&harness.frames_sent_to_host()),
|
||||
vec![(11, Err(NestedSessionKeybindsError::TooLarge))]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+15
-1
@@ -8,7 +8,10 @@ use zellij_utils::data::*;
|
||||
use zellij_utils::errors::prelude::*;
|
||||
use zellij_utils::input::actions::Action;
|
||||
pub use zellij_utils::plugin_api;
|
||||
use zellij_utils::plugin_api::event::ProtobufPaneScrollbackResponse;
|
||||
use zellij_utils::plugin_api::event::{
|
||||
nested_session_keybinds_response_from_protobuf, ProtobufNestedSessionKeybindsResponse,
|
||||
ProtobufPaneScrollbackResponse,
|
||||
};
|
||||
use zellij_utils::plugin_api::generated_api::api::plugin_command::{
|
||||
hide_floating_panes_response, save_session_response, show_floating_panes_response,
|
||||
};
|
||||
@@ -1305,6 +1308,17 @@ pub fn focus_host_session() {
|
||||
unsafe { host_run_plugin_command() };
|
||||
}
|
||||
|
||||
pub fn get_nested_session_keybinds(pane_id: PaneId) -> NestedSessionKeybindsResponse {
|
||||
let plugin_command = PluginCommand::GetNestedSessionKeybinds(pane_id);
|
||||
let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap();
|
||||
object_to_stdout(&protobuf_plugin_command.encode_to_vec());
|
||||
unsafe { host_run_plugin_command() };
|
||||
let response_bytes = bytes_from_stdin().unwrap();
|
||||
let protobuf_response =
|
||||
ProtobufNestedSessionKeybindsResponse::decode(response_bytes.as_slice()).unwrap();
|
||||
nested_session_keybinds_response_from_protobuf(protobuf_response).unwrap()
|
||||
}
|
||||
|
||||
/// Toggle the UI pane frames on or off
|
||||
pub fn toggle_pane_frames() {
|
||||
let plugin_command = PluginCommand::TogglePaneFrames;
|
||||
|
||||
Binary file not shown.
@@ -10,7 +10,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, 38, 39, 40, 41, 42, 43")]
|
||||
#[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, 38, 39, 40, 41, 42, 43, 44, 45")]
|
||||
pub payload: ::core::option::Option<event::Payload>,
|
||||
}
|
||||
/// Nested message and enum types in `Event`.
|
||||
@@ -100,6 +100,63 @@ pub mod event {
|
||||
HintTextPayload(super::HintTextPayload),
|
||||
#[prost(message, tag="43")]
|
||||
ActivePaneScrollPayload(super::ActivePaneScrollPayload),
|
||||
#[prost(message, tag="44")]
|
||||
NestedSessionModeUpdatePayload(super::NestedSessionModeUpdatePayload),
|
||||
#[prost(message, tag="45")]
|
||||
NestedSessionEndedPayload(super::NestedSessionEndedPayload),
|
||||
}
|
||||
}
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct NestedSessionModeUpdatePayload {
|
||||
#[prost(message, optional, tag="1")]
|
||||
pub pane_id: ::core::option::Option<PaneId>,
|
||||
#[prost(string, repeated, tag="2")]
|
||||
pub session_path: ::prost::alloc::vec::Vec<::prost::alloc::string::String>,
|
||||
#[prost(enumeration="super::input_mode::InputMode", tag="3")]
|
||||
pub mode: i32,
|
||||
#[prost(enumeration="super::input_mode::InputMode", optional, tag="4")]
|
||||
pub base_mode: ::core::option::Option<i32>,
|
||||
#[prost(uint64, tag="5")]
|
||||
pub keybinds_generation: u64,
|
||||
}
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct NestedSessionEndedPayload {
|
||||
#[prost(message, optional, tag="1")]
|
||||
pub pane_id: ::core::option::Option<PaneId>,
|
||||
#[prost(enumeration="NestedSessionEndReason", tag="2")]
|
||||
pub reason: i32,
|
||||
}
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct NestedSessionKeybindsResult {
|
||||
#[prost(string, repeated, tag="1")]
|
||||
pub session_path: ::prost::alloc::vec::Vec<::prost::alloc::string::String>,
|
||||
#[prost(enumeration="super::input_mode::InputMode", tag="2")]
|
||||
pub mode: i32,
|
||||
#[prost(enumeration="super::input_mode::InputMode", optional, tag="3")]
|
||||
pub base_mode: ::core::option::Option<i32>,
|
||||
#[prost(message, repeated, tag="4")]
|
||||
pub keybinds: ::prost::alloc::vec::Vec<InputModeKeybinds>,
|
||||
#[prost(uint64, tag="5")]
|
||||
pub keybinds_generation: u64,
|
||||
}
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct NestedSessionKeybindsResponse {
|
||||
#[prost(oneof="nested_session_keybinds_response::Response", tags="1, 2")]
|
||||
pub response: ::core::option::Option<nested_session_keybinds_response::Response>,
|
||||
}
|
||||
/// Nested message and enum types in `NestedSessionKeybindsResponse`.
|
||||
pub mod nested_session_keybinds_response {
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Oneof)]
|
||||
pub enum Response {
|
||||
#[prost(message, tag="1")]
|
||||
Ok(super::NestedSessionKeybindsResult),
|
||||
#[prost(enumeration="super::NestedSessionKeybindsError", tag="2")]
|
||||
Err(i32),
|
||||
}
|
||||
}
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
@@ -616,6 +673,8 @@ pub struct PaneInfo {
|
||||
pub default_fg: ::core::option::Option<::prost::alloc::string::String>,
|
||||
#[prost(string, optional, tag="25")]
|
||||
pub default_bg: ::core::option::Option<::prost::alloc::string::String>,
|
||||
#[prost(string, optional, tag="26")]
|
||||
pub nested_session_name: ::core::option::Option<::prost::alloc::string::String>,
|
||||
}
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
@@ -898,6 +957,8 @@ pub enum EventType {
|
||||
SoftKeyboardVisibilityChanged = 47,
|
||||
HintText = 48,
|
||||
ActivePaneScroll = 49,
|
||||
NestedSessionModeUpdate = 50,
|
||||
NestedSessionEnded = 51,
|
||||
}
|
||||
impl EventType {
|
||||
/// String value of the enum field names used in the ProtoBuf definition.
|
||||
@@ -954,6 +1015,8 @@ impl EventType {
|
||||
EventType::SoftKeyboardVisibilityChanged => "SoftKeyboardVisibilityChanged",
|
||||
EventType::HintText => "HintText",
|
||||
EventType::ActivePaneScroll => "ActivePaneScroll",
|
||||
EventType::NestedSessionModeUpdate => "NestedSessionModeUpdate",
|
||||
EventType::NestedSessionEnded => "NestedSessionEnded",
|
||||
}
|
||||
}
|
||||
/// Creates an enum from field names used in the ProtoBuf definition.
|
||||
@@ -1007,6 +1070,72 @@ impl EventType {
|
||||
"SoftKeyboardVisibilityChanged" => Some(Self::SoftKeyboardVisibilityChanged),
|
||||
"HintText" => Some(Self::HintText),
|
||||
"ActivePaneScroll" => Some(Self::ActivePaneScroll),
|
||||
"NestedSessionModeUpdate" => Some(Self::NestedSessionModeUpdate),
|
||||
"NestedSessionEnded" => Some(Self::NestedSessionEnded),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)]
|
||||
#[repr(i32)]
|
||||
pub enum NestedSessionEndReason {
|
||||
NestedSessionExited = 0,
|
||||
NestedSessionUnresponsive = 1,
|
||||
}
|
||||
impl NestedSessionEndReason {
|
||||
/// 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 {
|
||||
NestedSessionEndReason::NestedSessionExited => "NestedSessionExited",
|
||||
NestedSessionEndReason::NestedSessionUnresponsive => "NestedSessionUnresponsive",
|
||||
}
|
||||
}
|
||||
/// Creates an enum from field names used in the ProtoBuf definition.
|
||||
pub fn from_str_name(value: &str) -> ::core::option::Option<Self> {
|
||||
match value {
|
||||
"NestedSessionExited" => Some(Self::NestedSessionExited),
|
||||
"NestedSessionUnresponsive" => Some(Self::NestedSessionUnresponsive),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)]
|
||||
#[repr(i32)]
|
||||
pub enum NestedSessionKeybindsError {
|
||||
NestedKeybindsNotANestedSession = 0,
|
||||
NestedKeybindsNotSupported = 1,
|
||||
NestedKeybindsGuestUnresponsive = 2,
|
||||
NestedKeybindsGuestGone = 3,
|
||||
NestedKeybindsTooLarge = 4,
|
||||
NestedKeybindsTimeout = 5,
|
||||
}
|
||||
impl NestedSessionKeybindsError {
|
||||
/// 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 {
|
||||
NestedSessionKeybindsError::NestedKeybindsNotANestedSession => "NestedKeybindsNotANestedSession",
|
||||
NestedSessionKeybindsError::NestedKeybindsNotSupported => "NestedKeybindsNotSupported",
|
||||
NestedSessionKeybindsError::NestedKeybindsGuestUnresponsive => "NestedKeybindsGuestUnresponsive",
|
||||
NestedSessionKeybindsError::NestedKeybindsGuestGone => "NestedKeybindsGuestGone",
|
||||
NestedSessionKeybindsError::NestedKeybindsTooLarge => "NestedKeybindsTooLarge",
|
||||
NestedSessionKeybindsError::NestedKeybindsTimeout => "NestedKeybindsTimeout",
|
||||
}
|
||||
}
|
||||
/// Creates an enum from field names used in the ProtoBuf definition.
|
||||
pub fn from_str_name(value: &str) -> ::core::option::Option<Self> {
|
||||
match value {
|
||||
"NestedKeybindsNotANestedSession" => Some(Self::NestedKeybindsNotANestedSession),
|
||||
"NestedKeybindsNotSupported" => Some(Self::NestedKeybindsNotSupported),
|
||||
"NestedKeybindsGuestUnresponsive" => Some(Self::NestedKeybindsGuestUnresponsive),
|
||||
"NestedKeybindsGuestGone" => Some(Self::NestedKeybindsGuestGone),
|
||||
"NestedKeybindsTooLarge" => Some(Self::NestedKeybindsTooLarge),
|
||||
"NestedKeybindsTimeout" => Some(Self::NestedKeybindsTimeout),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,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, 164, 168, 169, 170, 171, 172, 173, 174")]
|
||||
#[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, 168, 169, 170, 171, 172, 173, 174, 175")]
|
||||
pub payload: ::core::option::Option<plugin_command::Payload>,
|
||||
}
|
||||
/// Nested message and enum types in `PluginCommand`.
|
||||
@@ -326,10 +326,18 @@ pub mod plugin_command {
|
||||
ApplyFloatingSwapLayoutPayload(::prost::alloc::string::String),
|
||||
#[prost(message, tag="174")]
|
||||
SetPaneBorderStylePayload(super::SetPaneBorderStylePayload),
|
||||
#[prost(message, tag="175")]
|
||||
GetNestedSessionKeybindsPayload(super::GetNestedSessionKeybindsPayload),
|
||||
}
|
||||
}
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct GetNestedSessionKeybindsPayload {
|
||||
#[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 SetPaneFrameStylePayload {
|
||||
#[prost(enumeration="super::pane_frame_style::PaneFrameStyle", tag="1")]
|
||||
pub pane_frame_style: i32,
|
||||
@@ -2246,6 +2254,7 @@ pub enum CommandName {
|
||||
ApplyTiledSwapLayout = 229,
|
||||
ApplyFloatingSwapLayout = 230,
|
||||
SetPaneBorderStyle = 231,
|
||||
GetNestedSessionKeybinds = 232,
|
||||
}
|
||||
impl CommandName {
|
||||
/// String value of the enum field names used in the ProtoBuf definition.
|
||||
@@ -2460,6 +2469,7 @@ impl CommandName {
|
||||
CommandName::ApplyTiledSwapLayout => "ApplyTiledSwapLayout",
|
||||
CommandName::ApplyFloatingSwapLayout => "ApplyFloatingSwapLayout",
|
||||
CommandName::SetPaneBorderStyle => "SetPaneBorderStyle",
|
||||
CommandName::GetNestedSessionKeybinds => "GetNestedSessionKeybinds",
|
||||
}
|
||||
}
|
||||
/// Creates an enum from field names used in the ProtoBuf definition.
|
||||
@@ -2671,6 +2681,7 @@ impl CommandName {
|
||||
"ApplyTiledSwapLayout" => Some(Self::ApplyTiledSwapLayout),
|
||||
"ApplyFloatingSwapLayout" => Some(Self::ApplyFloatingSwapLayout),
|
||||
"SetPaneBorderStyle" => Some(Self::SetPaneBorderStyle),
|
||||
"GetNestedSessionKeybinds" => Some(Self::GetNestedSessionKeybinds),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct NestedSessionMessage {
|
||||
#[prost(oneof="nested_session_message::Payload", tags="1, 2, 3, 4, 5, 8, 9, 10, 11, 12, 13, 14")]
|
||||
#[prost(oneof="nested_session_message::Payload", tags="1, 2, 3, 4, 5, 8, 9, 10, 11, 12, 13, 14, 15, 16, 18")]
|
||||
pub payload: ::core::option::Option<nested_session_message::Payload>,
|
||||
}
|
||||
/// Nested message and enum types in `NestedSessionMessage`.
|
||||
@@ -34,6 +34,12 @@ pub mod nested_session_message {
|
||||
Ping(super::Ping),
|
||||
#[prost(message, tag="14")]
|
||||
ShortcutUpdate(super::ShortcutUpdate),
|
||||
#[prost(message, tag="15")]
|
||||
GuestModeUpdate(super::GuestModeUpdate),
|
||||
#[prost(message, tag="16")]
|
||||
RequestGuestKeybinds(super::RequestGuestKeybinds),
|
||||
#[prost(message, tag="18")]
|
||||
GuestKeybindsReply(super::GuestKeybindsReply),
|
||||
}
|
||||
}
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
@@ -108,11 +114,64 @@ pub struct AncestryUpdate {
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct Ping {
|
||||
}
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct GuestModeUpdate {
|
||||
#[prost(string, tag="1")]
|
||||
pub mode: ::prost::alloc::string::String,
|
||||
#[prost(string, tag="2")]
|
||||
pub base_mode: ::prost::alloc::string::String,
|
||||
#[prost(string, repeated, tag="3")]
|
||||
pub session_path: ::prost::alloc::vec::Vec<::prost::alloc::string::String>,
|
||||
#[prost(uint64, tag="4")]
|
||||
pub keybinds_generation: u64,
|
||||
}
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct RequestGuestKeybinds {
|
||||
#[prost(uint64, tag="1")]
|
||||
pub request_id: u64,
|
||||
}
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct GuestKeybinds {
|
||||
#[prost(string, repeated, tag="1")]
|
||||
pub session_path: ::prost::alloc::vec::Vec<::prost::alloc::string::String>,
|
||||
#[prost(string, tag="2")]
|
||||
pub mode: ::prost::alloc::string::String,
|
||||
#[prost(string, tag="3")]
|
||||
pub base_mode: ::prost::alloc::string::String,
|
||||
#[prost(uint64, tag="4")]
|
||||
pub keybinds_generation: u64,
|
||||
#[prost(bytes="vec", tag="5")]
|
||||
pub keybinds_payload: ::prost::alloc::vec::Vec<u8>,
|
||||
}
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct GuestKeybindsReply {
|
||||
#[prost(uint64, tag="1")]
|
||||
pub request_id: u64,
|
||||
#[prost(oneof="guest_keybinds_reply::Result", tags="2, 3")]
|
||||
pub result: ::core::option::Option<guest_keybinds_reply::Result>,
|
||||
}
|
||||
/// Nested message and enum types in `GuestKeybindsReply`.
|
||||
pub mod guest_keybinds_reply {
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Oneof)]
|
||||
pub enum Result {
|
||||
#[prost(message, tag="2")]
|
||||
Keybinds(super::GuestKeybinds),
|
||||
#[prost(enumeration="super::GuestKeybindsError", tag="3")]
|
||||
Error(i32),
|
||||
}
|
||||
}
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)]
|
||||
#[repr(i32)]
|
||||
pub enum NestedCapability {
|
||||
Unspecified = 0,
|
||||
NestedControl = 1,
|
||||
/// The peer can report its input mode and keybindings to its host.
|
||||
HintReporting = 2,
|
||||
}
|
||||
impl NestedCapability {
|
||||
/// String value of the enum field names used in the ProtoBuf definition.
|
||||
@@ -123,6 +182,7 @@ impl NestedCapability {
|
||||
match self {
|
||||
NestedCapability::Unspecified => "NESTED_CAPABILITY_UNSPECIFIED",
|
||||
NestedCapability::NestedControl => "NESTED_CAPABILITY_NESTED_CONTROL",
|
||||
NestedCapability::HintReporting => "NESTED_CAPABILITY_HINT_REPORTING",
|
||||
}
|
||||
}
|
||||
/// Creates an enum from field names used in the ProtoBuf definition.
|
||||
@@ -130,6 +190,7 @@ impl NestedCapability {
|
||||
match value {
|
||||
"NESTED_CAPABILITY_UNSPECIFIED" => Some(Self::Unspecified),
|
||||
"NESTED_CAPABILITY_NESTED_CONTROL" => Some(Self::NestedControl),
|
||||
"NESTED_CAPABILITY_HINT_REPORTING" => Some(Self::HintReporting),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -169,3 +230,44 @@ impl NestedDirection {
|
||||
}
|
||||
}
|
||||
}
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)]
|
||||
#[repr(i32)]
|
||||
pub enum GuestKeybindsError {
|
||||
Unspecified = 0,
|
||||
NotNested = 1,
|
||||
Unsupported = 2,
|
||||
GuestUnresponsive = 3,
|
||||
GuestGone = 4,
|
||||
TooLarge = 5,
|
||||
Timeout = 6,
|
||||
}
|
||||
impl GuestKeybindsError {
|
||||
/// 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 {
|
||||
GuestKeybindsError::Unspecified => "GUEST_KEYBINDS_ERROR_UNSPECIFIED",
|
||||
GuestKeybindsError::NotNested => "GUEST_KEYBINDS_ERROR_NOT_NESTED",
|
||||
GuestKeybindsError::Unsupported => "GUEST_KEYBINDS_ERROR_UNSUPPORTED",
|
||||
GuestKeybindsError::GuestUnresponsive => "GUEST_KEYBINDS_ERROR_GUEST_UNRESPONSIVE",
|
||||
GuestKeybindsError::GuestGone => "GUEST_KEYBINDS_ERROR_GUEST_GONE",
|
||||
GuestKeybindsError::TooLarge => "GUEST_KEYBINDS_ERROR_TOO_LARGE",
|
||||
GuestKeybindsError::Timeout => "GUEST_KEYBINDS_ERROR_TIMEOUT",
|
||||
}
|
||||
}
|
||||
/// Creates an enum from field names used in the ProtoBuf definition.
|
||||
pub fn from_str_name(value: &str) -> ::core::option::Option<Self> {
|
||||
match value {
|
||||
"GUEST_KEYBINDS_ERROR_UNSPECIFIED" => Some(Self::Unspecified),
|
||||
"GUEST_KEYBINDS_ERROR_NOT_NESTED" => Some(Self::NotNested),
|
||||
"GUEST_KEYBINDS_ERROR_UNSUPPORTED" => Some(Self::Unsupported),
|
||||
"GUEST_KEYBINDS_ERROR_GUEST_UNRESPONSIVE" => Some(Self::GuestUnresponsive),
|
||||
"GUEST_KEYBINDS_ERROR_GUEST_GONE" => Some(Self::GuestGone),
|
||||
"GUEST_KEYBINDS_ERROR_TOO_LARGE" => Some(Self::TooLarge),
|
||||
"GUEST_KEYBINDS_ERROR_TIMEOUT" => Some(Self::Timeout),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1047,8 +1047,64 @@ pub enum Event {
|
||||
SoftKeyboardVisibilityChanged(bool),
|
||||
HintText(BTreeMap<usize, StyledText>),
|
||||
ActivePaneScroll(Option<(usize, usize)>),
|
||||
NestedSessionModeUpdate {
|
||||
pane_id: PaneId,
|
||||
session_path: Vec<String>,
|
||||
mode: InputMode,
|
||||
base_mode: Option<InputMode>,
|
||||
keybinds_generation: u64,
|
||||
},
|
||||
NestedSessionEnded {
|
||||
pane_id: PaneId,
|
||||
reason: NestedSessionEndReason,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub enum NestedSessionEndReason {
|
||||
Exited,
|
||||
Unresponsive,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct NestedSessionKeybinds {
|
||||
pub session_path: Vec<String>,
|
||||
pub mode: InputMode,
|
||||
pub base_mode: Option<InputMode>,
|
||||
pub keybinds: KeybindsVec,
|
||||
pub keybinds_generation: u64,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub enum NestedSessionKeybindsError {
|
||||
NotANestedSession,
|
||||
NotSupported,
|
||||
GuestUnresponsive,
|
||||
GuestGone,
|
||||
TooLarge,
|
||||
Timeout,
|
||||
}
|
||||
|
||||
impl fmt::Display for NestedSessionKeybindsError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let description = match self {
|
||||
NestedSessionKeybindsError::NotANestedSession => "no nested session in this pane",
|
||||
NestedSessionKeybindsError::NotSupported => {
|
||||
"the nested session cannot report its keybindings"
|
||||
},
|
||||
NestedSessionKeybindsError::GuestUnresponsive => "the nested session is not responding",
|
||||
NestedSessionKeybindsError::GuestGone => "the nested session has ended",
|
||||
NestedSessionKeybindsError::TooLarge => {
|
||||
"the nested session's keybindings are too large to send"
|
||||
},
|
||||
NestedSessionKeybindsError::Timeout => "the nested session did not answer in time",
|
||||
};
|
||||
write!(f, "{}", description)
|
||||
}
|
||||
}
|
||||
|
||||
pub type NestedSessionKeybindsResponse = Result<NestedSessionKeybinds, NestedSessionKeybindsError>;
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub enum HostTerminalThemeMode {
|
||||
Dark,
|
||||
@@ -2621,6 +2677,7 @@ pub struct PaneInfo {
|
||||
pub default_fg: Option<String>,
|
||||
/// The default background color of this pane, if set (e.g. "#001a3a")
|
||||
pub default_bg: Option<String>,
|
||||
pub nested_session_name: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
|
||||
@@ -4003,6 +4060,7 @@ pub enum PluginCommand {
|
||||
DeleteAllDeadSessionsAndReply, // no payload; sends a response back
|
||||
SetSoftKeyboard(bool),
|
||||
FocusHostSession,
|
||||
GetNestedSessionKeybinds(PaneId),
|
||||
}
|
||||
|
||||
// Response type for plugin API methods that open a pane in a new tab
|
||||
|
||||
@@ -324,6 +324,7 @@ pub enum ScreenContext {
|
||||
NestedSessionMessageFromPane,
|
||||
NestedGuestPingTick,
|
||||
NestedSessionMessageFromHost,
|
||||
GetNestedSessionKeybinds,
|
||||
GuestModalChoice,
|
||||
ForwardedReplyFromHost,
|
||||
ResumePaneAfterForward,
|
||||
|
||||
@@ -6813,6 +6813,7 @@ impl PaneInfo {
|
||||
index_in_pane_group: Default::default(), // we don't serialize this
|
||||
default_fg: None,
|
||||
default_bg: None,
|
||||
nested_session_name: None,
|
||||
};
|
||||
Ok((tab_position, pane_info))
|
||||
}
|
||||
@@ -6963,6 +6964,7 @@ fn serialize_and_deserialize_session_info_with_data() {
|
||||
index_in_pane_group: Default::default(), // we don't serialize this
|
||||
default_fg: None,
|
||||
default_bg: None,
|
||||
nested_session_name: None,
|
||||
},
|
||||
PaneInfo {
|
||||
id: 1,
|
||||
@@ -6990,6 +6992,7 @@ fn serialize_and_deserialize_session_info_with_data() {
|
||||
index_in_pane_group: Default::default(), // we don't serialize this
|
||||
default_fg: None,
|
||||
default_bg: None,
|
||||
nested_session_name: None,
|
||||
},
|
||||
];
|
||||
let mut panes = HashMap::new();
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
use crate::data::{Direction, KeyWithModifier};
|
||||
use crate::data::{
|
||||
BareKey, Direction, InputMode, KeyWithModifier, KeybindsVec, NestedSessionKeybinds,
|
||||
NestedSessionKeybindsError, NestedSessionKeybindsResponse,
|
||||
};
|
||||
use crate::nested_session_contract::nested_session_contract as proto;
|
||||
use crate::plugin_api::event::{keybinds_from_protobuf, keybinds_to_protobuf};
|
||||
use crate::plugin_api::generated_api::api::event::InitialKeybindsPayload as ProtobufInitialKeybindsPayload;
|
||||
use base64::alphabet::STANDARD as BASE64_STANDARD_ALPHABET;
|
||||
use base64::engine::general_purpose::{
|
||||
GeneralPurpose, GeneralPurposeConfig, STANDARD as BASE64_STANDARD,
|
||||
@@ -21,6 +26,8 @@ pub const NESTED_FRAME_TERMINATOR: &[u8] = b"\x1b\\";
|
||||
pub const REANNOUNCE_SILENCE_MS: u64 = 3000;
|
||||
pub const REANNOUNCE_CHECK_INTERVAL_MS: u64 = 1000;
|
||||
pub const MAX_UNACKED_ANNOUNCES: usize = 5;
|
||||
pub const KEYBINDS_REQUEST_TIMEOUT: Duration = Duration::from_secs(1);
|
||||
pub const MAX_HELD_KEYBINDS_REQUESTS: usize = 8;
|
||||
|
||||
pub fn reannounce_silence_ms() -> u64 {
|
||||
std::env::var("ZELLIJ_NESTED_REANNOUNCE_SILENCE_MS")
|
||||
@@ -97,6 +104,7 @@ impl ReannounceScheduler {
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum NestedSessionCapability {
|
||||
NestedControl,
|
||||
HintReporting,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
@@ -133,6 +141,19 @@ pub enum NestedSessionMessage {
|
||||
ascend_keys: Vec<KeyWithModifier>,
|
||||
descend_keys: Vec<KeyWithModifier>,
|
||||
},
|
||||
GuestModeUpdate {
|
||||
mode: InputMode,
|
||||
base_mode: Option<InputMode>,
|
||||
session_path: Vec<String>,
|
||||
keybinds_generation: u64,
|
||||
},
|
||||
RequestGuestKeybinds {
|
||||
request_id: u64,
|
||||
},
|
||||
GuestKeybindsReply {
|
||||
request_id: u64,
|
||||
result: NestedSessionKeybindsResponse,
|
||||
},
|
||||
}
|
||||
|
||||
fn keys_to_proto(keys: &[KeyWithModifier]) -> Vec<String> {
|
||||
@@ -156,6 +177,7 @@ fn capabilities_to_proto(capabilities: &[NestedSessionCapability]) -> Vec<i32> {
|
||||
.iter()
|
||||
.map(|capability| match capability {
|
||||
NestedSessionCapability::NestedControl => proto::NestedCapability::NestedControl as i32,
|
||||
NestedSessionCapability::HintReporting => proto::NestedCapability::HintReporting as i32,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
@@ -168,12 +190,94 @@ fn capabilities_from_proto(capabilities: &[i32]) -> Vec<NestedSessionCapability>
|
||||
Some(proto::NestedCapability::NestedControl) => {
|
||||
Some(NestedSessionCapability::NestedControl)
|
||||
},
|
||||
Some(proto::NestedCapability::HintReporting) => {
|
||||
Some(NestedSessionCapability::HintReporting)
|
||||
},
|
||||
_ => None,
|
||||
},
|
||||
)
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// An [`InputMode`] is written to the wire under the name [`InputMode::from_str`] reads,
|
||||
/// so that a mode one side does not know about is rejected rather than misread, and so
|
||||
/// that adding a mode needs no change to this contract.
|
||||
fn mode_to_proto(mode: InputMode) -> String {
|
||||
format!("{:?}", mode)
|
||||
}
|
||||
|
||||
fn mode_from_proto(mode: &str) -> Option<InputMode> {
|
||||
InputMode::from_str(mode).ok()
|
||||
}
|
||||
|
||||
/// Keybindings travel as an encoded plugin-API payload rather than as part of this
|
||||
/// contract, so that describing them here does not mean restating the whole `Action`
|
||||
/// schema. Anything the reading side cannot understand is dropped, which is what lets a
|
||||
/// host and a guest built from different Zellij versions still exchange the bindings they
|
||||
/// have in common.
|
||||
fn keybinds_to_proto(keybinds: KeybindsVec) -> Vec<u8> {
|
||||
let payload = ProtobufInitialKeybindsPayload {
|
||||
keybinds: keybinds_to_protobuf(keybinds),
|
||||
};
|
||||
payload.encode_to_vec()
|
||||
}
|
||||
|
||||
fn keybinds_from_proto(payload_bytes: &[u8]) -> Option<KeybindsVec> {
|
||||
let payload = ProtobufInitialKeybindsPayload::decode(payload_bytes).ok()?;
|
||||
let mut keybinds = keybinds_from_protobuf(payload.keybinds);
|
||||
for (_mode, bindings) in keybinds.iter_mut() {
|
||||
bindings.retain(|(key, _actions)| !binds_a_control_character(key));
|
||||
}
|
||||
Some(keybinds)
|
||||
}
|
||||
|
||||
/// Whether a binding is on a raw control character.
|
||||
///
|
||||
/// Nothing this side can read is authenticated: a guest's frames travel in the terminal
|
||||
/// stream of the pane it runs in, so any program with that pane can write them. A host that
|
||||
/// draws these bindings puts their keys on its own screen, and a control character there is
|
||||
/// a terminal escape in the host's output rather than a key anyone could press. Zellij
|
||||
/// spells the control keys that do exist as their own [`BareKey`] variants ([`BareKey::Esc`],
|
||||
/// [`BareKey::Tab`], [`BareKey::Enter`], [`BareKey::Backspace`]), so a `Char` holding one
|
||||
/// describes no real binding and is dropped.
|
||||
fn binds_a_control_character(key: &KeyWithModifier) -> bool {
|
||||
matches!(key.bare_key, BareKey::Char(character) if character.is_control())
|
||||
}
|
||||
|
||||
fn keybinds_error_to_proto(error: NestedSessionKeybindsError) -> proto::GuestKeybindsError {
|
||||
match error {
|
||||
NestedSessionKeybindsError::NotANestedSession => proto::GuestKeybindsError::NotNested,
|
||||
NestedSessionKeybindsError::NotSupported => proto::GuestKeybindsError::Unsupported,
|
||||
NestedSessionKeybindsError::GuestUnresponsive => {
|
||||
proto::GuestKeybindsError::GuestUnresponsive
|
||||
},
|
||||
NestedSessionKeybindsError::GuestGone => proto::GuestKeybindsError::GuestGone,
|
||||
NestedSessionKeybindsError::TooLarge => proto::GuestKeybindsError::TooLarge,
|
||||
NestedSessionKeybindsError::Timeout => proto::GuestKeybindsError::Timeout,
|
||||
}
|
||||
}
|
||||
|
||||
fn keybinds_error_from_proto(error: i32) -> NestedSessionKeybindsError {
|
||||
match proto::GuestKeybindsError::try_from(error).ok() {
|
||||
Some(proto::GuestKeybindsError::NotNested) => NestedSessionKeybindsError::NotANestedSession,
|
||||
Some(proto::GuestKeybindsError::GuestUnresponsive) => {
|
||||
NestedSessionKeybindsError::GuestUnresponsive
|
||||
},
|
||||
Some(proto::GuestKeybindsError::GuestGone) => NestedSessionKeybindsError::GuestGone,
|
||||
Some(proto::GuestKeybindsError::TooLarge) => NestedSessionKeybindsError::TooLarge,
|
||||
Some(proto::GuestKeybindsError::Timeout) => NestedSessionKeybindsError::Timeout,
|
||||
_ => NestedSessionKeybindsError::NotSupported,
|
||||
}
|
||||
}
|
||||
|
||||
fn base_mode_from_proto(base_mode: &str) -> Option<InputMode> {
|
||||
if base_mode.is_empty() {
|
||||
None
|
||||
} else {
|
||||
mode_from_proto(base_mode)
|
||||
}
|
||||
}
|
||||
|
||||
fn direction_to_proto(direction: Option<Direction>) -> i32 {
|
||||
match direction {
|
||||
Some(Direction::Left) => proto::NestedDirection::Left as i32,
|
||||
@@ -242,6 +346,40 @@ impl From<NestedSessionMessage> for proto::NestedSessionMessage {
|
||||
ascend_keys: keys_to_proto(&ascend_keys),
|
||||
descend_keys: keys_to_proto(&descend_keys),
|
||||
}),
|
||||
NestedSessionMessage::GuestModeUpdate {
|
||||
mode,
|
||||
base_mode,
|
||||
session_path,
|
||||
keybinds_generation,
|
||||
} => Payload::GuestModeUpdate(proto::GuestModeUpdate {
|
||||
mode: mode_to_proto(mode),
|
||||
base_mode: base_mode.map(mode_to_proto).unwrap_or_default(),
|
||||
session_path,
|
||||
keybinds_generation,
|
||||
}),
|
||||
NestedSessionMessage::RequestGuestKeybinds { request_id } => {
|
||||
Payload::RequestGuestKeybinds(proto::RequestGuestKeybinds { request_id })
|
||||
},
|
||||
NestedSessionMessage::GuestKeybindsReply { request_id, result } => {
|
||||
use proto::guest_keybinds_reply::Result as ReplyResult;
|
||||
let result = match result {
|
||||
Ok(nested_session_keybinds) => ReplyResult::Keybinds(proto::GuestKeybinds {
|
||||
session_path: nested_session_keybinds.session_path,
|
||||
mode: mode_to_proto(nested_session_keybinds.mode),
|
||||
base_mode: nested_session_keybinds
|
||||
.base_mode
|
||||
.map(mode_to_proto)
|
||||
.unwrap_or_default(),
|
||||
keybinds_generation: nested_session_keybinds.keybinds_generation,
|
||||
keybinds_payload: keybinds_to_proto(nested_session_keybinds.keybinds),
|
||||
}),
|
||||
Err(error) => ReplyResult::Error(keybinds_error_to_proto(error) as i32),
|
||||
};
|
||||
Payload::GuestKeybindsReply(proto::GuestKeybindsReply {
|
||||
request_id,
|
||||
result: Some(result),
|
||||
})
|
||||
},
|
||||
};
|
||||
proto::NestedSessionMessage {
|
||||
payload: Some(payload),
|
||||
@@ -294,6 +432,38 @@ impl TryFrom<proto::NestedSessionMessage> for NestedSessionMessage {
|
||||
descend_keys: keys_from_proto(&shortcut_update.descend_keys),
|
||||
})
|
||||
},
|
||||
Some(Payload::GuestModeUpdate(guest_mode_update)) => {
|
||||
let mode = mode_from_proto(&guest_mode_update.mode).ok_or(())?;
|
||||
Ok(NestedSessionMessage::GuestModeUpdate {
|
||||
mode,
|
||||
base_mode: base_mode_from_proto(&guest_mode_update.base_mode),
|
||||
session_path: guest_mode_update.session_path,
|
||||
keybinds_generation: guest_mode_update.keybinds_generation,
|
||||
})
|
||||
},
|
||||
Some(Payload::RequestGuestKeybinds(request)) => {
|
||||
Ok(NestedSessionMessage::RequestGuestKeybinds {
|
||||
request_id: request.request_id,
|
||||
})
|
||||
},
|
||||
Some(Payload::GuestKeybindsReply(reply)) => {
|
||||
use proto::guest_keybinds_reply::Result as ReplyResult;
|
||||
let result = match reply.result {
|
||||
Some(ReplyResult::Keybinds(guest_keybinds)) => Ok(NestedSessionKeybinds {
|
||||
mode: mode_from_proto(&guest_keybinds.mode).ok_or(())?,
|
||||
base_mode: base_mode_from_proto(&guest_keybinds.base_mode),
|
||||
keybinds: keybinds_from_proto(&guest_keybinds.keybinds_payload).ok_or(())?,
|
||||
session_path: guest_keybinds.session_path,
|
||||
keybinds_generation: guest_keybinds.keybinds_generation,
|
||||
}),
|
||||
Some(ReplyResult::Error(error)) => Err(keybinds_error_from_proto(error)),
|
||||
None => return Err(()),
|
||||
};
|
||||
Ok(NestedSessionMessage::GuestKeybindsReply {
|
||||
request_id: reply.request_id,
|
||||
result,
|
||||
})
|
||||
},
|
||||
None => Err(()),
|
||||
}
|
||||
}
|
||||
@@ -330,6 +500,24 @@ pub fn decode_base64(encoded: &[u8]) -> Option<Vec<u8>> {
|
||||
|
||||
const MAX_PARTIAL_FRAME_BYTES: usize = 1024 * 1024;
|
||||
|
||||
pub const MAX_FRAME_PAYLOAD_BYTES: usize =
|
||||
(MAX_PARTIAL_FRAME_BYTES - NESTED_FRAME_HEADER.len() - NESTED_FRAME_TERMINATOR.len()) / 4 * 3;
|
||||
|
||||
pub fn encode_keybinds_reply_payload(
|
||||
request_id: u64,
|
||||
result: NestedSessionKeybindsResponse,
|
||||
) -> Vec<u8> {
|
||||
let payload = encode_payload(&NestedSessionMessage::GuestKeybindsReply { request_id, result });
|
||||
if payload.len() > MAX_FRAME_PAYLOAD_BYTES {
|
||||
encode_payload(&NestedSessionMessage::GuestKeybindsReply {
|
||||
request_id,
|
||||
result: Err(NestedSessionKeybindsError::TooLarge),
|
||||
})
|
||||
} else {
|
||||
payload
|
||||
}
|
||||
}
|
||||
|
||||
enum FrameScanStatus {
|
||||
Complete(usize),
|
||||
NeedMore,
|
||||
@@ -424,12 +612,17 @@ impl NestedFrameExtractor {
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::data::BareKey;
|
||||
use crate::input::actions::Action;
|
||||
use strum::IntoEnumIterator;
|
||||
|
||||
fn all_message_arms() -> Vec<NestedSessionMessage> {
|
||||
vec![
|
||||
NestedSessionMessage::Announce {
|
||||
session_name: "guest".to_owned(),
|
||||
capabilities: vec![NestedSessionCapability::NestedControl],
|
||||
capabilities: vec![
|
||||
NestedSessionCapability::NestedControl,
|
||||
NestedSessionCapability::HintReporting,
|
||||
],
|
||||
},
|
||||
NestedSessionMessage::FocusHost {
|
||||
direction: Some(Direction::Left),
|
||||
@@ -440,7 +633,10 @@ mod tests {
|
||||
NestedSessionMessage::Bye,
|
||||
NestedSessionMessage::AnnounceAck {
|
||||
ancestry: vec!["outer".to_owned(), "middle".to_owned()],
|
||||
capabilities: vec![NestedSessionCapability::NestedControl],
|
||||
capabilities: vec![
|
||||
NestedSessionCapability::NestedControl,
|
||||
NestedSessionCapability::HintReporting,
|
||||
],
|
||||
descend_keys: vec![
|
||||
KeyWithModifier::new(BareKey::Char('o')).with_ctrl_modifier(),
|
||||
KeyWithModifier::new(BareKey::Down),
|
||||
@@ -462,6 +658,73 @@ mod tests {
|
||||
],
|
||||
descend_keys: vec![],
|
||||
},
|
||||
NestedSessionMessage::GuestModeUpdate {
|
||||
mode: InputMode::Locked,
|
||||
base_mode: Some(InputMode::Normal),
|
||||
session_path: vec!["middle".to_owned(), "inner".to_owned()],
|
||||
keybinds_generation: 4,
|
||||
},
|
||||
NestedSessionMessage::RequestGuestKeybinds { request_id: 17 },
|
||||
keybinds_reply(18, sample_keybinds()),
|
||||
NestedSessionMessage::GuestKeybindsReply {
|
||||
request_id: 19,
|
||||
result: Err(NestedSessionKeybindsError::GuestGone),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
fn keybinds_reply(request_id: u64, keybinds: KeybindsVec) -> NestedSessionMessage {
|
||||
NestedSessionMessage::GuestKeybindsReply {
|
||||
request_id,
|
||||
result: Ok(NestedSessionKeybinds {
|
||||
session_path: vec!["guest".to_owned()],
|
||||
mode: InputMode::Pane,
|
||||
base_mode: Some(InputMode::Normal),
|
||||
keybinds,
|
||||
keybinds_generation: 2,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn mode_update(mode: InputMode, base_mode: Option<InputMode>) -> NestedSessionMessage {
|
||||
NestedSessionMessage::GuestModeUpdate {
|
||||
mode,
|
||||
base_mode,
|
||||
session_path: vec!["guest".to_owned()],
|
||||
keybinds_generation: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn sample_keybinds() -> KeybindsVec {
|
||||
vec![
|
||||
(
|
||||
InputMode::Normal,
|
||||
vec![(
|
||||
KeyWithModifier::new(BareKey::Char('p')).with_ctrl_modifier(),
|
||||
vec![Action::SwitchToMode {
|
||||
input_mode: InputMode::Pane,
|
||||
}],
|
||||
)],
|
||||
),
|
||||
(
|
||||
InputMode::Pane,
|
||||
vec![
|
||||
(
|
||||
KeyWithModifier::new(BareKey::Char('n')),
|
||||
vec![Action::NewPane {
|
||||
direction: None,
|
||||
pane_name: None,
|
||||
start_suppressed: false,
|
||||
}],
|
||||
),
|
||||
(
|
||||
KeyWithModifier::new(BareKey::Esc),
|
||||
vec![Action::SwitchToMode {
|
||||
input_mode: InputMode::Normal,
|
||||
}],
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
@@ -538,6 +801,249 @@ mod tests {
|
||||
assert_eq!(decode_payload(&[]), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_capability_the_reading_side_does_not_know_is_dropped() {
|
||||
let capabilities = capabilities_from_proto(&[
|
||||
proto::NestedCapability::HintReporting as i32,
|
||||
9999,
|
||||
proto::NestedCapability::NestedControl as i32,
|
||||
]);
|
||||
assert_eq!(
|
||||
capabilities,
|
||||
vec![
|
||||
NestedSessionCapability::HintReporting,
|
||||
NestedSessionCapability::NestedControl,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn guest_mode_update_roundtrip_preserves_every_input_mode() {
|
||||
for mode in InputMode::iter() {
|
||||
let message = mode_update(mode, Some(mode));
|
||||
assert_eq!(
|
||||
decode_payload(&encode_payload(&message)),
|
||||
Some(message),
|
||||
"input mode {:?} did not survive the round trip",
|
||||
mode
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_guest_that_reports_no_base_mode_round_trips_as_none() {
|
||||
let message = mode_update(InputMode::Normal, None);
|
||||
assert_eq!(decode_payload(&encode_payload(&message)), Some(message));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_unknown_base_mode_name_costs_only_the_base_mode() {
|
||||
let payload = proto::NestedSessionMessage {
|
||||
payload: Some(proto::nested_session_message::Payload::GuestModeUpdate(
|
||||
proto::GuestModeUpdate {
|
||||
mode: "Normal".to_owned(),
|
||||
base_mode: "ModeFromAFutureZellij".to_owned(),
|
||||
session_path: vec!["guest".to_owned()],
|
||||
keybinds_generation: 0,
|
||||
},
|
||||
)),
|
||||
}
|
||||
.encode_to_vec();
|
||||
assert_eq!(
|
||||
decode_payload(&payload),
|
||||
Some(mode_update(InputMode::Normal, None))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_input_mode_name_is_rejected() {
|
||||
let payload = proto::NestedSessionMessage {
|
||||
payload: Some(proto::nested_session_message::Payload::GuestModeUpdate(
|
||||
proto::GuestModeUpdate {
|
||||
mode: "ModeFromAFutureZellij".to_owned(),
|
||||
base_mode: String::new(),
|
||||
session_path: vec![],
|
||||
keybinds_generation: 0,
|
||||
},
|
||||
)),
|
||||
}
|
||||
.encode_to_vec();
|
||||
assert_eq!(decode_payload(&payload), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn guest_keybinds_reply_preserves_modes_keys_and_actions() {
|
||||
let message = keybinds_reply(5, sample_keybinds());
|
||||
assert_eq!(decode_payload(&encode_payload(&message)), Some(message));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_keybinds_survive_the_roundtrip() {
|
||||
let message = keybinds_reply(5, vec![]);
|
||||
assert_eq!(decode_payload(&encode_payload(&message)), Some(message));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_keybinds_error_survives_the_roundtrip() {
|
||||
for error in [
|
||||
NestedSessionKeybindsError::NotANestedSession,
|
||||
NestedSessionKeybindsError::NotSupported,
|
||||
NestedSessionKeybindsError::GuestUnresponsive,
|
||||
NestedSessionKeybindsError::GuestGone,
|
||||
NestedSessionKeybindsError::TooLarge,
|
||||
NestedSessionKeybindsError::Timeout,
|
||||
] {
|
||||
let message = NestedSessionMessage::GuestKeybindsReply {
|
||||
request_id: 9,
|
||||
result: Err(error),
|
||||
};
|
||||
assert_eq!(decode_payload(&encode_payload(&message)), Some(message));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_unknown_keybinds_error_reads_as_not_supported() {
|
||||
let payload = proto::NestedSessionMessage {
|
||||
payload: Some(proto::nested_session_message::Payload::GuestKeybindsReply(
|
||||
proto::GuestKeybindsReply {
|
||||
request_id: 3,
|
||||
result: Some(proto::guest_keybinds_reply::Result::Error(9999)),
|
||||
},
|
||||
)),
|
||||
}
|
||||
.encode_to_vec();
|
||||
assert_eq!(
|
||||
decode_payload(&payload),
|
||||
Some(NestedSessionMessage::GuestKeybindsReply {
|
||||
request_id: 3,
|
||||
result: Err(NestedSessionKeybindsError::NotSupported),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_reply_without_a_result_is_rejected() {
|
||||
let payload = proto::NestedSessionMessage {
|
||||
payload: Some(proto::nested_session_message::Payload::GuestKeybindsReply(
|
||||
proto::GuestKeybindsReply {
|
||||
request_id: 3,
|
||||
result: None,
|
||||
},
|
||||
)),
|
||||
}
|
||||
.encode_to_vec();
|
||||
assert_eq!(decode_payload(&payload), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_binding_on_a_control_character_is_dropped_without_losing_the_rest() {
|
||||
let mut keybinds = sample_keybinds();
|
||||
keybinds[0].1.push((
|
||||
KeyWithModifier::new(BareKey::Char('\u{1b}')),
|
||||
vec![Action::Quit],
|
||||
));
|
||||
let bindings_in_normal_mode = keybinds[0].1.len();
|
||||
|
||||
let decoded = decode_payload(&encode_payload(&keybinds_reply(1, keybinds)));
|
||||
let Some(NestedSessionMessage::GuestKeybindsReply {
|
||||
result: Ok(NestedSessionKeybinds {
|
||||
keybinds: decoded, ..
|
||||
}),
|
||||
..
|
||||
}) = decoded
|
||||
else {
|
||||
panic!("expected a keybinding table, got {:?}", decoded);
|
||||
};
|
||||
|
||||
assert_eq!(decoded[0].1.len(), bindings_in_normal_mode - 1);
|
||||
assert!(!decoded.iter().flat_map(|(_mode, bindings)| bindings).any(
|
||||
|(key, _actions)| matches!(key.bare_key, BareKey::Char(character)
|
||||
if character.is_control())
|
||||
));
|
||||
assert_eq!(decoded[1], sample_keybinds()[1]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn undecodable_keybinds_payload_is_rejected() {
|
||||
let payload = proto::NestedSessionMessage {
|
||||
payload: Some(proto::nested_session_message::Payload::GuestKeybindsReply(
|
||||
proto::GuestKeybindsReply {
|
||||
request_id: 1,
|
||||
result: Some(proto::guest_keybinds_reply::Result::Keybinds(
|
||||
proto::GuestKeybinds {
|
||||
session_path: vec![],
|
||||
mode: "Normal".to_owned(),
|
||||
base_mode: String::new(),
|
||||
keybinds_generation: 0,
|
||||
keybinds_payload: vec![0xff, 0xff, 0xff, 0xff],
|
||||
},
|
||||
)),
|
||||
},
|
||||
)),
|
||||
}
|
||||
.encode_to_vec();
|
||||
assert_eq!(decode_payload(&payload), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_reply_that_fits_in_a_frame_is_sent_as_is() {
|
||||
let result = match keybinds_reply(4, sample_keybinds()) {
|
||||
NestedSessionMessage::GuestKeybindsReply { result, .. } => result,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let payload = encode_keybinds_reply_payload(4, result);
|
||||
assert_eq!(
|
||||
decode_payload(&payload),
|
||||
Some(keybinds_reply(4, sample_keybinds()))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_reply_too_large_for_a_frame_becomes_too_large() {
|
||||
let bindings: Vec<(KeyWithModifier, Vec<Action>)> = (0..MAX_FRAME_PAYLOAD_BYTES / 4)
|
||||
.map(|index| {
|
||||
(
|
||||
KeyWithModifier::new(BareKey::Char('a')),
|
||||
vec![Action::GoToTab {
|
||||
index: index as u32,
|
||||
}],
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
let payload = encode_keybinds_reply_payload(
|
||||
6,
|
||||
Ok(NestedSessionKeybinds {
|
||||
session_path: vec!["guest".to_owned()],
|
||||
mode: InputMode::Normal,
|
||||
base_mode: None,
|
||||
keybinds: vec![(InputMode::Normal, bindings)],
|
||||
keybinds_generation: 0,
|
||||
}),
|
||||
);
|
||||
assert!(payload.len() <= MAX_FRAME_PAYLOAD_BYTES);
|
||||
assert_eq!(
|
||||
decode_payload(&payload),
|
||||
Some(NestedSessionMessage::GuestKeybindsReply {
|
||||
request_id: 6,
|
||||
result: Err(NestedSessionKeybindsError::TooLarge),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_frame_at_the_payload_limit_is_extracted_even_when_split() {
|
||||
let payload = vec![0u8; MAX_FRAME_PAYLOAD_BYTES];
|
||||
let frame = encode_frame_from_payload(&payload);
|
||||
let mut extractor = NestedFrameExtractor::new();
|
||||
let (split_at, _) = frame.split_at(frame.len() / 2);
|
||||
let (cleaned_first, payloads_first) = extractor.extract(split_at);
|
||||
assert!(cleaned_first.is_empty());
|
||||
assert!(payloads_first.is_empty());
|
||||
let (cleaned_second, payloads_second) = extractor.extract(&frame[split_at.len()..]);
|
||||
assert!(cleaned_second.is_empty());
|
||||
assert_eq!(payloads_second, vec![payload]);
|
||||
}
|
||||
|
||||
fn test_scheduler(now: Instant) -> ReannounceScheduler {
|
||||
ReannounceScheduler::with_settings(now, Duration::from_millis(3000), 5)
|
||||
}
|
||||
|
||||
@@ -15,12 +15,18 @@ message NestedSessionMessage {
|
||||
AncestryUpdate ancestry_update = 12;
|
||||
Ping ping = 13;
|
||||
ShortcutUpdate shortcut_update = 14;
|
||||
GuestModeUpdate guest_mode_update = 15;
|
||||
RequestGuestKeybinds request_guest_keybinds = 16;
|
||||
GuestKeybindsReply guest_keybinds_reply = 18;
|
||||
}
|
||||
reserved 17;
|
||||
}
|
||||
|
||||
enum NestedCapability {
|
||||
NESTED_CAPABILITY_UNSPECIFIED = 0;
|
||||
NESTED_CAPABILITY_NESTED_CONTROL = 1;
|
||||
// The peer can report its input mode and keybindings to its host.
|
||||
NESTED_CAPABILITY_HINT_REPORTING = 2;
|
||||
}
|
||||
|
||||
enum NestedDirection {
|
||||
@@ -78,3 +84,40 @@ message AncestryUpdate {
|
||||
|
||||
message Ping {
|
||||
}
|
||||
|
||||
message GuestModeUpdate {
|
||||
string mode = 1;
|
||||
string base_mode = 2;
|
||||
repeated string session_path = 3;
|
||||
uint64 keybinds_generation = 4;
|
||||
}
|
||||
|
||||
message RequestGuestKeybinds {
|
||||
uint64 request_id = 1;
|
||||
}
|
||||
|
||||
enum GuestKeybindsError {
|
||||
GUEST_KEYBINDS_ERROR_UNSPECIFIED = 0;
|
||||
GUEST_KEYBINDS_ERROR_NOT_NESTED = 1;
|
||||
GUEST_KEYBINDS_ERROR_UNSUPPORTED = 2;
|
||||
GUEST_KEYBINDS_ERROR_GUEST_UNRESPONSIVE = 3;
|
||||
GUEST_KEYBINDS_ERROR_GUEST_GONE = 4;
|
||||
GUEST_KEYBINDS_ERROR_TOO_LARGE = 5;
|
||||
GUEST_KEYBINDS_ERROR_TIMEOUT = 6;
|
||||
}
|
||||
|
||||
message GuestKeybinds {
|
||||
repeated string session_path = 1;
|
||||
string mode = 2;
|
||||
string base_mode = 3;
|
||||
uint64 keybinds_generation = 4;
|
||||
bytes keybinds_payload = 5;
|
||||
}
|
||||
|
||||
message GuestKeybindsReply {
|
||||
uint64 request_id = 1;
|
||||
oneof result {
|
||||
GuestKeybinds keybinds = 2;
|
||||
GuestKeybindsError error = 3;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,6 +72,8 @@ enum EventType {
|
||||
SoftKeyboardVisibilityChanged = 47;
|
||||
HintText = 48;
|
||||
ActivePaneScroll = 49;
|
||||
NestedSessionModeUpdate = 50;
|
||||
NestedSessionEnded = 51;
|
||||
}
|
||||
|
||||
message EventNameList {
|
||||
@@ -123,6 +125,50 @@ message Event {
|
||||
SoftKeyboardVisibilityChangedPayload soft_keyboard_visibility_changed_payload = 41;
|
||||
HintTextPayload hint_text_payload = 42;
|
||||
ActivePaneScrollPayload active_pane_scroll_payload = 43;
|
||||
NestedSessionModeUpdatePayload nested_session_mode_update_payload = 44;
|
||||
NestedSessionEndedPayload nested_session_ended_payload = 45;
|
||||
}
|
||||
}
|
||||
|
||||
message NestedSessionModeUpdatePayload {
|
||||
PaneId pane_id = 1;
|
||||
repeated string session_path = 2;
|
||||
input_mode.InputMode mode = 3;
|
||||
optional input_mode.InputMode base_mode = 4;
|
||||
uint64 keybinds_generation = 5;
|
||||
}
|
||||
|
||||
enum NestedSessionEndReason {
|
||||
NestedSessionExited = 0;
|
||||
NestedSessionUnresponsive = 1;
|
||||
}
|
||||
|
||||
message NestedSessionEndedPayload {
|
||||
PaneId pane_id = 1;
|
||||
NestedSessionEndReason reason = 2;
|
||||
}
|
||||
|
||||
enum NestedSessionKeybindsError {
|
||||
NestedKeybindsNotANestedSession = 0;
|
||||
NestedKeybindsNotSupported = 1;
|
||||
NestedKeybindsGuestUnresponsive = 2;
|
||||
NestedKeybindsGuestGone = 3;
|
||||
NestedKeybindsTooLarge = 4;
|
||||
NestedKeybindsTimeout = 5;
|
||||
}
|
||||
|
||||
message NestedSessionKeybindsResult {
|
||||
repeated string session_path = 1;
|
||||
input_mode.InputMode mode = 2;
|
||||
optional input_mode.InputMode base_mode = 3;
|
||||
repeated InputModeKeybinds keybinds = 4;
|
||||
uint64 keybinds_generation = 5;
|
||||
}
|
||||
|
||||
message NestedSessionKeybindsResponse {
|
||||
oneof response {
|
||||
NestedSessionKeybindsResult ok = 1;
|
||||
NestedSessionKeybindsError err = 2;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -465,6 +511,7 @@ message PaneInfo {
|
||||
repeated IndexInPaneGroup index_in_pane_group = 23;
|
||||
optional string default_fg = 24;
|
||||
optional string default_bg = 25;
|
||||
optional string nested_session_name = 26;
|
||||
}
|
||||
|
||||
message IndexInPaneGroup {
|
||||
|
||||
@@ -20,6 +20,10 @@ pub use super::generated_api::api::{
|
||||
LayoutInfo as ProtobufLayoutInfo, LayoutMetadata as ProtobufLayoutMetadata,
|
||||
LayoutParsingError as ProtobufLayoutParsingError,
|
||||
LayoutWithError as ProtobufLayoutWithError, ModeUpdatePayload as ProtobufModeUpdatePayload,
|
||||
NestedSessionEndReason as ProtobufNestedSessionEndReason,
|
||||
NestedSessionKeybindsError as ProtobufNestedSessionKeybindsError,
|
||||
NestedSessionKeybindsResponse as ProtobufNestedSessionKeybindsResponse,
|
||||
NestedSessionKeybindsResult as ProtobufNestedSessionKeybindsResult,
|
||||
PaneContents as ProtobufPaneContents, PaneContentsEntry as ProtobufPaneContentsEntry,
|
||||
PaneFrameStyle as ProtobufPaneFrameStyle, PaneId as ProtobufPaneId,
|
||||
PaneInfo as ProtobufPaneInfo, PaneManifest as ProtobufPaneManifest,
|
||||
@@ -43,10 +47,11 @@ pub use super::generated_api::api::{
|
||||
#[allow(hidden_glob_reexports)]
|
||||
use crate::data::{
|
||||
ClientId, ClientInfo, CopyDestination, Event, EventType, FileMetadata, HostTerminalThemeMode,
|
||||
InputMode, KeyWithModifier, LayoutInfo, LayoutMetadata, ModeInfo, Mouse, PaneContents, PaneId,
|
||||
PaneInfo, PaneManifest, PaneMetadata, PaneScrollbackResponse, PermissionStatus,
|
||||
PluginCapabilities, PluginInfo, SelectedText, SessionInfo, Style, StyledText, TabInfo,
|
||||
TabMetadata, WebServerStatus, WebSharing,
|
||||
InputMode, KeyWithModifier, KeybindsVec, LayoutInfo, LayoutMetadata, ModeInfo, Mouse,
|
||||
NestedSessionEndReason, NestedSessionKeybinds, NestedSessionKeybindsError,
|
||||
NestedSessionKeybindsResponse, PaneContents, PaneId, PaneInfo, PaneManifest, PaneMetadata,
|
||||
PaneScrollbackResponse, PermissionStatus, PluginCapabilities, PluginInfo, SelectedText,
|
||||
SessionInfo, Style, StyledText, TabInfo, TabMetadata, WebServerStatus, WebSharing,
|
||||
};
|
||||
|
||||
use crate::errors::prelude::*;
|
||||
@@ -59,6 +64,76 @@ use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
use std::time::Duration;
|
||||
|
||||
/// Converts a keybinding table into the protobuf form used wherever keybindings cross the
|
||||
/// plugin boundary.
|
||||
///
|
||||
/// Anything that has no protobuf representation is dropped rather than failing the whole
|
||||
/// table: an unrepresentable action costs only that action, an unrepresentable key only
|
||||
/// that binding, and an unrepresentable mode only that mode. The reader
|
||||
/// ([`keybinds_from_protobuf`]) drops on the same terms, so a table survives the trip
|
||||
/// between builds that do not share every mode, key and action, minus whatever they do not
|
||||
/// have in common.
|
||||
pub fn keybinds_to_protobuf(keybinds: KeybindsVec) -> Vec<ProtobufInputModeKeybinds> {
|
||||
let mut protobuf_keybinds: Vec<ProtobufInputModeKeybinds> = vec![];
|
||||
for (input_mode, input_mode_keybinds) in keybinds {
|
||||
let Ok(mode) = ProtobufInputMode::try_from(input_mode) else {
|
||||
continue;
|
||||
};
|
||||
let mut key_binds: Vec<ProtobufKeyBind> = vec![];
|
||||
for (key, actions) in input_mode_keybinds {
|
||||
let Ok(protobuf_key) = ProtobufKey::try_from(key) else {
|
||||
continue;
|
||||
};
|
||||
let mut protobuf_actions: Vec<ProtobufAction> = vec![];
|
||||
for action in actions {
|
||||
if let Ok(protobuf_action) = action.try_into() {
|
||||
protobuf_actions.push(protobuf_action);
|
||||
}
|
||||
}
|
||||
key_binds.push(ProtobufKeyBind {
|
||||
key: Some(protobuf_key),
|
||||
action: protobuf_actions,
|
||||
});
|
||||
}
|
||||
protobuf_keybinds.push(ProtobufInputModeKeybinds {
|
||||
mode: mode as i32,
|
||||
key_bind: key_binds,
|
||||
});
|
||||
}
|
||||
protobuf_keybinds
|
||||
}
|
||||
|
||||
/// Converts a keybinding table back out of its protobuf form.
|
||||
///
|
||||
/// Anything that cannot be understood is dropped rather than failing the whole table: a
|
||||
/// mode, key, or action a given build does not know about simply does not appear. This
|
||||
/// keeps a table readable across builds that do not share every mode and action.
|
||||
pub fn keybinds_from_protobuf(protobuf_keybinds: Vec<ProtobufInputModeKeybinds>) -> KeybindsVec {
|
||||
protobuf_keybinds
|
||||
.into_iter()
|
||||
.filter_map(|input_mode_keybinds| {
|
||||
let input_mode: InputMode = ProtobufInputMode::try_from(input_mode_keybinds.mode)
|
||||
.ok()?
|
||||
.try_into()
|
||||
.ok()?;
|
||||
let key_binds = input_mode_keybinds
|
||||
.key_bind
|
||||
.into_iter()
|
||||
.filter_map(|key_bind| {
|
||||
let key: KeyWithModifier = key_bind.key?.try_into().ok()?;
|
||||
let actions: Vec<Action> = key_bind
|
||||
.action
|
||||
.into_iter()
|
||||
.filter_map(|action| action.try_into().ok())
|
||||
.collect();
|
||||
Some((key, actions))
|
||||
})
|
||||
.collect();
|
||||
Some((input_mode, key_binds))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
impl TryFrom<ProtobufEvent> for Event {
|
||||
type Error = &'static str;
|
||||
fn try_from(protobuf_event: ProtobufEvent) -> Result<Self, &'static str> {
|
||||
@@ -550,33 +625,52 @@ impl TryFrom<ProtobufEvent> for Event {
|
||||
},
|
||||
_ => Err("Malformed payload for HighlightClicked Event"),
|
||||
},
|
||||
Some(ProtobufEventType::NestedSessionModeUpdate) => match protobuf_event.payload {
|
||||
Some(ProtobufEventPayload::NestedSessionModeUpdatePayload(payload)) => {
|
||||
let pane_id = payload
|
||||
.pane_id
|
||||
.ok_or("Malformed payload for the NestedSessionModeUpdate Event")?;
|
||||
let mode: InputMode = ProtobufInputMode::try_from(payload.mode)
|
||||
.map_err(|_| "Malformed InputMode in the NestedSessionModeUpdate Event")?
|
||||
.try_into()?;
|
||||
let base_mode = payload
|
||||
.base_mode
|
||||
.and_then(|base_mode| ProtobufInputMode::try_from(base_mode).ok())
|
||||
.and_then(|base_mode| InputMode::try_from(base_mode).ok());
|
||||
Ok(Event::NestedSessionModeUpdate {
|
||||
pane_id: PaneId::try_from(pane_id)?,
|
||||
session_path: payload.session_path,
|
||||
mode,
|
||||
base_mode,
|
||||
keybinds_generation: payload.keybinds_generation,
|
||||
})
|
||||
},
|
||||
_ => Err("Malformed payload for the NestedSessionModeUpdate Event"),
|
||||
},
|
||||
Some(ProtobufEventType::NestedSessionEnded) => match protobuf_event.payload {
|
||||
Some(ProtobufEventPayload::NestedSessionEndedPayload(payload)) => {
|
||||
let pane_id = payload
|
||||
.pane_id
|
||||
.ok_or("Malformed payload for the NestedSessionEnded Event")?;
|
||||
let reason = match ProtobufNestedSessionEndReason::try_from(payload.reason) {
|
||||
Ok(ProtobufNestedSessionEndReason::NestedSessionExited) => {
|
||||
NestedSessionEndReason::Exited
|
||||
},
|
||||
Ok(ProtobufNestedSessionEndReason::NestedSessionUnresponsive) => {
|
||||
NestedSessionEndReason::Unresponsive
|
||||
},
|
||||
Err(_) => return Err("Unknown reason in the NestedSessionEnded Event"),
|
||||
};
|
||||
Ok(Event::NestedSessionEnded {
|
||||
pane_id: PaneId::try_from(pane_id)?,
|
||||
reason,
|
||||
})
|
||||
},
|
||||
_ => Err("Malformed payload for the NestedSessionEnded Event"),
|
||||
},
|
||||
Some(ProtobufEventType::InitialKeybinds) => match protobuf_event.payload {
|
||||
Some(ProtobufEventPayload::InitialKeybindsPayload(p)) => {
|
||||
let keybinds = p
|
||||
.keybinds
|
||||
.into_iter()
|
||||
.filter_map(|imk| {
|
||||
let mode: InputMode = ProtobufInputMode::try_from(imk.mode)
|
||||
.ok()?
|
||||
.try_into()
|
||||
.ok()?;
|
||||
let key_binds: Vec<(KeyWithModifier, Vec<Action>)> = imk
|
||||
.key_bind
|
||||
.into_iter()
|
||||
.filter_map(|kb| {
|
||||
let key: KeyWithModifier = kb.key?.try_into().ok()?;
|
||||
let actions: Vec<Action> = kb
|
||||
.action
|
||||
.into_iter()
|
||||
.filter_map(|a| a.try_into().ok())
|
||||
.collect();
|
||||
Some((key, actions))
|
||||
})
|
||||
.collect();
|
||||
Some((mode, key_binds))
|
||||
})
|
||||
.collect();
|
||||
Ok(Event::InitialKeybinds(keybinds))
|
||||
Ok(Event::InitialKeybinds(keybinds_from_protobuf(p.keybinds)))
|
||||
},
|
||||
_ => Err("Malformed payload for InitialKeybinds Event"),
|
||||
},
|
||||
@@ -1192,29 +1286,52 @@ impl TryFrom<Event> for ProtobufEvent {
|
||||
payload: Some(event::Payload::ActivePaneScrollPayload(payload)),
|
||||
})
|
||||
},
|
||||
Event::NestedSessionModeUpdate {
|
||||
pane_id,
|
||||
session_path,
|
||||
mode,
|
||||
base_mode,
|
||||
keybinds_generation,
|
||||
} => {
|
||||
let protobuf_mode: ProtobufInputMode = mode.try_into()?;
|
||||
let protobuf_base_mode = base_mode
|
||||
.map(ProtobufInputMode::try_from)
|
||||
.transpose()?
|
||||
.map(|base_mode| base_mode as i32);
|
||||
Ok(ProtobufEvent {
|
||||
name: ProtobufEventType::NestedSessionModeUpdate as i32,
|
||||
payload: Some(event::Payload::NestedSessionModeUpdatePayload(
|
||||
NestedSessionModeUpdatePayload {
|
||||
pane_id: Some(pane_id.try_into()?),
|
||||
session_path,
|
||||
mode: protobuf_mode as i32,
|
||||
base_mode: protobuf_base_mode,
|
||||
keybinds_generation,
|
||||
},
|
||||
)),
|
||||
})
|
||||
},
|
||||
Event::NestedSessionEnded { pane_id, reason } => {
|
||||
let protobuf_reason = match reason {
|
||||
NestedSessionEndReason::Exited => {
|
||||
ProtobufNestedSessionEndReason::NestedSessionExited
|
||||
},
|
||||
NestedSessionEndReason::Unresponsive => {
|
||||
ProtobufNestedSessionEndReason::NestedSessionUnresponsive
|
||||
},
|
||||
};
|
||||
Ok(ProtobufEvent {
|
||||
name: ProtobufEventType::NestedSessionEnded as i32,
|
||||
payload: Some(event::Payload::NestedSessionEndedPayload(
|
||||
NestedSessionEndedPayload {
|
||||
pane_id: Some(pane_id.try_into()?),
|
||||
reason: protobuf_reason as i32,
|
||||
},
|
||||
)),
|
||||
})
|
||||
},
|
||||
Event::InitialKeybinds(keybinds) => {
|
||||
let mut protobuf_keybinds: Vec<ProtobufInputModeKeybinds> = vec![];
|
||||
for (input_mode, input_mode_keybinds) in keybinds {
|
||||
let mode: ProtobufInputMode = input_mode.try_into()?;
|
||||
let mut key_binds: Vec<ProtobufKeyBind> = vec![];
|
||||
for (key, actions) in input_mode_keybinds {
|
||||
let protobuf_key: ProtobufKey = key.try_into()?;
|
||||
let mut protobuf_actions: Vec<ProtobufAction> = vec![];
|
||||
for action in actions {
|
||||
if let Ok(protobuf_action) = action.try_into() {
|
||||
protobuf_actions.push(protobuf_action);
|
||||
}
|
||||
}
|
||||
key_binds.push(ProtobufKeyBind {
|
||||
key: Some(protobuf_key),
|
||||
action: protobuf_actions,
|
||||
});
|
||||
}
|
||||
protobuf_keybinds.push(ProtobufInputModeKeybinds {
|
||||
mode: mode as i32,
|
||||
key_bind: key_binds,
|
||||
});
|
||||
}
|
||||
let protobuf_keybinds = keybinds_to_protobuf(keybinds);
|
||||
Ok(ProtobufEvent {
|
||||
name: ProtobufEventType::InitialKeybinds as i32,
|
||||
payload: Some(event::Payload::InitialKeybindsPayload(
|
||||
@@ -1825,6 +1942,7 @@ impl TryFrom<ProtobufPaneInfo> for PaneInfo {
|
||||
.collect(),
|
||||
default_fg: protobuf_pane_info.default_fg,
|
||||
default_bg: protobuf_pane_info.default_bg,
|
||||
nested_session_name: protobuf_pane_info.nested_session_name,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1870,6 +1988,7 @@ impl TryFrom<PaneInfo> for ProtobufPaneInfo {
|
||||
.collect(),
|
||||
default_fg: pane_info.default_fg,
|
||||
default_bg: pane_info.default_bg,
|
||||
nested_session_name: pane_info.nested_session_name,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1960,31 +2079,8 @@ impl TryFrom<ProtobufModeUpdatePayload> for ModeInfo {
|
||||
let base_mode: Option<InputMode> = protobuf_mode_update_payload
|
||||
.base_mode
|
||||
.and_then(|b_m| ProtobufInputMode::try_from(b_m).ok()?.try_into().ok());
|
||||
let keybinds: Vec<(InputMode, Vec<(KeyWithModifier, Vec<Action>)>)> =
|
||||
protobuf_mode_update_payload
|
||||
.keybinds
|
||||
.iter_mut()
|
||||
.filter_map(|k| {
|
||||
let input_mode: InputMode = ProtobufInputMode::try_from(k.mode)
|
||||
.ok()
|
||||
.ok_or("Malformed InputMode in the ModeUpdate Event")
|
||||
.ok()?
|
||||
.try_into()
|
||||
.ok()?;
|
||||
let mut keybinds: Vec<(KeyWithModifier, Vec<Action>)> = vec![];
|
||||
for mut protobuf_keybind in k.key_bind.drain(..) {
|
||||
let key: KeyWithModifier = protobuf_keybind.key.unwrap().try_into().ok()?;
|
||||
let mut actions: Vec<Action> = vec![];
|
||||
for action in protobuf_keybind.action.drain(..) {
|
||||
if let Ok(action) = action.try_into() {
|
||||
actions.push(action);
|
||||
}
|
||||
}
|
||||
keybinds.push((key, actions));
|
||||
}
|
||||
Some((input_mode, keybinds))
|
||||
})
|
||||
.collect();
|
||||
let keybinds: KeybindsVec =
|
||||
keybinds_from_protobuf(std::mem::take(&mut protobuf_mode_update_payload.keybinds));
|
||||
let style: Style = protobuf_mode_update_payload
|
||||
.style
|
||||
.and_then(|m| m.try_into().ok())
|
||||
@@ -2107,30 +2203,7 @@ impl TryFrom<ModeInfo> for ProtobufModeUpdatePayload {
|
||||
.iter()
|
||||
.map(|key| key.to_kdl())
|
||||
.collect();
|
||||
let mut protobuf_input_mode_keybinds: Vec<ProtobufInputModeKeybinds> = vec![];
|
||||
for (input_mode, input_mode_keybinds) in mode_info.keybinds {
|
||||
let mode: ProtobufInputMode = input_mode.try_into()?;
|
||||
let mut keybinds: Vec<ProtobufKeyBind> = vec![];
|
||||
for (key, actions) in input_mode_keybinds {
|
||||
let protobuf_key: ProtobufKey = key.try_into()?;
|
||||
let mut protobuf_actions: Vec<ProtobufAction> = vec![];
|
||||
for action in actions {
|
||||
if let Ok(protobuf_action) = action.try_into() {
|
||||
protobuf_actions.push(protobuf_action);
|
||||
}
|
||||
}
|
||||
let key_bind = ProtobufKeyBind {
|
||||
key: Some(protobuf_key),
|
||||
action: protobuf_actions,
|
||||
};
|
||||
keybinds.push(key_bind);
|
||||
}
|
||||
let input_mode_keybind = ProtobufInputModeKeybinds {
|
||||
mode: mode as i32,
|
||||
key_bind: keybinds,
|
||||
};
|
||||
protobuf_input_mode_keybinds.push(input_mode_keybind);
|
||||
}
|
||||
let protobuf_input_mode_keybinds = keybinds_to_protobuf(mode_info.keybinds);
|
||||
Ok(ProtobufModeUpdatePayload {
|
||||
current_mode: current_mode as i32,
|
||||
style: Some(style),
|
||||
@@ -2242,6 +2315,8 @@ impl TryFrom<ProtobufEventType> for EventType {
|
||||
},
|
||||
ProtobufEventType::HintText => EventType::HintText,
|
||||
ProtobufEventType::ActivePaneScroll => EventType::ActivePaneScroll,
|
||||
ProtobufEventType::NestedSessionModeUpdate => EventType::NestedSessionModeUpdate,
|
||||
ProtobufEventType::NestedSessionEnded => EventType::NestedSessionEnded,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -2300,6 +2375,8 @@ impl TryFrom<EventType> for ProtobufEventType {
|
||||
},
|
||||
EventType::HintText => ProtobufEventType::HintText,
|
||||
EventType::ActivePaneScroll => ProtobufEventType::ActivePaneScroll,
|
||||
EventType::NestedSessionModeUpdate => ProtobufEventType::NestedSessionModeUpdate,
|
||||
EventType::NestedSessionEnded => ProtobufEventType::NestedSessionEnded,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -2981,6 +3058,7 @@ fn serialize_session_update_event_with_non_default_values() {
|
||||
index_in_pane_group: index_in_pane_group_1,
|
||||
default_fg: None,
|
||||
default_bg: None,
|
||||
nested_session_name: None,
|
||||
},
|
||||
PaneInfo {
|
||||
id: 1,
|
||||
@@ -3008,6 +3086,7 @@ fn serialize_session_update_event_with_non_default_values() {
|
||||
index_in_pane_group: index_in_pane_group_2,
|
||||
default_fg: None,
|
||||
default_bg: None,
|
||||
nested_session_name: None,
|
||||
},
|
||||
];
|
||||
panes.insert(0, panes_list);
|
||||
@@ -3340,6 +3419,113 @@ impl TryFrom<PaneScrollbackResponse> for ProtobufPaneScrollbackResponse {
|
||||
}
|
||||
}
|
||||
|
||||
fn nested_session_keybinds_error_to_protobuf(
|
||||
error: NestedSessionKeybindsError,
|
||||
) -> ProtobufNestedSessionKeybindsError {
|
||||
match error {
|
||||
NestedSessionKeybindsError::NotANestedSession => {
|
||||
ProtobufNestedSessionKeybindsError::NestedKeybindsNotANestedSession
|
||||
},
|
||||
NestedSessionKeybindsError::NotSupported => {
|
||||
ProtobufNestedSessionKeybindsError::NestedKeybindsNotSupported
|
||||
},
|
||||
NestedSessionKeybindsError::GuestUnresponsive => {
|
||||
ProtobufNestedSessionKeybindsError::NestedKeybindsGuestUnresponsive
|
||||
},
|
||||
NestedSessionKeybindsError::GuestGone => {
|
||||
ProtobufNestedSessionKeybindsError::NestedKeybindsGuestGone
|
||||
},
|
||||
NestedSessionKeybindsError::TooLarge => {
|
||||
ProtobufNestedSessionKeybindsError::NestedKeybindsTooLarge
|
||||
},
|
||||
NestedSessionKeybindsError::Timeout => {
|
||||
ProtobufNestedSessionKeybindsError::NestedKeybindsTimeout
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn nested_session_keybinds_error_from_protobuf(
|
||||
error: ProtobufNestedSessionKeybindsError,
|
||||
) -> NestedSessionKeybindsError {
|
||||
match error {
|
||||
ProtobufNestedSessionKeybindsError::NestedKeybindsNotANestedSession => {
|
||||
NestedSessionKeybindsError::NotANestedSession
|
||||
},
|
||||
ProtobufNestedSessionKeybindsError::NestedKeybindsNotSupported => {
|
||||
NestedSessionKeybindsError::NotSupported
|
||||
},
|
||||
ProtobufNestedSessionKeybindsError::NestedKeybindsGuestUnresponsive => {
|
||||
NestedSessionKeybindsError::GuestUnresponsive
|
||||
},
|
||||
ProtobufNestedSessionKeybindsError::NestedKeybindsGuestGone => {
|
||||
NestedSessionKeybindsError::GuestGone
|
||||
},
|
||||
ProtobufNestedSessionKeybindsError::NestedKeybindsTooLarge => {
|
||||
NestedSessionKeybindsError::TooLarge
|
||||
},
|
||||
ProtobufNestedSessionKeybindsError::NestedKeybindsTimeout => {
|
||||
NestedSessionKeybindsError::Timeout
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn nested_session_keybinds_response_to_protobuf(
|
||||
response: NestedSessionKeybindsResponse,
|
||||
) -> Result<ProtobufNestedSessionKeybindsResponse, &'static str> {
|
||||
use super::generated_api::api::event::nested_session_keybinds_response::Response;
|
||||
let response_field = match response {
|
||||
Ok(nested_session_keybinds) => {
|
||||
let mode: ProtobufInputMode = nested_session_keybinds.mode.try_into()?;
|
||||
let base_mode = nested_session_keybinds
|
||||
.base_mode
|
||||
.map(ProtobufInputMode::try_from)
|
||||
.transpose()?
|
||||
.map(|base_mode| base_mode as i32);
|
||||
Response::Ok(ProtobufNestedSessionKeybindsResult {
|
||||
session_path: nested_session_keybinds.session_path,
|
||||
mode: mode as i32,
|
||||
base_mode,
|
||||
keybinds: keybinds_to_protobuf(nested_session_keybinds.keybinds),
|
||||
keybinds_generation: nested_session_keybinds.keybinds_generation,
|
||||
})
|
||||
},
|
||||
Err(error) => Response::Err(nested_session_keybinds_error_to_protobuf(error) as i32),
|
||||
};
|
||||
Ok(ProtobufNestedSessionKeybindsResponse {
|
||||
response: Some(response_field),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn nested_session_keybinds_response_from_protobuf(
|
||||
protobuf_response: ProtobufNestedSessionKeybindsResponse,
|
||||
) -> Result<NestedSessionKeybindsResponse, &'static str> {
|
||||
use super::generated_api::api::event::nested_session_keybinds_response::Response;
|
||||
match protobuf_response.response {
|
||||
Some(Response::Ok(result)) => {
|
||||
let mode: InputMode = ProtobufInputMode::try_from(result.mode)
|
||||
.map_err(|_| "Malformed InputMode in NestedSessionKeybindsResponse")?
|
||||
.try_into()?;
|
||||
let base_mode = result
|
||||
.base_mode
|
||||
.and_then(|base_mode| ProtobufInputMode::try_from(base_mode).ok())
|
||||
.and_then(|base_mode| InputMode::try_from(base_mode).ok());
|
||||
Ok(Ok(NestedSessionKeybinds {
|
||||
session_path: result.session_path,
|
||||
mode,
|
||||
base_mode,
|
||||
keybinds: keybinds_from_protobuf(result.keybinds),
|
||||
keybinds_generation: result.keybinds_generation,
|
||||
}))
|
||||
},
|
||||
Some(Response::Err(error)) => {
|
||||
let error = ProtobufNestedSessionKeybindsError::try_from(error)
|
||||
.map_err(|_| "Unknown error in NestedSessionKeybindsResponse")?;
|
||||
Ok(Err(nested_session_keybinds_error_from_protobuf(error)))
|
||||
},
|
||||
None => Err("NestedSessionKeybindsResponse missing response field"),
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<ProtobufSelectedText> for SelectedText {
|
||||
type Error = &'static str;
|
||||
fn try_from(protobuf_selected_text: ProtobufSelectedText) -> Result<Self, &'static str> {
|
||||
@@ -3365,3 +3551,121 @@ impl TryFrom<SelectedText> for ProtobufSelectedText {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialize_nested_session_mode_update_event() {
|
||||
use prost::Message;
|
||||
let nested_session_mode_update_event = Event::NestedSessionModeUpdate {
|
||||
pane_id: PaneId::Terminal(3),
|
||||
session_path: vec!["middle".to_owned(), "inner".to_owned()],
|
||||
mode: InputMode::Pane,
|
||||
base_mode: Some(InputMode::Locked),
|
||||
keybinds_generation: 7,
|
||||
};
|
||||
let protobuf_event: ProtobufEvent =
|
||||
nested_session_mode_update_event.clone().try_into().unwrap();
|
||||
let serialized_protobuf_event = protobuf_event.encode_to_vec();
|
||||
let deserialized_protobuf_event: ProtobufEvent =
|
||||
Message::decode(serialized_protobuf_event.as_slice()).unwrap();
|
||||
let deserialized_event: Event = deserialized_protobuf_event.try_into().unwrap();
|
||||
assert_eq!(
|
||||
nested_session_mode_update_event, deserialized_event,
|
||||
"Event properly serialized/deserialized without change"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialize_nested_session_ended_event() {
|
||||
use prost::Message;
|
||||
for reason in [
|
||||
NestedSessionEndReason::Exited,
|
||||
NestedSessionEndReason::Unresponsive,
|
||||
] {
|
||||
let nested_session_ended_event = Event::NestedSessionEnded {
|
||||
pane_id: PaneId::Terminal(3),
|
||||
reason,
|
||||
};
|
||||
let protobuf_event: ProtobufEvent = nested_session_ended_event.clone().try_into().unwrap();
|
||||
let serialized_protobuf_event = protobuf_event.encode_to_vec();
|
||||
let deserialized_protobuf_event: ProtobufEvent =
|
||||
Message::decode(serialized_protobuf_event.as_slice()).unwrap();
|
||||
let deserialized_event: Event = deserialized_protobuf_event.try_into().unwrap();
|
||||
assert_eq!(nested_session_ended_event, deserialized_event);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialize_nested_session_keybinds_response() {
|
||||
use crate::data::BareKey;
|
||||
use prost::Message;
|
||||
let ok_response: NestedSessionKeybindsResponse = Ok(NestedSessionKeybinds {
|
||||
session_path: vec!["guest session".to_owned()],
|
||||
mode: InputMode::Pane,
|
||||
base_mode: Some(InputMode::Normal),
|
||||
keybinds: vec![
|
||||
(
|
||||
InputMode::Normal,
|
||||
vec![(
|
||||
KeyWithModifier::new(BareKey::Char('g')).with_ctrl_modifier(),
|
||||
vec![Action::SwitchToMode {
|
||||
input_mode: InputMode::Pane,
|
||||
}],
|
||||
)],
|
||||
),
|
||||
(
|
||||
InputMode::Pane,
|
||||
vec![(
|
||||
KeyWithModifier::new(BareKey::Char('x')),
|
||||
vec![Action::CloseFocus],
|
||||
)],
|
||||
),
|
||||
],
|
||||
keybinds_generation: 3,
|
||||
});
|
||||
let mut responses = vec![ok_response];
|
||||
for error in [
|
||||
NestedSessionKeybindsError::NotANestedSession,
|
||||
NestedSessionKeybindsError::NotSupported,
|
||||
NestedSessionKeybindsError::GuestUnresponsive,
|
||||
NestedSessionKeybindsError::GuestGone,
|
||||
NestedSessionKeybindsError::TooLarge,
|
||||
NestedSessionKeybindsError::Timeout,
|
||||
] {
|
||||
responses.push(Err(error));
|
||||
}
|
||||
for response in responses {
|
||||
let encoded = nested_session_keybinds_response_to_protobuf(response.clone())
|
||||
.unwrap()
|
||||
.encode_to_vec();
|
||||
let decoded = nested_session_keybinds_response_from_protobuf(
|
||||
ProtobufNestedSessionKeybindsResponse::decode(encoded.as_slice()).unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(response, decoded);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_key_with_no_protobuf_form_costs_only_its_own_binding() {
|
||||
use crate::data::BareKey;
|
||||
// Only f1 through f12 have a protobuf representation.
|
||||
let unrepresentable_key = KeyWithModifier::new(BareKey::F(20));
|
||||
let representable_key = KeyWithModifier::new(BareKey::Char('x'));
|
||||
let keybinds = vec![(
|
||||
InputMode::Normal,
|
||||
vec![
|
||||
(unrepresentable_key, vec![Action::CloseFocus]),
|
||||
(representable_key.clone(), vec![Action::CloseFocus]),
|
||||
],
|
||||
)];
|
||||
|
||||
let converted = keybinds_from_protobuf(keybinds_to_protobuf(keybinds));
|
||||
|
||||
assert_eq!(
|
||||
converted,
|
||||
vec![(
|
||||
InputMode::Normal,
|
||||
vec![(representable_key, vec![Action::CloseFocus])]
|
||||
)]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -222,6 +222,7 @@ enum CommandName {
|
||||
ApplyTiledSwapLayout = 229;
|
||||
ApplyFloatingSwapLayout = 230;
|
||||
SetPaneBorderStyle = 231;
|
||||
GetNestedSessionKeybinds = 232;
|
||||
}
|
||||
|
||||
message PluginCommand {
|
||||
@@ -385,9 +386,14 @@ message PluginCommand {
|
||||
string apply_tiled_swap_layout_payload = 172;
|
||||
string apply_floating_swap_layout_payload = 173;
|
||||
SetPaneBorderStylePayload set_pane_border_style_payload = 174;
|
||||
GetNestedSessionKeybindsPayload get_nested_session_keybinds_payload = 175;
|
||||
}
|
||||
}
|
||||
|
||||
message GetNestedSessionKeybindsPayload {
|
||||
PaneId pane_id = 1;
|
||||
}
|
||||
|
||||
message SetPaneFrameStylePayload {
|
||||
pane_frame_style.PaneFrameStyle pane_frame_style = 1;
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ pub use super::generated_api::api::{
|
||||
GenerateRandomNameResponse as ProtobufGenerateRandomNameResponse,
|
||||
GenerateWebLoginTokenPayload, GetFocusedPaneInfoPayload,
|
||||
GetFocusedPaneInfoResponse as ProtobufGetFocusedPaneInfoResponse, GetLayoutDirPayload,
|
||||
GetLayoutDirResponse as ProtobufGetLayoutDirResponse,
|
||||
GetLayoutDirResponse as ProtobufGetLayoutDirResponse, GetNestedSessionKeybindsPayload,
|
||||
GetPaneCwdPayload as ProtobufGetPaneCwdPayload,
|
||||
GetPaneCwdResponse as ProtobufGetPaneCwdResponse, GetPaneInfoPayload,
|
||||
GetPaneInfoResponse as ProtobufGetPaneInfoResponse, GetPanePidPayload,
|
||||
@@ -2521,6 +2521,16 @@ impl TryFrom<ProtobufPluginCommand> for PluginCommand {
|
||||
Some(CommandName::CurrentSessionLastSavedTime) => {
|
||||
Ok(PluginCommand::CurrentSessionLastSavedTime)
|
||||
},
|
||||
Some(CommandName::GetNestedSessionKeybinds) => match protobuf_plugin_command.payload {
|
||||
Some(Payload::GetNestedSessionKeybindsPayload(payload)) => {
|
||||
let pane_id = payload
|
||||
.pane_id
|
||||
.ok_or("Malformed pane_id for GetNestedSessionKeybinds")
|
||||
.and_then(|pane_id| pane_id.try_into())?;
|
||||
Ok(PluginCommand::GetNestedSessionKeybinds(pane_id))
|
||||
},
|
||||
_ => Err("Malformed payload for GetNestedSessionKeybinds"),
|
||||
},
|
||||
Some(CommandName::GetPaneInfo) => match protobuf_plugin_command.payload {
|
||||
Some(Payload::GetPaneInfoPayload(get_pane_info_payload)) => {
|
||||
let pane_id = get_pane_info_payload
|
||||
@@ -4323,6 +4333,17 @@ impl TryFrom<PluginCommand> for ProtobufPluginCommand {
|
||||
CurrentSessionLastSavedTimePayload {},
|
||||
)),
|
||||
}),
|
||||
PluginCommand::GetNestedSessionKeybinds(pane_id) => {
|
||||
let protobuf_pane_id: ProtobufPaneId = pane_id.try_into()?;
|
||||
Ok(ProtobufPluginCommand {
|
||||
name: CommandName::GetNestedSessionKeybinds as i32,
|
||||
payload: Some(Payload::GetNestedSessionKeybindsPayload(
|
||||
GetNestedSessionKeybindsPayload {
|
||||
pane_id: Some(protobuf_pane_id),
|
||||
},
|
||||
)),
|
||||
})
|
||||
},
|
||||
PluginCommand::GetPaneInfo(pane_id) => {
|
||||
let protobuf_pane_id: ProtobufPaneId = pane_id.try_into()?;
|
||||
Ok(ProtobufPluginCommand {
|
||||
|
||||
Reference in New Issue
Block a user