Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/next/api/herdr-api.schema.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"protocol": 18,
"protocol": 19,
"schema_version": 1,
"schemas": {
"error_response": {
Expand Down
29 changes: 23 additions & 6 deletions src/app/input/clipboard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ impl App {
return false;
}

self.suppressed_repeat_keys.insert((source_id, key.code));
self.suppressed_repeat_keys
.insert(super::super::pressed_key_identity(source_id, &key));
true
}
}
Expand Down Expand Up @@ -132,7 +133,15 @@ mod tests {
assert_visible_selection(&app);
assert!(app.event_rx.try_recv().is_err());

let ctrl_c = TerminalKey::new(KeyCode::Char('c'), KeyModifiers::CONTROL);
let ctrl_c = TerminalKey::new(KeyCode::Char('c'), KeyModifiers::CONTROL)
.with_windows_record(crate::input::WindowsKeyRecord {
key_down: true,
repeat_count: 1,
virtual_key_code: 0x43,
virtual_scan_code: 0x2e,
unicode: 'c' as u16,
control_key_state: 0x0008,
});
let source_id = 41;
app.route_client_events_from(
source_id,
Expand All @@ -156,11 +165,13 @@ mod tests {

app.route_client_events_from(
source_id,
vec![crate::raw_input::RawInputEvent::Key(
ctrl_c.with_kind(KeyEventKind::Repeat),
)],
vec![
crate::raw_input::RawInputEvent::Key(ctrl_c),
crate::raw_input::RawInputEvent::Key(ctrl_c.with_kind(KeyEventKind::Repeat)),
],
false,
);
assert_eq!(app.suppressed_repeat_keys.len(), 1);
assert!(app.event_rx.try_recv().is_err());
assert!(input_rx.try_recv().is_err());

Expand All @@ -171,14 +182,20 @@ mod tests {
)],
false,
);
assert!(app.suppressed_repeat_keys.is_empty());
app.route_client_events_from(
source_id,
vec![crate::raw_input::RawInputEvent::Key(ctrl_c)],
false,
);
let expected = if cfg!(windows) {
b"\x1b[67;46;99;1;8;1_".as_slice()
} else {
b"\x03".as_slice()
};
assert_eq!(
input_rx.try_recv().expect("forwarded Ctrl-C").as_ref(),
b"\x03"
expected
);
assert!(app.event_rx.try_recv().is_err());
}
Expand Down
104 changes: 103 additions & 1 deletion src/app/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,18 @@ fn pressed_key_identity(
source_id: InputSourceId,
key: &crate::input::TerminalKey,
) -> (InputSourceId, crossterm::event::KeyCode) {
(source_id, key.code)
const ENHANCED_KEY: u32 = 0x0100;
let native_code = key.windows_record.and_then(|record| {
(record.virtual_scan_code != 0)
.then(|| {
0xf0000
+ u32::from(record.virtual_scan_code)
+ (u32::from(record.control_key_state & ENHANCED_KEY != 0) << 16)
})
.and_then(char::from_u32)
.map(crossterm::event::KeyCode::Char)
});
(source_id, native_code.unwrap_or(key.code))
}

fn auto_updates_enabled(no_session: bool) -> bool {
Expand Down Expand Up @@ -1616,6 +1627,15 @@ impl App {
match event {
crate::raw_input::RawInputEvent::Key(key) => {
let pressed_key_id = pressed_key_identity(source_id, &key);
let key = if key.kind == crossterm::event::KeyEventKind::Press
&& key.windows_record.is_some()
&& (self.pressed_terminal_keys.contains_key(&pressed_key_id)
|| self.suppressed_repeat_keys.contains(&pressed_key_id))
{
key.with_kind(crossterm::event::KeyEventKind::Repeat)
} else {
key
};
match key.kind {
crossterm::event::KeyEventKind::Press => {
if self.state.popup_pane.is_some() || self.state.mode == Mode::Terminal
Expand Down Expand Up @@ -1835,6 +1855,88 @@ mod tests {
)
}

#[cfg(windows)]
#[tokio::test]
async fn native_repeats_and_releases_follow_the_pressed_pane() {
let record = crate::input::WindowsKeyRecord {
key_down: true,
repeat_count: 1,
virtual_key_code: 27,
virtual_scan_code: 1,
unicode: 27,
control_key_state: 0,
};
let key = crate::input::TerminalKey::new(KeyCode::Esc, KeyModifiers::empty())
.with_windows_record(record);
let mut enhanced = key;
enhanced.windows_record.as_mut().unwrap().control_key_state = 0x0100;
assert_ne!(
pressed_key_identity(7, &key),
pressed_key_identity(7, &enhanced)
);

let mut app = test_app();
let mut workspace = Workspace::test_new("test");
let pressed_pane = workspace.focused_pane_id().unwrap();
let other_pane = workspace.test_split(ratatui::layout::Direction::Horizontal);
workspace.tabs[0].layout.focus_pane(pressed_pane);
let (pressed_runtime, mut pressed_rx) =
TerminalRuntime::test_with_channel_capacity(80, 24, 6);
let (other_runtime, mut other_rx) = TerminalRuntime::test_with_channel_capacity(80, 24, 6);
workspace.tabs[0]
.runtimes
.insert(pressed_pane, pressed_runtime);
workspace.tabs[0].runtimes.insert(other_pane, other_runtime);
app.state.workspaces = vec![workspace];
app.state.active = Some(0);
app.state.selected = 0;
app.state.mode = Mode::Terminal;

let release = crate::input::TerminalKey::new(KeyCode::Esc, KeyModifiers::empty())
.with_windows_record(crate::input::WindowsKeyRecord {
key_down: false,
repeat_count: 1,
unicode: 0,
..record
})
.with_kind(KeyEventKind::Release);
app.route_client_events(vec![crate::raw_input::RawInputEvent::Key(key)], false);
assert!(app.state.focus_pane_in_workspace(0, other_pane));
app.route_client_events(
vec![
crate::raw_input::RawInputEvent::Key(key),
crate::raw_input::RawInputEvent::Key(key.with_kind(KeyEventKind::Repeat)),
crate::raw_input::RawInputEvent::Key(release),
crate::raw_input::RawInputEvent::Key(key),
crate::raw_input::RawInputEvent::OuterFocusLost,
],
false,
);

for _ in 0..3 {
assert_eq!(
pressed_rx.try_recv().unwrap(),
bytes::Bytes::from_static(b"\x1b[27;1;27;1;0;1_")
);
}
assert_eq!(
pressed_rx.try_recv().unwrap(),
bytes::Bytes::from_static(b"\x1b[27;1;0;0;0;1_")
);
assert!(pressed_rx.try_recv().is_err());

assert_eq!(
other_rx.try_recv().unwrap(),
bytes::Bytes::from_static(b"\x1b[27;1;27;1;0;1_")
);
assert_eq!(
other_rx.try_recv().unwrap(),
bytes::Bytes::from_static(b"\x1b[27;1;27;0;0;1_")
);
assert!(other_rx.try_recv().is_err());
assert!(app.pressed_terminal_keys.is_empty());
}

fn release_notes_state() -> state::ReleaseNotesState {
state::ReleaseNotesState {
version: "0.1.0".into(),
Expand Down
Loading
Loading