mirror of
https://github.com/herdrdev/herdr.git
synced 2026-09-22 16:01:07 +00:00
@@ -399,9 +399,9 @@ fn main() {
|
||||
$kittyInitialHex = Get-LatestProbeHex -PaneText $report.kitty_initial
|
||||
$report.device_attributes_response = $kittyInitialHex -match "1b5b3f(?:3[0-9]|3b)+63"
|
||||
$report.kitty_query_response = $kittyInitialHex.Contains("1b5b3f3775")
|
||||
$report.kitty_alt_v = Send-KeyAndObserve -PaneId $kittyPane -Key "alt+v" -ExpectedHex "1b5b3131383b333a3175"
|
||||
$report.kitty_ctrl_u = Send-KeyAndObserve -PaneId $kittyPane -Key "ctrl+u" -ExpectedHex "1b5b3131373b353a3175"
|
||||
$report.kitty_ctrl_v = Send-KeyAndObserve -PaneId $kittyPane -Key "ctrl+v" -ExpectedHex "1b5b3131383b353a3175"
|
||||
$report.kitty_alt_v = Send-KeyAndObserve -PaneId $kittyPane -Key "alt+v" -ExpectedHex "1b5b3131383b3375"
|
||||
$report.kitty_ctrl_u = Send-KeyAndObserve -PaneId $kittyPane -Key "ctrl+u" -ExpectedHex "1b5b3131373b3575"
|
||||
$report.kitty_ctrl_v = Send-KeyAndObserve -PaneId $kittyPane -Key "ctrl+v" -ExpectedHex "1b5b3131383b3575"
|
||||
$report.kitty_shift_enter = Send-KeyAndObserve -PaneId $kittyPane -Key "shift+enter" -ExpectedHex "1b5b31333b3275"
|
||||
$report.kitty_ctrl_backspace = Send-KeyAndObserve -PaneId $kittyPane -Key "ctrl+backspace" -ExpectedHex "1b5b3132373b3575"
|
||||
$report.kitty_up = Send-KeyAndObserve -PaneId $kittyPane -Key "up" -ExpectedHex "1b5b313b313a3141"
|
||||
|
||||
@@ -5,6 +5,8 @@ mod parse;
|
||||
#[cfg(test)]
|
||||
pub(crate) mod test_support;
|
||||
|
||||
// Preserve mouse encoder re-exports for facade consumers even though Herdr's
|
||||
// runtime routes mouse encoding through terminal backends.
|
||||
#[allow(unused_imports)]
|
||||
pub use encode::{encode_mouse_button, encode_mouse_scroll};
|
||||
#[cfg(not(windows))]
|
||||
|
||||
+16
-1
@@ -29,7 +29,7 @@ fn parse_kitty_key_sequence(data: &str) -> Option<TerminalKey> {
|
||||
modifier_part
|
||||
};
|
||||
let (modifier_text, event_type) = split_modifier_and_event(modifier_part);
|
||||
let modifier = modifier_text.parse::<u8>().ok()?.checked_sub(1)?;
|
||||
let modifier = u8::try_from(modifier_text.parse::<u16>().ok()?.checked_sub(1)?).ok()?;
|
||||
|
||||
let mut key_fields = key_part.split(':');
|
||||
let codepoint = key_fields.next()?.parse::<u32>().ok()?;
|
||||
@@ -628,6 +628,21 @@ mod tests {
|
||||
assert_eq!(parse_terminal_key_sequence("\x1b[14;3~"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_kitty_sequence_accepts_every_modifier_bit() {
|
||||
let key = parse_terminal_key_sequence("\x1b[97;256u").unwrap();
|
||||
|
||||
assert_eq!(
|
||||
key.modifiers,
|
||||
KeyModifiers::SHIFT
|
||||
| KeyModifiers::ALT
|
||||
| KeyModifiers::CONTROL
|
||||
| KeyModifiers::SUPER
|
||||
| KeyModifiers::HYPER
|
||||
| KeyModifiers::META
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_kitty_sequence_preserves_shifted_symbol_pair() {
|
||||
let key = parse_terminal_key_sequence("\x1b[49:33;2:1u").unwrap();
|
||||
|
||||
@@ -60,7 +60,7 @@ pub(crate) fn keyboard_corpus_cases(corpus: &str) -> Vec<KeyboardCorpusCase<'_>>
|
||||
.get(9)
|
||||
.filter(|field| !field.is_empty())
|
||||
.map(|field| field.parse::<u32>().expect("base layout codepoint")),
|
||||
generated_text: (columns[6] != "-").then(|| {
|
||||
generated_text: (!columns[6].is_empty() && columns[6] != "-").then(|| {
|
||||
String::from_utf8(decode_hex(columns[6])).expect("generated text must be UTF-8")
|
||||
}),
|
||||
pane_profile: columns[7],
|
||||
@@ -144,3 +144,11 @@ fn parse_kind(value: &str) -> KeyEventKind {
|
||||
other => panic!("unsupported fixture kind: {other}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn blank_generated_text_fixture_field_is_absent() {
|
||||
let cases = keyboard_corpus_cases("legacy\t61\tchar:a\t-\tpress\t\t\tlegacy\t61");
|
||||
|
||||
assert_eq!(cases.len(), 1);
|
||||
assert_eq!(cases[0].generated_text, None);
|
||||
}
|
||||
|
||||
+15
-10
@@ -2184,25 +2184,30 @@ impl GhosttyPaneTerminal {
|
||||
}
|
||||
|
||||
fn log_key_encoding_unavailable(reason: KeyEncodingUnavailable) {
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
|
||||
static EVENT_ALLOCATION_LOGGED: AtomicBool = AtomicBool::new(false);
|
||||
static ENCODER_LOCK_LOGGED: AtomicBool = AtomicBool::new(false);
|
||||
static ENCODER_ERROR_LOGGED: AtomicBool = AtomicBool::new(false);
|
||||
const LOG_INTERVAL: u64 = 1024;
|
||||
static EVENT_ALLOCATION_FAILURES: AtomicU64 = AtomicU64::new(0);
|
||||
static ENCODER_LOCK_FAILURES: AtomicU64 = AtomicU64::new(0);
|
||||
static ENCODER_ERROR_FAILURES: AtomicU64 = AtomicU64::new(0);
|
||||
|
||||
let first_failure = match reason {
|
||||
let failures = match reason {
|
||||
KeyEncodingUnavailable::Adapter(GhosttyKeyEventAdapterError::UnsupportedKey) => {
|
||||
debug!(?reason, "Ghostty key encoding unavailable; suppressing key");
|
||||
return;
|
||||
}
|
||||
KeyEncodingUnavailable::Adapter(GhosttyKeyEventAdapterError::EventAllocation) => {
|
||||
&EVENT_ALLOCATION_LOGGED
|
||||
&EVENT_ALLOCATION_FAILURES
|
||||
}
|
||||
KeyEncodingUnavailable::EncoderLockPoisoned => &ENCODER_LOCK_LOGGED,
|
||||
KeyEncodingUnavailable::EncoderError => &ENCODER_ERROR_LOGGED,
|
||||
KeyEncodingUnavailable::EncoderLockPoisoned => &ENCODER_LOCK_FAILURES,
|
||||
KeyEncodingUnavailable::EncoderError => &ENCODER_ERROR_FAILURES,
|
||||
};
|
||||
if !first_failure.swap(true, Ordering::Relaxed) {
|
||||
error!(?reason, "Ghostty key encoding failed; suppressing key");
|
||||
let count = failures.fetch_add(1, Ordering::Relaxed) + 1;
|
||||
if count == 1 || count.is_multiple_of(LOG_INTERVAL) {
|
||||
error!(
|
||||
?reason,
|
||||
count, "Ghostty key encoding failed; suppressing key"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+18
-3
@@ -1334,10 +1334,14 @@ fn discard_oversized_csi_tail(buffer: &mut Vec<u8>) -> bool {
|
||||
for (index, byte) in buffer.iter().copied().enumerate() {
|
||||
match byte {
|
||||
0x20..=0x3f => {}
|
||||
_ => {
|
||||
0x40..=0x7e => {
|
||||
buffer.drain(..=index);
|
||||
return true;
|
||||
}
|
||||
_ => {
|
||||
buffer.drain(..index);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2567,7 +2571,18 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn oversized_csi_discard_precedes_excess_escape_batching() {
|
||||
fn oversized_csi_discard_preserves_a_following_escape_sequence() {
|
||||
let mut framer = RawInputByteFramer::default();
|
||||
let mut oversized = b"\x1b[".to_vec();
|
||||
oversized.extend(std::iter::repeat_n(b'1', MAX_CSI_SEQUENCE_BYTES));
|
||||
assert!(framer.push(&oversized).is_empty());
|
||||
|
||||
assert_eq!(framer.push(b"\x1b[A"), vec![b"\x1b[A".to_vec()]);
|
||||
assert!(!framer.has_pending_input());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn oversized_csi_discard_preserves_excess_escape_batching() {
|
||||
let mut framer = RawInputByteFramer::default();
|
||||
let mut oversized = b"\x1b[".to_vec();
|
||||
oversized.extend(std::iter::repeat_n(b'1', MAX_CSI_SEQUENCE_BYTES));
|
||||
@@ -2581,7 +2596,7 @@ mod tests {
|
||||
.flatten()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(rebuilt, continuation[1..]);
|
||||
assert_eq!(rebuilt, continuation);
|
||||
assert!(!framer.has_pending_input());
|
||||
assert!(framer.discard_until.is_none());
|
||||
}
|
||||
|
||||
Vendored
+8
-3
@@ -135,7 +135,10 @@ reason: libghostty-vt models F13-F25 but its legacy encoder has no entries for
|
||||
them, silently suppressing keys that Herdr receives through Kitty input. The
|
||||
extension uses the standard xterm/terminfo sequences, corrects modified F3 to
|
||||
that same standard, and composes additional modifiers with each extended key's
|
||||
implicit Shift or Control modifier.
|
||||
implicit Shift or Control modifier. Modified F3 therefore shares the
|
||||
`CSI 1;modifier R` byte shape used by a cursor position report, but terminal
|
||||
input and terminal responses travel in opposite directions and are interpreted
|
||||
in that context.
|
||||
|
||||
remove when: upstream libghostty-vt encodes F13-F25 in legacy mode with the
|
||||
standard xterm sequences and modifier composition, and emits the standard
|
||||
@@ -206,8 +209,10 @@ local files:
|
||||
|
||||
reason: for legacy destinations, a Shift modifier consumed to produce an
|
||||
uppercase character must not turn Ctrl-Shift-C into CSI-u instead of the
|
||||
traditional Ctrl-C byte. modifyOtherKeys mode 2 still receives every modifier,
|
||||
and Kitty panes still preserve Ctrl-Shift as distinct metadata.
|
||||
traditional Ctrl-C byte. That legacy protocol cannot reliably preserve the
|
||||
physical Shift after it produced uppercase text. modifyOtherKeys mode 2 still
|
||||
receives every modifier, and Kitty panes still preserve Ctrl-Shift as distinct
|
||||
metadata.
|
||||
|
||||
remove when: upstream libghostty-vt uses consumed modifier metadata for legacy
|
||||
control-sequence selection while preserving modifyOtherKeys and Kitty behavior.
|
||||
|
||||
@@ -309,6 +309,7 @@ fn pcStyle(comptime fmt: []const u8) []Entry {
|
||||
|
||||
fn pcStyleWithImplicitMods(comptime fmt: []const u8, comptime implicit: key.Mods) []Entry {
|
||||
comptime {
|
||||
@setEvalBranchQuota(500_000);
|
||||
var entries: [modifiers.len]Entry = undefined;
|
||||
for (modifiers, 0..) |mods, i| {
|
||||
const code: u8 = 1 +
|
||||
|
||||
+17
-1
@@ -550,7 +550,8 @@ fn legacy(
|
||||
const codepoint = it.nextCodepoint() orelse break :unshifted;
|
||||
if (it.nextCodepoint() == null and codepoint != event.unshifted_codepoint) {
|
||||
var buf: [4]u8 = undefined;
|
||||
const len = try std.unicode.utf8Encode(event.unshifted_codepoint, &buf);
|
||||
const len = std.unicode.utf8Encode(event.unshifted_codepoint, &buf) catch
|
||||
return try writer.writeAll(utf8);
|
||||
return try writer.writeAll(buf[0..len]);
|
||||
}
|
||||
}
|
||||
@@ -2044,6 +2045,21 @@ test "legacy: alt+unicode prefixes the complete utf8 text" {
|
||||
try testing.expectEqualStrings("\x1bé", writer.buffered());
|
||||
}
|
||||
|
||||
test "legacy: alt with invalid unshifted codepoint preserves utf8 text" {
|
||||
var buf: [128]u8 = undefined;
|
||||
var writer: std.Io.Writer = .fixed(&buf);
|
||||
try legacy(&writer, .{
|
||||
.key = .unidentified,
|
||||
.mods = .{ .alt = true },
|
||||
.utf8 = "é",
|
||||
.unshifted_codepoint = 0xD800,
|
||||
}, .{
|
||||
.alt_esc_prefix = true,
|
||||
.macos_option_as_alt = .true,
|
||||
});
|
||||
try testing.expectEqualStrings("\x1bé", writer.buffered());
|
||||
}
|
||||
|
||||
test "legacy: alt+shift preserves shifted text" {
|
||||
var buf: [128]u8 = undefined;
|
||||
var writer: std.Io.Writer = .fixed(&buf);
|
||||
|
||||
+8
@@ -168,6 +168,14 @@ pub const Mods = packed struct(Mods.Backing) {
|
||||
@as(Backing, @bitCast(Mods{ .shift = true })),
|
||||
@as(Backing, 0b0000_0001),
|
||||
);
|
||||
try testing.expectEqual(
|
||||
@as(Backing, @bitCast(Mods{ .hyper = true })),
|
||||
@as(Backing, 1 << 10),
|
||||
);
|
||||
try testing.expectEqual(
|
||||
@as(Backing, @bitCast(Mods{ .meta = true })),
|
||||
@as(Backing, 1 << 11),
|
||||
);
|
||||
}
|
||||
|
||||
test "translation macos-option-as-alt" {
|
||||
|
||||
@@ -117,6 +117,24 @@ index 35e1c103..d89e2674 100644
|
||||
};
|
||||
}
|
||||
|
||||
@@ -159,9 +163,17 @@ pub const Mods = packed struct(Mods.Backing) {
|
||||
// For our own understanding
|
||||
test {
|
||||
const testing = std.testing;
|
||||
try testing.expectEqual(@as(Backing, @bitCast(Mods{})), @as(Backing, 0b0));
|
||||
try testing.expectEqual(
|
||||
@as(Backing, @bitCast(Mods{ .shift = true })),
|
||||
@as(Backing, 0b0000_0001),
|
||||
);
|
||||
+ try testing.expectEqual(
|
||||
+ @as(Backing, @bitCast(Mods{ .hyper = true })),
|
||||
+ @as(Backing, 1 << 10),
|
||||
+ );
|
||||
+ try testing.expectEqual(
|
||||
+ @as(Backing, @bitCast(Mods{ .meta = true })),
|
||||
+ @as(Backing, 1 << 11),
|
||||
+ );
|
||||
}
|
||||
diff --git a/vendor/libghostty-vt/src/lib_vt.zig b/vendor/libghostty-vt/src/lib_vt.zig
|
||||
index e01cdbb8..096f1a87 100644
|
||||
--- a/vendor/libghostty-vt/src/lib_vt.zig
|
||||
|
||||
@@ -46,12 +46,13 @@ index 66ab4bc4..b1a0f0a5 100644
|
||||
|
||||
// Keypad keys
|
||||
result.set(.numpad_0, kpKeys("p"));
|
||||
@@ -294,6 +307,24 @@ fn pcStyle(comptime fmt: []const u8) []Entry {
|
||||
@@ -294,6 +307,25 @@ fn pcStyle(comptime fmt: []const u8) []Entry {
|
||||
}
|
||||
}
|
||||
|
||||
+fn pcStyleWithImplicitMods(comptime fmt: []const u8, comptime implicit: key.Mods) []Entry {
|
||||
+ comptime {
|
||||
+ @setEvalBranchQuota(500_000);
|
||||
+ var entries: [modifiers.len]Entry = undefined;
|
||||
+ for (modifiers, 0..) |mods, i| {
|
||||
+ const code: u8 = 1 +
|
||||
|
||||
@@ -31,7 +31,7 @@ index 4fdea85b..3d386179 100644
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -538,13 +537,26 @@ fn legacy(
|
||||
@@ -538,13 +537,27 @@ fn legacy(
|
||||
|
||||
// If we have alt-pressed and alt-esc-prefix is enabled, then
|
||||
// we need to prefix the utf8 sequence with an esc.
|
||||
@@ -55,7 +55,8 @@ index 4fdea85b..3d386179 100644
|
||||
+ const codepoint = it.nextCodepoint() orelse break :unshifted;
|
||||
+ if (it.nextCodepoint() == null and codepoint != event.unshifted_codepoint) {
|
||||
+ var buf: [4]u8 = undefined;
|
||||
+ const len = try std.unicode.utf8Encode(event.unshifted_codepoint, &buf);
|
||||
+ const len = std.unicode.utf8Encode(event.unshifted_codepoint, &buf) catch
|
||||
+ return try writer.writeAll(utf8);
|
||||
+ return try writer.writeAll(buf[0..len]);
|
||||
+ }
|
||||
+ }
|
||||
@@ -147,7 +148,7 @@ index 4fdea85b..3d386179 100644
|
||||
test "legacy: ctrl+shift+minus (underscore on US)" {
|
||||
var buf: [128]u8 = undefined;
|
||||
var writer: std.Io.Writer = .fixed(&buf);
|
||||
@@ -2026,6 +2029,37 @@ test "legacy: ctrl+alt+c" {
|
||||
@@ -2026,6 +2029,52 @@ test "legacy: ctrl+alt+c" {
|
||||
try testing.expectEqualStrings("\x1b\x03", writer.buffered());
|
||||
}
|
||||
|
||||
@@ -166,6 +167,21 @@ index 4fdea85b..3d386179 100644
|
||||
+ try testing.expectEqualStrings("\x1bé", writer.buffered());
|
||||
+}
|
||||
+
|
||||
+test "legacy: alt with invalid unshifted codepoint preserves utf8 text" {
|
||||
+ var buf: [128]u8 = undefined;
|
||||
+ var writer: std.Io.Writer = .fixed(&buf);
|
||||
+ try legacy(&writer, .{
|
||||
+ .key = .unidentified,
|
||||
+ .mods = .{ .alt = true },
|
||||
+ .utf8 = "é",
|
||||
+ .unshifted_codepoint = 0xD800,
|
||||
+ }, .{
|
||||
+ .alt_esc_prefix = true,
|
||||
+ .macos_option_as_alt = .true,
|
||||
+ });
|
||||
+ try testing.expectEqualStrings("\x1bé", writer.buffered());
|
||||
+}
|
||||
+
|
||||
+test "legacy: alt+shift preserves shifted text" {
|
||||
+ var buf: [128]u8 = undefined;
|
||||
+ var writer: std.Io.Writer = .fixed(&buf);
|
||||
|
||||
Reference in New Issue
Block a user