mirror of
https://github.com/herdrdev/herdr.git
synced 2026-09-22 08:01:06 +00:00
767 lines
28 KiB
Rust
767 lines
28 KiB
Rust
use super::*;
|
|
use crate::protocol::ClientPaneInputEvent;
|
|
use crate::raw_input::RawInputEvent;
|
|
use crossterm::event::{KeyCode, KeyEventKind, KeyModifiers};
|
|
|
|
fn is_retained_selection_copy_key(key: &crate::input::TerminalKey) -> bool {
|
|
matches!(key.code, KeyCode::Char('c' | 'C'))
|
|
&& matches!(key.modifiers, KeyModifiers::CONTROL | KeyModifiers::SUPER)
|
|
}
|
|
|
|
impl ClientShellState {
|
|
#[cfg(any(unix, test))]
|
|
pub(crate) fn handle_input_bytes(&mut self, data: &[u8]) -> ClientShellInput {
|
|
self.handle_raw_events(crate::raw_input::parse_raw_input_bytes_sync(data))
|
|
}
|
|
|
|
#[cfg(any(unix, test))]
|
|
pub(crate) fn handle_pixel_mouse(
|
|
&mut self,
|
|
data: &[u8],
|
|
geometry: crate::input::mouse::HostGeometry,
|
|
) -> ClientShellInput {
|
|
let Some((x, y)) = crate::input::mouse::parse_report(data) else {
|
|
return ClientShellInput::default();
|
|
};
|
|
let Some((column, row)) = geometry.cell(x, y) else {
|
|
return ClientShellInput::default();
|
|
};
|
|
let Some(cell_report) = crate::input::mouse::report_at_cell(data, column, row) else {
|
|
return ClientShellInput::default();
|
|
};
|
|
let events = crate::raw_input::parse_raw_input_bytes_sync(&cell_report);
|
|
if events.len() != 1 || !matches!(events[0], RawInputEvent::Mouse(_)) {
|
|
return ClientShellInput::default();
|
|
}
|
|
self.host_mouse_pixels = Some(crate::input::mouse::HostPixels { x, y, geometry });
|
|
let outcome = self.handle_raw_events(events);
|
|
self.host_mouse_pixels = None;
|
|
outcome
|
|
}
|
|
|
|
#[cfg(windows)]
|
|
pub(crate) fn handle_client_events(
|
|
&mut self,
|
|
events: &[crate::protocol::ClientInputEvent],
|
|
) -> ClientShellInput {
|
|
self.handle_raw_events(
|
|
events
|
|
.iter()
|
|
.map(crate::protocol::ClientInputEvent::to_raw_input_event)
|
|
.collect(),
|
|
)
|
|
}
|
|
|
|
fn prepare_committed_text(&mut self, text: &str, outcome: &mut ClientShellInput) -> bool {
|
|
if self.insert_copy_search_text(text) {
|
|
outcome.repaint = true;
|
|
return true;
|
|
}
|
|
self.pending_word_selection = None;
|
|
if self.copy_or_terminal_mode() != ClientShellMode::Copy && self.selection.take().is_some()
|
|
{
|
|
self.stop_selection_autoscroll();
|
|
self.selection_highlight_clear_deadline = None;
|
|
outcome.repaint = true;
|
|
}
|
|
false
|
|
}
|
|
|
|
pub(super) fn handle_raw_events(&mut self, events: Vec<RawInputEvent>) -> ClientShellInput {
|
|
let mut outcome = ClientShellInput::default();
|
|
if !events.is_empty() && self.endpoint_error.take().is_some() {
|
|
outcome.repaint = true;
|
|
}
|
|
for event in events {
|
|
match event {
|
|
RawInputEvent::Key(key) => self.handle_key(key, &mut outcome),
|
|
RawInputEvent::Text(text) => {
|
|
let text = text.into_string();
|
|
if matches!(
|
|
self.overlay,
|
|
Some(
|
|
ClientShellOverlay::Onboarding
|
|
| ClientShellOverlay::ProductAnnouncement(_)
|
|
| ClientShellOverlay::ReleaseNotes(_)
|
|
)
|
|
) {
|
|
continue;
|
|
}
|
|
if self.prepare_committed_text(&text, &mut outcome) {
|
|
continue;
|
|
}
|
|
if let Some(target) = self.popup_input_target() {
|
|
super::push_target_event(
|
|
target,
|
|
ClientPaneInputEvent::TextCommit(text),
|
|
&mut outcome,
|
|
);
|
|
} else if !self.popup_pending {
|
|
if self.insert_overlay_text(&text) {
|
|
outcome.repaint = true;
|
|
} else if self.overlay.is_none() && self.mode == ClientShellMode::Terminal {
|
|
self.push_focused_pane_event(
|
|
ClientPaneInputEvent::TextCommit(text),
|
|
&mut outcome,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
RawInputEvent::Paste(text) => {
|
|
if matches!(
|
|
self.overlay,
|
|
Some(
|
|
ClientShellOverlay::Onboarding
|
|
| ClientShellOverlay::ProductAnnouncement(_)
|
|
| ClientShellOverlay::ReleaseNotes(_)
|
|
)
|
|
) {
|
|
continue;
|
|
}
|
|
if self.prepare_committed_text(&text, &mut outcome) {
|
|
continue;
|
|
}
|
|
if let Some(target) = self.popup_input_target() {
|
|
super::push_target_event(
|
|
target,
|
|
ClientPaneInputEvent::Paste(text),
|
|
&mut outcome,
|
|
);
|
|
} else if !self.popup_pending {
|
|
if self.insert_overlay_text(&text) {
|
|
outcome.repaint = true;
|
|
} else if self.overlay.is_none() && self.mode == ClientShellMode::Terminal {
|
|
self.push_focused_pane_event(
|
|
ClientPaneInputEvent::Paste(text),
|
|
&mut outcome,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
RawInputEvent::Mouse(mouse) => self.handle_mouse(mouse, &mut outcome),
|
|
RawInputEvent::OuterFocusGained => {
|
|
self.outer_focused = Some(true);
|
|
outcome.query_host_appearance = true;
|
|
}
|
|
RawInputEvent::OuterFocusLost => self.outer_focused = Some(false),
|
|
RawInputEvent::HostColorSchemeChanged(appearance) => {
|
|
if self.config.theme_runtime.auto_switch {
|
|
self.config.palette = crate::app::client_palette_for_appearance(
|
|
&self.config.theme_runtime,
|
|
appearance,
|
|
);
|
|
outcome.repaint = true;
|
|
}
|
|
}
|
|
RawInputEvent::HostDefaultColor { .. }
|
|
| RawInputEvent::HostPaletteColors { .. }
|
|
| RawInputEvent::HostCellSizeReport { .. }
|
|
| RawInputEvent::Unsupported => {}
|
|
}
|
|
}
|
|
outcome.repaint |= self.resume_mobile_switcher_if_ready();
|
|
outcome
|
|
}
|
|
|
|
pub(super) fn handle_key(
|
|
&mut self,
|
|
key: crate::input::TerminalKey,
|
|
outcome: &mut ClientShellInput,
|
|
) {
|
|
if self.copy_operation_in_flight {
|
|
self.copy_input_queue.push_back(key);
|
|
return;
|
|
}
|
|
const LOCAL_INPUT_SOURCE: u8 = 0;
|
|
let lease_key = crate::input::InputLeaseKey::new(LOCAL_INPUT_SOURCE, &key);
|
|
let key = self.input_leases.normalize_press(&lease_key, key);
|
|
match key.kind {
|
|
KeyEventKind::Press => {
|
|
let initial_context = self.input_context();
|
|
let target = self.route_key_press(&key, outcome);
|
|
if let Some(target) = target.as_ref() {
|
|
self.push_pane_key(target.clone(), key.clone(), outcome);
|
|
}
|
|
let resulting_context = self.input_context();
|
|
let plan = self.input_leases.complete_press(
|
|
lease_key,
|
|
&key,
|
|
Some(&initial_context),
|
|
Some(&resulting_context),
|
|
target,
|
|
);
|
|
self.execute_repeat_plan(lease_key, key, plan, outcome);
|
|
}
|
|
KeyEventKind::Repeat => {
|
|
let context = self.input_context();
|
|
let plan = self
|
|
.input_leases
|
|
.plan_repeat(lease_key, &key, Some(&context));
|
|
self.execute_repeat_plan(lease_key, key, plan, outcome);
|
|
}
|
|
KeyEventKind::Release => {
|
|
if let Some(lease) = self.input_leases.remove_forwarded(&lease_key) {
|
|
self.push_pane_key(lease.target, key, outcome);
|
|
} else {
|
|
let _ = self.input_leases.remove(&lease_key);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn execute_repeat_plan(
|
|
&mut self,
|
|
lease_key: crate::input::InputLeaseKey<u8>,
|
|
key: crate::input::TerminalKey,
|
|
plan: crate::input::RepeatPlan<ClientInputContext, ClientInputTarget>,
|
|
outcome: &mut ClientShellInput,
|
|
) {
|
|
match plan {
|
|
crate::input::RepeatPlan::Forwarded(target) => {
|
|
let pane_blocked_by_popup = matches!(&target, ClientInputTarget::Pane(_))
|
|
&& (self.popup_pending || self.popup_terminal_id.is_some());
|
|
if !pane_blocked_by_popup {
|
|
self.push_pane_key(target, key, outcome);
|
|
}
|
|
}
|
|
crate::input::RepeatPlan::Reprocess {
|
|
context,
|
|
repetitions,
|
|
tracked,
|
|
} => {
|
|
for _ in 0..repetitions {
|
|
let current = self.input_context();
|
|
if !self.input_leases.reprocess_allowed(
|
|
lease_key,
|
|
&context,
|
|
Some(¤t),
|
|
tracked,
|
|
) {
|
|
break;
|
|
}
|
|
let repeated = key
|
|
.clone()
|
|
.with_repeat_count(1)
|
|
.with_kind(KeyEventKind::Repeat);
|
|
if let Some(target) = self.route_key_press(&repeated, outcome) {
|
|
self.push_pane_key(target, repeated, outcome);
|
|
}
|
|
}
|
|
}
|
|
crate::input::RepeatPlan::Ignore => {}
|
|
}
|
|
}
|
|
|
|
fn route_key_press(
|
|
&mut self,
|
|
key: &crate::input::TerminalKey,
|
|
outcome: &mut ClientShellInput,
|
|
) -> Option<ClientInputTarget> {
|
|
if matches!(
|
|
self.overlay,
|
|
Some(
|
|
ClientShellOverlay::Onboarding
|
|
| ClientShellOverlay::ProductAnnouncement(_)
|
|
| ClientShellOverlay::ReleaseNotes(_)
|
|
)
|
|
) {
|
|
if key.kind == KeyEventKind::Press {
|
|
self.route_overlay_key(key, outcome);
|
|
}
|
|
return None;
|
|
}
|
|
if let Some(target) = self.popup_input_target() {
|
|
return Some(target);
|
|
}
|
|
if self.popup_pending {
|
|
return None;
|
|
}
|
|
if self.overlay.is_some() {
|
|
self.route_overlay_key(key, outcome);
|
|
return None;
|
|
}
|
|
if matches!(key.code, KeyCode::Modifier(_)) {
|
|
return None;
|
|
}
|
|
self.pending_word_selection = None;
|
|
if self.mode != ClientShellMode::Copy
|
|
&& self.copy_or_terminal_mode() != ClientShellMode::Copy
|
|
&& !self.config.copy_on_select
|
|
&& is_retained_selection_copy_key(key)
|
|
&& self
|
|
.selection
|
|
.as_ref()
|
|
.is_some_and(crate::selection::Selection::is_visible)
|
|
{
|
|
self.request_selection_copy(outcome);
|
|
self.selection = None;
|
|
self.stop_selection_autoscroll();
|
|
self.selection_highlight_clear_deadline = None;
|
|
outcome.repaint = true;
|
|
return None;
|
|
}
|
|
if self.mode != ClientShellMode::Copy
|
|
&& self.copy_or_terminal_mode() != ClientShellMode::Copy
|
|
&& self.selection.take().is_some()
|
|
{
|
|
self.stop_selection_autoscroll();
|
|
self.selection_highlight_clear_deadline = None;
|
|
outcome.repaint = true;
|
|
}
|
|
|
|
match self.mode {
|
|
ClientShellMode::Terminal => {
|
|
if let Some(binding) =
|
|
crate::input::resolve_direct_binding(&self.config.keybinds.keybinds, key)
|
|
{
|
|
self.record_binding(binding, outcome);
|
|
return None;
|
|
}
|
|
if crate::config::terminal_key_matches_combo(key, self.config.keybinds.prefix) {
|
|
self.mode = ClientShellMode::Prefix;
|
|
outcome.repaint = true;
|
|
return None;
|
|
}
|
|
self.focused_pane_id().map(ClientInputTarget::Pane)
|
|
}
|
|
ClientShellMode::Prefix => {
|
|
let return_mode = if self.copy_mode.as_ref().is_some_and(|copy_mode| {
|
|
self.focused_pane_id().as_deref() == Some(copy_mode.pane_id.as_str())
|
|
}) {
|
|
ClientShellMode::Copy
|
|
} else {
|
|
ClientShellMode::Terminal
|
|
};
|
|
if crate::config::terminal_key_matches_combo(key, self.config.keybinds.prefix) {
|
|
self.mode = return_mode;
|
|
outcome.repaint = true;
|
|
return self.focused_pane_id().map(ClientInputTarget::Pane);
|
|
}
|
|
if key.code == KeyCode::Esc {
|
|
self.mode = return_mode;
|
|
outcome.repaint = true;
|
|
return None;
|
|
}
|
|
if let Some(binding) =
|
|
crate::input::resolve_prefix_binding(&self.config.keybinds.keybinds, key)
|
|
{
|
|
self.mode = return_mode;
|
|
outcome.repaint = true;
|
|
self.record_binding(binding, outcome);
|
|
return None;
|
|
}
|
|
self.mode = return_mode;
|
|
outcome.repaint = true;
|
|
None
|
|
}
|
|
ClientShellMode::Navigate => {
|
|
self.route_navigate_key(key, outcome);
|
|
None
|
|
}
|
|
ClientShellMode::Resize => {
|
|
self.route_resize_key(key, outcome);
|
|
None
|
|
}
|
|
ClientShellMode::Copy => {
|
|
if crate::config::terminal_key_matches_combo(key, self.config.keybinds.prefix) {
|
|
self.mode = ClientShellMode::Prefix;
|
|
outcome.repaint = true;
|
|
} else {
|
|
self.route_copy_mode_key(key, outcome);
|
|
}
|
|
None
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(super) fn copy_or_terminal_mode(&self) -> ClientShellMode {
|
|
if self.copy_mode.as_ref().is_some_and(|copy_mode| {
|
|
self.focused_pane_id().as_deref() == Some(copy_mode.pane_id.as_str())
|
|
}) {
|
|
ClientShellMode::Copy
|
|
} else {
|
|
ClientShellMode::Terminal
|
|
}
|
|
}
|
|
|
|
fn route_navigate_key(
|
|
&mut self,
|
|
key: &crate::input::TerminalKey,
|
|
outcome: &mut ClientShellInput,
|
|
) {
|
|
use crate::input::{KeybindAction, KeybindDispatch, KeybindMatch};
|
|
|
|
if key.code == KeyCode::Esc
|
|
|| crate::config::terminal_key_matches_combo(key, self.config.keybinds.prefix)
|
|
{
|
|
self.mode = self.copy_or_terminal_mode();
|
|
self.navigate_workspace_id = None;
|
|
outcome.repaint = true;
|
|
return;
|
|
}
|
|
|
|
if self
|
|
.config
|
|
.keybinds
|
|
.keybinds
|
|
.navigate
|
|
.workspace_up
|
|
.matches_direct_key(key)
|
|
{
|
|
self.move_navigate_workspace(-1);
|
|
outcome.repaint = true;
|
|
return;
|
|
}
|
|
if self
|
|
.config
|
|
.keybinds
|
|
.keybinds
|
|
.navigate
|
|
.workspace_down
|
|
.matches_direct_key(key)
|
|
{
|
|
self.move_navigate_workspace(1);
|
|
outcome.repaint = true;
|
|
return;
|
|
}
|
|
|
|
if let Some(index) = ('1'..='9').position(|digit| {
|
|
crate::config::terminal_key_matches_combo(
|
|
key,
|
|
(KeyCode::Char(digit), KeyModifiers::empty()),
|
|
)
|
|
}) {
|
|
let valid = self.snapshot.as_deref().is_some_and(|snapshot| {
|
|
self.navigation_workspace_entries(snapshot)
|
|
.get(index)
|
|
.is_some()
|
|
});
|
|
if valid {
|
|
self.mode = ClientShellMode::Terminal;
|
|
self.navigate_workspace_id = None;
|
|
self.record_binding(
|
|
KeybindMatch::Action(KeybindAction::SwitchWorkspace(index)),
|
|
outcome,
|
|
);
|
|
outcome.repaint = true;
|
|
}
|
|
return;
|
|
}
|
|
|
|
let (code, modifiers) = crate::config::normalize_key_combo((key.code, key.modifiers));
|
|
if modifiers.is_empty() {
|
|
match code {
|
|
KeyCode::Enter => {
|
|
let selected = self.navigate_workspace_id.clone();
|
|
self.mode = ClientShellMode::Terminal;
|
|
self.navigate_workspace_id = None;
|
|
if let Some(workspace_id) = selected {
|
|
self.push_endpoint_method(
|
|
crate::api::schema::Method::WorkspaceFocus(
|
|
crate::api::schema::WorkspaceTarget { workspace_id },
|
|
),
|
|
outcome,
|
|
);
|
|
}
|
|
outcome.repaint = true;
|
|
return;
|
|
}
|
|
KeyCode::Tab => {
|
|
self.record_navigate_binding(
|
|
KeybindMatch::Action(KeybindAction::CyclePaneNext),
|
|
false,
|
|
outcome,
|
|
);
|
|
return;
|
|
}
|
|
KeyCode::BackTab => {
|
|
self.record_navigate_binding(
|
|
KeybindMatch::Action(KeybindAction::CyclePanePrevious),
|
|
false,
|
|
outcome,
|
|
);
|
|
return;
|
|
}
|
|
KeyCode::Left => {
|
|
self.record_navigate_binding(
|
|
KeybindMatch::Action(KeybindAction::FocusPaneLeft),
|
|
true,
|
|
outcome,
|
|
);
|
|
return;
|
|
}
|
|
KeyCode::Right => {
|
|
self.record_navigate_binding(
|
|
KeybindMatch::Action(KeybindAction::FocusPaneRight),
|
|
true,
|
|
outcome,
|
|
);
|
|
return;
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
let pane_action = [
|
|
(
|
|
&self.config.keybinds.keybinds.navigate.pane_left,
|
|
KeybindAction::FocusPaneLeft,
|
|
),
|
|
(
|
|
&self.config.keybinds.keybinds.navigate.pane_down,
|
|
KeybindAction::FocusPaneDown,
|
|
),
|
|
(
|
|
&self.config.keybinds.keybinds.navigate.pane_up,
|
|
KeybindAction::FocusPaneUp,
|
|
),
|
|
(
|
|
&self.config.keybinds.keybinds.navigate.pane_right,
|
|
KeybindAction::FocusPaneRight,
|
|
),
|
|
]
|
|
.into_iter()
|
|
.find_map(|(bindings, action)| bindings.matches_direct_key(key).then_some(action));
|
|
if let Some(action) = pane_action {
|
|
self.record_navigate_binding(KeybindMatch::Action(action), true, outcome);
|
|
return;
|
|
}
|
|
|
|
let binding = crate::input::resolve_non_indexed_action(
|
|
&self.config.keybinds.keybinds,
|
|
key,
|
|
KeybindDispatch::Prefix,
|
|
)
|
|
.filter(|action| {
|
|
!matches!(
|
|
action,
|
|
KeybindAction::FocusPaneLeft
|
|
| KeybindAction::FocusPaneDown
|
|
| KeybindAction::FocusPaneUp
|
|
| KeybindAction::FocusPaneRight
|
|
)
|
|
})
|
|
.map(KeybindMatch::Action)
|
|
.or_else(|| {
|
|
crate::input::resolve_custom_command(
|
|
&self.config.keybinds.keybinds,
|
|
key,
|
|
KeybindDispatch::Prefix,
|
|
)
|
|
.map(KeybindMatch::Command)
|
|
})
|
|
.or_else(|| {
|
|
crate::input::resolve_indexed_action(
|
|
&self.config.keybinds.keybinds,
|
|
key,
|
|
KeybindDispatch::Prefix,
|
|
)
|
|
.map(KeybindMatch::Action)
|
|
});
|
|
if let Some(binding) = binding {
|
|
self.record_navigate_binding(binding, false, outcome);
|
|
}
|
|
}
|
|
|
|
fn record_navigate_binding(
|
|
&mut self,
|
|
binding: crate::input::KeybindMatch,
|
|
preserve_navigate: bool,
|
|
outcome: &mut ClientShellInput,
|
|
) {
|
|
use crate::input::{KeybindAction, KeybindMatch};
|
|
|
|
let indexed_target_exists = match &binding {
|
|
KeybindMatch::Action(KeybindAction::SwitchWorkspace(index)) => {
|
|
self.snapshot.as_deref().is_some_and(|snapshot| {
|
|
self.navigation_workspace_entries(snapshot)
|
|
.get(*index)
|
|
.is_some()
|
|
})
|
|
}
|
|
KeybindMatch::Action(KeybindAction::SwitchTab(index)) => self
|
|
.snapshot
|
|
.as_deref()
|
|
.and_then(|snapshot| {
|
|
let workspace_id = snapshot.focused_workspace_id.as_deref()?;
|
|
snapshot
|
|
.tabs
|
|
.iter()
|
|
.filter(|tab| tab.workspace_id == workspace_id)
|
|
.nth(*index)
|
|
})
|
|
.is_some(),
|
|
KeybindMatch::Action(KeybindAction::FocusAgent(index)) => {
|
|
self.snapshot.as_deref().is_some_and(|snapshot| {
|
|
super::agent_sidebar::ordered_agent_pane_ids(
|
|
snapshot,
|
|
self.config.agent_panel_sort,
|
|
)
|
|
.get(*index)
|
|
.is_some()
|
|
})
|
|
}
|
|
_ => true,
|
|
};
|
|
if !indexed_target_exists {
|
|
return;
|
|
}
|
|
|
|
if let KeybindMatch::Action(KeybindAction::CyclePaneNext) = binding {
|
|
self.cycle_pane(false, outcome);
|
|
} else if let KeybindMatch::Action(KeybindAction::CyclePanePrevious) = binding {
|
|
self.cycle_pane(true, outcome);
|
|
} else {
|
|
if !preserve_navigate {
|
|
self.mode = ClientShellMode::Terminal;
|
|
}
|
|
self.record_binding(binding, outcome);
|
|
}
|
|
if !preserve_navigate {
|
|
if self.mode == ClientShellMode::Navigate {
|
|
self.mode = self.copy_or_terminal_mode();
|
|
}
|
|
self.navigate_workspace_id = None;
|
|
}
|
|
outcome.repaint = true;
|
|
}
|
|
|
|
fn move_navigate_workspace(&mut self, delta: isize) {
|
|
let Some(snapshot) = self.snapshot.as_deref() else {
|
|
return;
|
|
};
|
|
let mobile = self.mobile_layout_active();
|
|
let entries = self.navigation_workspace_entries(snapshot);
|
|
if entries.is_empty() {
|
|
return;
|
|
}
|
|
let current = self
|
|
.navigate_workspace_id
|
|
.as_deref()
|
|
.and_then(|selected| {
|
|
entries
|
|
.iter()
|
|
.position(|entry| snapshot.workspaces[entry.index].workspace_id == selected)
|
|
})
|
|
.unwrap_or(0);
|
|
let next = if mobile {
|
|
(current as isize + delta).clamp(0, entries.len().saturating_sub(1) as isize) as usize
|
|
} else {
|
|
(current as isize + delta).rem_euclid(entries.len() as isize) as usize
|
|
};
|
|
self.navigate_workspace_id = Some(
|
|
snapshot.workspaces[entries[next].index]
|
|
.workspace_id
|
|
.clone(),
|
|
);
|
|
self.reveal_mobile_workspace = mobile;
|
|
}
|
|
|
|
fn cycle_pane(&mut self, reverse: bool, outcome: &mut ClientShellInput) {
|
|
let Some(snapshot) = self.snapshot.as_deref() else {
|
|
return;
|
|
};
|
|
let Some(surface) = self.pane_surface.as_ref() else {
|
|
return;
|
|
};
|
|
if surface.panes.is_empty() {
|
|
return;
|
|
}
|
|
let current = snapshot
|
|
.focused_pane_id
|
|
.as_deref()
|
|
.and_then(|focused| {
|
|
surface
|
|
.panes
|
|
.iter()
|
|
.position(|pane| pane.pane_id == focused)
|
|
})
|
|
.unwrap_or(0);
|
|
let next = if reverse {
|
|
(current + surface.panes.len() - 1) % surface.panes.len()
|
|
} else {
|
|
(current + 1) % surface.panes.len()
|
|
};
|
|
let pane_id = surface.panes[next].pane_id.clone();
|
|
self.push_endpoint_method(
|
|
crate::api::schema::Method::PaneFocus(crate::api::schema::PaneTarget { pane_id }),
|
|
outcome,
|
|
);
|
|
}
|
|
|
|
fn route_resize_key(
|
|
&mut self,
|
|
key: &crate::input::TerminalKey,
|
|
outcome: &mut ClientShellInput,
|
|
) {
|
|
let resize_bindings = &self.config.keybinds.keybinds.resize_mode;
|
|
if key.code == KeyCode::Esc
|
|
|| key.code == KeyCode::Enter
|
|
|| resize_bindings.matches_prefix_key(key)
|
|
|| resize_bindings.matches_direct_key(key)
|
|
{
|
|
self.mode = self.copy_or_terminal_mode();
|
|
outcome.repaint = true;
|
|
return;
|
|
}
|
|
|
|
let action = match key.code {
|
|
KeyCode::Char('h') | KeyCode::Left => Some(crate::input::KeybindAction::ResizePaneLeft),
|
|
KeyCode::Char('j') | KeyCode::Down => Some(crate::input::KeybindAction::ResizePaneDown),
|
|
KeyCode::Char('k') | KeyCode::Up => Some(crate::input::KeybindAction::ResizePaneUp),
|
|
KeyCode::Char('l') | KeyCode::Right => {
|
|
Some(crate::input::KeybindAction::ResizePaneRight)
|
|
}
|
|
_ => None,
|
|
};
|
|
if let Some(action) = action {
|
|
self.record_binding(crate::input::KeybindMatch::Action(action), outcome);
|
|
}
|
|
}
|
|
|
|
fn input_context(&self) -> ClientInputContext {
|
|
ClientInputContext {
|
|
mode: self.mode,
|
|
overlay: self.overlay.as_ref().map(ClientShellOverlay::kind),
|
|
popup_terminal_id: self.popup_input_target().and_then(|target| match target {
|
|
ClientInputTarget::Popup(terminal_id) => Some(terminal_id),
|
|
ClientInputTarget::Pane(_) => None,
|
|
}),
|
|
popup_pending: self.popup_pending,
|
|
retained_selection: self
|
|
.selection
|
|
.as_ref()
|
|
.is_some_and(crate::selection::Selection::is_visible),
|
|
}
|
|
}
|
|
|
|
pub(super) fn focused_pane_id(&self) -> Option<String> {
|
|
self.snapshot
|
|
.as_deref()
|
|
.and_then(|snapshot| snapshot.focused_pane_id.clone())
|
|
}
|
|
|
|
fn popup_input_target(&self) -> Option<ClientInputTarget> {
|
|
self.popup_terminal_id
|
|
.as_ref()
|
|
.map(|terminal_id| ClientInputTarget::Popup(terminal_id.clone()))
|
|
}
|
|
|
|
fn push_pane_key(
|
|
&self,
|
|
target: ClientInputTarget,
|
|
key: crate::input::TerminalKey,
|
|
outcome: &mut ClientShellInput,
|
|
) {
|
|
if let Some(event) = ClientPaneInputEvent::from_terminal_key(key) {
|
|
super::push_target_event(target, event, outcome);
|
|
}
|
|
}
|
|
|
|
fn push_focused_pane_event(&self, event: ClientPaneInputEvent, outcome: &mut ClientShellInput) {
|
|
if let Some(pane_id) = self.focused_pane_id() {
|
|
super::push_target_event(ClientInputTarget::Pane(pane_id), event, outcome);
|
|
}
|
|
}
|
|
}
|