This repository has no description
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045304630473048304930503051305230533054305530563057305830593060306130623063306430653066306730683069307030713072307330743075307630773078307930803081308230833084308530863087308830893090309130923093309430953096309730983099310031013102310331043105310631073108310931103111311231133114311531163117311831193120312131223123312431253126312731283129313031313132313331343135313631373138//! A binding maps some input trigger to an action. When the trigger//! occurs, the action is performed.const Binding = @This();
const std = @import("std");const Allocator = std.mem.Allocator;const assert = std.debug.assert;const ziglyph = @import("ziglyph");const key = @import("key.zig");const KeyEvent = key.KeyEvent;
/// The trigger that needs to be performed to execute the action.trigger: Trigger,
/// The action to take if this binding matchesaction: Action,
/// Boolean flags that can be set per binding.flags: Flags = .{},
pub const Error = error{ InvalidFormat, InvalidAction,};
/// Flags the full binding-scoped flags that can be set per binding.pub const Flags = packed struct { /// True if this binding should consume the input when the /// action is triggered. consumed: bool = true,
/// True if this binding should be forwarded to all active surfaces /// in the application. all: bool = false,
/// True if this binding is global. Global bindings should work system-wide /// and not just while Ghostty is focused. This may not work on all platforms. /// See the keybind config documentation for more information. global: bool = false,
/// True if this binding should only be triggered if the action can be /// performed. If the action can't be performed then the binding acts as /// if it doesn't exist. performable: bool = false,};
/// Full binding parser. The binding parser is implemented as an iterator/// which yields elements to support multi-key sequences without allocation.pub const Parser = struct { trigger_it: SequenceIterator, action: Action, flags: Flags = .{},
pub const Elem = union(enum) { /// A leader trigger in a sequence. leader: Trigger,
/// The final trigger and action in a sequence. binding: Binding, };
pub fn init(raw_input: []const u8) Error!Parser { const flags, const start_idx = try parseFlags(raw_input); const input = raw_input[start_idx..];
// Find the last = which splits are mapping into the trigger // and action, respectively. // We use the last = because the keybind itself could contain // raw equal signs (for the = codepoint) const eql_idx = std.mem.lastIndexOf(u8, input, "=") orelse return Error.InvalidFormat;
// Sequence iterator goes up to the equal, action is after. We can // parse the action now. return .{ .trigger_it = .{ .input = input[0..eql_idx] }, .action = try .parse(input[eql_idx + 1 ..]), .flags = flags, }; }
fn parseFlags(raw_input: []const u8) Error!struct { Flags, usize } { var flags: Flags = .{};
var start_idx: usize = 0; var input: []const u8 = raw_input; while (true) { // Find the next prefix const idx = std.mem.indexOf(u8, input, ":") orelse break; const prefix = input[0..idx];
// If the prefix is one of our flags then set it. if (std.mem.eql(u8, prefix, "all")) { if (flags.all) return Error.InvalidFormat; flags.all = true; } else if (std.mem.eql(u8, prefix, "global")) { if (flags.global) return Error.InvalidFormat; flags.global = true; } else if (std.mem.eql(u8, prefix, "unconsumed")) { if (!flags.consumed) return Error.InvalidFormat; flags.consumed = false; } else if (std.mem.eql(u8, prefix, "performable")) { if (flags.performable) return Error.InvalidFormat; flags.performable = true; } else { // If we don't recognize the prefix then we're done. We // let any unknown prefix fallthrough to trigger-specific // parsing in case there are trigger-specific prefixes // (none currently but historically there was `physical:` // at one point). Breaking here lets us always implement new // prefixes. break; }
// Move past the prefix start_idx += idx + 1; input = input[idx + 1 ..]; }
return .{ flags, start_idx }; }
pub fn next(self: *Parser) Error!?Elem { // Get our trigger. If we're out of triggers then we're done. const trigger = (try self.trigger_it.next()) orelse return null;
// If this is our last trigger then it is our final binding. if (!self.trigger_it.done()) { // Global/all bindings can't be sequences if (self.flags.global or self.flags.all) return error.InvalidFormat; return .{ .leader = trigger }; }
// Out of triggers, yield the final action. return .{ .binding = .{ .trigger = trigger, .action = self.action, .flags = self.flags, } }; }
pub fn reset(self: *Parser) void { self.trigger_it.i = 0; }};
/// An iterator that yields each trigger in a sequence of triggers. For/// example, the sequence "ctrl+a>ctrl+b" would yield "ctrl+a" and then/// "ctrl+b". The iterator approach allows us to parse a sequence of/// triggers without allocations.const SequenceIterator = struct { /// The input of triggers. This is expected to be ONLY triggers. Things /// like the "unconsumed:" prefix or action must be stripped before /// passing to this iterator. input: []const u8, i: usize = 0,
/// Returns the next trigger in the sequence if there is no parsing error. pub fn next(self: *SequenceIterator) Error!?Trigger { if (self.done()) return null; const rem = self.input[self.i..]; const idx = std.mem.indexOf(u8, rem, ">") orelse rem.len; defer self.i += idx + 1; return try .parse(rem[0..idx]); }
/// Returns true if there are no more triggers to parse. pub fn done(self: *const SequenceIterator) bool { return self.i > self.input.len; }};
/// Parse a single, non-sequenced binding. To support sequences you must/// use parse. This is a convenience function for single bindings aimed/// primarily at tests.fn parseSingle(raw_input: []const u8) (Error || error{UnexpectedSequence})!Binding { var p = try Parser.init(raw_input); const elem = (try p.next()) orelse return Error.InvalidFormat; return switch (elem) { .leader => error.UnexpectedSequence, .binding => elem.binding, };}
/// Returns true if lhs should be sorted before rhspub fn lessThan(_: void, lhs: Binding, rhs: Binding) bool { const lhs_count: usize = blk: { var count: usize = 0; if (lhs.trigger.mods.super) count += 1; if (lhs.trigger.mods.ctrl) count += 1; if (lhs.trigger.mods.shift) count += 1; if (lhs.trigger.mods.alt) count += 1; break :blk count; }; const rhs_count: usize = blk: { var count: usize = 0; if (rhs.trigger.mods.super) count += 1; if (rhs.trigger.mods.ctrl) count += 1; if (rhs.trigger.mods.shift) count += 1; if (rhs.trigger.mods.alt) count += 1; break :blk count; };
if (lhs_count != rhs_count) return lhs_count > rhs_count;
if (lhs.trigger.mods.int() != rhs.trigger.mods.int()) return lhs.trigger.mods.int() > rhs.trigger.mods.int();
const lhs_key: c_int = blk: { switch (lhs.trigger.key) { .physical => break :blk @intFromEnum(lhs.trigger.key.physical), .unicode => break :blk @intCast(lhs.trigger.key.unicode), } }; const rhs_key: c_int = blk: { switch (rhs.trigger.key) { .physical => break :blk @intFromEnum(rhs.trigger.key.physical), .unicode => break :blk @intCast(rhs.trigger.key.unicode), } };
return lhs_key < rhs_key;}
/// The set of actions that a keybinding can take.pub const Action = union(enum) { /// Ignore this key combination. /// /// Ghostty will not process this combination nor forward it to the child /// process within the terminal, but it may still be processed by the OS or /// other applications. ignore,
/// Unbind a previously bound key binding. /// /// This cannot unbind bindings that were not bound by Ghostty or the user /// (e.g. bindings set by the OS or some other application). unbind,
/// Send a CSI sequence. /// /// The value should be the CSI sequence without the CSI header (`ESC [` or /// `\x1b[`). /// /// For example, `csi:0m` can be sent to reset all styles of the current text. csi: []const u8,
/// Send an `ESC` sequence. esc: []const u8,
/// Send the specified text. /// /// Uses Zig string literal syntax. This is currently not validated. /// If the text is invalid (i.e. contains an invalid escape sequence), /// the error will currently only show up in logs. text: []const u8,
/// Send data to the pty depending on whether cursor key mode is enabled /// (`application`) or disabled (`normal`). cursor_key: CursorKey,
/// Reset the terminal. /// /// This can fix a lot of issues when a running program puts the terminal /// into a broken state, equivalent to running the `reset` command. /// /// If you do this while in a TUI program such as vim, this may break /// the program. If you do this while in a shell, you may have to press /// enter after to get a new prompt. reset,
/// Copy the selected text to the clipboard. copy_to_clipboard,
/// Paste the contents of the default clipboard. paste_from_clipboard,
/// Paste the contents of the selection clipboard. paste_from_selection,
/// If there is a URL under the cursor, copy it to the default clipboard. copy_url_to_clipboard,
/// Copy the terminal title to the clipboard. If the terminal title is not /// set or is empty this has no effect. copy_title_to_clipboard,
/// Increase the font size by the specified amount in points (pt). /// /// For example, `increase_font_size:1.5` will increase the font size /// by 1.5 points. increase_font_size: f32,
/// Decrease the font size by the specified amount in points (pt). /// /// For example, `decrease_font_size:1.5` will decrease the font size /// by 1.5 points. decrease_font_size: f32,
/// Reset the font size to the original configured size. reset_font_size,
/// Set the font size to the specified size in points (pt). /// /// For example, `set_font_size:14.5` will set the font size /// to 14.5 points. set_font_size: f32,
/// Clear the screen and all scrollback. clear_screen,
/// Select all text on the screen. select_all,
/// Scroll to the top of the screen. scroll_to_top,
/// Scroll to the bottom of the screen. scroll_to_bottom,
/// Scroll to the selected text. scroll_to_selection,
/// Scroll the screen up by one page. scroll_page_up,
/// Scroll the screen down by one page. scroll_page_down,
/// Scroll the screen by the specified fraction of a page. /// /// Positive values scroll downwards, and negative values scroll upwards. /// /// For example, `scroll_page_fractional:0.5` would scroll the screen /// downwards by half a page, while `scroll_page_fractional:-1.5` would /// scroll it upwards by one and a half pages. scroll_page_fractional: f32,
/// Scroll the screen by the specified amount of lines. /// /// Positive values scroll downwards, and negative values scroll upwards. /// /// For example, `scroll_page_lines:3` would scroll the screen downwards /// by 3 lines, while `scroll_page_lines:-10` would scroll it upwards by 10 /// lines. scroll_page_lines: i16,
/// Adjust the current selection in the given direction or position, /// relative to the cursor. /// /// WARNING: This does not create a new selection, and does nothing when /// there currently isn't one. /// /// Valid arguments are: /// /// - `left`, `right` /// /// Adjust the selection one cell to the left or right respectively. /// /// - `up`, `down` /// /// Adjust the selection one line upwards or downwards respectively. /// /// - `page_up`, `page_down` /// /// Adjust the selection one page upwards or downwards respectively. /// /// - `home`, `end` /// /// Adjust the selection to the top-left or the bottom-right corner /// of the screen respectively. /// /// - `beginning_of_line`, `end_of_line` /// /// Adjust the selection to the beginning or the end of the line /// respectively. /// adjust_selection: AdjustSelection,
/// Jump the viewport forward or back by the given number of prompts. /// /// Requires shell integration. /// /// Positive values scroll downwards, and negative values scroll upwards. jump_to_prompt: i16,
/// Write the entire scrollback into a temporary file with the specified /// action. The action determines what to do with the filepath. /// /// Valid actions are: /// /// - `copy` /// /// Copy the file path into the clipboard. /// /// - `paste` /// /// Paste the file path into the terminal. /// /// - `open` /// /// Open the file in the default OS editor for text files. /// /// The default OS editor is determined by using `open` on macOS /// and `xdg-open` on Linux. /// write_scrollback_file: WriteScreenAction,
/// Write the contents of the screen into a temporary file with the /// specified action. /// /// See `write_scrollback_file` for possible actions. write_screen_file: WriteScreenAction,
/// Write the currently selected text into a temporary file with the /// specified action. /// /// See `write_scrollback_file` for possible actions. /// /// Does nothing when no text is selected. write_selection_file: WriteScreenAction,
/// Open a new window. /// /// If the application isn't currently focused, /// this will bring it to the front. new_window,
/// Open a new tab. new_tab,
/// Go to the previous tab. previous_tab,
/// Go to the next tab. next_tab,
/// Go to the last tab. last_tab,
/// Go to the tab with the specific index, starting from 1. /// /// If the tab number is higher than the number of tabs, /// this will go to the last tab. goto_tab: usize,
/// Moves a tab by a relative offset. /// /// Positive values move the tab forwards, and negative values move it /// backwards. If the new position is out of bounds, it is wrapped around /// cyclically within the tab list. /// /// For example, `move_tab:1` moves the tab one position forwards, and if /// it was already the last tab in the list, it wraps around and becomes /// the first tab in the list. Likewise, `move_tab:-1` moves the tab one /// position backwards, and if it was the first tab, then it will become /// the last tab. move_tab: isize,
/// Toggle the tab overview. /// /// This is only supported on Linux and when the system's libadwaita /// version is 1.4 or newer. The current libadwaita version can be /// found by running `ghostty +version`. toggle_tab_overview,
/// Change the title of the current focused surface via a pop-up prompt. /// /// This requires libadwaita 1.5 or newer on Linux. The current libadwaita /// version can be found by running `ghostty +version`. prompt_surface_title,
/// Create a new split in the specified direction. /// /// Valid arguments: /// /// - `right`, `down`, `left`, `up` /// /// Creates a new split in the corresponding direction. /// /// - `auto` /// /// Creates a new split along the larger direction. /// For example, if the parent split is currently wider than it is tall, /// then a left-right split would be created, and vice versa. /// new_split: SplitDirection,
/// Focus on a split either in the specified direction (`right`, `down`, /// `left` and `up`), or in the adjacent split in the order of creation /// (`previous` and `next`). goto_split: SplitFocusDirection,
/// Zoom in or out of the current split. /// /// When a split is zoomed into, it will take up the entire space in /// the current tab, hiding other splits. The tab or tab bar would also /// reflect this by displaying an icon indicating the zoomed state. toggle_split_zoom,
/// Resize the current split in the specified direction and amount in /// pixels. The two arguments should be joined with a comma (`,`), /// like in `resize_split:up,10`. resize_split: SplitResizeParameter,
/// Equalize the size of all splits in the current window. equalize_splits,
/// Reset the window to the default size. The "default size" is the /// size that a new window would be created with. This has no effect /// if the window is fullscreen. /// /// Only implemented on macOS. reset_window_size,
/// Control the visibility of the terminal inspector. /// /// Valid arguments: `toggle`, `show`, `hide`. inspector: InspectorMode,
/// Show the GTK inspector. /// /// Has no effect on macOS. show_gtk_inspector,
/// Open the configuration file in the default OS editor. /// /// If your default OS editor isn't configured then this will fail. /// Currently, any failures to open the configuration will show up only in /// the logs. open_config,
/// Reload the configuration. /// /// The exact meaning depends on the app runtime in use, but this usually /// involves re-reading the configuration file and applying any changes /// Note that not all changes can be applied at runtime. reload_config,
/// Close the current "surface", whether that is a window, tab, split, etc. /// /// This might trigger a close confirmation popup, depending on the value /// of the `confirm-close-surface` configuration setting. close_surface,
/// Close the current tab and all splits therein. /// /// This might trigger a close confirmation popup, depending on the value /// of the `confirm-close-surface` configuration setting. close_tab,
/// Close the current window and all tabs and splits therein. /// /// This might trigger a close confirmation popup, depending on the value /// of the `confirm-close-surface` configuration setting. close_window,
/// Close all windows. /// /// WARNING: This action has been deprecated and has no effect on either /// Linux or macOS. Users are instead encouraged to use `all:close_window` /// instead. close_all_windows,
/// Maximize or unmaximize the current window. /// /// This has no effect on macOS as it does not have the concept of /// maximized windows. toggle_maximize,
/// Fullscreen or unfullscreen the current window. toggle_fullscreen,
/// Toggle window decorations (titlebar, buttons, etc.) for the current window. /// /// Only implemented on Linux. toggle_window_decorations,
/// Toggle whether the terminal window should always float on top of other /// windows even when unfocused. /// /// Terminal windows always start as normal (not float-on-top) windows. /// /// Only implemented on macOS. toggle_window_float_on_top,
/// Toggle secure input mode. /// /// This is used to prevent apps from monitoring your keyboard input /// when entering passwords or other sensitive information. /// /// This applies to the entire application, not just the focused terminal. /// You must manually untoggle it or quit Ghostty entirely to disable it. /// /// Only implemented on macOS, as this uses a built-in system API. toggle_secure_input,
/// Toggle the command palette. /// /// The command palette is a popup that lets you see what actions /// you can perform, their associated keybindings (if any), a search bar /// to filter the actions, and the ability to then execute the action. /// /// This requires libadwaita 1.5 or newer on Linux. The current libadwaita /// version can be found by running `ghostty +version`. toggle_command_palette,
/// Toggle the quick terminal. /// /// The quick terminal, also known as the "Quake-style" or drop-down /// terminal, is a terminal window that appears on demand from a keybinding, /// often sliding in from a screen edge such as the top. This is useful for /// quick access to a terminal without having to open a new window or tab. /// /// The terminal state is preserved between appearances, so showing the /// quick terminal after it was already hidden would display the same /// window instead of creating a new one. /// /// As quick terminals are often useful when other windows are currently /// focused, they are best used with *global* keybinds. For example, one /// can define the following key bind to toggle the quick terminal from /// anywhere within the system by pressing `` Cmd+` ``: /// /// ```ini /// keybind = global:cmd+backquote=toggle_quick_terminal /// ``` /// /// The quick terminal has some limitations: /// /// - Only one quick terminal instance can exist at a time. /// /// - Unlike normal terminal windows, the quick terminal will not be /// restored when the application is restarted on systems that support /// window restoration like macOS. /// /// - On Linux, the quick terminal is only supported on Wayland and not /// X11, and only on Wayland compositors that support the `wlr-layer-shell-v1` /// protocol. In practice, this means that only GNOME users would not be /// able to use this feature. /// /// - On Linux, slide-in animations are only supported on KDE, and when /// the "Sliding Popups" KWin plugin is enabled. /// /// If you do not have this plugin enabled, open System Settings > Apps /// & Windows > Window Management > Desktop Effects, and enable the /// plugin in the plugin list. Ghostty would then need to be restarted /// fully for this to take effect. /// /// - Quick terminal tabs are only supported on Linux and not on macOS. /// This is because tabs on macOS require a title bar. /// /// - On macOS, a fullscreened quick terminal will always be in non-native /// fullscreen mode. This is a requirement due to how the quick terminal /// is rendered. /// /// See the various configurations for the quick terminal in the /// configuration file to customize its behavior. toggle_quick_terminal,
/// Show or hide all windows. If all windows become shown, we also ensure /// Ghostty becomes focused. When hiding all windows, focus is yielded /// to the next application as determined by the OS. /// /// Note: When the focused surface is fullscreen, this method does nothing. /// /// Only implemented on macOS. toggle_visibility,
/// Check for updates. /// /// Only implemented on macOS. check_for_updates,
/// Undo the last undoable action for the focused surface or terminal, /// if possible. This can undo actions such as closing tabs or /// windows. /// /// Not every action in Ghostty can be undone or redone. The list /// of actions support undo/redo is currently limited to: /// /// - New window, close window /// - New tab, close tab /// - New split, close split /// /// All actions are only undoable/redoable for a limited time. /// For example, restoring a closed split can only be done for /// some number of seconds since the split was closed. The exact /// amount is configured with `TODO`. /// /// The undo/redo actions being limited ensures that there is /// bounded memory usage over time, closed surfaces don't continue running /// in the background indefinitely, and the keybinds become available /// for terminal applications to use. /// /// Only implemented on macOS. undo,
/// Redo the last undoable action for the focused surface or terminal, /// if possible. See "undo" for more details on what can and cannot /// be undone or redone. redo,
/// Quit Ghostty. quit,
/// Crash Ghostty in the desired thread for the focused surface. /// /// WARNING: This is a hard crash (panic) and data can be lost. /// /// The purpose of this action is to test crash handling. For some /// users, it may be useful to test crash reporting functionality in /// order to determine if it all works as expected. /// /// The value determines the crash location: /// /// - `main` /// /// Crash on the main (GUI) thread. /// /// - `io` /// /// Crash on the IO thread for the focused surface. /// /// - `render` /// /// Crash on the render thread for the focused surface. /// crash: CrashThread,
pub const Key = @typeInfo(Action).@"union".tag_type.?;
pub const CrashThread = enum { main, io, render, };
pub const CursorKey = struct { normal: []const u8, application: []const u8,
pub fn clone( self: CursorKey, alloc: Allocator, ) Allocator.Error!CursorKey { return .{ .normal = try alloc.dupe(u8, self.normal), .application = try alloc.dupe(u8, self.application), }; } };
pub const AdjustSelection = enum { left, right, up, down, page_up, page_down, home, end, beginning_of_line, end_of_line, };
pub const SplitDirection = enum { right, down, left, up, auto, // splits along the larger direction
pub const default: SplitDirection = .auto; };
pub const SplitFocusDirection = enum { previous, next, up, left, down, right,
pub fn parse(input: []const u8) !SplitFocusDirection { return std.meta.stringToEnum(SplitFocusDirection, input) orelse { // For backwards compatibility we map "top" and "bottom" onto the enum // values "up" and "down" if (std.mem.eql(u8, input, "top")) { return .up; } else if (std.mem.eql(u8, input, "bottom")) { return .down; } else { return Error.InvalidFormat; } }; }
test "parse" { const testing = std.testing;
try testing.expectEqual(.previous, try SplitFocusDirection.parse("previous")); try testing.expectEqual(.next, try SplitFocusDirection.parse("next"));
try testing.expectEqual(.up, try SplitFocusDirection.parse("up")); try testing.expectEqual(.left, try SplitFocusDirection.parse("left")); try testing.expectEqual(.down, try SplitFocusDirection.parse("down")); try testing.expectEqual(.right, try SplitFocusDirection.parse("right"));
try testing.expectEqual(.up, try SplitFocusDirection.parse("top")); try testing.expectEqual(.down, try SplitFocusDirection.parse("bottom"));
try testing.expectError(error.InvalidFormat, SplitFocusDirection.parse("")); try testing.expectError(error.InvalidFormat, SplitFocusDirection.parse("green")); } };
pub const SplitResizeDirection = enum { up, down, left, right, };
pub const SplitResizeParameter = struct { SplitResizeDirection, u16, };
pub const WriteScreenAction = enum { copy, paste, open, };
// Extern because it is used in the embedded runtime ABI. pub const InspectorMode = enum { toggle, show, hide, };
fn parseEnum(comptime T: type, value: []const u8) !T { return std.meta.stringToEnum(T, value) orelse return Error.InvalidFormat; }
fn parseInt(comptime T: type, value: []const u8) !T { return std.fmt.parseInt(T, value, 10) catch return Error.InvalidFormat; }
fn parseFloat(comptime T: type, value: []const u8) !T { return std.fmt.parseFloat(T, value) catch return Error.InvalidFormat; }
fn parseParameter( comptime field: std.builtin.Type.UnionField, param: []const u8, ) !field.type { const field_info = @typeInfo(field.type);
// Fields can provide a custom "parse" function if (field_info == .@"struct" or field_info == .@"union" or field_info == .@"enum") { if (@hasDecl(field.type, "parse") and @typeInfo(@TypeOf(field.type.parse)) == .@"fn") { return field.type.parse(param); } }
return switch (field_info) { .@"enum" => try parseEnum(field.type, param), .int => try parseInt(field.type, param), .float => try parseFloat(field.type, param), .@"struct" => |info| blk: { // Only tuples are supported to avoid ambiguity with field // ordering comptime assert(info.is_tuple);
var it = std.mem.splitAny(u8, param, ","); var value: field.type = undefined; inline for (info.fields) |field_| { const next = it.next() orelse return Error.InvalidFormat; @field(value, field_.name) = switch (@typeInfo(field_.type)) { .@"enum" => try parseEnum(field_.type, next), .int => try parseInt(field_.type, next), .float => try parseFloat(field_.type, next), else => unreachable, }; }
// If we have extra parameters it is an error if (it.next() != null) return Error.InvalidFormat;
break :blk value; },
else => unreachable, }; }
/// Parse an action in the format of "key=value" where key is the /// action name and value is the action parameter. The parameter /// is optional depending on the action. pub fn parse(input: []const u8) !Action { // Split our action by colon. A colon may not exist for some // actions so it is optional. The part preceding the colon is the // action name. const colonIdx = std.mem.indexOf(u8, input, ":"); const action = input[0..(colonIdx orelse input.len)];
// An action name is always required if (action.len == 0) return Error.InvalidFormat;
const actionInfo = @typeInfo(Action).@"union"; inline for (actionInfo.fields) |field| { if (std.mem.eql(u8, action, field.name)) { // If the field type is void we expect no value switch (field.type) { void => { if (colonIdx != null) return Error.InvalidFormat; return @unionInit(Action, field.name, {}); },
[]const u8 => { const idx = colonIdx orelse return Error.InvalidFormat; const param = input[idx + 1 ..]; return @unionInit(Action, field.name, param); },
// Cursor keys can't be set currently Action.CursorKey => return Error.InvalidAction,
else => { // Get the parameter after the colon. The parameter // can be optional for action types that can have a // "default" decl. const idx = colonIdx orelse { switch (@typeInfo(field.type)) { .@"struct", .@"union", .@"enum", => if (@hasDecl(field.type, "default")) { return @unionInit( Action, field.name, @field(field.type, "default"), ); },
else => {}, }
return Error.InvalidFormat; };
const param = input[idx + 1 ..]; return @unionInit( Action, field.name, try parseParameter(field, param), ); }, } } }
return Error.InvalidAction; }
/// The scope of an action. The scope is the context in which an action /// must be executed. pub const Scope = enum { app, surface, };
/// Returns the scope of an action. pub fn scope(self: Action) Scope { return switch (self) { // Doesn't really matter, so we'll see app. .ignore, .unbind, => .app,
// Obviously app actions. .open_config, .reload_config, .close_all_windows, .quit, .toggle_quick_terminal, .toggle_visibility, .check_for_updates, .show_gtk_inspector, => .app,
// These are app but can be special-cased in a surface context. .new_window, .undo, .redo, => .app,
// Obviously surface actions. .csi, .esc, .text, .cursor_key, .reset, .copy_to_clipboard, .copy_url_to_clipboard, .copy_title_to_clipboard, .paste_from_clipboard, .paste_from_selection, .increase_font_size, .decrease_font_size, .reset_font_size, .set_font_size, .prompt_surface_title, .clear_screen, .select_all, .scroll_to_top, .scroll_to_bottom, .scroll_to_selection, .scroll_page_up, .scroll_page_down, .scroll_page_fractional, .scroll_page_lines, .adjust_selection, .jump_to_prompt, .write_scrollback_file, .write_screen_file, .write_selection_file, .close_surface, .close_tab, .close_window, .toggle_maximize, .toggle_fullscreen, .toggle_window_decorations, .toggle_window_float_on_top, .toggle_secure_input, .toggle_command_palette, .reset_window_size, .crash, => .surface,
// These are less obvious surface actions. They're surface // actions because they are relevant to the surface they // come from. For example `new_window` needs to be sourced to // a surface so inheritance can be done correctly. .new_tab, .previous_tab, .next_tab, .last_tab, .goto_tab, .move_tab, .toggle_tab_overview, .new_split, .goto_split, .toggle_split_zoom, .resize_split, .equalize_splits, .inspector, => .surface, }; }
/// Returns a union type that only contains actions that are scoped to /// the given scope. pub fn Scoped(comptime s: Scope) type { @setEvalBranchQuota(100_000);
const all_fields = @typeInfo(Action).@"union".fields;
// Find all fields that are app-scoped var i: usize = 0; var union_fields: [all_fields.len]std.builtin.Type.UnionField = undefined; var enum_fields: [all_fields.len]std.builtin.Type.EnumField = undefined; for (all_fields) |field| { const action = @unionInit(Action, field.name, undefined); if (action.scope() == s) { union_fields[i] = field; enum_fields[i] = .{ .name = field.name, .value = i }; i += 1; } }
// Build our union return @Type(.{ .@"union" = .{ .layout = .auto, .tag_type = @Type(.{ .@"enum" = .{ .tag_type = std.math.IntFittingRange(0, i), .fields = enum_fields[0..i], .decls = &.{}, .is_exhaustive = true, } }), .fields = union_fields[0..i], .decls = &.{}, } }); }
/// Returns the scoped version of this action. If the action is not /// scoped to the given scope then this returns null. /// /// The benefit of this function is that it allows us to use Zig's /// exhaustive switch safety to ensure we always properly handle certain /// scoped actions. pub fn scoped(self: Action, comptime s: Scope) ?Scoped(s) { switch (self) { inline else => |v, tag| { // Use comptime to prune out non-app actions if (comptime @unionInit( Action, @tagName(tag), undefined, ).scope() != s) return null;
// Initialize our app action return @unionInit( Scoped(s), @tagName(tag), v, ); }, } }
/// Implements the formatter for the fmt package. This encodes the /// action back into the format used by parse. pub fn format( self: Action, comptime layout: []const u8, opts: std.fmt.FormatOptions, writer: anytype, ) !void { _ = layout; _ = opts;
switch (self) { inline else => |value| { // All actions start with the tag. try writer.print("{s}", .{@tagName(self)});
// Only write the value depending on the type if it's not void if (@TypeOf(value) != void) { try writer.writeAll(":"); try formatValue(writer, value); } }, } }
fn formatValue( writer: anytype, value: anytype, ) !void { const Value = @TypeOf(value); const value_info = @typeInfo(Value); switch (Value) { void => {}, []const u8 => try writer.print("{s}", .{value}), else => switch (value_info) { .@"enum" => try writer.print("{s}", .{@tagName(value)}), .float => try writer.print("{d}", .{value}), .int => try writer.print("{d}", .{value}), .@"struct" => |info| if (!info.is_tuple) { try writer.print("{} (not configurable)", .{value}); } else { inline for (info.fields, 0..) |field, i| { try formatValue(writer, @field(value, field.name)); if (i + 1 < info.fields.len) try writer.writeAll(","); } }, else => @compileError("unhandled type: " ++ @typeName(Value)), }, } }
/// Clone this action with the given allocator. The allocator /// should be an arena-style allocator since fine-grained /// deallocation is not possible. pub fn clone(self: Action, alloc: Allocator) Allocator.Error!Action { return switch (self) { inline else => |value, tag| @unionInit( Action, @tagName(tag), try cloneValue(alloc, value), ), }; }
fn cloneValue( alloc: Allocator, value: anytype, ) Allocator.Error!@TypeOf(value) { return switch (@typeInfo(@TypeOf(value))) { .void, .int, .float, .@"enum", => value,
.pointer => |info| slice: { comptime assert(info.size == .slice); break :slice try alloc.dupe( info.child, value, ); },
.@"struct" => |info| if (info.is_tuple) value else try value.clone(alloc),
else => { @compileLog(@TypeOf(value)); @compileError("unexpected type"); }, }; }
/// Returns a hash code that can be used to uniquely identify this /// action. pub fn hash(self: Action) u64 { var hasher = std.hash.Wyhash.init(0); self.hashIncremental(&hasher); return hasher.final(); }
/// Hash the action into the given hasher. fn hashIncremental(self: Action, hasher: anytype) void { // Always has the active tag. const Tag = @typeInfo(Action).@"union".tag_type.?; std.hash.autoHash(hasher, @as(Tag, self));
// Hash the value of the field. switch (self) { inline else => |field| { const FieldType = @TypeOf(field); switch (FieldType) { // Do nothing for void void => {},
// Floats are hashed by their bits. This is totally not // portable and there are edge cases such as NaNs and // signed zeros but these are not cases we expect for // our bindings. f32 => std.hash.autoHash( hasher, @as(u32, @bitCast(field)), ), f64 => std.hash.autoHash( hasher, @as(u64, @bitCast(field)), ),
// Everything else automatically handle. else => std.hash.autoHashStrat( hasher, field, .DeepRecursive, ), } }, } }};
/// Trigger is the associated key state that can trigger an action./// This is an extern struct because this is also used in the C API.////// This must be kept in sync with include/ghostty.h ghostty_input_trigger_spub const Trigger = struct { /// The key that has to be pressed for a binding to take action. key: Trigger.Key = .{ .physical = .unidentified },
/// The key modifiers that must be active for this to match. mods: key.Mods = .{},
pub const Key = union(C.Tag) { /// key is the "physical" version. This is the same as mapped for /// standard US keyboard layouts. For non-US keyboard layouts, this /// is used to bind to a physical key location rather than a translated /// key. physical: key.Key,
/// This is used for binding to keys that produce a certain unicode /// codepoint. This is useful for binding to keys that don't have a /// registered keycode with Ghostty. unicode: u21, };
/// The extern struct used for triggers in the C API. pub const C = extern struct { tag: Tag = .physical, key: C.Key = .{ .physical = .unidentified }, mods: key.Mods = .{},
pub const Tag = enum(c_int) { physical, unicode, };
pub const Key = extern union { physical: key.Key, unicode: u32, }; };
/// Parse a single trigger. The input is expected to be ONLY the trigger /// (i.e. in the sequence `a=ignore` input is only `a`). The trigger may /// not be part of a sequence (i.e. `a>b`). This parses exactly a single /// trigger. pub fn parse(input: []const u8) !Trigger { if (input.len == 0) return Error.InvalidFormat; var result: Trigger = .{}; var rem: []const u8 = input; loop: while (rem.len > 0) { const idx = std.mem.indexOfScalar(u8, rem, '+') orelse rem.len; const part = rem[0..idx]; rem = if (idx >= rem.len) "" else rem[idx + 1 ..];
// Check if its a modifier const modsInfo = @typeInfo(key.Mods).@"struct"; inline for (modsInfo.fields) |field| { if (field.type == bool) { if (std.mem.eql(u8, part, field.name)) { // Repeat not allowed if (@field(result.mods, field.name)) return Error.InvalidFormat; @field(result.mods, field.name) = true; continue :loop; } } }
// Alias modifiers const alias_mods = .{ .{ "cmd", "super" }, .{ "command", "super" }, .{ "opt", "alt" }, .{ "option", "alt" }, .{ "control", "ctrl" }, }; inline for (alias_mods) |pair| { if (std.mem.eql(u8, part, pair[0])) { // Repeat not allowed if (@field(result.mods, pair[1])) return Error.InvalidFormat; @field(result.mods, pair[1]) = true; continue :loop; } }
// Anything after this point is a key and we only support // single keys. if (!result.isKeyUnset()) return Error.InvalidFormat;
// If the part is empty it means that it is actually // a literal `+`, which we treat as a Unicode character. if (part.len == 0) { result.key = .{ .unicode = '+' }; continue :loop; }
// Check if its a key const keysInfo = @typeInfo(key.Key).@"enum"; inline for (keysInfo.fields) |field| { if (!std.mem.eql(u8, field.name, "unidentified")) { if (std.mem.eql(u8, part, field.name)) { const keyval = @field(key.Key, field.name); result.key = .{ .physical = keyval }; continue :loop; } } }
// If we're still unset and we have exactly one unicode // character then we can use that as a key. if (result.isKeyUnset()) unicode: { // Invalid UTF8 drops to invalid format const view = std.unicode.Utf8View.init(part) catch break :unicode; var it = view.iterator();
// No codepoints or multiple codepoints drops to invalid format const cp = it.nextCodepoint() orelse break :unicode; if (it.nextCodepoint() != null) break :unicode;
result.key = .{ .unicode = cp }; continue :loop; }
// Look for a matching w3c name next. if (key.Key.fromW3C(part)) |w3c_key| { result.key = .{ .physical = w3c_key }; continue :loop; }
// If we're still unset then we look for backwards compatible // keys with Ghostty 1.1.x. We do this last so its least likely // to impact performance for modern users. if (backwards_compatible_keys.get(part)) |old_key| { result.key = old_key; continue :loop; }
// We didn't recognize this value return Error.InvalidFormat; }
return result; }
/// The values that are backwards compatible with Ghostty 1.1.x. /// Ghostty 1.2+ doesn't support these anymore since we moved to /// W3C key codes. const backwards_compatible_keys = std.StaticStringMap(Key).initComptime(.{ .{ "zero", Key{ .unicode = '0' } }, .{ "one", Key{ .unicode = '1' } }, .{ "two", Key{ .unicode = '2' } }, .{ "three", Key{ .unicode = '3' } }, .{ "four", Key{ .unicode = '4' } }, .{ "five", Key{ .unicode = '5' } }, .{ "six", Key{ .unicode = '6' } }, .{ "seven", Key{ .unicode = '7' } }, .{ "eight", Key{ .unicode = '8' } }, .{ "nine", Key{ .unicode = '9' } }, .{ "plus", Key{ .unicode = '+' } }, .{ "apostrophe", Key{ .unicode = '\'' } }, .{ "grave_accent", Key{ .physical = .backquote } }, .{ "left_bracket", Key{ .physical = .bracket_left } }, .{ "right_bracket", Key{ .physical = .bracket_right } }, .{ "up", Key{ .physical = .arrow_up } }, .{ "down", Key{ .physical = .arrow_down } }, .{ "left", Key{ .physical = .arrow_left } }, .{ "right", Key{ .physical = .arrow_right } }, .{ "kp_0", Key{ .physical = .numpad_0 } }, .{ "kp_1", Key{ .physical = .numpad_1 } }, .{ "kp_2", Key{ .physical = .numpad_2 } }, .{ "kp_3", Key{ .physical = .numpad_3 } }, .{ "kp_4", Key{ .physical = .numpad_4 } }, .{ "kp_5", Key{ .physical = .numpad_5 } }, .{ "kp_6", Key{ .physical = .numpad_6 } }, .{ "kp_7", Key{ .physical = .numpad_7 } }, .{ "kp_8", Key{ .physical = .numpad_8 } }, .{ "kp_9", Key{ .physical = .numpad_9 } }, .{ "kp_add", Key{ .physical = .numpad_add } }, .{ "kp_subtract", Key{ .physical = .numpad_subtract } }, .{ "kp_multiply", Key{ .physical = .numpad_multiply } }, .{ "kp_divide", Key{ .physical = .numpad_divide } }, .{ "kp_decimal", Key{ .physical = .numpad_decimal } }, .{ "kp_enter", Key{ .physical = .numpad_enter } }, .{ "kp_equal", Key{ .physical = .numpad_equal } }, .{ "kp_separator", Key{ .physical = .numpad_separator } }, .{ "kp_left", Key{ .physical = .numpad_left } }, .{ "kp_right", Key{ .physical = .numpad_right } }, .{ "kp_up", Key{ .physical = .numpad_up } }, .{ "kp_down", Key{ .physical = .numpad_down } }, .{ "kp_page_up", Key{ .physical = .numpad_page_up } }, .{ "kp_page_down", Key{ .physical = .numpad_page_down } }, .{ "kp_home", Key{ .physical = .numpad_home } }, .{ "kp_end", Key{ .physical = .numpad_end } }, .{ "kp_insert", Key{ .physical = .numpad_insert } }, .{ "kp_delete", Key{ .physical = .numpad_delete } }, .{ "kp_begin", Key{ .physical = .numpad_begin } }, .{ "left_shift", Key{ .physical = .shift_left } }, .{ "right_shift", Key{ .physical = .shift_right } }, .{ "left_control", Key{ .physical = .control_left } }, .{ "right_control", Key{ .physical = .control_right } }, .{ "left_alt", Key{ .physical = .alt_left } }, .{ "right_alt", Key{ .physical = .alt_right } }, .{ "left_super", Key{ .physical = .meta_left } }, .{ "right_super", Key{ .physical = .meta_right } },
// Physical variants. This is a blunt approach to this but its // glue for backwards compatibility so I'm not too worried about // making this super nice. .{ "physical:zero", Key{ .physical = .digit_0 } }, .{ "physical:one", Key{ .physical = .digit_1 } }, .{ "physical:two", Key{ .physical = .digit_2 } }, .{ "physical:three", Key{ .physical = .digit_3 } }, .{ "physical:four", Key{ .physical = .digit_4 } }, .{ "physical:five", Key{ .physical = .digit_5 } }, .{ "physical:six", Key{ .physical = .digit_6 } }, .{ "physical:seven", Key{ .physical = .digit_7 } }, .{ "physical:eight", Key{ .physical = .digit_8 } }, .{ "physical:nine", Key{ .physical = .digit_9 } }, .{ "physical:apostrophe", Key{ .physical = .quote } }, .{ "physical:grave_accent", Key{ .physical = .backquote } }, .{ "physical:left_bracket", Key{ .physical = .bracket_left } }, .{ "physical:right_bracket", Key{ .physical = .bracket_right } }, .{ "physical:up", Key{ .physical = .arrow_up } }, .{ "physical:down", Key{ .physical = .arrow_down } }, .{ "physical:left", Key{ .physical = .arrow_left } }, .{ "physical:right", Key{ .physical = .arrow_right } }, .{ "physical:kp_0", Key{ .physical = .numpad_0 } }, .{ "physical:kp_1", Key{ .physical = .numpad_1 } }, .{ "physical:kp_2", Key{ .physical = .numpad_2 } }, .{ "physical:kp_3", Key{ .physical = .numpad_3 } }, .{ "physical:kp_4", Key{ .physical = .numpad_4 } }, .{ "physical:kp_5", Key{ .physical = .numpad_5 } }, .{ "physical:kp_6", Key{ .physical = .numpad_6 } }, .{ "physical:kp_7", Key{ .physical = .numpad_7 } }, .{ "physical:kp_8", Key{ .physical = .numpad_8 } }, .{ "physical:kp_9", Key{ .physical = .numpad_9 } }, .{ "physical:kp_add", Key{ .physical = .numpad_add } }, .{ "physical:kp_subtract", Key{ .physical = .numpad_subtract } }, .{ "physical:kp_multiply", Key{ .physical = .numpad_multiply } }, .{ "physical:kp_divide", Key{ .physical = .numpad_divide } }, .{ "physical:kp_decimal", Key{ .physical = .numpad_decimal } }, .{ "physical:kp_enter", Key{ .physical = .numpad_enter } }, .{ "physical:kp_equal", Key{ .physical = .numpad_equal } }, .{ "physical:kp_separator", Key{ .physical = .numpad_separator } }, .{ "physical:kp_left", Key{ .physical = .numpad_left } }, .{ "physical:kp_right", Key{ .physical = .numpad_right } }, .{ "physical:kp_up", Key{ .physical = .numpad_up } }, .{ "physical:kp_down", Key{ .physical = .numpad_down } }, .{ "physical:kp_page_up", Key{ .physical = .numpad_page_up } }, .{ "physical:kp_page_down", Key{ .physical = .numpad_page_down } }, .{ "physical:kp_home", Key{ .physical = .numpad_home } }, .{ "physical:kp_end", Key{ .physical = .numpad_end } }, .{ "physical:kp_insert", Key{ .physical = .numpad_insert } }, .{ "physical:kp_delete", Key{ .physical = .numpad_delete } }, .{ "physical:kp_begin", Key{ .physical = .numpad_begin } }, .{ "physical:left_shift", Key{ .physical = .shift_left } }, .{ "physical:right_shift", Key{ .physical = .shift_right } }, .{ "physical:left_control", Key{ .physical = .control_left } }, .{ "physical:right_control", Key{ .physical = .control_right } }, .{ "physical:left_alt", Key{ .physical = .alt_left } }, .{ "physical:right_alt", Key{ .physical = .alt_right } }, .{ "physical:left_super", Key{ .physical = .meta_left } }, .{ "physical:right_super", Key{ .physical = .meta_right } }, });
/// Returns true if this trigger has no key set. pub fn isKeyUnset(self: Trigger) bool { return switch (self.key) { .physical => |v| v == .unidentified, else => false, }; }
/// Returns a hash code that can be used to uniquely identify this trigger. pub fn hash(self: Trigger) u64 { var hasher = std.hash.Wyhash.init(0); self.hashIncremental(&hasher); return hasher.final(); }
/// Hash the trigger into the given hasher. fn hashIncremental(self: Trigger, hasher: anytype) void { std.hash.autoHash(hasher, std.meta.activeTag(self.key)); switch (self.key) { .physical => |v| std.hash.autoHash(hasher, v), .unicode => |cp| std.hash.autoHash( hasher, foldedCodepoint(cp), ), } std.hash.autoHash(hasher, self.mods.binding()); }
/// The codepoint we use for comparisons. Case folding can result /// in more codepoints so we need to use a 3 element array. fn foldedCodepoint(cp: u21) [3]u21 { // ASCII fast path if (ziglyph.letter.isAsciiLetter(cp)) { return .{ ziglyph.letter.toLower(cp), 0, 0 }; }
// Unicode slow path. Case folding can resultin more codepoints. // If more codepoints are produced then we return the codepoint // as-is which isn't correct but until we have a failing test // then I don't want to handle this. return ziglyph.letter.toCaseFold(cp); }
/// Convert the trigger to a C API compatible trigger. pub fn cval(self: Trigger) C { return .{ .tag = self.key, .key = switch (self.key) { .physical => |v| .{ .physical = v }, .unicode => |v| .{ .unicode = @intCast(v) }, }, .mods = self.mods, }; }
/// Format implementation for fmt package. pub fn format( self: Trigger, comptime layout: []const u8, opts: std.fmt.FormatOptions, writer: anytype, ) !void { _ = layout; _ = opts;
// Modifiers first if (self.mods.super) try writer.writeAll("super+"); if (self.mods.ctrl) try writer.writeAll("ctrl+"); if (self.mods.alt) try writer.writeAll("alt+"); if (self.mods.shift) try writer.writeAll("shift+");
// Key switch (self.key) { .physical => |k| try writer.print("{s}", .{@tagName(k)}), .unicode => |c| try writer.print("{u}", .{c}), } }};
/// A structure that contains a set of bindings and focuses on fast lookup./// The use case is that this will be called on EVERY key input to look/// for an associated action so it must be fast.pub const Set = struct { const HashMap = std.HashMapUnmanaged( Trigger, Value, Context(Trigger), std.hash_map.default_max_load_percentage, );
const ReverseMap = std.HashMapUnmanaged( Action, Trigger, Context(Action), std.hash_map.default_max_load_percentage, );
/// The set of bindings. bindings: HashMap = .{},
/// The reverse mapping of action to binding. Note that multiple /// bindings can map to the same action and this map will only have /// the most recently added binding for an action. /// /// Sequenced triggers are never present in the reverse map at this time. /// This is a conscious decision since the primary use case of the reverse /// map is to support GUI toolkit keyboard accelerators and no mainstream /// GUI toolkit supports sequences. /// /// Performable triggers are also not present in the reverse map. This /// is so that GUI toolkits don't register performable triggers as /// menu shortcuts (the primary use case of the reverse map). GUI toolkits /// such as GTK handle menu shortcuts too early in the event lifecycle /// for performable to work so this is a conscious decision to ease the /// integration with GUI toolkits. reverse: ReverseMap = .{},
/// The entry type for the forward mapping of trigger to action. pub const Value = union(enum) { /// This key is a leader key in a sequence. You must follow the given /// set to find the next key in the sequence. leader: *Set,
/// This trigger completes a sequence and the value is the action /// to take along with the flags that may define binding behavior. leaf: Leaf,
/// Implements the formatter for the fmt package. This encodes the /// action back into the format used by parse. pub fn format( self: Value, comptime layout: []const u8, opts: std.fmt.FormatOptions, writer: anytype, ) !void { _ = layout; _ = opts;
switch (self) { .leader => |set| { // the leader key was already printed. var iter = set.bindings.iterator(); while (iter.next()) |binding| { try writer.print( ">{s}{s}", .{ binding.key_ptr.*, binding.value_ptr.* }, ); } },
.leaf => |leaf| { // action implements the format try writer.print("={s}", .{leaf.action}); }, } }
/// Writes the configuration entries for the binding /// that this value is part of. /// /// The value may be part of multiple configuration entries /// if they're all part of the same prefix sequence (e.g. 'a>b', 'a>c'). /// These will result in multiple separate entries in the configuration. /// /// `buffer_stream` is a FixedBufferStream used for temporary storage /// that is shared between calls to nested levels of the set. /// For example, 'a>b>c=x' and 'a>b>d=y' will re-use the 'a>b' written /// to the buffer before flushing it to the formatter with 'c=x' and 'd=y'. pub fn formatEntries(self: Value, buffer_stream: anytype, formatter: anytype) !void { switch (self) { .leader => |set| { // We'll rewind to this position after each sub-entry, // sharing the prefix between siblings. const pos = try buffer_stream.getPos();
var iter = set.bindings.iterator(); while (iter.next()) |binding| { buffer_stream.seekTo(pos) catch unreachable; // can't fail std.fmt.format(buffer_stream.writer(), ">{s}", .{binding.key_ptr.*}) catch return error.OutOfMemory; try binding.value_ptr.*.formatEntries(buffer_stream, formatter); } },
.leaf => |leaf| { // When we get to the leaf, the buffer_stream contains // the full sequence of keys needed to reach this action. std.fmt.format(buffer_stream.writer(), "={s}", .{leaf.action}) catch return error.OutOfMemory; try formatter.formatEntry([]const u8, buffer_stream.getWritten()); }, } } };
/// Leaf node of a set is an action to trigger. This is a "leaf" compared /// to the inner nodes which are "leaders" for sequences. pub const Leaf = struct { action: Action, flags: Flags,
pub fn clone( self: Leaf, alloc: Allocator, ) Allocator.Error!Leaf { return .{ .action = try self.action.clone(alloc), .flags = self.flags, }; }
pub fn hash(self: Leaf) u64 { var hasher = std.hash.Wyhash.init(0); self.action.hash(&hasher); std.hash.autoHash(&hasher, self.flags); return hasher.final(); } };
/// A full key-value entry for the set. pub const Entry = HashMap.Entry;
pub fn deinit(self: *Set, alloc: Allocator) void { // Clear any leaders if we have them var it = self.bindings.iterator(); while (it.next()) |entry| switch (entry.value_ptr.*) { .leader => |s| { s.deinit(alloc); alloc.destroy(s); }, .leaf => {}, };
self.bindings.deinit(alloc); self.reverse.deinit(alloc); self.* = undefined; }
/// Parse a user input binding and add it to the set. This will handle /// the "unbind" case, ensure consumed/unconsumed fields are set correctly, /// handle sequences, etc. /// /// If this returns an OutOfMemory error then the set is in a broken /// state and should not be used again. Any Error returned is validated /// before any set modifications are made. pub fn parseAndPut( self: *Set, alloc: Allocator, input: []const u8, ) (Allocator.Error || Error)!void { // To make cleanup easier, we ensure that the full sequence is // valid before making any set modifications. This is more expensive // computationally but it makes cleanup way, way easier. var it = try Parser.init(input); while (try it.next()) |_| {} it.reset();
// We use recursion so that we can utilize the stack as our state // for cleanup. self.parseAndPutRecurse(alloc, &it) catch |err| switch (err) { // If this gets sent up to the root then we've unbound // all the way up and this put was a success. error.SequenceUnbind => {},
// Unrecoverable error.OutOfMemory => return error.OutOfMemory, }; }
const ParseAndPutRecurseError = Allocator.Error || error{ SequenceUnbind, };
fn parseAndPutRecurse( set: *Set, alloc: Allocator, it: *Parser, ) ParseAndPutRecurseError!void { const elem = (it.next() catch unreachable) orelse return; switch (elem) { .leader => |t| { // If we have a leader, we need to upsert a set for it. // Since we remove the value, we need to copy it. const old: ?Value = if (set.get(t)) |entry| entry.value_ptr.* else null; if (old) |entry| switch (entry) { // We have an existing leader for this key already // so recurse into this set. .leader => |s| return parseAndPutRecurse( s, alloc, it, ) catch |err| switch (err) { // Our child put unbound. If our set is empty we // need to dealloc and continue up. If our set is // not empty then we're done. error.SequenceUnbind => if (s.bindings.count() == 0) { set.remove(alloc, t); return error.SequenceUnbind; },
error.OutOfMemory => return error.OutOfMemory, },
.leaf => { // Remove the existing action. Fallthrough as if // we don't have a leader. set.remove(alloc, t); }, };
// Create our new set for this leader const next = try alloc.create(Set); errdefer alloc.destroy(next); next.* = .{}; errdefer next.deinit(alloc);
// Insert the leader entry try set.bindings.put(alloc, t, .{ .leader = next });
// Recurse parseAndPutRecurse(next, alloc, it) catch |err| switch (err) { // If our action was to unbind, we restore the old // action if we have it. error.SequenceUnbind => { set.remove(alloc, t); if (old) |entry| switch (entry) { .leader => unreachable, // Handled above .leaf => |leaf| set.putFlags( alloc, t, leaf.action, leaf.flags, ) catch {}, }; },
error.OutOfMemory => return error.OutOfMemory, }; },
.binding => |b| switch (b.action) { .unbind => { set.remove(alloc, b.trigger); return error.SequenceUnbind; },
else => try set.putFlags( alloc, b.trigger, b.action, b.flags, ), }, } }
/// Add a binding to the set. If the binding already exists then /// this will overwrite it. pub fn put( self: *Set, alloc: Allocator, t: Trigger, action: Action, ) Allocator.Error!void { try self.putFlags(alloc, t, action, .{}); }
/// Add a binding to the set with explicit flags. pub fn putFlags( self: *Set, alloc: Allocator, t: Trigger, action: Action, flags: Flags, ) Allocator.Error!void { // unbind should never go into the set, it should be handled prior assert(action != .unbind);
// This is true if we're going to track this entry as // a reverse mapping. There are certain scenarios we don't. // See the reverse map docs for more information. const track_reverse: bool = !flags.performable;
const gop = try self.bindings.getOrPut(alloc, t);
if (gop.found_existing) switch (gop.value_ptr.*) { // If we have a leader we need to clean up the memory .leader => |s| { s.deinit(alloc); alloc.destroy(s); },
// If we have an existing binding for this trigger, we have to // update the reverse mapping to remove the old action. .leaf => if (track_reverse) { const t_hash = t.hash(); var it = self.reverse.iterator(); while (it.next()) |reverse_entry| it: { if (t_hash == reverse_entry.value_ptr.hash()) { self.reverse.removeByPtr(reverse_entry.key_ptr); break :it; } } }, };
gop.value_ptr.* = .{ .leaf = .{ .action = action, .flags = flags, } }; errdefer _ = self.bindings.remove(t);
if (track_reverse) try self.reverse.put(alloc, action, t); errdefer if (track_reverse) self.reverse.remove(action); }
/// Get a binding for a given trigger. pub fn get(self: Set, t: Trigger) ?Entry { return self.bindings.getEntry(t); }
/// Get a trigger for the given action. An action can have multiple /// triggers so this will return the first one found. pub fn getTrigger(self: Set, a: Action) ?Trigger { return self.reverse.get(a); }
/// Get an entry for the given key event. This will attempt to find /// a binding using multiple parts of the event in the following order: /// /// 1. Translated key (event.key) /// 2. Physical key (event.physical_key) /// 3. Unshifted Unicode codepoint (event.unshifted_codepoint) /// pub fn getEvent(self: *const Set, event: KeyEvent) ?Entry { var trigger: Trigger = .{ .mods = event.mods.binding(), .key = .{ .physical = event.key }, }; if (self.get(trigger)) |v| return v;
// If our UTF-8 text is exactly one codepoint, we try to match that. if (event.utf8.len > 0) unicode: { const view = std.unicode.Utf8View.init(event.utf8) catch break :unicode; var it = view.iterator();
// No codepoints or multiple codepoints drops to invalid format const cp = it.nextCodepoint() orelse break :unicode; if (it.nextCodepoint() != null) break :unicode;
trigger.key = .{ .unicode = cp }; if (self.get(trigger)) |v| return v; }
// Finally fallback to the full unshifted codepoint if we have one. // Question: should we be doing this if we have UTF-8 text? I // suspect "no" but we don't currently have any failing scenarios // to verify this. if (event.unshifted_codepoint > 0) { trigger.key = .{ .unicode = event.unshifted_codepoint }; if (self.get(trigger)) |v| return v; }
return null; }
/// Remove a binding for a given trigger. pub fn remove(self: *Set, alloc: Allocator, t: Trigger) void { self.removeExact(alloc, t); }
fn removeExact(self: *Set, alloc: Allocator, t: Trigger) void { const entry = self.bindings.get(t) orelse return; _ = self.bindings.remove(t);
switch (entry) { // For a leader removal, we need to deallocate our child set. // Leaders are never part of reverse maps so no other accounting // needs to be done. .leader => |s| { s.deinit(alloc); alloc.destroy(s); },
// For an action we need to fix up the reverse mapping. // Note: we'd LIKE to replace this with the most recent binding but // our hash map obviously has no concept of ordering so we have to // choose whatever. Maybe a switch to an array hash map here. .leaf => |leaf| { const action_hash = leaf.action.hash();
var it = self.bindings.iterator(); while (it.next()) |it_entry| { switch (it_entry.value_ptr.*) { .leader => {}, .leaf => |leaf_search| { if (leaf_search.action.hash() == action_hash) { self.reverse.putAssumeCapacity(leaf.action, it_entry.key_ptr.*); break; } }, } } else { // No other trigger points to this action so we remove // the reverse mapping completely. _ = self.reverse.remove(leaf.action); } }, } }
/// Deep clone the set. pub fn clone(self: *const Set, alloc: Allocator) !Set { var result: Set = .{ .bindings = try self.bindings.clone(alloc), .reverse = try self.reverse.clone(alloc), };
// If we have any leaders we need to clone them. { var it = result.bindings.iterator(); while (it.next()) |entry| switch (entry.value_ptr.*) { // Leaves could have data to clone (i.e. text actions // contain allocated strings). .leaf => |*s| s.* = try s.clone(alloc),
// Must be deep cloned. .leader => |*s| { const ptr = try alloc.create(Set); errdefer alloc.destroy(ptr); ptr.* = try s.*.clone(alloc); errdefer ptr.deinit(alloc); s.* = ptr; }, }; }
// We need to clone the action keys in the reverse map since // they may contain allocated values. { var it = result.reverse.keyIterator(); while (it.next()) |action| action.* = try action.clone(alloc); }
return result; }
/// The hash map context for the set. This defines how the hash map /// gets the hash key and checks for equality. fn Context(comptime KeyType: type) type { return struct { pub fn hash(ctx: @This(), k: KeyType) u64 { _ = ctx; return k.hash(); }
pub fn eql(ctx: @This(), a: KeyType, b: KeyType) bool { return ctx.hash(a) == ctx.hash(b); } }; }};
test "parse: triggers" { const testing = std.testing;
// single character try testing.expectEqual( Binding{ .trigger = .{ .key = .{ .unicode = 'a' } }, .action = .{ .ignore = {} }, }, try parseSingle("a=ignore"), );
// single modifier try testing.expectEqual(Binding{ .trigger = .{ .mods = .{ .shift = true }, .key = .{ .unicode = 'a' }, }, .action = .{ .ignore = {} }, }, try parseSingle("shift+a=ignore")); try testing.expectEqual(Binding{ .trigger = .{ .mods = .{ .ctrl = true }, .key = .{ .unicode = 'a' }, }, .action = .{ .ignore = {} }, }, try parseSingle("ctrl+a=ignore"));
// multiple modifier try testing.expectEqual(Binding{ .trigger = .{ .mods = .{ .shift = true, .ctrl = true }, .key = .{ .unicode = 'a' }, }, .action = .{ .ignore = {} }, }, try parseSingle("shift+ctrl+a=ignore"));
// key can come before modifier try testing.expectEqual(Binding{ .trigger = .{ .mods = .{ .shift = true }, .key = .{ .unicode = 'a' }, }, .action = .{ .ignore = {} }, }, try parseSingle("a+shift=ignore"));
// physical keys try testing.expectEqual(Binding{ .trigger = .{ .mods = .{ .shift = true }, .key = .{ .physical = .key_a }, }, .action = .{ .ignore = {} }, }, try parseSingle("shift+key_a=ignore"));
// unicode keys try testing.expectEqual(Binding{ .trigger = .{ .mods = .{ .shift = true }, .key = .{ .unicode = 'ö' }, }, .action = .{ .ignore = {} }, }, try parseSingle("shift+ö=ignore"));
// unconsumed keys try testing.expectEqual(Binding{ .trigger = .{ .mods = .{ .shift = true }, .key = .{ .unicode = 'a' }, }, .action = .{ .ignore = {} }, .flags = .{ .consumed = false }, }, try parseSingle("unconsumed:shift+a=ignore"));
// unconsumed physical keys try testing.expectEqual(Binding{ .trigger = .{ .mods = .{ .shift = true }, .key = .{ .physical = .key_a }, }, .action = .{ .ignore = {} }, .flags = .{ .consumed = false }, }, try parseSingle("unconsumed:key_a+shift=ignore"));
// performable keys try testing.expectEqual(Binding{ .trigger = .{ .mods = .{ .shift = true }, .key = .{ .unicode = 'a' }, }, .action = .{ .ignore = {} }, .flags = .{ .performable = true }, }, try parseSingle("performable:shift+a=ignore"));
// invalid key try testing.expectError(Error.InvalidFormat, parseSingle("foo=ignore"));
// repeated control try testing.expectError(Error.InvalidFormat, parseSingle("shift+shift+a=ignore"));
// multiple character try testing.expectError(Error.InvalidFormat, parseSingle("a+b=ignore"));}
test "parse: w3c key names" { const testing = std.testing;
// Exact match try testing.expectEqual( Binding{ .trigger = .{ .key = .{ .physical = .key_a } }, .action = .{ .ignore = {} }, }, try parseSingle("KeyA=ignore"), );
// Case-sensitive try testing.expectError(Error.InvalidFormat, parseSingle("Keya=ignore"));}
test "parse: plus sign" { const testing = std.testing;
try testing.expectEqual( Binding{ .trigger = .{ .key = .{ .unicode = '+' } }, .action = .{ .ignore = {} }, }, try parseSingle("+=ignore"), );
// Modifier try testing.expectEqual( Binding{ .trigger = .{ .key = .{ .unicode = '+' }, .mods = .{ .ctrl = true }, }, .action = .{ .ignore = {} }, }, try parseSingle("ctrl++=ignore"), );
try testing.expectError(Error.InvalidFormat, parseSingle("++=ignore"));}
test "parse: equals sign" { const testing = std.testing;
try testing.expectEqual( Binding{ .trigger = .{ .key = .{ .unicode = '=' } }, .action = .ignore, }, try parseSingle("==ignore"), );
// Modifier try testing.expectEqual( Binding{ .trigger = .{ .key = .{ .unicode = '=' }, .mods = .{ .ctrl = true }, }, .action = .ignore, }, try parseSingle("ctrl+==ignore"), );
try testing.expectError(Error.InvalidFormat, parseSingle("=ignore"));}
// For Ghostty 1.2+ we changed our key names to match the W3C and removed// `physical:`. This tests the backwards compatibility with the old format.// Note that our backwards compatibility isn't 100% perfect since triggers// like `a` now map to unicode instead of "translated" (which was also// removed). But we did our best here with what was unambiguous.test "parse: backwards compatibility with <= 1.1.x" { const testing = std.testing;
// simple, for sanity try testing.expectEqual( Binding{ .trigger = .{ .key = .{ .unicode = '0' } }, .action = .{ .ignore = {} }, }, try parseSingle("zero=ignore"), ); try testing.expectEqual( Binding{ .trigger = .{ .key = .{ .physical = .digit_0 } }, .action = .{ .ignore = {} }, }, try parseSingle("physical:zero=ignore"), );
// duplicates try testing.expectError(Error.InvalidFormat, parseSingle("zero+one=ignore"));
// test our full map for ( Trigger.backwards_compatible_keys.keys(), Trigger.backwards_compatible_keys.values(), ) |k, v| { var buf: [128]u8 = undefined; try testing.expectEqual( Binding{ .trigger = .{ .key = v }, .action = .{ .ignore = {} }, }, try parseSingle(try std.fmt.bufPrint(&buf, "{s}=ignore", .{k})), ); }}
test "parse: global triggers" { const testing = std.testing;
// global keys try testing.expectEqual(Binding{ .trigger = .{ .mods = .{ .shift = true }, .key = .{ .unicode = 'a' }, }, .action = .{ .ignore = {} }, .flags = .{ .global = true }, }, try parseSingle("global:shift+a=ignore"));
// global physical keys try testing.expectEqual(Binding{ .trigger = .{ .mods = .{ .shift = true }, .key = .{ .physical = .key_a }, }, .action = .{ .ignore = {} }, .flags = .{ .global = true }, }, try parseSingle("global:key_a+shift=ignore"));
// global unconsumed keys try testing.expectEqual(Binding{ .trigger = .{ .mods = .{ .shift = true }, .key = .{ .unicode = 'a' }, }, .action = .{ .ignore = {} }, .flags = .{ .global = true, .consumed = false, }, }, try parseSingle("unconsumed:global:a+shift=ignore"));
// global sequences not allowed { var p = try Parser.init("global:a>b=ignore"); try testing.expectError(Error.InvalidFormat, p.next()); }}
test "parse: all triggers" { const testing = std.testing;
// all keys try testing.expectEqual(Binding{ .trigger = .{ .mods = .{ .shift = true }, .key = .{ .unicode = 'a' }, }, .action = .{ .ignore = {} }, .flags = .{ .all = true }, }, try parseSingle("all:shift+a=ignore"));
// all physical keys try testing.expectEqual(Binding{ .trigger = .{ .mods = .{ .shift = true }, .key = .{ .physical = .key_a }, }, .action = .{ .ignore = {} }, .flags = .{ .all = true }, }, try parseSingle("all:key_a+shift=ignore"));
// all unconsumed keys try testing.expectEqual(Binding{ .trigger = .{ .mods = .{ .shift = true }, .key = .{ .unicode = 'a' }, }, .action = .{ .ignore = {} }, .flags = .{ .all = true, .consumed = false, }, }, try parseSingle("unconsumed:all:a+shift=ignore"));
// all sequences not allowed { var p = try Parser.init("all:a>b=ignore"); try testing.expectError(Error.InvalidFormat, p.next()); }}
test "parse: modifier aliases" { const testing = std.testing;
try testing.expectEqual(Binding{ .trigger = .{ .mods = .{ .super = true }, .key = .{ .unicode = 'a' }, }, .action = .{ .ignore = {} }, }, try parseSingle("cmd+a=ignore")); try testing.expectEqual(Binding{ .trigger = .{ .mods = .{ .super = true }, .key = .{ .unicode = 'a' }, }, .action = .{ .ignore = {} }, }, try parseSingle("command+a=ignore"));
try testing.expectEqual(Binding{ .trigger = .{ .mods = .{ .alt = true }, .key = .{ .unicode = 'a' }, }, .action = .{ .ignore = {} }, }, try parseSingle("opt+a=ignore")); try testing.expectEqual(Binding{ .trigger = .{ .mods = .{ .alt = true }, .key = .{ .unicode = 'a' }, }, .action = .{ .ignore = {} }, }, try parseSingle("option+a=ignore"));
try testing.expectEqual(Binding{ .trigger = .{ .mods = .{ .ctrl = true }, .key = .{ .unicode = 'a' }, }, .action = .{ .ignore = {} }, }, try parseSingle("control+a=ignore"));}
test "parse: action invalid" { const testing = std.testing;
// invalid action try testing.expectError(Error.InvalidAction, parseSingle("a=nopenopenope"));}
test "parse: action no parameters" { const testing = std.testing;
// no parameters try testing.expectEqual( Binding{ .trigger = .{ .key = .{ .unicode = 'a' } }, .action = .{ .ignore = {} }, }, try parseSingle("a=ignore"), ); try testing.expectError(Error.InvalidFormat, parseSingle("a=ignore:A"));}
test "parse: action with string" { const testing = std.testing;
// parameter { const binding = try parseSingle("a=csi:A"); try testing.expect(binding.action == .csi); try testing.expectEqualStrings("A", binding.action.csi); } // parameter { const binding = try parseSingle("a=esc:A"); try testing.expect(binding.action == .esc); try testing.expectEqualStrings("A", binding.action.esc); }}
test "parse: action with enum" { const testing = std.testing;
// parameter { const binding = try parseSingle("a=new_split:right"); try testing.expect(binding.action == .new_split); try testing.expectEqual(Action.SplitDirection.right, binding.action.new_split); }}
test "parse: action with enum with default" { const testing = std.testing;
// parameter { const binding = try parseSingle("a=new_split"); try testing.expect(binding.action == .new_split); try testing.expectEqual(Action.SplitDirection.auto, binding.action.new_split); }}
test "parse: action with int" { const testing = std.testing;
// parameter { const binding = try parseSingle("a=jump_to_prompt:-1"); try testing.expect(binding.action == .jump_to_prompt); try testing.expectEqual(@as(i16, -1), binding.action.jump_to_prompt); } { const binding = try parseSingle("a=jump_to_prompt:10"); try testing.expect(binding.action == .jump_to_prompt); try testing.expectEqual(@as(i16, 10), binding.action.jump_to_prompt); }}
test "parse: action with float" { const testing = std.testing;
// parameter { const binding = try parseSingle("a=scroll_page_fractional:-0.5"); try testing.expect(binding.action == .scroll_page_fractional); try testing.expectEqual(@as(f32, -0.5), binding.action.scroll_page_fractional); } { const binding = try parseSingle("a=scroll_page_fractional:+0.5"); try testing.expect(binding.action == .scroll_page_fractional); try testing.expectEqual(@as(f32, 0.5), binding.action.scroll_page_fractional); }}
test "parse: action with a tuple" { const testing = std.testing;
// parameter { const binding = try parseSingle("a=resize_split:up,10"); try testing.expect(binding.action == .resize_split); try testing.expectEqual(Action.SplitResizeDirection.up, binding.action.resize_split[0]); try testing.expectEqual(@as(u16, 10), binding.action.resize_split[1]); }
// missing parameter try testing.expectError(Error.InvalidFormat, parseSingle("a=resize_split:up"));
// too many try testing.expectError(Error.InvalidFormat, parseSingle("a=resize_split:up,10,12"));
// invalid type try testing.expectError(Error.InvalidFormat, parseSingle("a=resize_split:up,four"));}
test "sequence iterator" { const testing = std.testing;
// single character { var it: SequenceIterator = .{ .input = "a" }; try testing.expectEqual(Trigger{ .key = .{ .unicode = 'a' } }, (try it.next()).?); try testing.expect(try it.next() == null); }
// multi character { var it: SequenceIterator = .{ .input = "a>b" }; try testing.expectEqual(Trigger{ .key = .{ .unicode = 'a' } }, (try it.next()).?); try testing.expectEqual(Trigger{ .key = .{ .unicode = 'b' } }, (try it.next()).?); try testing.expect(try it.next() == null); }
// empty { var it: SequenceIterator = .{ .input = "" }; try testing.expectError(Error.InvalidFormat, it.next()); }
// empty starting sequence { var it: SequenceIterator = .{ .input = ">a" }; try testing.expectError(Error.InvalidFormat, it.next()); }
// empty ending sequence { var it: SequenceIterator = .{ .input = "a>" }; try testing.expectEqual(Trigger{ .key = .{ .unicode = 'a' } }, (try it.next()).?); try testing.expectError(Error.InvalidFormat, it.next()); }}
test "parse: sequences" { const testing = std.testing;
// single character { var p = try Parser.init("ctrl+a=ignore"); try testing.expectEqual(Parser.Elem{ .binding = .{ .trigger = .{ .mods = .{ .ctrl = true }, .key = .{ .unicode = 'a' }, }, .action = .{ .ignore = {} }, } }, (try p.next()).?); try testing.expect(try p.next() == null); }
// sequence { var p = try Parser.init("a>b=ignore"); try testing.expectEqual(Parser.Elem{ .leader = .{ .key = .{ .unicode = 'a' }, } }, (try p.next()).?); try testing.expectEqual(Parser.Elem{ .binding = .{ .trigger = .{ .key = .{ .unicode = 'b' }, }, .action = .{ .ignore = {} }, } }, (try p.next()).?); try testing.expect(try p.next() == null); }}
test "set: parseAndPut typical binding" { const testing = std.testing; const alloc = testing.allocator;
var s: Set = .{}; defer s.deinit(alloc);
try s.parseAndPut(alloc, "a=new_window");
// Creates forward mapping { const action = s.get(.{ .key = .{ .unicode = 'a' } }).?.value_ptr.*.leaf; try testing.expect(action.action == .new_window); try testing.expectEqual(Flags{}, action.flags); }
// Creates reverse mapping { const trigger = s.getTrigger(.{ .new_window = {} }).?; try testing.expect(trigger.key.unicode == 'a'); }}
test "set: parseAndPut unconsumed binding" { const testing = std.testing; const alloc = testing.allocator;
var s: Set = .{}; defer s.deinit(alloc);
try s.parseAndPut(alloc, "unconsumed:a=new_window");
// Creates forward mapping { const trigger: Trigger = .{ .key = .{ .unicode = 'a' } }; const action = s.get(trigger).?.value_ptr.*.leaf; try testing.expect(action.action == .new_window); try testing.expectEqual(Flags{ .consumed = false }, action.flags); }
// Creates reverse mapping { const trigger = s.getTrigger(.{ .new_window = {} }).?; try testing.expect(trigger.key.unicode == 'a'); }}
test "set: parseAndPut removed binding" { const testing = std.testing; const alloc = testing.allocator;
var s: Set = .{}; defer s.deinit(alloc);
try s.parseAndPut(alloc, "a=new_window"); try s.parseAndPut(alloc, "a=unbind");
// Creates forward mapping { const trigger: Trigger = .{ .key = .{ .unicode = 'a' } }; try testing.expect(s.get(trigger) == null); } try testing.expect(s.getTrigger(.{ .new_window = {} }) == null);}
test "set: parseAndPut sequence" { const testing = std.testing; const alloc = testing.allocator;
var s: Set = .{}; defer s.deinit(alloc);
try s.parseAndPut(alloc, "a>b=new_window"); var current: *Set = &s; { const t: Trigger = .{ .key = .{ .unicode = 'a' } }; const e = current.get(t).?.value_ptr.*; try testing.expect(e == .leader); current = e.leader; } { const t: Trigger = .{ .key = .{ .unicode = 'b' } }; const e = current.get(t).?.value_ptr.*; try testing.expect(e == .leaf); try testing.expect(e.leaf.action == .new_window); try testing.expectEqual(Flags{}, e.leaf.flags); }}
test "set: parseAndPut sequence with two actions" { const testing = std.testing; const alloc = testing.allocator;
var s: Set = .{}; defer s.deinit(alloc);
try s.parseAndPut(alloc, "a>b=new_window"); try s.parseAndPut(alloc, "a>c=new_tab"); var current: *Set = &s; { const t: Trigger = .{ .key = .{ .unicode = 'a' } }; const e = current.get(t).?.value_ptr.*; try testing.expect(e == .leader); current = e.leader; } { const t: Trigger = .{ .key = .{ .unicode = 'b' } }; const e = current.get(t).?.value_ptr.*; try testing.expect(e == .leaf); try testing.expect(e.leaf.action == .new_window); try testing.expectEqual(Flags{}, e.leaf.flags); } { const t: Trigger = .{ .key = .{ .unicode = 'c' } }; const e = current.get(t).?.value_ptr.*; try testing.expect(e == .leaf); try testing.expect(e.leaf.action == .new_tab); try testing.expectEqual(Flags{}, e.leaf.flags); }}
test "set: parseAndPut overwrite sequence" { const testing = std.testing; const alloc = testing.allocator;
var s: Set = .{}; defer s.deinit(alloc);
try s.parseAndPut(alloc, "a>b=new_tab"); try s.parseAndPut(alloc, "a>b=new_window"); var current: *Set = &s; { const t: Trigger = .{ .key = .{ .unicode = 'a' } }; const e = current.get(t).?.value_ptr.*; try testing.expect(e == .leader); current = e.leader; } { const t: Trigger = .{ .key = .{ .unicode = 'b' } }; const e = current.get(t).?.value_ptr.*; try testing.expect(e == .leaf); try testing.expect(e.leaf.action == .new_window); try testing.expectEqual(Flags{}, e.leaf.flags); }}
test "set: parseAndPut overwrite leader" { const testing = std.testing; const alloc = testing.allocator;
var s: Set = .{}; defer s.deinit(alloc);
try s.parseAndPut(alloc, "a=new_tab"); try s.parseAndPut(alloc, "a>b=new_window"); var current: *Set = &s; { const t: Trigger = .{ .key = .{ .unicode = 'a' } }; const e = current.get(t).?.value_ptr.*; try testing.expect(e == .leader); current = e.leader; } { const t: Trigger = .{ .key = .{ .unicode = 'b' } }; const e = current.get(t).?.value_ptr.*; try testing.expect(e == .leaf); try testing.expect(e.leaf.action == .new_window); try testing.expectEqual(Flags{}, e.leaf.flags); }}
test "set: parseAndPut unbind sequence unbinds leader" { const testing = std.testing; const alloc = testing.allocator;
var s: Set = .{}; defer s.deinit(alloc);
try s.parseAndPut(alloc, "a>b=new_window"); try s.parseAndPut(alloc, "a>b=unbind"); var current: *Set = &s; { const t: Trigger = .{ .key = .{ .unicode = 'a' } }; try testing.expect(current.get(t) == null); }}
test "set: parseAndPut unbind sequence unbinds leader if not set" { const testing = std.testing; const alloc = testing.allocator;
var s: Set = .{}; defer s.deinit(alloc);
try s.parseAndPut(alloc, "a>b=unbind"); var current: *Set = &s; { const t: Trigger = .{ .key = .{ .unicode = 'a' } }; try testing.expect(current.get(t) == null); }}
test "set: parseAndPut sequence preserves reverse mapping" { const testing = std.testing; const alloc = testing.allocator;
var s: Set = .{}; defer s.deinit(alloc);
try s.parseAndPut(alloc, "a=new_window"); try s.parseAndPut(alloc, "ctrl+a>b=new_window");
// Creates reverse mapping { const trigger = s.getTrigger(.{ .new_window = {} }).?; try testing.expect(trigger.key.unicode == 'a'); }}
test "set: put overwrites sequence" { const testing = std.testing; const alloc = testing.allocator;
var s: Set = .{}; defer s.deinit(alloc);
try s.parseAndPut(alloc, "ctrl+a>b=new_window"); try s.put(alloc, .{ .mods = .{ .ctrl = true }, .key = .{ .unicode = 'a' }, }, .{ .new_window = {} });
// Creates reverse mapping { const trigger = s.getTrigger(.{ .new_window = {} }).?; try testing.expect(trigger.key.unicode == 'a'); }}
test "set: maintains reverse mapping" { const testing = std.testing; const alloc = testing.allocator;
var s: Set = .{}; defer s.deinit(alloc);
try s.put(alloc, .{ .key = .{ .unicode = 'a' } }, .{ .new_window = {} }); { const trigger = s.getTrigger(.{ .new_window = {} }).?; try testing.expect(trigger.key.unicode == 'a'); }
// should be most recent try s.put(alloc, .{ .key = .{ .unicode = 'b' } }, .{ .new_window = {} }); { const trigger = s.getTrigger(.{ .new_window = {} }).?; try testing.expect(trigger.key.unicode == 'b'); }
// removal should replace s.remove(alloc, .{ .key = .{ .unicode = 'b' } }); { const trigger = s.getTrigger(.{ .new_window = {} }).?; try testing.expect(trigger.key.unicode == 'a'); }}
test "set: performable is not part of reverse mappings" { const testing = std.testing; const alloc = testing.allocator;
var s: Set = .{}; defer s.deinit(alloc);
try s.put(alloc, .{ .key = .{ .unicode = 'a' } }, .{ .new_window = {} }); { const trigger = s.getTrigger(.{ .new_window = {} }).?; try testing.expect(trigger.key.unicode == 'a'); }
// trigger should be non-performable try s.putFlags( alloc, .{ .key = .{ .unicode = 'b' } }, .{ .new_window = {} }, .{ .performable = true }, ); { const trigger = s.getTrigger(.{ .new_window = {} }).?; try testing.expect(trigger.key.unicode == 'a'); }
// removal of performable should do nothing s.remove(alloc, .{ .key = .{ .unicode = 'b' } }); { const trigger = s.getTrigger(.{ .new_window = {} }).?; try testing.expect(trigger.key.unicode == 'a'); }}
test "set: overriding a mapping updates reverse" { const testing = std.testing; const alloc = testing.allocator;
var s: Set = .{}; defer s.deinit(alloc);
try s.put(alloc, .{ .key = .{ .unicode = 'a' } }, .{ .new_window = {} }); { const trigger = s.getTrigger(.{ .new_window = {} }).?; try testing.expect(trigger.key.unicode == 'a'); }
// should be most recent try s.put(alloc, .{ .key = .{ .unicode = 'a' } }, .{ .new_tab = {} }); { const trigger = s.getTrigger(.{ .new_window = {} }); try testing.expect(trigger == null); }}
test "set: consumed state" { const testing = std.testing; const alloc = testing.allocator;
var s: Set = .{}; defer s.deinit(alloc);
try s.put(alloc, .{ .key = .{ .unicode = 'a' } }, .{ .new_window = {} }); try testing.expect(s.get(.{ .key = .{ .unicode = 'a' } }).?.value_ptr.* == .leaf); try testing.expect(s.get(.{ .key = .{ .unicode = 'a' } }).?.value_ptr.*.leaf.flags.consumed);
try s.putFlags( alloc, .{ .key = .{ .unicode = 'a' } }, .{ .new_window = {} }, .{ .consumed = false }, ); try testing.expect(s.get(.{ .key = .{ .unicode = 'a' } }).?.value_ptr.* == .leaf); try testing.expect(!s.get(.{ .key = .{ .unicode = 'a' } }).?.value_ptr.*.leaf.flags.consumed);
try s.put(alloc, .{ .key = .{ .unicode = 'a' } }, .{ .new_window = {} }); try testing.expect(s.get(.{ .key = .{ .unicode = 'a' } }).?.value_ptr.* == .leaf); try testing.expect(s.get(.{ .key = .{ .unicode = 'a' } }).?.value_ptr.*.leaf.flags.consumed);}
test "set: getEvent physical" { const testing = std.testing; const alloc = testing.allocator;
var s: Set = .{}; defer s.deinit(alloc);
try s.parseAndPut(alloc, "ctrl+quote=new_window");
// Physical matches on physical { const action = s.getEvent(.{ .key = .quote, .mods = .{ .ctrl = true }, }).?.value_ptr.*.leaf; try testing.expect(action.action == .new_window); }
// Physical does not match on UTF8/codepoint { const action = s.getEvent(.{ .key = .key_a, .mods = .{ .ctrl = true }, .utf8 = "'", .unshifted_codepoint = '\'', }); try testing.expect(action == null); }}
test "set: getEvent codepoint" { const testing = std.testing; const alloc = testing.allocator;
var s: Set = .{}; defer s.deinit(alloc);
try s.parseAndPut(alloc, "ctrl+'=new_window");
// Matches on codepoint { const action = s.getEvent(.{ .key = .key_a, .mods = .{ .ctrl = true }, .utf8 = "", .unshifted_codepoint = '\'', }).?.value_ptr.*.leaf; try testing.expect(action.action == .new_window); }
// Matches on UTF-8 { const action = s.getEvent(.{ .key = .key_a, .mods = .{ .ctrl = true }, .utf8 = "'", }).?.value_ptr.*.leaf; try testing.expect(action.action == .new_window); }
// Doesn't match on physical { const action = s.getEvent(.{ .key = .key_a, .mods = .{ .ctrl = true }, }); try testing.expect(action == null); }}
test "set: getEvent codepoint case folding" { const testing = std.testing; const alloc = testing.allocator;
var s: Set = .{}; defer s.deinit(alloc);
try s.parseAndPut(alloc, "ctrl+A=new_window");
// Lowercase codepoint { const action = s.getEvent(.{ .key = .key_j, .mods = .{ .ctrl = true }, .utf8 = "", .unshifted_codepoint = 'a', }).?.value_ptr.*.leaf; try testing.expect(action.action == .new_window); }
// Uppercase codepoint { const action = s.getEvent(.{ .key = .key_a, .mods = .{ .ctrl = true }, .utf8 = "", .unshifted_codepoint = 'A', }).?.value_ptr.*.leaf; try testing.expect(action.action == .new_window); }
// Negative case for sanity { const action = s.getEvent(.{ .key = .key_j, .mods = .{ .ctrl = true }, }); try testing.expect(action == null); }}
test "Action: clone" { const testing = std.testing; var arena = std.heap.ArenaAllocator.init(testing.allocator); defer arena.deinit(); const alloc = arena.allocator();
{ var a: Action = .ignore; const b = try a.clone(alloc); try testing.expect(b == .ignore); }
{ var a: Action = .{ .text = "foo" }; const b = try a.clone(alloc); try testing.expect(b == .text); }}
test "parse: increase_font_size" { const testing = std.testing;
{ const binding = try parseSingle("a=increase_font_size:1.5"); try testing.expect(binding.action == .increase_font_size); try testing.expectEqual(1.5, binding.action.increase_font_size); }}
test "parse: decrease_font_size" { const testing = std.testing;
{ const binding = try parseSingle("a=decrease_font_size:2.5"); try testing.expect(binding.action == .decrease_font_size); try testing.expectEqual(2.5, binding.action.decrease_font_size); }}
test "parse: reset_font_size" { const testing = std.testing;
{ const binding = try parseSingle("a=reset_font_size"); try testing.expect(binding.action == .reset_font_size); }}
test "parse: set_font_size" { const testing = std.testing;
{ const binding = try parseSingle("a=set_font_size:13.5"); try testing.expect(binding.action == .set_font_size); try testing.expectEqual(13.5, binding.action.set_font_size); }}