Something went wrong. Try again.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541const std = @import("std");const builtin = @import("builtin");const testing = std.testing;const KittyFlags = @import("../terminal/kitty/key.zig").Flags;const OptionAsAlt = @import("config.zig").OptionAsAlt;const Terminal = @import("../terminal/Terminal.zig");const function_keys = @import("function_keys.zig");const key = @import("key.zig");const KittyEntry = @import("kitty.zig").Entry;const kitty_entries = @import("kitty.zig").entries;
/// Options that affect key encoding behavior. This is a mix of behavior/// from terminal state as well as application configuration.pub const Options = struct { /// Terminal DEC mode 1 cursor_key_application: bool = false,
/// Terminal DEC mode 66 keypad_key_application: bool = false,
// DEC Backarrow Key Mode (DECBKM) // See https://vt100.net/dec/ek-vt3xx-tp-002.pdf page 170 // If `false` (the default), `backspace` emits 0x7f // If `true`, `backspace` emits 0x08 backarrow_key_mode: bool = false,
/// Terminal DEC mode 1035 ignore_keypad_with_numlock: bool = false,
/// Terminal DEC mode 1036 alt_esc_prefix: bool = false,
/// xterm "modifyOtherKeys mode 2". Details here: /// https://invisible-island.net/xterm/modified-keys.html modify_other_keys_state_2: bool = false,
/// Kitty keyboard protocol flags. kitty_flags: KittyFlags = .disabled,
/// Determines whether the "option" key on macOS is treated /// as "alt" or not. See the Ghostty `macos_option-as-alt` config /// docs for a more detailed description of why this is needed. macos_option_as_alt: OptionAsAlt = .false,
pub const default: Options = .{ .cursor_key_application = false, .keypad_key_application = false, .ignore_keypad_with_numlock = false, .alt_esc_prefix = false, .modify_other_keys_state_2 = false, .kitty_flags = .disabled, .macos_option_as_alt = .false, };
/// Initialize our options from the terminal state. /// /// Note that `macos_option_as_alt` cannot be determined from /// terminal state so it must be set manually after this call. pub fn fromTerminal(t: *const Terminal) Options { return .{ .alt_esc_prefix = t.modes.get(.alt_esc_prefix), .cursor_key_application = t.modes.get(.cursor_keys), .keypad_key_application = t.modes.get(.keypad_keys), .backarrow_key_mode = t.modes.get(.backarrow_key_mode), .ignore_keypad_with_numlock = t.modes.get(.ignore_keypad_with_numlock), .modify_other_keys_state_2 = t.flags.modify_other_keys_2, .kitty_flags = t.screens.active.kitty_keyboard.current(),
// These can't be known from the terminal state. .macos_option_as_alt = .false, }; }};
/// Encode the key event to the writer in the proper format given/// the options. For example, this will properly encode a key press/// such as "ctrl+A" to Kitty format if Kitty encoding is enabled.////// Not all key events will result in output. It is up to the caller/// to use a writer that can track whether any output was written if/// they care about that.pub fn encode( writer: *std.Io.Writer, event: key.KeyEvent, opts: Options,) std.Io.Writer.Error!void { //std.log.warn("KEYENCODER event={} opts={}", .{ event, opts }); return if (opts.kitty_flags.int() != 0) try kitty( writer, event, opts, ) else try legacy( writer, event, opts, );}
/// Perform Kitty keyboard protocol encoding of the key event.fn kitty( writer: *std.Io.Writer, event: key.KeyEvent, opts: Options,) std.Io.Writer.Error!void { // This should never happen but we'll check anyway. if (opts.kitty_flags.int() == 0) return try legacy( writer, event, opts, );
// We only processed "press" events unless report events is active if (event.action == .release) { if (!opts.kitty_flags.report_events) return;
// Enter, backspace, and tab do not report release events unless "report // all" is set if (!opts.kitty_flags.report_all) { switch (event.key) { .enter, .backspace, .tab => return, else => {}, } } }
const all_mods = event.mods; const effective_mods = event.effectiveMods(); const binding_mods = effective_mods.binding();
// Find the entry for this key in the kitty table. const entry_: ?KittyEntry = entry: { // Functional or predefined keys for (kitty_entries) |entry| { if (entry.key == event.key) break :entry entry; }
// Otherwise, we use our unicode codepoint from UTF8. We // always use the unshifted value. if (event.unshifted_codepoint > 0) { break :entry .{ .key = event.key, .code = event.unshifted_codepoint, .final = 'u', .modifier = false, }; }
break :entry null; };
preprocessing: { // When composing, the only keys sent are plain modifiers. if (event.composing) { if (entry_) |entry| { if (entry.modifier) break :preprocessing; }
return; }
// IME confirmation still sends an enter key so if we have enter // and UTF8 text we just send it directly since we assume that is // what's happening. See legacy()'s similar logic for more details // on how to verify this. if (event.utf8.len > 0) utf8: { switch (event.key) { else => {}, inline .enter, .backspace => |tag| { // See legacy for why we handle this way. if (isControlUtf8(event.utf8)) break :utf8; if (comptime tag == .backspace) return; return try writer.writeAll(event.utf8); }, } }
// If we're reporting all then we always send CSI sequences. if (!opts.kitty_flags.report_all) { // Quote: // The only exceptions are the Enter, Tab and Backspace keys which // still generate the same bytes as in legacy mode this is to allow the // user to type and execute commands in the shell such as reset after a // program that sets this mode crashes without clearing it. // // Quote ("report all" mode): // Note that all keys are reported as escape codes, including Enter, // Tab, Backspace etc. if (binding_mods.empty()) { switch (event.key) { .enter => return try writer.writeByte('\r'), .tab => return try writer.writeByte('\t'), .backspace => return try writer.writeByte(0x7F), else => {}, } }
// Send plain-text non-modified text directly to the terminal. // We don't send release events because those are specially encoded. if (event.utf8.len > 0 and binding_mods.empty() and event.action != .release) plain_text: { // We only do this for printable characters. We should // inspect the real unicode codepoint properties here but // the real world issue is usually control characters. const view = std.unicode.Utf8View.init(event.utf8) catch { // Invalid UTF-8 so let's fallback to encoding the // key press as if it didn't produce UTF-8 text. I'm // not sure what should happen here according to the spec, // since it doesn't specify this behavior. Presumably // this is a caller bug. break :plain_text; }; var it = view.iterator(); while (it.nextCodepoint()) |cp| { if (isControl(cp)) break :plain_text; }
return try writer.writeAll(event.utf8); } } }
const entry = entry_ orelse { // No entry found. If we have UTF-8 text this is a pure text event // (e.g. composed/IME text), so send it as-is so programs can // still receive it. if (event.utf8.len > 0) return try writer.writeAll(event.utf8); return; };
// If this is just a modifier we require "report all" to send the sequence. if (entry.modifier and !opts.kitty_flags.report_all) return;
const seq: KittySequence = seq: { var seq: KittySequence = .{ .key = entry.code, .final = entry.final, .mods = .fromInput( event.action, event.key, all_mods, ), };
if (opts.kitty_flags.report_events) { seq.event = switch (event.action) { .press => .press, .release => .release, .repeat => .repeat, }; }
if (opts.kitty_flags.report_alternates) alternates: { // Break early if this is a control key if (isControl(seq.key)) break :alternates;
const view = std.unicode.Utf8View.init(event.utf8) catch { // Assume invalid UTF-8 means no UTF-8. break :alternates; }; var it = view.iterator();
// If we have a codepoint in our UTF-8 sequence, then we can // report the shifted version. if (it.nextCodepoint()) |cp1| { // Set the first alternate (shifted version) if (cp1 != seq.key and seq.mods.shift) seq.alternates[0] = cp1;
// We want to know if there are additional codepoints because // our logic below depends on the utf8 being a single codepoint. const has_cp2 = it.nextCodepoint() != null;
// Set the base layout key. We only report this if this codepoint // differs from our pressed key. if (event.key.codepoint()) |base| { if (base != seq.key and (cp1 != base and !has_cp2)) { seq.alternates[1] = base; } } } else { // No UTF-8 so we can't report a shifted key but we can still // report a base layout key. if (event.key.codepoint()) |base| { if (base != seq.key) seq.alternates[1] = base; } } }
if (opts.kitty_flags.report_associated and seq.event != .release) associated: { // Determine if the Alt modifier should be treated as an actual // modifier (in which case it prevents associated text) or as // the macOS Option key, which does not prevent associated text. const alt_prevents_text = if (comptime builtin.os.tag == .macos) switch (opts.macos_option_as_alt) { .left => all_mods.sides.alt == .left, .right => all_mods.sides.alt == .right, .true => true, .false => false, } else true;
if (seq.mods.preventsText(alt_prevents_text)) break :associated;
seq.text = event.utf8; }
break :seq seq; };
return try seq.encode(writer);}
/// Perform legacy encoding of the key event. "Legacy" in this case/// is referring to the behavior of traditional terminals, plus/// xterm's `modifyOtherKeys`, plus Paul Evans's "fixterms" spec./// These together combine the legacy protocol because they're all/// meant to be extensions that do not change any existing behavior/// and therefore safe to combine.fn legacy( writer: *std.Io.Writer, event: key.KeyEvent, opts: Options,) std.Io.Writer.Error!void { const all_mods = event.mods; const effective_mods = event.effectiveMods(); const binding_mods = effective_mods.binding();
// Legacy encoding only does press/repeat if (event.action != .press and event.action != .repeat) return;
// If we're in a dead key state then we never emit a sequence. if (event.composing) return;
// If we match a PC style function key then that is our result. if (pcStyleFunctionKey( event.key, all_mods, opts.cursor_key_application, opts.keypad_key_application, opts.ignore_keypad_with_numlock, opts.modify_other_keys_state_2, opts.backarrow_key_mode, )) |sequence| pc_style: { // If we have UTF-8 text, then we never emit PC style function // keys. Many function keys (escape, enter, backspace) have // a specific meaning when dead keys are active and so we don't // want to send that to the terminal. Examples: // // - Japanese: escape clears the dead key state // - Korean: escape commits the dead key state // - Korean: backspace should delete a single preedit char // if (event.utf8.len > 0) utf8: { switch (event.key) { else => {}, inline .backspace, .enter, .escape => |tag| { // We want to ignore control characters. This is because // some apprts (macOS) will send control characters as // UTF-8 encodings and we handle that manually. if (isControlUtf8(event.utf8)) break :utf8;
// Backspace encodes nothing because we modified IME. // Enter/escape don't encode the PC-style encoding // because we want to encode committed text. if (comptime tag == .backspace) return; break :pc_style; }, } }
return try writer.writeAll(sequence); }
// If we match a control sequence, we output that directly. For // ctrlSeq we have to use all mods because we want it to only // match ctrl+<char>. if (ctrlSeq( event.key, event.utf8, event.unshifted_codepoint, all_mods, )) |char| { // C0 sequences support alt-as-esc prefixing. if (binding_mods.alt) { try writer.writeByte(0x1B); try writer.writeByte(char); return; }
try writer.writeByte(char); return; }
// If we have no UTF8 text then the only possibility is the // alt-prefix handling of unshifted codepoints... so we process that. const utf8 = event.utf8; if (utf8.len == 0) { if (try legacyAltPrefix( event, binding_mods, all_mods, opts, )) |byte| try writer.print("\x1B{c}", .{byte}); return; }
// In modify other keys state 2, we send the CSI 27 sequence // for any char with a modifier. Ctrl sequences like Ctrl+a // are already handled above. if (opts.modify_other_keys_state_2) modify_other: { const view = std.unicode.Utf8View.init(utf8) catch { // Assume invalid UTF-8 means we no UTF-8. break :modify_other; }; var it = view.iterator(); const codepoint = it.nextCodepoint() orelse break :modify_other;
// We only do this if we have a single codepoint. There shouldn't // ever be a multi-codepoint sequence that triggers this. if (it.nextCodepoint() != null) break :modify_other;
// The mods we encode for this are just the binding mods (shift, ctrl, // super, alt unless it is actually option). const mods = mods: { var mods_binding = event.mods.binding(); if (comptime builtin.target.os.tag.isDarwin()) alt: { switch (opts.macos_option_as_alt) { .false => {}, .true => break :alt, .left => if (event.mods.sides.alt == .left) break :alt, .right => if (event.mods.sides.alt == .right) break :alt, } mods_binding.alt = false; } break :mods mods_binding; };
// This copies xterm's `ModifyOtherKeys` function that returns // whether modify other keys should be encoded for the given // input. const should_modify = should_modify: { // xterm IsControlInput if (codepoint >= 0x40 and codepoint <= 0x7F) break :should_modify true;
// If we have anything other than shift pressed, encode. var mods_no_shift = mods; mods_no_shift.shift = false; if (!mods_no_shift.empty()) break :should_modify true;
// We only have shift pressed. We only allow space. if (codepoint == ' ') break :should_modify true;
// This logic isn't complete but I don't fully understand // the rest so I'm going to wait until we can have a // reasonable test scenario. break :should_modify false; };
if (should_modify) { for (function_keys.modifiers, 2..) |modset, code| { if (!mods.equal(modset)) continue; return try writer.print( "\x1B[27;{};{}~", .{ code, codepoint }, ); } } }
// Let's see if we should apply fixterms to this codepoint. // At this stage of key processing, we only need to apply fixterms // to unicode codepoints if we have ctrl set. if (event.mods.ctrl) csiu: { // Important: we want to use the original mods here, not the // effective mods. The fixterms spec states the shifted chars // should be sent uppercase but Kitty changes that behavior // so we'll send all the mods. const csi_u_mods, const char = mods: { var mods = CsiUMods.fromInput(event.mods);
// Get our codepoint. If we have more than one codepoint this // can't be valid CSIu. const view = std.unicode.Utf8View.init(event.utf8) catch break :csiu; var it = view.iterator(); var char = it.nextCodepoint() orelse break :csiu; if (it.nextCodepoint() != null) break :csiu;
// If our character is A to Z and we have shift set, then // we lowercase it. This is a Kitty-specific behavior that // we choose to follow and diverge from the fixterms spec. // This makes it easier for programs to detect shifted letters // for keybindings and is not just theoretical but used by // real programs. if (char >= 'A' and char <= 'Z' and mods.shift) { // We want to rely on apprt to send us the correct // unshifted codepoint... char = @intCast(std.ascii.toLower(@intCast(char))); }
// If our unshifted codepoint is identical to the shifted // then we consider shift. Otherwise, we do not because the // shift key was used to obtain the character. This is specified // by fixterms. if (event.unshifted_codepoint != char) { mods.shift = false; }
break :mods .{ mods, char }; }; return try writer.print( "\x1B[{};{}u", .{ char, csi_u_mods.seqInt() }, ); }
// If we have alt-pressed and alt-esc-prefix is enabled, then // we need to prefix the utf8 sequence with an esc. if (try legacyAltPrefix( event, binding_mods, all_mods, opts, )) |byte| { return try writer.print("\x1B{c}", .{byte}); }
// If we are on macOS, command+keys do not encode text. It isn't // typical for command+keys on macOS to ever encode text. They // don't in native text inputs (i.e. TextEdit) and they also don't // in other native terminals (Terminal.app officially but also // iTerm2). // // For Linux, we continue to encode text because it is typical. // For example on Gnome Console Super+b will encode a "b" character // with legacy encoding. if ((comptime builtin.os.tag == .macos) and all_mods.super) { return; }
return try writer.writeAll(utf8);}
fn legacyAltPrefix( event: key.KeyEvent, binding_mods: key.Mods, mods: key.Mods, opts: Options,) !?u8 { // This only takes effect with alt pressed if (!binding_mods.alt or !opts.alt_esc_prefix) return null;
// On macOS, we only handle option like alt in certain // circumstances. Otherwise, macOS does a unicode translation // and we allow that to happen. if (comptime builtin.os.tag == .macos) { switch (opts.macos_option_as_alt) { .false => return null, .left => if (mods.sides.alt == .right) return null, .right => if (mods.sides.alt == .left) return null, .true => {}, } }
// Otherwise, we require utf8 to already have the byte represented. const utf8 = event.utf8; if (utf8.len == 1) { if (std.math.cast(u8, utf8[0])) |byte| { return byte; } }
// If UTF8 isn't set, we will allow unshifted codepoints through. if (event.unshifted_codepoint > 0) { if (std.math.cast( u8, event.unshifted_codepoint, )) |byte| { return byte; } }
// Else, we can't figure out the byte to alt-prefix so we // exit this handling. return null;}
/// A helper to memcpy a src value to a buffer and return the result.fn copyToBuf(buf: []u8, src: []const u8) ![]const u8 { if (src.len > buf.len) return error.OutOfMemory; const result = buf[0..src.len]; @memcpy(result, src); return result;}
/// Determines whether the key should be encoded in the xterm/// "PC-style Function Key" syntax (roughly). This is a hardcoded/// table of keys and modifiers that result in a specific sequence.fn pcStyleFunctionKey( keyval: key.Key, mods: key.Mods, cursor_key_application: bool, keypad_key_application_req: bool, ignore_keypad_with_numlock: bool, modify_other_keys: bool, // True if state 2 backarrow_key_mode: bool,) ?[]const u8 { // We only want binding-sensitive mods because lock keys // and directional modifiers (left/right) don't matter for // pc-style function keys. const mods_int = mods.binding().int();
// Keypad application keymode isn't super straightforward. // On xterm, in VT220 mode, numlock alone is enough to trigger // application mode. But in more modern modes, numlock is // ignored by default via mode 1035 (default true). If mode // 1035 is on, we always are in numerical keypad mode. If // mode 1035 is off, we are in application mode if the // proper numlock state is pressed. The numlock state is implicitly // determined based on the keycode sent (i.e. 1 with numlock // on will be kp_end). const keypad_key_application = keypad: { // If we're ignoring keypad then this is always false. // In other words, we're always in numerical keypad mode. if (ignore_keypad_with_numlock) break :keypad false;
// If we're not ignoring then we enable the desired state. break :keypad keypad_key_application_req; };
for (function_keys.keys.get(keyval)) |entry| { switch (entry.cursor) { .any => {}, .normal => if (cursor_key_application) continue, .application => if (!cursor_key_application) continue, }
switch (entry.keypad) { .any => {}, .normal => if (keypad_key_application) continue, .application => if (!keypad_key_application) continue, }
switch (entry.modify_other_keys) { .any => {}, .set => if (modify_other_keys) continue, .set_other => if (!modify_other_keys) continue, }
const entry_mods_int = entry.mods.int(); if (entry_mods_int == 0) { if (mods_int != 0 and !entry.mods_empty_is_any) continue; // mods are either empty, or empty means any so we allow it. } else if (entry_mods_int != mods_int) { // any set mods require an exact match continue; }
if (backarrow_key_mode) if (entry.sequence_decbkm) |sequence| return sequence;
return entry.sequence; }
return null;}
/// Returns the C0 byte for the key event if it should be used./// This converts a key event into the expected terminal behavior/// such as Ctrl+C turning into 0x03, amongst many other translations.////// This will return null if the key event should not be converted/// into a C0 byte. There are many cases for this and you should read/// the source code to understand them.fn ctrlSeq( logical_key: key.Key, utf8: []const u8, unshifted_codepoint: u21, mods: key.Mods,) ?u8 { const ctrl_only = comptime (key.Mods{ .ctrl = true }).int();
// If ctrl is not pressed then we never do anything. if (!mods.ctrl) return null;
const char, const unset_mods = unset_mods: { // We need to only get binding modifiers so we strip lock // keys, sides, etc. var unset_mods = mods.binding();
// Remove alt from our modifiers because it does not impact whether // we are generating a ctrl sequence and we handle the ESC-prefix // logic separately. unset_mods.alt = false;
var char: u8 = char: { // If we have exactly one UTF8 byte, we assume that is the // character we want to convert to a C0 byte. if (utf8.len == 1) break :char utf8[0];
// If we have a logical key that maps to a single byte // printable character, we use that. History to explain this: // this was added to support cyrillic keyboard layouts such // as Russian and Mongolian. These layouts have a `c` key that // maps to U+0441 (cyrillic small letter "c") but every // terminal I've tested encodes this as ctrl+c. if (logical_key.codepoint()) |cp| { if (std.math.cast(u8, cp)) |byte| { // For this specific case, we only map to the key if // we have exactly ctrl pressed. This is because shift // would modify the key and we don't know how to do that // properly here (don't have the layout). And we want // to encode shift as CSIu. if (unset_mods.int() != ctrl_only) return null; break :char byte; } }
// Otherwise we don't have a character to convert that // we can reliably map to a C0 byte. return null; };
// Remove shift if we have something outside of the US letter // range. This is so that characters such as `ctrl+shift+-` // generate the correct ctrl-seq (used by emacs). if (unset_mods.shift and (char < 'A' or char > 'Z')) shift: { // Special case for fixterms awkward case as specified. if (char == '@') break :shift; unset_mods.shift = false; }
// If the character is uppercase, we convert it to lowercase. We // rely on the unshifted codepoint to do this. This handles // the scenario where we have caps lock pressed. Note that // shifted characters are handled above, if we are just pressing // shift then the ctrl-only check will fail later and we won't // ctrl-seq encode. if (char >= 'A' and char <= 'Z' and unshifted_codepoint > 0) { if (std.math.cast(u8, unshifted_codepoint)) |byte| { char = byte; } }
// An additional note on caps lock and shift interaction. // If we have caps lock set and an ASCII letter is pressed, // we lowercase it (above). If we have only control pressed, // we process it as a ctrl seq. For example ctrl+M with caps // lock but no shift will encode as 0x0D. // // But, if you press ctrl+shift+m, this will not encode as a // ctrl-seq and falls through to CSIu encoding. This lets programs // detect the difference between ctrl+M and ctrl+shift+M. This // diverges from the fixterms "spec" and most terminals. This // only matches Kitty in behavior. But I believe this is a // justified divergence because it's a useful distinction.
break :unset_mods .{ char, unset_mods }; };
// After unsetting, we only continue if we have ONLY control set. if (unset_mods.int() != ctrl_only) return null;
// From Kitty's key encoding logic. I tried to discern the exact // behavior across different terminals but it's not clear, so I'm // just going to repeat what Kitty does. return switch (char) { ' ' => 0, '/' => 31, '0' => 48, '1' => 49, '2' => 0, '3' => 27, '4' => 28, '5' => 29, '6' => 30, '7' => 31, '8' => 127, '9' => 57, '?' => 127, '@' => 0, '\\' => 28, ']' => 29, '^' => 30, '_' => 31, 'a' => 1, 'b' => 2, 'c' => 3, 'd' => 4, 'e' => 5, 'f' => 6, 'g' => 7, 'h' => 8, 'j' => 10, 'k' => 11, 'l' => 12, 'n' => 14, 'o' => 15, 'p' => 16, 'q' => 17, 'r' => 18, 's' => 19, 't' => 20, 'u' => 21, 'v' => 22, 'w' => 23, 'x' => 24, 'y' => 25, 'z' => 26, '~' => 30,
// These are purposely NOT handled here because of the fixterms // specification: https://www.leonerd.org.uk/hacks/fixterms/ // These are processed as CSI u. // 'i' => 0x09, // 'm' => 0x0D, // '[' => 0x1B,
else => null, };}
/// Returns true if this is an ASCII control character, matches libc implementation.fn isControl(cp: u21) bool { return cp < 0x20 or cp == 0x7F;}
/// Returns true if this string is comprised of a single/// control character. This returns false for multi-byte strings.fn isControlUtf8(str: []const u8) bool { return str.len == 1 and isControl(@intCast(str[0]));}
/// This is the bitmask for fixterm CSI u modifiers.const CsiUMods = packed struct(u3) { shift: bool = false, alt: bool = false, ctrl: bool = false,
/// Convert an input mods value into the CSI u mods value. pub fn fromInput(mods: key.Mods) CsiUMods { return .{ .shift = mods.shift, .alt = mods.alt, .ctrl = mods.ctrl, }; }
/// Returns the raw int value of this packed struct. pub fn int(self: CsiUMods) u3 { return @bitCast(self); }
/// Returns the integer value sent as part of the CSI u sequence. /// This adds 1 to the bitmask value as described in the spec. pub fn seqInt(self: CsiUMods) u4 { const raw: u4 = @intCast(self.int()); return raw + 1; }
test "modifier sequence values" { // This is all sort of trivially seen by looking at the code but // we want to make sure we never regress this. var mods: CsiUMods = .{}; try testing.expectEqual(@as(u4, 1), mods.seqInt());
mods = .{ .shift = true }; try testing.expectEqual(@as(u4, 2), mods.seqInt());
mods = .{ .alt = true }; try testing.expectEqual(@as(u4, 3), mods.seqInt());
mods = .{ .ctrl = true }; try testing.expectEqual(@as(u4, 5), mods.seqInt());
mods = .{ .alt = true, .shift = true }; try testing.expectEqual(@as(u4, 4), mods.seqInt());
mods = .{ .ctrl = true, .shift = true }; try testing.expectEqual(@as(u4, 6), mods.seqInt());
mods = .{ .alt = true, .ctrl = true }; try testing.expectEqual(@as(u4, 7), mods.seqInt());
mods = .{ .alt = true, .ctrl = true, .shift = true }; try testing.expectEqual(@as(u4, 8), mods.seqInt()); }};
/// This is the bitfields for Kitty modifiers.const KittyMods = packed struct(u8) { shift: bool = false, alt: bool = false, ctrl: bool = false, super: bool = false, hyper: bool = false, meta: bool = false, caps_lock: bool = false, num_lock: bool = false,
/// Convert an input mods value into the CSI u mods value. pub fn fromInput( action: key.Action, k: key.Key, mods: key.Mods, ) KittyMods { _ = action; _ = k; return .{ .shift = mods.shift, .alt = mods.alt, .ctrl = mods.ctrl, .super = mods.super, .caps_lock = mods.caps_lock, .num_lock = mods.num_lock, }; }
/// Returns true if the modifiers prevent printable text. /// /// The alt_prevents_text parameter determines whether or not the Alt /// modifier prevents printable text. On Linux, this is always true. On /// macOS, this is only true if macos-option-as-alt is set. pub fn preventsText(self: KittyMods, alt_prevents_text: bool) bool { return (self.alt and alt_prevents_text) or self.ctrl or self.super or self.hyper or self.meta; }
/// Returns the raw int value of this packed struct. pub fn int(self: KittyMods) u8 { return @bitCast(self); }
/// Returns the integer value sent as part of the Kitty sequence. /// This adds 1 to the bitmask value as described in the spec. pub fn seqInt(self: KittyMods) u9 { const raw: u9 = @intCast(self.int()); return raw + 1; }
test "modifier sequence values" { // This is all sort of trivially seen by looking at the code but // we want to make sure we never regress this. var mods: KittyMods = .{}; try testing.expectEqual(@as(u9, 1), mods.seqInt());
mods = .{ .shift = true }; try testing.expectEqual(@as(u9, 2), mods.seqInt());
mods = .{ .alt = true }; try testing.expectEqual(@as(u9, 3), mods.seqInt());
mods = .{ .ctrl = true }; try testing.expectEqual(@as(u9, 5), mods.seqInt());
mods = .{ .alt = true, .shift = true }; try testing.expectEqual(@as(u9, 4), mods.seqInt());
mods = .{ .ctrl = true, .shift = true }; try testing.expectEqual(@as(u9, 6), mods.seqInt());
mods = .{ .alt = true, .ctrl = true }; try testing.expectEqual(@as(u9, 7), mods.seqInt());
mods = .{ .alt = true, .ctrl = true, .shift = true }; try testing.expectEqual(@as(u9, 8), mods.seqInt()); }};
/// Represents a kitty key sequence and has helpers for encoding it./// The sequence from the Kitty specification:////// CSI unicode-key-code:alternate-key-codes ; modifiers:event-type ; text-as-codepoints uconst KittySequence = struct { key: u21, final: u8, mods: KittyMods = .{}, event: Event = .none, alternates: [2]?u21 = .{ null, null }, text: []const u8 = "",
/// Values for the event code (see "event-type" in above comment). /// Note that Kitty omits the ":1" for the press event but other /// terminals include it. We'll include it. const Event = enum(u2) { none = 0, press = 1, repeat = 2, release = 3, };
pub fn encode( self: KittySequence, writer: *std.Io.Writer, ) std.Io.Writer.Error!void { if (self.final == 'u' or self.final == '~') return try self.encodeFull(writer); return try self.encodeSpecial(writer); }
fn encodeFull( self: KittySequence, writer: *std.Io.Writer, ) std.Io.Writer.Error!void { // Key section try writer.print("\x1B[{d}", .{self.key}); // Write our alternates if (self.alternates[0]) |shifted| try writer.print(":{d}", .{shifted}); if (self.alternates[1]) |base| { if (self.alternates[0] == null) { try writer.print("::{d}", .{base}); } else { try writer.print(":{d}", .{base}); } }
// Mods and events section const mods = self.mods.seqInt(); var emit_prior = false; if (self.event != .none and self.event != .press) { try writer.print( ";{d}:{d}", .{ mods, @intFromEnum(self.event) }, ); emit_prior = true; } else if (mods > 1) { try writer.print(";{d}", .{mods}); emit_prior = true; }
// Text section if (self.text.len > 0) text: { const view = std.unicode.Utf8View.init(self.text) catch { // Assume invalid UTF-8 means we have no text. break :text; }; var it = view.iterator(); var count: usize = 0; while (it.nextCodepoint()) |cp| { // If the codepoint is non-printable ASCII character, skip. if (isControl(cp)) continue;
// We need to add our ";". We need to add two if we didn't emit // the modifier section. We only do this initially. if (count == 0) { if (!emit_prior) try writer.writeByte(';'); try writer.writeByte(';'); } else { try writer.writeByte(':'); }
try writer.print("{d}", .{cp}); count += 1; } }
try writer.print("{c}", .{self.final}); }
fn encodeSpecial( self: KittySequence, writer: *std.Io.Writer, ) std.Io.Writer.Error!void { const mods = self.mods.seqInt(); if (self.event != .none) { return try writer.print("\x1B[1;{d}:{d}{c}", .{ mods, @intFromEnum(self.event), self.final, }); }
if (mods > 1) { return try writer.print("\x1B[1;{d}{c}", .{ mods, self.final, }); }
return try writer.print("\x1B[{c}", .{self.final}); }};
test "KittySequence: backspace" { var buf: [128]u8 = undefined;
// Plain { var writer: std.Io.Writer = .fixed(&buf); var seq: KittySequence = .{ .key = 127, .final = 'u' }; try seq.encode(&writer); try testing.expectEqualStrings("\x1B[127u", writer.buffered()); }
// Release event { var writer: std.Io.Writer = .fixed(&buf); var seq: KittySequence = .{ .key = 127, .final = 'u', .event = .release }; try seq.encode(&writer); try testing.expectEqualStrings("\x1B[127;1:3u", writer.buffered()); }
// Shift { var writer: std.Io.Writer = .fixed(&buf); var seq: KittySequence = .{ .key = 127, .final = 'u', .mods = .{ .shift = true }, }; try seq.encode(&writer); try testing.expectEqualStrings("\x1B[127;2u", writer.buffered()); }}
test "KittySequence: text" { var buf: [128]u8 = undefined;
// Plain { var writer: std.Io.Writer = .fixed(&buf); var seq: KittySequence = .{ .key = 127, .final = 'u', .text = "A", }; try seq.encode(&writer); try testing.expectEqualStrings("\x1B[127;;65u", writer.buffered()); }
// Release { var writer: std.Io.Writer = .fixed(&buf); var seq: KittySequence = .{ .key = 127, .final = 'u', .event = .release, .text = "A", }; try seq.encode(&writer); try testing.expectEqualStrings("\x1B[127;1:3;65u", writer.buffered()); }
// Shift { var writer: std.Io.Writer = .fixed(&buf); var seq: KittySequence = .{ .key = 127, .final = 'u', .mods = .{ .shift = true }, .text = "A", }; try seq.encode(&writer); try testing.expectEqualStrings("\x1B[127;2;65u", writer.buffered()); }}//test "KittySequence: text with control characters" { var buf: [128]u8 = undefined;
// By itself { var writer: std.Io.Writer = .fixed(&buf); var seq: KittySequence = .{ .key = 127, .final = 'u', .text = "\n", }; try seq.encode(&writer); try testing.expectEqualStrings("\x1b[127u", writer.buffered()); }
// With other printables { var writer: std.Io.Writer = .fixed(&buf); var seq: KittySequence = .{ .key = 127, .final = 'u', .text = "A\n", }; try seq.encode(&writer); try testing.expectEqualStrings("\x1b[127;;65u", writer.buffered()); }}//test "KittySequence: special no mods" { var buf: [128]u8 = undefined; var writer: std.Io.Writer = .fixed(&buf); var seq: KittySequence = .{ .key = 1, .final = 'A' }; try seq.encode(&writer); try testing.expectEqualStrings("\x1B[A", writer.buffered());}
test "KittySequence: special mods only" { var buf: [128]u8 = undefined; var writer: std.Io.Writer = .fixed(&buf); var seq: KittySequence = .{ .key = 1, .final = 'A', .mods = .{ .shift = true } }; try seq.encode(&writer); try testing.expectEqualStrings("\x1B[1;2A", writer.buffered());}
test "KittySequence: special mods and event" { var buf: [128]u8 = undefined; var writer: std.Io.Writer = .fixed(&buf); var seq: KittySequence = .{ .key = 1, .final = 'A', .event = .release, .mods = .{ .shift = true }, }; try seq.encode(&writer); try testing.expectEqualStrings("\x1B[1;2:3A", writer.buffered());}
test "kitty: plain text" { var buf: [128]u8 = undefined; var writer: std.Io.Writer = .fixed(&buf); try kitty(&writer, .{ .key = .key_a, .mods = .{}, .utf8 = "abcd", }, .{ .kitty_flags = .{ .disambiguate = true }, }); try testing.expectEqualStrings("abcd", writer.buffered());}
test "kitty: repeat with just disambiguate" { var buf: [128]u8 = undefined; var writer: std.Io.Writer = .fixed(&buf); try kitty(&writer, .{ .key = .key_a, .action = .repeat, .mods = .{}, .utf8 = "a", }, .{ .kitty_flags = .{ .disambiguate = true }, }); try testing.expectEqualStrings("a", writer.buffered());}//test "kitty: enter, backspace, tab" { var buf: [128]u8 = undefined; { var writer: std.Io.Writer = .fixed(&buf); try kitty(&writer, .{ .key = .enter, .mods = .{}, .utf8 = "" }, .{ .kitty_flags = .{ .disambiguate = true }, }); try testing.expectEqualStrings("\r", writer.buffered()); } { // DECBKM reset var writer: std.Io.Writer = .fixed(&buf); try kitty(&writer, .{ .key = .backspace, .mods = .{}, .utf8 = "" }, .{ .kitty_flags = .{ .disambiguate = true }, .backarrow_key_mode = false, }); try testing.expectEqualStrings("\x7f", writer.buffered()); } { // DECBKM set (Kitty does not support DECBKM so there should be no change) var writer: std.Io.Writer = .fixed(&buf); try kitty(&writer, .{ .key = .backspace, .mods = .{}, .utf8 = "" }, .{ .kitty_flags = .{ .disambiguate = true }, .backarrow_key_mode = true, }); try testing.expectEqualStrings("\x7f", writer.buffered()); } { var writer: std.Io.Writer = .fixed(&buf); try kitty(&writer, .{ .key = .tab, .mods = .{}, .utf8 = "" }, .{ .kitty_flags = .{ .disambiguate = true }, }); try testing.expectEqualStrings("\t", writer.buffered()); }
// No release events if "report_all" is not set { var writer: std.Io.Writer = .fixed(&buf); try kitty(&writer, .{ .action = .release, .key = .enter, .mods = .{}, .utf8 = "" }, .{ .kitty_flags = .{ .disambiguate = true, .report_events = true }, }); try testing.expectEqualStrings("", writer.buffered()); } { var writer: std.Io.Writer = .fixed(&buf); try kitty(&writer, .{ .action = .release, .key = .backspace, .mods = .{}, .utf8 = "" }, .{ .kitty_flags = .{ .disambiguate = true, .report_events = true }, }); try testing.expectEqualStrings("", writer.buffered()); } { var writer: std.Io.Writer = .fixed(&buf); try kitty(&writer, .{ .action = .release, .key = .tab, .mods = .{}, .utf8 = "" }, .{ .kitty_flags = .{ .disambiguate = true, .report_events = true }, }); try testing.expectEqualStrings("", writer.buffered()); }
// Release events if "report_all" is set { var writer: std.Io.Writer = .fixed(&buf); try kitty(&writer, .{ .action = .release, .key = .enter, .mods = .{}, .utf8 = "" }, .{ .kitty_flags = .{ .disambiguate = true, .report_events = true, .report_all = true, }, }); try testing.expectEqualStrings("\x1b[13;1:3u", writer.buffered()); } { var writer: std.Io.Writer = .fixed(&buf); try kitty(&writer, .{ .action = .release, .key = .backspace, .mods = .{}, .utf8 = "" }, .{ .kitty_flags = .{ .disambiguate = true, .report_events = true, .report_all = true, }, }); try testing.expectEqualStrings("\x1b[127;1:3u", writer.buffered()); } { var writer: std.Io.Writer = .fixed(&buf); try kitty(&writer, .{ .action = .release, .key = .tab, .mods = .{}, .utf8 = "" }, .{ .kitty_flags = .{ .disambiguate = true, .report_events = true, .report_all = true, }, }); try testing.expectEqualStrings("\x1b[9;1:3u", writer.buffered()); }}
test "kitty: shift+backspace emits CSI u" { // Backspace with shift modifier should emit CSI u sequence, not raw 0x7F. // This is important for programs that want to distinguish shift+backspace. var buf: [128]u8 = undefined; var writer: std.Io.Writer = .fixed(&buf); try kitty(&writer, .{ .key = .backspace, .mods = .{ .shift = true }, .utf8 = "", }, .{ .kitty_flags = .{ .disambiguate = true }, }); try testing.expectEqualStrings("\x1b[127;2u", writer.buffered());}
test "kitty: shift+enter emits CSI u" { var buf: [128]u8 = undefined; var writer: std.Io.Writer = .fixed(&buf); try kitty(&writer, .{ .key = .enter, .mods = .{ .shift = true }, .utf8 = "", }, .{ .kitty_flags = .{ .disambiguate = true }, }); try testing.expectEqualStrings("\x1b[13;2u", writer.buffered());}
test "kitty: shift+tab emits CSI u" { var buf: [128]u8 = undefined; var writer: std.Io.Writer = .fixed(&buf); try kitty(&writer, .{ .key = .tab, .mods = .{ .shift = true }, .utf8 = "", }, .{ .kitty_flags = .{ .disambiguate = true }, }); try testing.expectEqualStrings("\x1b[9;2u", writer.buffered());}
test "kitty: enter with all flags" { var buf: [128]u8 = undefined; var writer: std.Io.Writer = .fixed(&buf); try kitty(&writer, .{ .key = .enter, .mods = .{}, .utf8 = "" }, .{ .kitty_flags = .{ .disambiguate = true, .report_events = true, .report_alternates = true, .report_all = true, .report_associated = true, }, }); const actual = writer.buffered(); try testing.expectEqualStrings("[13u", actual[1..]);}//test "kitty: ctrl with all flags" { var buf: [128]u8 = undefined; var writer: std.Io.Writer = .fixed(&buf); try kitty(&writer, .{ .key = .control_left, .mods = .{ .ctrl = true }, .utf8 = "" }, .{ .kitty_flags = .{ .disambiguate = true, .report_events = true, .report_alternates = true, .report_all = true, .report_associated = true, }, }); const actual = writer.buffered(); try testing.expectEqualStrings("[57442;5u", actual[1..]);}
test "kitty: ctrl release with ctrl mod set" { var buf: [128]u8 = undefined; var writer: std.Io.Writer = .fixed(&buf); try kitty(&writer, .{ .action = .release, .key = .control_left, .mods = .{ .ctrl = true }, .utf8 = "", }, .{ .kitty_flags = .{ .disambiguate = true, .report_events = true, .report_alternates = true, .report_all = true, .report_associated = true, }, }); const actual = writer.buffered(); try testing.expectEqualStrings("[57442;5:3u", actual[1..]);}
test "kitty: delete" { var buf: [128]u8 = undefined; { var writer: std.Io.Writer = .fixed(&buf); try kitty(&writer, .{ .key = .delete, .mods = .{}, .utf8 = "\x7F" }, .{ .kitty_flags = .{ .disambiguate = true }, }); try testing.expectEqualStrings("\x1b[3~", writer.buffered()); }}
test "kitty: composing with no modifier" { var buf: [128]u8 = undefined; var writer: std.Io.Writer = .fixed(&buf); try kitty(&writer, .{ .key = .key_a, .mods = .{ .shift = true }, .composing = true, }, .{ .kitty_flags = .{ .disambiguate = true }, }); try testing.expectEqualStrings("", writer.buffered());}
test "kitty: composing with modifier" { var buf: [128]u8 = undefined; var writer: std.Io.Writer = .fixed(&buf); try kitty(&writer, .{ .key = .shift_left, .mods = .{ .shift = true }, .composing = true, }, .{ .kitty_flags = .{ .disambiguate = true, .report_all = true }, }); try testing.expectEqualStrings("\x1b[57441;2u", writer.buffered());}
test "kitty: composed text with report all" { var buf: [128]u8 = undefined; var writer: std.Io.Writer = .fixed(&buf); try kitty(&writer, .{ .key = .unidentified, .mods = .{}, .utf8 = "\xc3\xbb", // û }, .{ .kitty_flags = .{ .disambiguate = true, .report_events = true, .report_alternates = true, .report_all = true, .report_associated = true, }, }); try testing.expectEqualStrings("\xc3\xbb", writer.buffered());}
test "kitty: shift+a on US keyboard" { var buf: [128]u8 = undefined; var writer: std.Io.Writer = .fixed(&buf); try kitty(&writer, .{ .key = .key_a, .mods = .{ .shift = true }, .utf8 = "A", .unshifted_codepoint = 97, // lowercase A }, .{ .kitty_flags = .{ .disambiguate = true, .report_alternates = true, }, }); try testing.expectEqualStrings("\x1b[97:65;2u", writer.buffered());}
test "kitty: matching unshifted codepoint" { var buf: [128]u8 = undefined; var writer: std.Io.Writer = .fixed(&buf); try kitty(&writer, .{ .key = .key_a, .mods = .{ .shift = true }, .utf8 = "A", .unshifted_codepoint = 65, }, .{ .kitty_flags = .{ .disambiguate = true, .report_alternates = true, }, }); // WARNING: This is not a valid encoding. This is a hypothetical encoding // just to test that our logic is correct around matching unshifted // codepoints. We get an alternate here because the unshifted_codepoint does // not match the base key try testing.expectEqualStrings("\x1b[65::97;2u", writer.buffered());}
test "kitty: report alternates with caps" { var buf: [128]u8 = undefined; var writer: std.Io.Writer = .fixed(&buf); try kitty(&writer, .{ .key = .key_j, .mods = .{ .caps_lock = true }, .utf8 = "J", .unshifted_codepoint = 106, }, .{ .kitty_flags = .{ .disambiguate = true, .report_all = true, .report_alternates = true, .report_associated = true, }, }); try testing.expectEqualStrings("\x1b[106;65;74u", writer.buffered());}
test "kitty: report alternates colon (shift+';')" { var buf: [128]u8 = undefined; var writer: std.Io.Writer = .fixed(&buf); try kitty(&writer, .{ .key = .semicolon, .mods = .{ .shift = true }, .utf8 = ":", .unshifted_codepoint = ';', }, .{ .kitty_flags = .{ .disambiguate = true, .report_all = true, .report_alternates = true, .report_associated = true, }, }); try testing.expectEqualStrings("\x1b[59:58;2;58u", writer.buffered());}
test "kitty: report alternates with ru layout" { var buf: [128]u8 = undefined; var writer: std.Io.Writer = .fixed(&buf); try kitty(&writer, .{ .key = .semicolon, .mods = .{}, .utf8 = "ч", .unshifted_codepoint = 1095, }, .{ .kitty_flags = .{ .disambiguate = true, .report_all = true, .report_alternates = true, .report_associated = true, }, }); try testing.expectEqualStrings("\x1b[1095::59;;1095u", writer.buffered());}
test "kitty: report alternates with ru layout shifted" { var buf: [128]u8 = undefined; var writer: std.Io.Writer = .fixed(&buf); try kitty(&writer, .{ .key = .semicolon, .mods = .{ .shift = true }, .utf8 = "Ч", .unshifted_codepoint = 1095, }, .{ .kitty_flags = .{ .disambiguate = true, .report_all = true, .report_alternates = true, .report_associated = true, }, }); try testing.expectEqualStrings("\x1b[1095:1063:59;2;1063u", writer.buffered());}
test "kitty: report alternates with ru layout caps lock" { var buf: [128]u8 = undefined; var writer: std.Io.Writer = .fixed(&buf); try kitty(&writer, .{ .key = .semicolon, .mods = .{ .caps_lock = true }, .utf8 = "Ч", .unshifted_codepoint = 1095, }, .{ .kitty_flags = .{ .disambiguate = true, .report_all = true, .report_alternates = true, .report_associated = true, }, }); try testing.expectEqualStrings("\x1b[1095::59;65;1063u", writer.buffered());}
test "kitty: report alternates with hu layout release" { var buf: [128]u8 = undefined; var writer: std.Io.Writer = .fixed(&buf); try kitty(&writer, .{ .action = .release, .key = .bracket_left, .mods = .{ .ctrl = true }, .utf8 = "", .unshifted_codepoint = 337, }, .{ .kitty_flags = .{ .disambiguate = true, .report_all = true, .report_alternates = true, .report_associated = true, .report_events = true, }, }); const actual = writer.buffered(); try testing.expectEqualStrings("[337::91;5:3u", actual[1..]);}
// macOS generates utf8 text for arrow keys.test "kitty: up arrow with utf8" { var buf: [128]u8 = undefined; var writer: std.Io.Writer = .fixed(&buf); try kitty(&writer, .{ .key = .arrow_up, .mods = .{}, .utf8 = &.{30}, }, .{ .kitty_flags = .{ .disambiguate = true }, }); try testing.expectEqualStrings("\x1b[A", writer.buffered());}
test "kitty: shift+tab" { var buf: [128]u8 = undefined; var writer: std.Io.Writer = .fixed(&buf); try kitty(&writer, .{ .key = .tab, .mods = .{ .shift = true }, .utf8 = "", // tab }, .{ .kitty_flags = .{ .disambiguate = true, .report_alternates = true }, }); try testing.expectEqualStrings("\x1b[9;2u", writer.buffered());}
test "kitty: left shift" { var buf: [128]u8 = undefined; var writer: std.Io.Writer = .fixed(&buf); try kitty(&writer, .{ .key = .shift_left, .mods = .{}, .utf8 = "", }, .{ .kitty_flags = .{ .disambiguate = true, .report_alternates = true }, }); try testing.expectEqualStrings("", writer.buffered());}
test "kitty: left shift with report all" { var buf: [128]u8 = undefined; var writer: std.Io.Writer = .fixed(&buf); try kitty(&writer, .{ .key = .shift_left, .mods = .{}, .utf8 = "", }, .{ .kitty_flags = .{ .disambiguate = true, .report_all = true }, }); try testing.expectEqualStrings("\x1b[57441u", writer.buffered());}
test "kitty: report associated with alt text on macOS with option" { if (comptime !builtin.target.os.tag.isDarwin()) return error.SkipZigTest;
var buf: [128]u8 = undefined; var writer: std.Io.Writer = .fixed(&buf); try kitty(&writer, .{ .key = .key_w, .mods = .{ .alt = true }, .utf8 = "∑", .unshifted_codepoint = 119, }, .{ .kitty_flags = .{ .disambiguate = true, .report_all = true, .report_alternates = true, .report_associated = true, }, .macos_option_as_alt = .false, }); try testing.expectEqualStrings("\x1b[119;3;8721u", writer.buffered());}
test "kitty: report associated with alt text on macOS with alt" { if (comptime !builtin.target.os.tag.isDarwin()) return error.SkipZigTest;
{ // With Alt modifier var buf: [128]u8 = undefined; var writer: std.Io.Writer = .fixed(&buf); try kitty(&writer, .{ .key = .key_w, .mods = .{ .alt = true }, .utf8 = "∑", .unshifted_codepoint = 119, }, .{ .kitty_flags = .{ .disambiguate = true, .report_all = true, .report_alternates = true, .report_associated = true, }, .macos_option_as_alt = .true, }); try testing.expectEqualStrings("\x1b[119;3u", writer.buffered()); }
{ // Without Alt modifier var buf: [128]u8 = undefined; var writer: std.Io.Writer = .fixed(&buf); try kitty(&writer, .{ .key = .key_w, .mods = .{}, .utf8 = "∑", .unshifted_codepoint = 119, }, .{ .kitty_flags = .{ .disambiguate = true, .report_all = true, .report_alternates = true, .report_associated = true, }, .macos_option_as_alt = .true, }); try testing.expectEqualStrings("\x1b[119;;8721u", writer.buffered()); }}
test "kitty: report associated with modifiers" { var buf: [128]u8 = undefined; var writer: std.Io.Writer = .fixed(&buf); try kitty(&writer, .{ .key = .key_j, .mods = .{ .ctrl = true }, .utf8 = "j", .unshifted_codepoint = 106, }, .{ .kitty_flags = .{ .disambiguate = true, .report_all = true, .report_alternates = true, .report_associated = true, }, }); try testing.expectEqualStrings("\x1b[106;5u", writer.buffered());}
test "kitty: report associated" { var buf: [128]u8 = undefined; var writer: std.Io.Writer = .fixed(&buf); try kitty(&writer, .{ .key = .key_j, .mods = .{ .shift = true }, .utf8 = "J", .unshifted_codepoint = 106, }, .{ .kitty_flags = .{ .disambiguate = true, .report_all = true, .report_alternates = true, .report_associated = true, }, }); try testing.expectEqualStrings("\x1b[106:74;2;74u", writer.buffered());}
test "kitty: report associated on release" { var buf: [128]u8 = undefined; var writer: std.Io.Writer = .fixed(&buf); try kitty(&writer, .{ .action = .release, .key = .key_j, .mods = .{ .shift = true }, .utf8 = "J", .unshifted_codepoint = 106, }, .{ .kitty_flags = .{ .disambiguate = true, .report_all = true, .report_alternates = true, .report_associated = true, .report_events = true, }, }); const actual = writer.buffered(); try testing.expectEqualStrings("[106:74;2:3u", actual[1..]);}
test "kitty: alternates omit control characters" { var buf: [128]u8 = undefined; var writer: std.Io.Writer = .fixed(&buf); try kitty(&writer, .{ .key = .delete, .mods = .{}, .utf8 = &.{0x7F}, }, .{ .kitty_flags = .{ .disambiguate = true, .report_alternates = true, .report_all = true, }, }); try testing.expectEqualStrings("\x1b[3~", writer.buffered());}
test "kitty: enter with utf8 (dead key state)" { var buf: [128]u8 = undefined; var writer: std.Io.Writer = .fixed(&buf); try kitty(&writer, .{ .key = .enter, .utf8 = "A", .unshifted_codepoint = 0x0D, }, .{ .kitty_flags = .{ .disambiguate = true, .report_alternates = true, .report_all = true, }, }); try testing.expectEqualStrings("A", writer.buffered());}
test "kitty: keypad number" { var buf: [128]u8 = undefined; var writer: std.Io.Writer = .fixed(&buf); try kitty(&writer, .{ .key = .numpad_1, .mods = .{}, .utf8 = "1", }, .{ .kitty_flags = .{ .disambiguate = true, .report_events = true, .report_alternates = true, .report_all = true, .report_associated = true, }, }); const actual = writer.buffered(); try testing.expectEqualStrings("[57400;;49u", actual[1..]);}
test "kitty: backspace with utf8 (dead key state)" { var buf: [128]u8 = undefined; var writer: std.Io.Writer = .fixed(&buf); try kitty(&writer, .{ .key = .backspace, .utf8 = "A", .unshifted_codepoint = 0x0D, }, .{ .kitty_flags = .{ .disambiguate = true, .report_events = true, .report_alternates = true, .report_all = true, .report_associated = true, }, }); try testing.expectEqualStrings("", writer.buffered());}
test "legacy: backspace with utf8 (dead key state)" { var buf: [128]u8 = undefined; var writer: std.Io.Writer = .fixed(&buf); try legacy(&writer, .{ .key = .backspace, .utf8 = "A", .unshifted_codepoint = 0x0D, }, .{}); try testing.expectEqualStrings("", writer.buffered());}
test "kitty: backspace (DECBKM reset) (report_all: true)" { var buf: [128]u8 = undefined; var writer: std.Io.Writer = .fixed(&buf); try kitty(&writer, .{ .key = .backspace, }, .{ .kitty_flags = .{ .disambiguate = true, .report_events = true, .report_alternates = true, .report_all = true, .report_associated = true, }, .backarrow_key_mode = false, }); try testing.expectEqualStrings("\x1b[127u", writer.buffered());}
test "kitty: backspace (DECBKM set) (report_all: true)" { // Kitty does not support DECBKM so there should be no difference. var buf: [128]u8 = undefined; var writer: std.Io.Writer = .fixed(&buf); try kitty(&writer, .{ .key = .backspace, }, .{ .kitty_flags = .{ .disambiguate = true, .report_events = true, .report_alternates = true, .report_all = true, .report_associated = true, }, .backarrow_key_mode = true, }); try testing.expectEqualStrings("\x1b[127u", writer.buffered());}
test "legacy: enter with utf8 (dead key state)" { var buf: [128]u8 = undefined; var writer: std.Io.Writer = .fixed(&buf); try legacy(&writer, .{ .key = .enter, .utf8 = "A", .unshifted_codepoint = 0x0D, }, .{}); try testing.expectEqualStrings("A", writer.buffered());}
test "legacy: esc with utf8 (dead key state)" { var buf: [128]u8 = undefined; var writer: std.Io.Writer = .fixed(&buf); try legacy(&writer, .{ .key = .escape, .utf8 = "A", .unshifted_codepoint = 0x0D, }, .{}); try testing.expectEqualStrings("A", writer.buffered());}
test "legacy: ctrl+shift+minus (underscore on US)" { var buf: [128]u8 = undefined; var writer: std.Io.Writer = .fixed(&buf); try legacy(&writer, .{ .key = .minus, .mods = .{ .ctrl = true, .shift = true }, .utf8 = "_", }, .{}); try testing.expectEqualStrings("\x1F", writer.buffered());}
test "legacy: ctrl+alt+c" { var buf: [128]u8 = undefined; var writer: std.Io.Writer = .fixed(&buf); try legacy(&writer, .{ .key = .key_c, .mods = .{ .ctrl = true, .alt = true }, .utf8 = "c", }, .{}); try testing.expectEqualStrings("\x1b\x03", writer.buffered());}
test "legacy: alt+c" { var buf: [128]u8 = undefined; var writer: std.Io.Writer = .fixed(&buf); try legacy(&writer, .{ .key = .key_c, .utf8 = "c", .mods = .{ .alt = true }, }, .{ .alt_esc_prefix = true, .macos_option_as_alt = .true, }); try testing.expectEqualStrings("\x1Bc", writer.buffered());}
test "legacy: alt+e only unshifted" { var buf: [128]u8 = undefined; var writer: std.Io.Writer = .fixed(&buf); try legacy(&writer, .{ .key = .key_e, .unshifted_codepoint = 'e', .mods = .{ .alt = true }, }, .{ .alt_esc_prefix = true, .macos_option_as_alt = .true, }); try testing.expectEqualStrings("\x1Be", writer.buffered());}
test "legacy: alt+x macos" { if (comptime !builtin.target.os.tag.isDarwin()) return error.SkipZigTest;
var buf: [128]u8 = undefined; var writer: std.Io.Writer = .fixed(&buf); try legacy(&writer, .{ .key = .key_c, .utf8 = "≈", .unshifted_codepoint = 'c', .mods = .{ .alt = true }, }, .{ .alt_esc_prefix = true, .macos_option_as_alt = .true, }); try testing.expectEqualStrings("\x1Bc", writer.buffered());}
test "legacy: shift+alt+. macos" { if (comptime !builtin.target.os.tag.isDarwin()) return error.SkipZigTest;
var buf: [128]u8 = undefined; var writer: std.Io.Writer = .fixed(&buf); try legacy(&writer, .{ .key = .period, .utf8 = ">", .unshifted_codepoint = '.', .mods = .{ .alt = true, .shift = true }, }, .{ .alt_esc_prefix = true, .macos_option_as_alt = .true, }); try testing.expectEqualStrings("\x1B>", writer.buffered());}
test "legacy: alt+ф" { var buf: [128]u8 = undefined; var writer: std.Io.Writer = .fixed(&buf); try legacy(&writer, .{ .key = .key_f, .utf8 = "ф", .mods = .{ .alt = true }, }, .{ .alt_esc_prefix = true, }); try testing.expectEqualStrings("ф", writer.buffered());}
test "legacy: ctrl+c" { var buf: [128]u8 = undefined; var writer: std.Io.Writer = .fixed(&buf); try legacy(&writer, .{ .key = .key_c, .mods = .{ .ctrl = true }, .utf8 = "c", }, .{}); try testing.expectEqualStrings("\x03", writer.buffered());}
test "legacy: ctrl+space" { var buf: [128]u8 = undefined; var writer: std.Io.Writer = .fixed(&buf); try legacy(&writer, .{ .key = .space, .mods = .{ .ctrl = true }, .utf8 = " ", }, .{}); try testing.expectEqualStrings("\x00", writer.buffered());}
test "legacy: ctrl+shift+backspace" { var buf: [128]u8 = undefined; var writer: std.Io.Writer = .fixed(&buf); try legacy(&writer, .{ .key = .backspace, .mods = .{ .ctrl = true, .shift = true }, }, .{}); try testing.expectEqualStrings("\x08", writer.buffered());}
test "legacy: backspace (DECBKM reset)" { var buf: [128]u8 = undefined; var writer: std.Io.Writer = .fixed(&buf); try legacy(&writer, .{ .key = .backspace, .mods = .{}, }, .{ .backarrow_key_mode = false }); try testing.expectEqualStrings("\x7f", writer.buffered());}
test "legacy: backspace (DECBKM reset, with ctrl)" { var buf: [128]u8 = undefined; var writer: std.Io.Writer = .fixed(&buf); try legacy(&writer, .{ .key = .backspace, .mods = .{ .ctrl = true, }, }, .{ .backarrow_key_mode = false }); try testing.expectEqualStrings("\x08", writer.buffered());}
test "legacy: backspace (DECBKM set)" { var buf: [128]u8 = undefined; var writer: std.Io.Writer = .fixed(&buf); try legacy(&writer, .{ .key = .backspace, .mods = .{}, }, .{ .backarrow_key_mode = true }); try testing.expectEqualStrings("\x08", writer.buffered());}
test "legacy: backspace (DECBKM set, with ctrl)" { var buf: [128]u8 = undefined; var writer: std.Io.Writer = .fixed(&buf); try legacy(&writer, .{ .key = .backspace, .mods = .{ .ctrl = true, }, }, .{ .backarrow_key_mode = true }); try testing.expectEqualStrings("\x7f", writer.buffered());}
test "legacy: ctrl+shift+char with modify other state 2" { var buf: [128]u8 = undefined; var writer: std.Io.Writer = .fixed(&buf); try legacy(&writer, .{ .key = .key_h, .mods = .{ .ctrl = true, .shift = true }, .utf8 = "H", }, .{ .modify_other_keys_state_2 = true, }); try testing.expectEqualStrings("\x1b[27;6;72~", writer.buffered());}
test "legacy: ctrl+shift+char with modify other state 2 and consumed mods" { var buf: [128]u8 = undefined; var writer: std.Io.Writer = .fixed(&buf); try legacy(&writer, .{ .key = .key_h, .mods = .{ .ctrl = true, .shift = true }, .consumed_mods = .{ .shift = true }, .utf8 = "H", }, .{ .modify_other_keys_state_2 = true, }); try testing.expectEqualStrings("\x1b[27;6;72~", writer.buffered());}
test "legacy: alt+digit with modify other state 2" { var buf: [128]u8 = undefined; var writer: std.Io.Writer = .fixed(&buf); try legacy(&writer, .{ .key = .digit_8, .mods = .{ .alt = true }, .consumed_mods = .{}, .utf8 = "8", }, .{ .modify_other_keys_state_2 = true, .macos_option_as_alt = .true, }); try testing.expectEqualStrings("\x1b[27;3;56~", writer.buffered());}
test "legacy: alt+digit with modify other state 2 and macos-option-as-alt = false" { if (comptime builtin.os.tag != .macos) return error.SkipZigTest; var buf: [128]u8 = undefined; var writer: std.Io.Writer = .fixed(&buf); try legacy(&writer, .{ .key = .digit_8, .mods = .{ .alt = true }, .consumed_mods = .{ .alt = true }, .utf8 = "[", // common translation of option+8 with European keyboard layouts }, .{ .modify_other_keys_state_2 = true, .macos_option_as_alt = .false, }); try testing.expectEqualStrings("[", writer.buffered());}
test "legacy: fixterm awkward letters" { var buf: [128]u8 = undefined; { var writer: std.Io.Writer = .fixed(&buf); try legacy(&writer, .{ .key = .key_i, .mods = .{ .ctrl = true }, .utf8 = "i", }, .{}); try testing.expectEqualStrings("\x1b[105;5u", writer.buffered()); } { var writer: std.Io.Writer = .fixed(&buf); try legacy(&writer, .{ .key = .key_m, .mods = .{ .ctrl = true }, .utf8 = "m", }, .{}); try testing.expectEqualStrings("\x1b[109;5u", writer.buffered()); } { var writer: std.Io.Writer = .fixed(&buf); try legacy(&writer, .{ .key = .bracket_left, .mods = .{ .ctrl = true }, .utf8 = "[", }, .{}); try testing.expectEqualStrings("\x1b[91;5u", writer.buffered()); } { var writer: std.Io.Writer = .fixed(&buf); try legacy(&writer, .{ .key = .digit_2, .mods = .{ .ctrl = true, .shift = true }, .utf8 = "@", .unshifted_codepoint = '2', }, .{}); try testing.expectEqualStrings("\x1b[64;5u", writer.buffered()); }}
// These tests apply Kitty's behavior to CSIu where ctrl+shift+letter// is sent as the unshifted letter with the shift modifier present.test "legacy: ctrl+shift+letter ascii" { var buf: [128]u8 = undefined; { var writer: std.Io.Writer = .fixed(&buf); try legacy(&writer, .{ .key = .key_m, .mods = .{ .ctrl = true, .shift = true }, .utf8 = "M", .unshifted_codepoint = 'm', }, .{}); try testing.expectEqualStrings("\x1b[109;6u", writer.buffered()); }}
test "legacy: shift+function key should use all mods" { var buf: [128]u8 = undefined; var writer: std.Io.Writer = .fixed(&buf); try legacy(&writer, .{ .key = .arrow_up, .mods = .{ .shift = true }, .consumed_mods = .{ .shift = true }, }, .{}); try testing.expectEqualStrings("\x1b[1;2A", writer.buffered());}
test "legacy: keypad enter" { var buf: [128]u8 = undefined; var writer: std.Io.Writer = .fixed(&buf); try legacy(&writer, .{ .key = .numpad_enter, .mods = .{}, .consumed_mods = .{}, }, .{}); try testing.expectEqualStrings("\r", writer.buffered());}
test "legacy: keypad 1" { var buf: [128]u8 = undefined; var writer: std.Io.Writer = .fixed(&buf); try legacy(&writer, .{ .key = .numpad_1, .mods = .{}, .consumed_mods = .{}, .utf8 = "1", }, .{}); try testing.expectEqualStrings("1", writer.buffered());}
test "legacy: keypad 1 with application keypad" { var buf: [128]u8 = undefined; var writer: std.Io.Writer = .fixed(&buf); try legacy(&writer, .{ .key = .numpad_1, .mods = .{}, .consumed_mods = .{}, .utf8 = "1", }, .{ .keypad_key_application = true, }); try testing.expectEqualStrings("\x1bOq", writer.buffered());}
test "legacy: keypad 1 with application keypad and numlock" { var buf: [128]u8 = undefined; var writer: std.Io.Writer = .fixed(&buf); try legacy(&writer, .{ .key = .numpad_1, .mods = .{ .num_lock = true }, .consumed_mods = .{}, .utf8 = "1", }, .{ .keypad_key_application = true, }); try testing.expectEqualStrings("\x1bOq", writer.buffered());}
test "legacy: keypad 1 with application keypad and numlock ignore" { var buf: [128]u8 = undefined; var writer: std.Io.Writer = .fixed(&buf); try legacy(&writer, .{ .key = .numpad_1, .mods = .{ .num_lock = false }, .consumed_mods = .{}, .utf8 = "1", }, .{ .keypad_key_application = true, .ignore_keypad_with_numlock = true, }); try testing.expectEqualStrings("1", writer.buffered());}
test "legacy: f1" { var buf: [128]u8 = undefined;
// F1 { var writer: std.Io.Writer = .fixed(&buf); try legacy(&writer, .{ .key = .f1, .mods = .{ .ctrl = true }, .consumed_mods = .{}, }, .{}); try testing.expectEqualStrings("\x1b[1;5P", writer.buffered()); }
// F2 { var writer: std.Io.Writer = .fixed(&buf); try legacy(&writer, .{ .key = .f2, .mods = .{ .ctrl = true }, .consumed_mods = .{}, }, .{}); try testing.expectEqualStrings("\x1b[1;5Q", writer.buffered()); }
// F3 { var writer: std.Io.Writer = .fixed(&buf); try legacy(&writer, .{ .key = .f3, .mods = .{ .ctrl = true }, .consumed_mods = .{}, }, .{}); try testing.expectEqualStrings("\x1b[13;5~", writer.buffered()); }
// F4 { var writer: std.Io.Writer = .fixed(&buf); try legacy(&writer, .{ .key = .f4, .mods = .{ .ctrl = true }, .consumed_mods = .{}, }, .{}); try testing.expectEqualStrings("\x1b[1;5S", writer.buffered()); }
// F5 uses new encoding { var writer: std.Io.Writer = .fixed(&buf); try legacy(&writer, .{ .key = .f5, .mods = .{ .ctrl = true }, .consumed_mods = .{}, }, .{}); try testing.expectEqualStrings("\x1b[15;5~", writer.buffered()); }}
test "legacy: left_shift+tab" { var buf: [128]u8 = undefined; var writer: std.Io.Writer = .fixed(&buf); try legacy(&writer, .{ .key = .tab, .mods = .{ .shift = true, .sides = .{ .shift = .left }, }, }, .{}); try testing.expectEqualStrings("\x1b[Z", writer.buffered());}
test "legacy: right_shift+tab" { var buf: [128]u8 = undefined; var writer: std.Io.Writer = .fixed(&buf); try legacy(&writer, .{ .key = .tab, .mods = .{ .shift = true, .sides = .{ .shift = .right }, }, }, .{}); try testing.expectEqualStrings("\x1b[Z", writer.buffered());}
test "legacy: hu layout ctrl+ő sends proper codepoint" { var buf: [128]u8 = undefined; var writer: std.Io.Writer = .fixed(&buf); try legacy(&writer, .{ .key = .bracket_left, .mods = .{ .ctrl = true }, .utf8 = "ő", .unshifted_codepoint = 337, }, .{}); const actual = writer.buffered(); try testing.expectEqualStrings("[337;5u", actual[1..]);}
test "legacy: super-only on macOS with text" { if (comptime builtin.os.tag != .macos) return error.SkipZigTest;
var buf: [128]u8 = undefined; var writer: std.Io.Writer = .fixed(&buf); try legacy(&writer, .{ .key = .key_b, .utf8 = "b", .mods = .{ .super = true }, }, .{}); try testing.expectEqualStrings("", writer.buffered());}
test "legacy: super and other mods on macOS with text" { if (comptime builtin.os.tag != .macos) return error.SkipZigTest;
var buf: [128]u8 = undefined; var writer: std.Io.Writer = .fixed(&buf); try legacy(&writer, .{ .key = .key_b, .utf8 = "B", .mods = .{ .super = true, .shift = true }, }, .{}); try testing.expectEqualStrings("", writer.buffered());}
test "legacy: backspace with DEL utf8 (DECBKM reset)" { var buf: [128]u8 = undefined; var writer: std.Io.Writer = .fixed(&buf); try legacy(&writer, .{ .key = .backspace, .utf8 = &.{0x7F}, .unshifted_codepoint = 0x08, }, .{ .backarrow_key_mode = false }); try testing.expectEqualStrings("\x7F", writer.buffered());}
test "legacy: backspace with DEL utf8 (DECBKM set)" { var buf: [128]u8 = undefined; var writer: std.Io.Writer = .fixed(&buf); try legacy(&writer, .{ .key = .backspace, .utf8 = &.{0x7F}, .unshifted_codepoint = 0x08, }, .{ .backarrow_key_mode = true }); try testing.expectEqualStrings("\x08", writer.buffered());}
test "ctrlseq: normal ctrl c" { const seq = ctrlSeq(.unidentified, "c", 'c', .{ .ctrl = true }); try testing.expectEqual(@as(u8, 0x03), seq.?);}
test "ctrlseq: normal ctrl c, right control" { const seq = ctrlSeq(.unidentified, "c", 'c', .{ .ctrl = true, .sides = .{ .ctrl = .right } }); try testing.expectEqual(@as(u8, 0x03), seq.?);}
test "ctrlseq: alt should be allowed" { const seq = ctrlSeq(.unidentified, "c", 'c', .{ .alt = true, .ctrl = true }); try testing.expectEqual(@as(u8, 0x03), seq.?);}
test "ctrlseq: no ctrl does nothing" { try testing.expect(ctrlSeq(.unidentified, "c", 'c', .{}) == null);}
test "ctrlseq: shifted non-character" { const seq = ctrlSeq(.unidentified, "_", '-', .{ .ctrl = true, .shift = true }); try testing.expectEqual(@as(u8, 0x1F), seq.?);}
test "ctrlseq: caps ascii letter" { const seq = ctrlSeq(.unidentified, "C", 'c', .{ .ctrl = true, .caps_lock = true }); try testing.expectEqual(@as(u8, 0x03), seq.?);}
test "ctrlseq: shift does not generate ctrl seq" { try testing.expect(ctrlSeq(.unidentified, "C", 'c', .{ .shift = true }) == null); try testing.expect(ctrlSeq(.unidentified, "C", 'c', .{ .shift = true, .ctrl = true }) == null);}
test "ctrlseq: russian ctrl c" { const seq = ctrlSeq(.key_c, "с", 0x0441, .{ .ctrl = true }); try testing.expectEqual(@as(u8, 0x03), seq.?);}
test "ctrlseq: russian shifted ctrl c" { const seq = ctrlSeq(.key_c, "с", 0x0441, .{ .ctrl = true, .shift = true }); try testing.expect(seq == null);}
test "ctrlseq: russian alt ctrl c" { const seq = ctrlSeq(.key_c, "с", 0x0441, .{ .ctrl = true, .alt = true }); try testing.expectEqual(@as(u8, 0x03), seq.?);}
test "ctrlseq: right ctrl c" { const seq = ctrlSeq(.key_c, "с", 'c', .{ .ctrl = true, .sides = .{ .ctrl = .right }, }); try testing.expectEqual(@as(u8, 0x03), seq.?);}