crokey-1.0.1/.cargo_vcs_info.json0000644000000001360000000000100123270ustar { "git": { "sha1": "2bccdd4e92714e7984214e673d97352d202c4427" }, "path_in_vcs": "" }crokey-1.0.1/.gitignore000064400000000000000000000000221046102023000131010ustar 00000000000000target Cargo.lock crokey-1.0.1/Cargo.toml0000644000000023640000000000100103320ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" rust-version = "1.56" name = "crokey" version = "1.0.1" authors = ["dystroy "] description = "Parse and describe keys - helping incorporate keybindings in terminal applications" readme = "README.md" keywords = [ "key", "parse", ] categories = [ "command-line-interface", "parsing", ] license = "MIT" repository = "https://github.com/Canop/crokey" [dependencies.crokey-proc_macros] version = "1.0.1" [dependencies.crossterm] version = "0.27" [dependencies.once_cell] version = "1.12" [dependencies.serde] version = "1.0.130" features = ["derive"] optional = true [dependencies.strict] version = "0.2" [dev-dependencies.deser-hjson] version = "1.0" [dev-dependencies.trybuild] version = "1.0.55" [features] default = ["serde"] crokey-1.0.1/Cargo.toml.orig000064400000000000000000000016011046102023000140040ustar 00000000000000[package] name = "crokey" version = "1.0.1" authors = ["dystroy "] edition = "2021" keywords = ["key", "parse"] license = "MIT" categories = ["command-line-interface", "parsing"] description = "Parse and describe keys - helping incorporate keybindings in terminal applications" repository = "https://github.com/Canop/crokey" readme = "README.md" rust-version = "1.56" [features] default = ["serde"] [dependencies] crossterm = "0.27" crokey-proc_macros = { path = "src/proc_macros", version = "1.0.1" } once_cell = "1.12" serde = { optional = true, version = "1.0.130", features = ["derive"] } strict = "0.2" [dev-dependencies] deser-hjson = "1.0" trybuild = "1.0.55" [workspace] members = [ "src/proc_macros", "examples/deser_keybindings", "examples/print_key", "examples/print_key_no_combiner", ] [patch.crates-io] # strict = { path = "../strict" } crokey-1.0.1/README.md000064400000000000000000000122651046102023000124040ustar 00000000000000[![MIT][s2]][l2] [![Latest Version][s1]][l1] [![docs][s3]][l3] [![Chat on Miaou][s4]][l4] [s1]: https://img.shields.io/crates/v/crokey.svg [l1]: https://crates.io/crates/crokey [s2]: https://img.shields.io/badge/license-MIT-blue.svg [l2]: LICENSE [s3]: https://docs.rs/crokey/badge.svg [l3]: https://docs.rs/crokey/ [s4]: https://miaou.dystroy.org/static/shields/room.svg [l4]: https://miaou.dystroy.org/3490?crokey # Crokey Crokey helps incorporate configurable keybindings in [crossterm](https://github.com/crossterm-rs/crossterm) based terminal applications by providing functions - parsing key combinations from strings - describing key combinations in strings - parsing key combinations at compile time - combining Crossterm key events in key combinations ## The KeyCombination A `KeyCombination` is made of 1 to 3 "normal" keys with some optional modifiers (alt, shift, ctrl). It can be parsed, ergonomically built with the `key!` macro, obtained from key events. ## The Combiner With a `Combiner`, you can change raw Crossterm key events into key combinations. When the terminal is modern enough and supports the Kitty protocol, complex combinations with up to three non-modifier keys may be formed, for example `Ctrl-Alt-Shift-g-y` or `Space-!`. For standard ANSI terminals, only regular combinations are available, like `Shift-o`, `Ctrl-Alt-Shift-g` or `i`. The combiner works in both cases: if you presses the `ctrl`, `i`, and `u ` keys at the same time, it will result in one combination (`ctrl-i-u`) on a kitty-compatible terminal, and as a sequence of 2 key combinations (`ctrl-i` then `ctrl-u` assuming you started pressing the `i` before the `u`) in other terminals. The `print_key` example shows how to deal with that: ```rust let fmt = KeyCombinationFormat::default(); let mut combiner = Combiner::default(); let combines = combiner.enable_combining().unwrap(); if combines { println!("Your terminal supports combining keys"); } else { println!("Your terminal doesn't support combining non-modifier keys"); } println!("Type any key combination"); loop { terminal::enable_raw_mode().unwrap(); let e = read(); terminal::disable_raw_mode().unwrap(); match e { Ok(Event::Key(key_event)) => { if let Some(key_combination) = combiner.transform(key_event) { match key_combination { key!(ctrl-c) | key!(ctrl-q) => { println!("quitting"); break; } _ => { println!("You typed {}", fmt.to_string(key_combination)); } } } }, ... } } ``` ## Parse a string Those strings are usually provided by a configuration file. ```rust use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; assert_eq!( crokey::parse("alt-enter").unwrap(), KeyEvent::new(KeyCode::Enter, KeyModifiers::ALT), ); assert_eq!( crokey::parse("shift-F6").unwrap(), KeyEvent::new(KeyCode::F(6), KeyModifiers::SHIFT), ); ``` ## Use key combination "literals" thanks to procedural macros Those key combinations are parsed at compile time and have zero runtime cost. They're efficient and convenient for matching events or defining hardcoded keybindings. ```rust match key_event.into() { key!(ctrl-c) => { println!("Arg! You savagely killed me with a {}", fmt.to_string(key_event).red()); break; } key!(ctrl-q) => { println!("You typed {} which gracefully quits", fmt.to_string(key_event).green()); break; } _ => { println!("You typed {}", fmt.to_string(key_event).blue()); } } ``` Complete example in `/examples/print_key`: ![print_key](doc/print_key.png) The `key!` macro can be called in const contexts: ```rust const quit: KeyCombination = key!(ctrl-q); ``` ## Display a string with a configurable format ```rust use crokey::*; use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; // The default format let format = KeyCombinationFormat::default(); assert_eq!(format.to_string(key!(shift-a)), "Shift-a"); assert_eq!(format.to_string(key!(ctrl-c)), "Ctrl-c"); // A more compact format let format = KeyCombinationFormat::default() .with_implicit_shift() .with_control("^"); assert_eq!(format.to_string(key!(shift-a)), "A"); assert_eq!(format.to_string(key!(ctrl-c)), "^c"); ``` ## Deserialize keybindings using Serde With the "serde" feature enabled, you can read configuration files in a direct way: ``` use { crokey::*, crossterm::event::KeyEvent, serde::Deserialize, std::collections::HashMap, }; #[derive(Debug, Deserialize)] struct Config { keybindings: HashMap, } static CONFIG_HJSON: &str = r#" { keybindings: { a: aardvark shift-b: babirussa ctrl-k: koala alt-j: jaguar } } "#; let config: Config = deser_hjson::from_str(CONFIG_HJSON).unwrap(); let key: KeyCombination = key!(shift-b); assert_eq!( config.keybindings.get(&key).unwrap(), "babirussa", ); ``` You can use any Serde compatible format such as JSON or TOML. ## Crossterm Compatibility Crokey includes and reexports Crossterm, so you don't have to import it and to avoid conflicts. crokey-1.0.1/doc/print_key.png000064400000000000000000000614131046102023000144030ustar 00000000000000PNG  IHDRg!sBIT|dtEXtSoftwaregnome-screenshot>-tEXtCreation TimeMon 31 Jan 2022 11:30:06 AM CETz3 IDATxy\TU,( n) f-VVO/-|Ls{%qARD\ؔ}dA"\_/_sιgS  ՆA   PFt=<Ó9y`/ɷs(F (mcؾr9woӐm-NYƉ=1W>y{a,J+<3?앏g\aACsB ?zZ #f|¿׾텶7\3+)o4\竡˯oZv|kυ}-y8u}|{ҩÃWc|ivGtpmɂ&4tUI|G_{e9>Ϋʻ~=݊ئU.hh~ub=v1e1gZHҴM(o[ʐˤQK MjףX~}ׯ}^gQy B!g~|@C <*JVp`RXrw}ˍbD.+HgזĦZg:Zhd\MbʀXURq=ae;TD{#ꍹ>T@Q1bĻqSp-V͔729Ɂ&W^ tOLl`GX% %8+x&<5tG%ç|}.Q7m`vQXmiu k{13;`;R?07W[Jg:Z:*JC`5_uJώ1ᥝY=m4q4O-52zVX8q/RFjYZ6W͢f(2ˠ{·sG#mMSkmJJt,.#EhFɂ9\Ow<3֎8 _PGvb x}+2rde!x}H\6n?ʛ/bi3|P/F]eܑ߽z W9z2L?P|Օ/O3Sbol$IPW-ͥXyɎkmdɓ><\j,6 b\x&ã޵=nZq#&m"vNן!2w_֎wW>ی )iY){% EplZTBߕaNm3J4_**V`atݫ[]\*"*_v3~{˟N-_3s%Kzc.O鿚3[ywiұ-8WsE+t̪*Le3zW6ݮؾKV)ő٠-y UՇMfq3.FzS0>oWլ\h`tjoʞG:B~L$կLe,kWsILՐ|8K$+GˤٸŽ?jFT֔3?[v2"v'5]CXZ` 7;be)c?U];rB1>nƎoyv%myU>Rjwb̹_[0ǬB)8#%|thR}-i¾1߾O|f> _icm_1+}m[ڌ yhm:IhJJW3#?-/˥6i0}<̞ܼB>7Yr|;V܀0Pڇǻue1 0oɋ+i#$OhD z?N\ 51y! S|ڌ@:k}n%u=_/}=:P(9@1@.dWPB+=6ceg #DŽئ{T:T==;dW|ପk7?244_˶\RLt\ cT:~?^ȜhZ )}5iFɼYք]Q^Z&qA!*TU?*`@S&Ӫ^̘`Y!A2LNnXu 1/wc3sR~9XVm&2>ߎu>?QH@?Sw+u1f̓|E6uf4,}ЈHO(rFf#f*_MN8(.iܜ_?]mRjMIHzL)iY9~ 5nNL\ m녱q}*ةbSPK8v*;l1eud~>.ߧ+Aau uGixZVkbItlSa~V1Q @?kjQȲ͆\~ͻ/[q+R5h4жMYSw1".tOLՐeFKM^fW&b2:_ +^y|6 RJN:hضlci˺^̩*`cUvFJbnк" ~R\N>Wլ>kjd횕A??}Ahݢ}Z8))ёMomJh3v6^O ~MQֿ*[R5w(\Fn~'z׃Ю%_N{X2oW<~Eݨպ-17˾Ɨ}nK 3>_4B:GKńQ?tT_m˗P:v!Yu.G| ãk_uOL.MrjJ;kʦWH(PE˪B?O<ޭ#W⸓.}#piHBr>} uPsV5;^I.sM qvȯt~}1nF| /gRŘ^eW_R5;bjJU%޷Aߑ_njn"'B1JE魺ʟH+_WMv%A2;m{ hH_39}rX!05]Z!3KNIiڵV2wQ~&g l_~ֿ 17QT`;6䃨&5>CZvѥ# 陆ݫahr#6kщXF +R`E}<ch$* 9XNB9i٥96#!\ཡwJণ.7fg*I$dUD+2sLx.U_Xʱ*ײP!SFUjX?31[%<ɲo`r矱uVq+tyKόU;S2WW>QILp1Kh`s%KńEyTe!ft7۔L O1ܑONbdKT*k>oD Hp4G )(u\{fO9<.θWBR24dh%ǿc/"1UÕj SѡmI!n한8"V Y5kr 8r";%Ejz~=/vqg_7L$w9'ڹ-;a0ouŎrre6~)~VJu|wg[18'.F8ޒc:hSD!뎆Jzy#W@I R~t7a[|%NY5s%hx۔TA}tgp I35۔(u!" կǖ-?ۿ:#2iôy<^`M;lޕ /hƔDK0 ':NMϮ&̝nUaWo{K15'GJoc\n&j0/?7lj s /ftTj8~CXʙ;݊'<%E^~߬kY8r~/b'{׳fڶiNt\z|h:swۓؾ_$f̥j"ώHv:ΖIOJwvhܜ aѬ\yynkHnWSWUo3jhvT*5C"Dhdտ\Ҙ3 OCR4tuiֿ2dϖuMAfL~*}2Ƥ1|_7<3_y)r}sx3Rjlד9eDDų:AGUzQC* O).i9V̅bV~`пCӣ>*bU5ђAWuWcܡ)]ܜ3J }D6kžϔtQwMR[W;Tt/v,[ N *..mn&4+Z5էG}5e`ߊcե궯@&1yl}r 9柃$ﴌ;H(z,$c|è7_KLgpo?}K 7XRyELi[[+dAc_}y{qb2zxM|}kQ`Ɍѷ/yyq YA£HF'K\[ V!Jӎ RNm7P{e9>ΫU>1om<l/ĨNS~vo>熢4RͺI! 1i}_ĵ BCkߐv#6̬|tбOz[GN^&F܈MOkdϦofOHIJ/޸e+wr<$UKgs)2nv#->|wJ )rMA06VwqGR>}ܙ\Z;RPxHk6F~APz[bx4ݻǭC+n$MdΑMd2}\Z;QXT\m|L@k';3sظ:sg¯or9C"XjEb+/ 7;kv8A=IJDΆoͦ%++_-A^\lյ–kub2^[T~uʙ?V:؈#;wV1a<־Y|k>ڦ7~ ퟆW)”JOħvSu>yn?__m_ n?ׅW{wVw6uW)+r^z`fjBT$m+ M/\yI3gӉ@~Z( BG駍Ͳ;9yyE:m~޴IldUѼɒ`ǯ̘>ZG-ymF`e51y! SXzmF ۵RXTl~uQ~O|42<ó|ΧĮ~ ٞwӽ-yY&w\㝘0s)69 8[wVT(xgXЈ?.TeU?T3 ό7'p>>˝X o£fo4u2|G,zҎmz~J?_u_QCpȟ<FFJWwOuR/)j m?ׅ0[JU_?rh֑7)?c/P(?nnhhsmڿ&wF\$m5t0"}Z]NL\ m녱 ýY.#㈽ʗkvӥ36Ytn&s_0-sT1)%;cj3 W]쪃ն|?y)R2 ~vX?Ġ^zX.GÖC~~jZ*[%HJ}S2'Dr,$[zcia NCDR]KY1TBm3+W>K7(,*\ur $mխڦ[ՕQq-:ҮMsn??l_RG34oHmN]ouoWu~}S3DJZG_"z`xhsmRڿ&헑KSK:J) 9ʫGڑ |t%-R!N/^!_Gg,C~q8]Z`cm @}'Ԍ| U44Qxn ǝ|M ֯延mA+373ڒ踲[c0wJ4nsi4Z41|NFܡ7b __{Pꟗ_d0>ChQ7*~6]U"Օ[iϹyX[[?>Hݾr1Tǐ"EVwUׄ wEf݊׹M+\ 6.CGCk{5ib5&J%sǹ ʵ[t92ryE8\毕9%:bd|`*'N_fg[c'o|r:|:d5ߐ"漷 1z7?=4 IDAT ~-_& /xoh9IUW*uۇ!*%JNԆ:UL&uZ5?+_]]syu{,W}_}?CǏ{5&R[[jU_ĸG^x{GxK#%L&k4ʗ5i?&h}i.@O E{_e;]ti㈺DCzfiڹ ;k6<r ̵DVc3Mѱtغy*B?}ۖoJqmv-hg[cIy=O.vVUuUC$ڡj6]Uߺ)2uTS_TަPmmRA]پ_.Cn^!˷gSsn@tlr.lTEl}{snδI9|,b5ZM;xqǩ-=X޳- +;`gOwṳF:rBk $ׯ6o}3z7-ѵs[&?_?0!.93ub[;ǝV-z#:ڑfMHj/5= V_?LLPVeUE 9w=YjUHY:ퟆ7s6 n5*[p-:'k+*r-5ί5_OZ86Jt|ϵ9Ki_m9zFWE J9Wݿfl`?97BX4+۴( 7f=ESkKS8y ~/b'{׳fڶi^^sm⯉~f㪷5H_m?w? 3=;l~bceΧ)ڟ~Qbtlגx~2+VOOP"YצpJB!~3Sct_P ~RWOu? m_ގjt?n;̄QuF0݆|$ɩ0sC} _PE*;V-((Tt*-?! }~PKm?#{1뭕5*2777V,IDT|Z:aldU_RP; O&J<;4\x0bGZ[7z7u_Nt_.xyECC uc bw%a_'3iҎkYg?, C/ԍ=x?]o|+5)  ԯݹ  P/DLAi43\: ƌwY~'|k󞲈>T9+:3'|PnL&Z* BR>?>{1O/U*l^=G-J \[  PʫKV }[OƦN":d7W(XNx8D).+GѧwW;Ƒ˨4*IM+*iX6%dB: !9fD `RRfɾCgh:?8 yn?_`N-lٺ',ЏpɞM߾͸韐VG\ Xr'C"Xt6"cJk';Ϸ}Pș6i0XIhD,_ޥ=ʂ控g{R܀ #cl CnZ1Ăca܅.ʓm3\ڀ916{Gd_Ř8 )M{sDr=5iγvlK̹e$q}GK[AP &==cHM?n/0!ygzZ<2m: 19x+|8_[D]O KW !Ffܼ"^6B?oڤ O62{*r hd9`k+X0O=GrZM Zu1Zwv6H ?NO!W(i=J?/,LJ%/V_:3[P }/k13$-"v.(LQEz:_A2rNu{7 Ə{ʽU}Ġ^zX.GÖCC `E}=t%(8L@Pp81q)hZ>F(rFf#f*_MN8`bbDk6 >!೑ N%::{:w\zE!'5[aGibLttZ:gѬ%٢;gQ sGPr   GxT Kn˙`cmYc0”zȉP^>֎$$gӧ /Pn , 9vt:,-| :7wboħЦ/$-:v:y_{Wclf%9 |n妥8υ]_3mI:?ylNK'gq<k?xAA) ãkdr(nG܅kzr-T\Se _qth4Zƾ yv/[<"I &Yzǵ;і>j-/{WɢnƩM+).ߩ39W~=qQAQA`/Pŝ|\]Z觵oےBr /~~V5D.( '}<9|,|gKG%3sHȦPEm+7-#b5휛맵mӼeRF&02Q:M fiiKodۯ(/̤rroKWP=!P(,VAA&׾yaB]pwsfĀroz#?/:wlCn V@Rӳh}W_/X!}ߞn<9휛3m` XVc ^yq8>}qjawO7,*S|n(έƀ~5{"Zyхf;9`*9q5CnX9aba]iG*w2i47r12FeW0ܴxM W]7܌[;x  H4| 6V|4~!x%3a_/u{ymz OqJxH]ܜT!N36휛s!,mq\%ɩYȨJ}=@:d[ZyuM&0g XWaD Tw  4rC.3fx_QCQ*l9"{8uh i֦SC# #JRYsfdߡh4Z?U?CR`bbYEvQ*}39zv`Ȁ<7Ο/j0l]{ɞM߾͸韐ݽqVxHRd,<\idGԵ[|6>I3m`ꅱЈXX+YV|0w<=<BXdl;i1i58v#3D#g=-Gh u W_]Mo%EA#җ'&g0LGjz6qA ٞwӽ-&3 ό7'p>>nN|izxu@Pr>J?m0oɋ+i#M@O>d#"7Mϟ3=++ [ԓ}j\I%=&WdD|K'Dɯ^ʿ̝ZvOs@PZJ :2nkӕ 6bP/o=LXd,a!F ~y0"et%(8 D?-(84Z-v cc#r9{]G{3/K'gm011"ϋ5FH(>3k{,$#.̸m1oڼoHRIZt*-­WϠըZ  #C!srm=s3l-KO){GNv$!9>]xɆr$&g?'fTqFaia狦WѾ{3U?=&>6$ؾ;ѨJof%];q״ |n妥8υ]_|%NΉT;®lAAro)@0F~^t؆>=׭Ɓgj灉?5^vtɁ=hܜisX(jZvʋS [{gP gsCqnwO7}ۮh5%EI^fyIEF]}&5)7N:F4F&WH 9L-ҲS_"kVACagg?\.㏓Kkf<$C{r5Yҫ]1)xcA8e`S)V_PČ)CxatؚAe0Fsk,LXriEHΧDU\@|DT<&<7~ s6\|u.P\Z;3iރg  H|lNGVaĶPiq8y6`Mejh 3iJ哒O`i،kǷJN' B"sss{gڟ~/7} c;3GOx\;EAhޭYK S|ѮTmݙc   `3iҎk4:'|E\9T AAhۚ  wmMAA 9AAhDML!3ק1#9   @}{6@T2u⠇Hז,xkC+OA1or1'R`A   ?RRfɾCghgg/\ɞM߾͸韐V~I\ Xr'C"Xt6"cJk';Ϸ}'tyi3|P/F]eʂ控g{RQ#*F yk!>­$E?NAM/OL`8Əl~~&ó=,Zϛ^{[M\'$&g8o~O||ևN|izxu@Pr>J?m0oɋ+i#M@O>d#"7Mϟ3=++ [ԓ}.^ډr&֠j(AA:2nkӕ 6bP/o=LXd,a!F ~y0"e/JPpju~ZPp81q)hZ>F(rFf#f*_MN8`bbDk6 >!೑ , ƒȌ@."#.uQ^. ?WqGš˹t춞 6֖D%]IK S=#'Byu\Z;O.dCe3SP*8Y鰴0E+hT߁Y6~L| mZ98;)15N# pro)@0 삻3S'y7s6 n5$5= V_?LLPwlбƓ{ι9& Ph:6 xtc{R :n QAA"X/Xގjt?n;̄Q~5 $/o,?Y?]*!tqszLR;~sn΅hVۣiQr9oz֖$fqe/V¢w凯 :. 5_A.jCbL"u V-0z"AAݚձ0׻ ^NAAt[XT9+:3'|PnL&Z* BR>?>{1O/W*l^=G-J \[  PʫKV }[OƦN":d7W(XNx8DQgWH,AGr/do ˶_0jwi砆*JcSB +GgL4q>ѧwW;Ƒ˨4TAA9Jf̙͆1}΢hy~@Vꌉsg¯oE(|u~q!8&Zزu;OYC@k'{6}6BJZPz t,[!Z:KtpQnȾSډQ(L4zal$4"/V"-V|0w<=<BXdTcNOtԊ&;[.e$p)-ĶMgzO\@̙_ R.m[=#2/sfbm?k~aMĹ}b0~t8uQ>MlzV~I>AHn AAjO/OL`8Əl~ ٞwӽ-&3 ό7'p>>nN|izxu@Pr>J?m0oɋ+i#M@O>d#"7Mϟ3=++ [ԓ}$ǯՔUӬujk-rvޣ2o]$_ 4_b҉h5jNoZ9u1٩_::< IDAT`҉_:QR. l]LVuwؚYԩ<AArNu{7 Ə{ʆ?1&,2Q#?Ђ<t_O>] ?#NL\ m녱 ýY.#㈽ʗkvӥ36Ś OH#l$G$Ǧj8/xr]*,}gIVQط':d>Up'BR]$ `7GB.rf&X[v=& K S,-L/ #piHBr>} ILNNB`gNŒM}S}.f~zL| mZ9H/IҢCo牭;=GݞAua>fV-Wi]nZ\֥Nj5 $Ao7'2:ʿd@.:8w\v"U1.TYBW\:/.!5=Bޭֆ(`"IzcqN%!^%:~.7pZPA`/Pŝ|\]Z觵oےBr /~~V5D.( '}<9|,|gKG%3sHȦPEm+7-#b5휛맵mӼeRF&02Q:M fiiKodۯ(/̤rro?P9  /I> 삻3S'y7s6 n5$5= V_?LLPwlбƓ{ι9& Ph:6 xtc{R :n{5: XQPރ}=51~1y-11bM5{"tβ d~rg3s9wf9ʗMϦlV8yxQ\5Fr}2Usyu>uqt+YC+7ғt\@ $m*Z!\Wk64:p5*dBaah/ER2XvGveִAWOQ39y&5}+p^Dwy]SB)l^CږPLӝΎDF'p[E+gi$BGqQn\CԨ vh33x(+U'p-_CPZslx Kd 01 VDZ3# @ (ؚy`K5Sr@ ?"+aє-Ʋ5'@P)R˚@ tIJ@ a @ " qЪޛV^] TLm^L ?CmX .;[[w2rP_iX2V}=YAA#گ;~qW*f:mT.)_[yjr:}g{-ښ(h mT::X@PHR>ѕ?^YL^yC>\_i$$wzl)W(@ m@.ӫs3T*oN"Dq/9#9\ǟ86/hR8uRʔ*SPk޴:? Zɪ0aT7uV~mX~Lll<-4DXbaY[Cre' Q1 @M3vٯmzڊ#~ŸOW0wSFOYJBb*~Mj0OkyRsMVmtcl.b\~I^v|Q_7FDcu|I?,= 9Nf!a4.i6@ TWf OkMϣW ߎ}tz:_~2:|ݾV <2\ 'uIzXx,wѮEý~( ] 4nS}c9eGtam؁6~MЫ:q`Ȯ8990lbʖvcִA<\2 \ݐi6-%BϺUQm:侴t ~Td5zd`k\}3u4vpU}0Ϊx! 8wNMWn9ČIxoh}:_|2l jiXf/Q^&yo>ަUZLUWy|Гekr+!5}+=gkk%ГvHRl6I ʋaSŻ ?[3'%E%Y@NMhT T5|ˣP@ozekѷтk.~aBE^_mچ3dff?s{tl}-`mmB.g,a70MjpwƊv-jvp/r|oX{qEs^Q1 9y;^X@ i _ύ(r2HHñ;"ptԴ #1~dy(2&5yMG~GF'T)^G^`>6 eKJS- ~t[*T ; v`"#!8 p_Gd߾Fw&}.PRPZb SRqvvx^TxW̅yyJrq&44YʕJ3nxg|DUjު^ѬܘD4L*U0.UǒzëEtY^b\ҤQX4$LeӤAF/cŽ+RGn͹ҷۄGTTlsSHh$ժ˗V:+ s3?zwr_*56ֹ+i$%Qسn,?RRU&5(JMju&`NT([ |iݼVtI[U'+t]-9Kׂ֡ OI][.YQ, ˟3܀tИp.]:4& a~'#5W9J/f0p>EnS<=JV O|ύ?\w׷yzEW)OwJ>79%TzvnFI7gԠ}˺HR+8$wԧ3N9}JtšWگժI_:B? CNMiۢ6%SA@ 6$g~>|p}l>hHuY2o ic3R2XvGvί0t:n`wY4tekҶ#oH_w3i$Glf.>Ei:I 44kнcܹN+@Y> jx4e=? C |0t:RS*\"n8kOdƙp`.F'̘ԗ|ύ{NqM `H0kR?NWf|6w=r0jIKR7[ 'vߏW+/Vn, ̮}g_H|Peyw6-w_v"_u„׷xK]8qM EZ@ #;q֤Sw˙Afˠ<ӟ s){@ -ض7!YXDX>i5 -Rs$:GrM¼?%6йZ-8?OK L1gz=;͸eHHΑRz^W[k|6/A7:Maٞ|>3MҥP?]i 振7]~ASwThlK/vo|P@ '`Zs5W5b)NL-OTJC``e(xNiAt^Fg`۞4ƿ_e z^l}A!ĩ%*l&KOY˗1IZwVo]HVY ƁMYBO֍m7_R8zZN6zOL1d8"Lku|C GӢo4g?&:.;V`ph&Gj 읚q(o|#g27>=">*݇KLL9$&="<,2=D⋅2'%18U:{E.DDk69v6gqsK*COoȿcߏ!5=exzGqH4H2A1 ;CY-c3#'PMcNuh>$c},YYz-Ohb8BGq0>t/eϹǬޚϲpxb?Rsa&臭g2eA/@ ( K0g ?,?%<Ξvdi^(p~jUʍƀBGt-^M_ wѯgbc~*-[3z#;z_)CU|78wũR Ow.u;}1W9+猁iDzvt`Jw>BüIѭ̜ɼ骭}M͒/|n ԰rK:!,劽b*QRNJ'kj:hFRog+cdgO!2K?8md&vqu2Ҭ8N'#Mej&e_!NCcjY'6͒Rؾ5ϢWG 9xRńNT,E#[fj}:uvuk*W2u33.M35n$?vNM9co+`t:W{ʖRҰ5;8[{{ױNN8;f~LgG԰-_kF,r׊)fWq*&\έ^V=xGkIM12OtOwm1fF!7.,X LK!84lޭJ[%~k4Ʃr,)'+KOt򥟛z)wjeǮQ, jв-kܘ/R%re"%E)K+cՁxyѩaIwT NrZÝP*8ڼ I:s{_hմqKDueνpmdI __`H"V.rԵ 5Xak#n i" :VK|"bT*4eZ6᫩.ps蔊V&Kn3YҷIҿ{ה%g/i3#/>r[;{CKO~R97uuA/@ (JotþqNڃ>8jǦ|'L:.|TrSA6e'l^-%YaFSkhC9OS{+hH<0'yqq&Wd2hԆcT?AfvinTV5638pqjJ}Ʉ?ـ-~ﶶ 'EV_,H?́  I: H'.BrӕA~+ {;9ji>NdڞS]O'\2͟%ddh^R!__@P5r) -?%,Ɇ}So1/ҬiS;x{`7s3ԅF9|Y.ѱ~N79qq2WtPKMӱ/{t5`c-ms;nH&4,>m"ǑEkS8~^MXdvLknjNA*`pUcZѣ=r=@?ң[Ҥ 'K_릶k9t*NLeU?)7L#9U!NZgͶ .为ٱ?8-e1*|X/f؜;lynrGɾ*$&$@:OfL.\hYRSE\BڧUt/ѿxȢ]KT`ΐ:-?eJ:Ɇ}^ƹ|ḋyîe~?[t+J:>Z^9ZB&@IDATK᷃**URy91Lˊ9qh V H"!IKi%-`ujX#W@Vԯif&l;ř`u2nJg聎jۈ& +;-_.U2fF1óxeK=A?~IQo6li,ϾWI^K "I&(p,6w[®_AS@ <_L 0qʙn3OIENDB`crokey-1.0.1/examples/README.md000064400000000000000000000017161046102023000142210ustar 00000000000000 # Crokey Examples To run an example, `cd` to its directory then do `cargo run`. ## deser_keybindings Shows how a set of key-bindings can be read from JSON (might have been TOML, Hjson, YAML, etc.) and the action executed when the user presses the relevant key combination. ## print_key Shows how a combiner transforms crossterm key events into key combinations. The `Combiner` is configured to recognize combinations which aren't normally available, when the terminal supports the Kitty Keyboard protocol. When using a combiner, key combinations involving a modifier (ctrl, alt, shift, space) are detected on key release. ## print_key_no_combiner Similar to print_key, but simpler, uses no `Combiner`. Key combinations which are standard on ANSI terminals are handled, but the capabilities of more modern terminals won't be used and you won't get combinations like `ctrl-a-b`, or `space-n`. When not using a combiner, all combinations are detected on key press. crokey-1.0.1/src/combiner.rs000064400000000000000000000205001046102023000140470ustar 00000000000000use { crate::*, crossterm::{ event::{ KeyCode, KeyEvent, KeyboardEnhancementFlags, KeyEventKind, KeyModifiers, ModifierKeyCode, PopKeyboardEnhancementFlags, PushKeyboardEnhancementFlags, }, execute, terminal, }, std::{ io, ops::Drop, }, }; /// This is the maximum number of keys we can combine. /// It can't be changed just here, as the KeyCombination type doesn't support /// more than 3 non-modifier keys const MAX_PRESS_COUNT: usize = 3; /// Consumes key events and combines them into key combinations. /// /// See the print_key_events example. #[derive(Debug)] pub struct Combiner { combining: bool, keyboard_enhancement_flags_pushed: bool, keyboard_enhancement_flags_externally_managed: bool, mandate_modifier_for_multiple_keys: bool, down_keys: Vec, shift_pressed: bool, } impl Default for Combiner { fn default() -> Self { Self { combining: false, keyboard_enhancement_flags_pushed: false, keyboard_enhancement_flags_externally_managed: false, mandate_modifier_for_multiple_keys: true, down_keys: Vec::new(), shift_pressed: false, } } } impl Combiner { /// Try to enable combining more than one non-modifier key into a combination. /// /// Return Ok(false) when the terminal doesn't support the kitty protocol. /// /// Behind the scene, this function pushes the keyboard enhancement flags /// to the terminal. The flags are popped, and the normal state of the terminal /// restored, when the Combiner is dropped. /// /// This function does nothing if combining is already enabled. pub fn enable_combining(&mut self) -> io::Result { if self.combining { return Ok(true); } if !self.keyboard_enhancement_flags_externally_managed { if self.keyboard_enhancement_flags_pushed { return Ok(self.combining); } if !terminal::supports_keyboard_enhancement()? { return Ok(false); } push_keyboard_enhancement_flags()?; self.keyboard_enhancement_flags_pushed = true; } self.combining = true; Ok(true) } /// Disable combining. pub fn disable_combining(&mut self) -> io::Result<()> { if !self.keyboard_enhancement_flags_externally_managed && self.keyboard_enhancement_flags_pushed { pop_keyboard_enhancement_flags()?; self.keyboard_enhancement_flags_pushed = false; } self.combining = false; Ok(()) } /// Tell the Combiner not to push/pop the keyboard enhancement flags. /// /// Call before enable_combining if you want to manage the flags yourself. /// (for example: if you need to use stderr instead of stdout in crossterm::execute!) pub fn set_keyboard_enhancement_flags_externally_managed(&mut self) { self.keyboard_enhancement_flags_externally_managed = true; } pub fn is_combining(&self) -> bool { self.combining } /// When combining is enabled, you may either want "simple" keys /// (i.e. without modifier or space) to be handled on key press, /// or to wait for a key release so that maybe they may /// be part of a combination like 'a-b'. /// If combinations without modifier or space are unlikely in your application, you /// may make it feel snappier by setting this to true. /// /// This setting has no effect when combining isn't enabled. pub fn set_mandate_modifier_for_multiple_keys(&mut self, mandate: bool) { self.mandate_modifier_for_multiple_keys = mandate; } /// Take all the down_keys, combine them into a KeyCombination fn combine(&mut self, clear: bool) -> Option { let mut key_combination = KeyCombination::try_from(self.down_keys.as_slice()) .ok(); // it may be empty, in which case we return None if self.shift_pressed { if let Some(ref mut key_combination) = key_combination { key_combination.modifiers |= KeyModifiers::SHIFT; } } if clear { self.down_keys.clear(); self.shift_pressed = false; } key_combination } /// Receive a key event and return a key combination if one is ready. /// /// When combining is enabled, the key combination is only returned on a /// key release event. pub fn transform(&mut self, key: KeyEvent) -> Option { if self.combining { self.transform_combining(key) } else { self.transform_ansi(key) } } fn transform_combining(&mut self, key: KeyEvent) -> Option { if let KeyCode::Modifier(modifier) = key.code { if modifier == ModifierKeyCode::LeftShift || modifier == ModifierKeyCode::RightShift { self.shift_pressed = key.kind != KeyEventKind::Release; } // we ignore modifier keys as independent events // (which means we never return a combination with only modifiers) return None; } if self.mandate_modifier_for_multiple_keys && is_key_simple(key) && !self.shift_pressed && self.down_keys.is_empty() { // "simple key" are handled differently: they're returned on press and repeat match key.kind { KeyEventKind::Press | KeyEventKind::Repeat => { self.down_keys.push(key); self.combine(true) } KeyEventKind::Release => { None } } } else { // not a single simple key match key.kind { KeyEventKind::Press => { self.down_keys.push(key); if self.down_keys.len() == MAX_PRESS_COUNT { self.combine(true) } else { None } } KeyEventKind::Release => { // this release ends the combination in progress self.combine(true) } KeyEventKind::Repeat => { self.combine(false) } } } } /// In ansi mode, no combination is possible, and we don't expect to /// receive anything else than a single key or than key presses. fn transform_ansi(&mut self, key: KeyEvent) -> Option { match key.kind { KeyEventKind::Press => Some(key.into()), _ => { // this is unexpected, we don't seem to be really in ansi mode // but for consistency we must filter out this event None } } } } /// For the purpose of key combination, we consider that a key is "simple" /// when it's neither a modifier (ctrl,alt,shift) nor a space. pub fn is_key_simple(key: KeyEvent) -> bool { key.modifiers.is_empty() && key.code != KeyCode::Char(' ') } impl Drop for Combiner { fn drop(&mut self) { if self.keyboard_enhancement_flags_pushed { let _ = pop_keyboard_enhancement_flags(); } } } /// Change the state of the terminal to enable combining keys. /// This is done automatically by Combiner::enable_combining /// so you should usually not need to call this function. pub fn push_keyboard_enhancement_flags() -> io::Result<()> { let mut stdout = io::stdout(); execute!( stdout, PushKeyboardEnhancementFlags( KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES | KeyboardEnhancementFlags::REPORT_ALL_KEYS_AS_ESCAPE_CODES | KeyboardEnhancementFlags::REPORT_ALTERNATE_KEYS | KeyboardEnhancementFlags::REPORT_EVENT_TYPES ) ) } /// Restore the "normal" state of the terminal. /// This is done automatically by the combiner on drop, /// so you should usually not need to call this function. pub fn pop_keyboard_enhancement_flags() -> io::Result<()>{ let mut stdout = io::stdout(); execute!(stdout, PopKeyboardEnhancementFlags) } crokey-1.0.1/src/format.rs000064400000000000000000000122111046102023000135410ustar 00000000000000//! Crokey helps incorporate configurable keybindings in [crossterm](https://github.com/crossterm-rs/crossterm) //! based terminal applications by providing functions //! - parsing key combinations from strings //! - describing key combinations in strings use { crate::KeyCombination, crossterm::event::{KeyCode::*, KeyModifiers}, std::fmt, }; /// A formatter to produce key combinations descriptions. /// /// ``` /// use { /// crokey::*, /// crossterm::event::{ /// KeyCode, /// KeyEvent, /// KeyModifiers, /// }, /// }; /// /// let format = KeyCombinationFormat::default(); /// assert_eq!(format.to_string(key!(shift-a)), "Shift-a"); /// assert_eq!(format.to_string(key!(ctrl-c)), "Ctrl-c"); /// /// // A more compact format /// let format = KeyCombinationFormat::default() /// .with_implicit_shift() /// .with_control("^"); /// assert_eq!(format.to_string(key!(shift-a)), "A"); /// assert_eq!(format.to_string(key!(ctrl-c)), "^c"); /// /// // A long format with lowercased modifiers /// let format = KeyCombinationFormat::default() /// .with_lowercase_modifiers(); /// assert_eq!(format.to_string(key!(ctrl-enter)), "ctrl-Enter"); /// assert_eq!(format.to_string(key!(home)), "Home"); /// assert_eq!( /// format.to_string( /// KeyCombination::new( /// KeyCode::F(6), /// KeyModifiers::ALT, /// ) /// ), /// "alt-F6", /// ); /// assert_eq!( /// format.to_string( /// KeyCombination::new( /// (KeyCode::Char('u'), KeyCode::Char('i')), /// KeyModifiers::NONE, /// ) /// ), /// "i-u", /// ); /// /// ``` #[derive(Debug, Clone)] pub struct KeyCombinationFormat { pub control: String, pub alt: String, pub shift: String, pub enter: String, pub uppercase_shift: bool, pub key_separator: String, } impl Default for KeyCombinationFormat { fn default() -> Self { Self { control: "Ctrl-".to_string(), alt: "Alt-".to_string(), shift: "Shift-".to_string(), enter: "Enter".to_string(), uppercase_shift: false, key_separator: "-".to_string(), } } } impl KeyCombinationFormat { pub fn with_lowercase_modifiers(mut self) -> Self { self.control = self.control.to_lowercase(); self.alt = self.alt.to_lowercase(); self.shift = self.shift.to_lowercase(); self } pub fn with_control>(mut self, s: S) -> Self { self.control = s.into(); self } pub fn with_alt>(mut self, s: S) -> Self { self.alt = s.into(); self } pub fn with_shift>(mut self, s: S) -> Self { self.shift = s.into(); self } pub fn with_implicit_shift(mut self) -> Self { self.shift = "".to_string(); self.uppercase_shift = true; self } /// return a wrapper of the key implementing Display /// /// ``` /// use crokey::*; /// let format = KeyCombinationFormat::default(); /// let k = format.format(key!(f6)); /// let s = format!("k={}", k); /// assert_eq!(s, "k=F6"); /// ``` pub fn format>(&self, key: K) -> FormattedKeyCombination { FormattedKeyCombination { format: self, key: key.into() } } /// return the key formatted into a string /// /// `format.to_string(key)` is equivalent to `format.format(key).to_string()`. pub fn to_string>(&self, key: K) -> String { self.format(key).to_string() } } pub struct FormattedKeyCombination<'s> { format: &'s KeyCombinationFormat, key: KeyCombination, } impl<'s> fmt::Display for FormattedKeyCombination<'s> { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let format = &self.format; let key = &self.key; if key.modifiers.contains(KeyModifiers::CONTROL) { write!(f, "{}", format.control)?; } if key.modifiers.contains(KeyModifiers::ALT) { write!(f, "{}", format.alt)?; } if key.modifiers.contains(KeyModifiers::SHIFT) { write!(f, "{}", format.shift)?; } for (i, code) in key.codes.iter().enumerate() { if i > 0 { write!(f, "{}", format.key_separator)?; } match code { Char(' ') => { write!(f, "Space")?; } Char('-') => { write!(f, "Hyphen")?; } Char('\r') | Char('\n') | Enter => { write!(f, "{}", format.enter)?; } Char(c) if key.modifiers.contains(KeyModifiers::SHIFT) && format.uppercase_shift => { write!(f, "{}", c.to_ascii_uppercase())?; } Char(c) => { write!(f, "{}", c.to_ascii_lowercase())?; } F(u) => { write!(f, "F{u}")?; } _ => { write!(f, "{:?}", code)?; } } } Ok(()) } } crokey-1.0.1/src/key_combination.rs000064400000000000000000000133631046102023000154340ustar 00000000000000use { super::*, crossterm::event::{ KeyEvent, KeyEventKind, KeyEventState, }, std::{ fmt, str::FromStr, }, strict::OneToThree, }; #[cfg(feature = "serde")] use serde::{ de, Deserialize, Deserializer, Serialize, Serializer, }; /// A Key combination wraps from one to three standard keys with optional modifiers /// (ctrl, alt, shift). #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] pub struct KeyCombination { pub codes: OneToThree, pub modifiers: KeyModifiers, } /// Change the char to uppercase when the modifier shift is present, /// otherwise if the char is uppercase, return true. /// If the key is the `\r' or '\n' char, change it to KeyCode::Enter. fn normalize_key_code(code: &mut KeyCode, modifiers: KeyModifiers) -> bool { if matches!(code, KeyCode::Char('\r') | KeyCode::Char('\n')) { *code = KeyCode::Enter; } else if modifiers.contains(KeyModifiers::SHIFT) { if let KeyCode::Char(c) = code { if c.is_ascii_lowercase() { *code = KeyCode::Char(c.to_ascii_uppercase()); } } } else if let KeyCode::Char(c) = code { if c.is_ascii_uppercase() { return true; } } false } impl KeyCombination { /// Create a new KeyCombination from one to three keycodes and a set of modifiers pub fn new>>(codes: C, modifiers: KeyModifiers) -> Self { let codes = codes.into().sorted(); Self { codes, modifiers } } /// Create a new KeyCombination from one keycode and a set of modifiers pub const fn one_key(code: KeyCode, modifiers: KeyModifiers) -> Self { let codes = OneToThree::One(code); Self { codes, modifiers } } /// Ansi terminals don't manage key press/release/repeat, so they /// don't allow to determine whether 2 keys are pressed at the same /// time. This means a combination involving several key codes can't /// be distiguished from a sequences of combinations involving a single key code. /// For this reason, only combinations involving a single key code are /// considered "ansi compatible" pub const fn is_ansi_compatible(self) -> bool { matches!(self.codes, OneToThree::One(_)) } /// Return a normailzed version of the combination. /// /// Fix the case of the code to uppercase if the shift modifier is present. /// Add the SHIFT modifier if one code is uppercase. /// /// This allows direct comparisons with the fields of crossterm::event::KeyEvent /// whose code is uppercase when the shift modifier is present. And supports the /// case where the modifier isn't mentionned but the key is uppercase. pub fn normalized(mut self) -> Self { let mut shift = normalize_key_code(self.codes.first_mut(), self.modifiers); if let Some(ref mut code) = self.codes.get_mut(1) { shift |= normalize_key_code(code, self.modifiers); } if let Some(ref mut code) = self.codes.get_mut(2) { shift |= normalize_key_code(code, self.modifiers); } if shift { self.modifiers |= KeyModifiers::SHIFT; } self } /// return the raw char if the combination is a letter event pub const fn as_letter(self) -> Option { match self { Self { codes: OneToThree::One(KeyCode::Char(l)), modifiers: KeyModifiers::NONE, } => Some(l), _ => None, } } } #[cfg(feature = "serde")] impl<'de> Deserialize<'de> for KeyCombination { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { let s = String::deserialize(deserializer)?; FromStr::from_str(&s).map_err(de::Error::custom) } } #[cfg(feature = "serde")] impl Serialize for KeyCombination { fn serialize(&self, serializer: S) -> Result where S: Serializer, { serializer.serialize_str(&self.to_string()) } } impl FromStr for KeyCombination { type Err = ParseKeyError; fn from_str(s: &str) -> Result { parse(s) } } impl fmt::Display for KeyCombination { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { STANDARD_FORMAT.format(*self).fmt(f) } } impl From for KeyCombination { fn from(key_event: KeyEvent) -> Self { let raw = Self { codes: key_event.code.into(), modifiers: key_event.modifiers, }; raw.normalized() } } impl TryFrom<&[KeyEvent]> for KeyCombination { type Error = &'static str; /// Try to create a KeyCombination from a slice of key events, /// will fail if and only if the slice is empty. fn try_from(key_events: &[KeyEvent]) -> Result { let mut modifiers = KeyModifiers::empty(); let mut codes = Vec::new(); for key_event in key_events { modifiers |= key_event.modifiers; codes.push(key_event.code); } let codes: OneToThree = codes.try_into()?; let raw = Self::new(codes, modifiers); Ok(raw.normalized()) } } impl From for KeyCombination { fn from(key_code: KeyCode) -> Self { Self { codes: key_code.into(), modifiers: KeyModifiers::empty(), } } } #[allow(clippy::from_over_into)] impl Into for KeyCombination { fn into(self) -> KeyEvent { let Self { codes, modifiers } = self; KeyEvent { code: *codes.first(), modifiers, kind: KeyEventKind::Press, // the only one in ANSI terminals state: KeyEventState::empty(), } } } crokey-1.0.1/src/key_event.rs000064400000000000000000000006761046102023000142560ustar 00000000000000use { crossterm::event::{KeyCode, KeyEvent, KeyModifiers}, }; /// Return the raw char if the crossterm key event is a letter event. /// /// Case of the code is not normalized, just as in the original event. pub const fn as_letter(key: KeyEvent) -> Option { match key { KeyEvent { code: KeyCode::Char(l), modifiers: KeyModifiers::NONE, .. } => Some(l), _ => None, } } crokey-1.0.1/src/lib.rs000064400000000000000000000267601046102023000130350ustar 00000000000000//! Crokey helps incorporate configurable keybindings in [crossterm](https://github.com/crossterm-rs/crossterm) //! based terminal applications by providing functions //! - parsing key combinations from strings //! - describing key combinations in strings //! - parsing key combinations at compile time //! - combining Crossterm key events in key combinations //! //! ## The KeyCombination //! //! A `KeyCombination` is made of 1 to 3 "normal" keys with some optional modifiers (alt, shift, ctrl). //! //! It can be parsed, ergonomically built with the `key!` macro, obtained from key events. //! //! ## The Combiner //! //! With a `Combiner`, you can change raw Crossterm key events into key combinations. //! //! When the terminal is modern enough and supports the Kitty protocol, complex combinations with up to three non-modifier keys may be formed, for example `Ctrl-Alt-Shift-g-y` or `i-u`. //! //! For standard ANSI terminals, only regular combinations are available, like `Shift-o`, `Ctrl-Alt-Shift-g` or `i`. //! //! The combiner works in both cases: //! if you presses the `ctrl`, `i`, and `u ` keys at the same time, it will result in one combination (`ctrl-i-u`) on a kitty-compatible terminal, and as a sequence of 2 key combinations (`ctrl-i` then `ctrl-u` assuming you started pressing the `i` before the `u`) in other terminals. //! //! //! The `print_key` example shows how to use the combiner. //! //! ```no_run //! # use { //! # crokey::*, //! # crossterm::{ //! # event::{read, Event}, //! # style::Stylize, //! # terminal, //! # }, //! # }; //! let fmt = KeyCombinationFormat::default(); //! let mut combiner = Combiner::default(); //! let combines = combiner.enable_combining().unwrap(); //! if combines { //! println!("Your terminal supports combining keys"); //! } else { //! println!("Your terminal doesn't support combining non-modifier keys"); //! } //! println!("Type any key combination"); //! loop { //! terminal::enable_raw_mode().unwrap(); //! let e = read(); //! terminal::disable_raw_mode().unwrap(); //! match e { //! Ok(Event::Key(key_event)) => { //! if let Some(key_combination) = combiner.transform(key_event) { //! match key_combination { //! key!(ctrl-c) | key!(ctrl-q) => { //! println!("quitting"); //! break; //! } //! _ => { //! println!("You typed {}", fmt.to_string(key_combination)); //! } //! } //! } //! }, //! _ => {} //! } //! } //! ``` //! //! ## Parse a string //! //! Those strings are usually provided by a configuration file. //! //! ``` //! use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; //! assert_eq!( //! crokey::parse("alt-enter").unwrap(), //! KeyEvent::new(KeyCode::Enter, KeyModifiers::ALT).into(), //! ); //! assert_eq!( //! crokey::parse("shift-F6").unwrap(), //! KeyEvent::new(KeyCode::F(6), KeyModifiers::SHIFT).into(), //! ); //! ``` //! //! ## Use key event "literals" thanks to procedural macros //! //! Those key events are parsed at compile time and have zero runtime cost. //! //! They're efficient and convenient for matching events or defining hardcoded keybindings. //! //! ```no_run //! # use crokey::*; //! # use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; //! # use crossterm::style::Stylize; //! # let key_event = key!(a); //! let fmt = KeyCombinationFormat::default(); //! # loop { //! match key_event { //! key!(ctrl-c) => { //! println!("Arg! You savagely killed me with a {}", fmt.to_string(key_event).red()); //! break; //! } //! key!(ctrl-q) => { //! println!("You typed {} which gracefully quits", fmt.to_string(key_event).green()); //! break; //! } //! _ => { //! println!("You typed {}", fmt.to_string(key_event).blue()); //! } //! } //! # } //! ``` //! Complete example in `/examples/print_key` //! //! ## Display a string with a configurable format //! //! ``` //! use crokey::*; //! use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; //! //! // The default format //! let format = KeyCombinationFormat::default(); //! assert_eq!(format.to_string(key!(shift-a)), "Shift-a"); //! assert_eq!(format.to_string(key!(ctrl-c)), "Ctrl-c"); //! //! // A more compact format //! let format = KeyCombinationFormat::default() //! .with_implicit_shift() //! .with_control("^"); //! assert_eq!(format.to_string(key!(shift-a)), "A"); //! assert_eq!(format.to_string(key!(ctrl-c)), "^c"); //! ``` //! //! ## Deserialize keybindings using Serde //! //! With the "serde" feature enabled, you can read configuration files in a direct way: //! //! ``` //! use { //! crokey::*, //! crossterm::event::KeyEvent, //! serde::Deserialize, //! std::collections::HashMap, //! }; //! #[derive(Debug, Deserialize)] //! struct Config { //! keybindings: HashMap, //! } //! static CONFIG_HJSON: &str = r#" //! { //! keybindings: { //! a: aardvark //! shift-b: babirussa //! ctrl-k: koala //! alt-j: jaguar //! } //! } //! "#; //! let config: Config = deser_hjson::from_str(CONFIG_HJSON).unwrap(); //! let key: KeyCombination = key!(shift-b); //! assert_eq!( //! config.keybindings.get(&key).unwrap(), //! "babirussa", //! ); //! ``` //! //! Instead of Hjson, you can use any Serde compatible format such as JSON or TOML. //! mod combiner; mod format; mod key_event; mod parse; mod key_combination; pub use { combiner::*, crossterm, format::*, key_event::*, parse::*, key_combination::*, strict::OneToThree, }; use { crossterm::event::{KeyCode, KeyModifiers}, once_cell::sync::Lazy, }; /// A lazy initialized KeyCombinationFormat which can be considered as standard /// and which is used in the Display implementation of the [KeyCombination] type. pub static STANDARD_FORMAT: Lazy = Lazy::new(KeyCombinationFormat::default); /// check and expand at compile-time the provided expression /// into a valid KeyCombination. /// /// /// For example: /// ``` /// # use crokey::key; /// let key_event = key!(ctrl-c); /// ``` /// is expanded into (roughly): /// /// ``` /// let key_event = crokey::KeyCombination { /// modifiers: crossterm::event::KeyModifiers::CONTROL, /// codes: crokey::OneToThree::One(crossterm::event::KeyCode::Char('c')), /// }; /// ``` /// /// Keys which can't be valid identifiers or digits in Rust must be put between simple quotes: /// ``` /// # use crokey::key; /// let ke = key!(shift-'?'); /// let ke = key!(alt-']'); /// ``` #[macro_export] macro_rules! key { ($($tt:tt)*) => { $crate::__private::key!(($crate) $($tt)*) }; } // Not public API. This is internal and to be used only by `key!`. #[doc(hidden)] pub mod __private { pub use crokey_proc_macros::key; pub use crossterm; pub use strict::OneToThree; use crossterm::event::KeyModifiers; pub const MODS: KeyModifiers = KeyModifiers::NONE; pub const MODS_CTRL: KeyModifiers = KeyModifiers::CONTROL; pub const MODS_ALT: KeyModifiers = KeyModifiers::ALT; pub const MODS_SHIFT: KeyModifiers = KeyModifiers::SHIFT; pub const MODS_CTRL_ALT: KeyModifiers = KeyModifiers::CONTROL.union(KeyModifiers::ALT); pub const MODS_ALT_SHIFT: KeyModifiers = KeyModifiers::ALT.union(KeyModifiers::SHIFT); pub const MODS_CTRL_SHIFT: KeyModifiers = KeyModifiers::CONTROL.union(KeyModifiers::SHIFT); pub const MODS_CTRL_ALT_SHIFT: KeyModifiers = KeyModifiers::CONTROL .union(KeyModifiers::ALT) .union(KeyModifiers::SHIFT); } #[cfg(test)] mod tests { use { crate::{key, KeyCombination, OneToThree}, crossterm::event::{KeyCode, KeyModifiers}, }; const _: () = { key!(x); key!(ctrl - '{'); key!(alt - '{'); key!(shift - '{'); key!(ctrl - alt - f10); key!(alt - shift - f10); key!(ctrl - shift - f10); key!(ctrl - alt - shift - enter); }; fn no_mod(code: KeyCode) -> KeyCombination { code.into() } #[test] fn key() { assert_eq!(key!(backspace), no_mod(KeyCode::Backspace)); assert_eq!(key!(bAcKsPaCe), no_mod(KeyCode::Backspace)); assert_eq!(key!(0), no_mod(KeyCode::Char('0'))); assert_eq!(key!(9), no_mod(KeyCode::Char('9'))); assert_eq!(key!('x'), no_mod(KeyCode::Char('x'))); assert_eq!(key!('X'), no_mod(KeyCode::Char('x'))); assert_eq!(key!(']'), no_mod(KeyCode::Char(']'))); assert_eq!(key!('ඞ'), no_mod(KeyCode::Char('ඞ'))); assert_eq!(key!(f), no_mod(KeyCode::Char('f'))); assert_eq!(key!(F), no_mod(KeyCode::Char('f'))); assert_eq!(key!(ඞ), no_mod(KeyCode::Char('ඞ'))); assert_eq!(key!(f10), no_mod(KeyCode::F(10))); assert_eq!(key!(F10), no_mod(KeyCode::F(10))); assert_eq!( key!(ctrl - c), KeyCombination::new(KeyCode::Char('c'), KeyModifiers::CONTROL) ); assert_eq!( key!(alt - shift - c), KeyCombination::new(KeyCode::Char('C'), KeyModifiers::ALT | KeyModifiers::SHIFT) ); assert_eq!(key!(shift - alt - '2'), key!(ALT - SHIFT - 2)); assert_eq!(key!(space), key!(' ')); assert_eq!(key!(hyphen), key!('-')); assert_eq!(key!(minus), key!('-')); assert_eq!( key!(ctrl-alt-a-b), KeyCombination::new( OneToThree::Two(KeyCode::Char('a'), KeyCode::Char('b')), KeyModifiers::CONTROL | KeyModifiers::ALT, ) ); assert_eq!( key!(alt-f4-a-b), KeyCombination::new( OneToThree::Three(KeyCode::F(4), KeyCode::Char('a'), KeyCode::Char('b')), KeyModifiers::ALT, ) ); assert_eq!( // check that key codes are sorted key!(alt-a-b-f4), KeyCombination::new( OneToThree::Three(KeyCode::F(4), KeyCode::Char('a'), KeyCode::Char('b')), KeyModifiers::ALT, ) ); assert_eq!( key!(z-e), KeyCombination::new( OneToThree::Two(KeyCode::Char('e'), KeyCode::Char('z')), KeyModifiers::NONE, ) ); } #[test] fn format() { let format = crate::KeyCombinationFormat::default(); assert_eq!(format.to_string(key!(insert)), "Insert"); assert_eq!(format.to_string(key!(space)), "Space"); assert_eq!(format.to_string(key!(alt-Space)), "Alt-Space"); assert_eq!(format.to_string(key!(shift-' ')), "Shift-Space"); assert_eq!(format.to_string(key!(alt-hyphen)), "Alt-Hyphen"); } #[test] fn key_pattern() { assert!(matches!(key!(ctrl-alt-shift-c), key!(ctrl-alt-shift-c))); assert!(!matches!(key!(ctrl-c), key!(ctrl-alt-shift-c))); assert!(matches!(key!(ctrl-alt-b), key!(ctrl-alt-b))); assert!(matches!(key!(ctrl-b), key!(ctrl-b))); assert!(matches!(key!(alt-b), key!(alt-b))); assert!(!matches!(key!(ctrl-b), key!(alt-b))); assert!(!matches!(key!(alt-b), key!(ctrl-b))); assert!(!matches!(key!(alt-b), key!(ctrl-alt-b))); assert!(!matches!(key!(ctrl-b), key!(ctrl-alt-b))); assert!(!matches!(key!(ctrl-alt-b), key!(alt-b))); assert!(!matches!(key!(ctrl-alt-b), key!(ctrl-b))); } #[test] fn ui() { trybuild::TestCases::new().compile_fail("tests/ui/*.rs"); } } crokey-1.0.1/src/parse.rs000064400000000000000000000142451046102023000133740ustar 00000000000000//! Crokey helps incorporate configurable keybindings in [crossterm](https://github.com/crossterm-rs/crossterm) //! based terminal applications by providing functions //! - parsing key combinations from strings //! - describing key combinations in strings use { crate::{ OneToThree, KeyCombination, }, crossterm::event::{ KeyCode::{self, *}, KeyModifiers, }, std::fmt, }; #[derive(Debug)] pub struct ParseKeyError { /// the string which couldn't be parsed pub raw: String, } impl ParseKeyError { pub fn new>(s: S) -> Self { Self { raw: s.into() } } } impl fmt::Display for ParseKeyError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{:?} can't be parsed as a key", self.raw) } } impl std::error::Error for ParseKeyError {} pub fn parse_key_code(raw: &str, shift: bool) -> Result { let code = match raw { "esc" => Esc, "enter" => Enter, "left" => Left, "right" => Right, "up" => Up, "down" => Down, "home" => Home, "end" => End, "pageup" => PageUp, "pagedown" => PageDown, "backtab" => BackTab, "backspace" => Backspace, "del" => Delete, "delete" => Delete, "insert" => Insert, "ins" => Insert, "f1" => F(1), "f2" => F(2), "f3" => F(3), "f4" => F(4), "f5" => F(5), "f6" => F(6), "f7" => F(7), "f8" => F(8), "f9" => F(9), "f10" => F(10), "f11" => F(11), "f12" => F(12), "space" => Char(' '), "hyphen" => Char('-'), "minus" => Char('-'), "tab" => Tab, c if c.len() == 1 => { let mut c = c.chars().next().unwrap(); if shift { c = c.to_ascii_uppercase(); } Char(c) } _ => { return Err(ParseKeyError::new(raw)); } }; Ok(code) } /// parse a string as a keyboard key combination definition. /// /// About the case: /// The char we receive as code from crossterm is usually lowercase /// but uppercase when it was typed with shift (i.e. we receive /// "g" for a lowercase, and "shift-G" for an uppercase) pub fn parse(raw: &str) -> Result { let mut modifiers = KeyModifiers::empty(); let raw = raw.to_ascii_lowercase(); let mut raw: &str = raw.as_ref(); loop { if let Some(end) = raw.strip_prefix("ctrl-") { raw = end; modifiers.insert(KeyModifiers::CONTROL); } else if let Some(end) = raw.strip_prefix("alt-") { raw = end; modifiers.insert(KeyModifiers::ALT); } else if let Some(end) = raw.strip_prefix("shift-") { raw = end; modifiers.insert(KeyModifiers::SHIFT); } else { break; } } let codes = if raw == "-" { OneToThree::One(Char('-')) } else { let mut codes = Vec::new(); let shift = modifiers.contains(KeyModifiers::SHIFT); for raw in raw.split('-') { let code = parse_key_code(raw, shift)?; if code == BackTab { // Crossterm always sends SHIFT with backtab modifiers.insert(KeyModifiers::SHIFT); } codes.push(code); } codes.try_into().map_err(|_| ParseKeyError::new("".to_string()))? }; Ok(KeyCombination::new(codes, modifiers)) } #[test] fn check_key_parsing() { use crate::*; fn check_ok(raw: &str, key: KeyCombination) { let parsed = parse(raw); assert!(parsed.is_ok(), "failed to parse {:?} as key combination", raw); assert_eq!(parsed.unwrap(), key); } assert!(parse("").is_err()); check_ok("left", key!(left)); check_ok("RIGHT", key!(right)); check_ok("Home", key!(HOME)); check_ok( "backtab", KeyCombination::new(KeyCode::BackTab, KeyModifiers::SHIFT), ); check_ok("f1", KeyCombination::from(F(1))); check_ok("F2", KeyCombination::from(F(2))); check_ok("Enter", KeyCombination::from(Enter)); check_ok("alt-enter", KeyCombination::new(Enter, KeyModifiers::ALT)); check_ok("insert", KeyCombination::from(Insert)); check_ok( "ctrl-q", KeyCombination::new(Char('q'), KeyModifiers::CONTROL), ); check_ok( "shift-q", KeyCombination::new(Char('Q'), KeyModifiers::SHIFT), ); check_ok( "ctrl-Q", KeyCombination::new(Char('q'), KeyModifiers::CONTROL), ); check_ok( "shift-Q", KeyCombination::new(Char('Q'), KeyModifiers::SHIFT), ); check_ok( "ctrl-shift-Q", KeyCombination::new(Char('Q'), KeyModifiers::SHIFT | KeyModifiers::CONTROL), ); check_ok("-", KeyCombination::new(Char('-'), KeyModifiers::NONE)); check_ok("Hyphen", KeyCombination::new(Char('-'), KeyModifiers::NONE)); check_ok("alt--", KeyCombination::new(Char('-'), KeyModifiers::ALT)); check_ok( "alt-hyphen", KeyCombination::new(Char('-'), KeyModifiers::ALT), ); check_ok( "alt-hyphen", KeyCombination::new(Char('-'), KeyModifiers::ALT), ); check_ok( "ctrl-Shift-alt-space", KeyCombination::new( Char(' '), KeyModifiers::ALT | KeyModifiers::SHIFT | KeyModifiers::ALT | KeyModifiers::CONTROL, ), ); check_ok( "ctrl-shift-alt--", KeyCombination::new( Char('-'), KeyModifiers::ALT | KeyModifiers::SHIFT | KeyModifiers::ALT | KeyModifiers::CONTROL, ), ); // multiple codes check_ok( "alt-f12-@", KeyCombination::new( OneToThree::Two(F(12), Char('@')), KeyModifiers::ALT, ), ); check_ok( "alt-f12-@", KeyCombination::new( OneToThree::Two(Char('@'), F(12)), // it's the same because the codes are sorted KeyModifiers::ALT, ), ); check_ok( "a-b", KeyCombination::new( OneToThree::Two(Char('a'), Char('b')), KeyModifiers::NONE, ), ); } crokey-1.0.1/tests/hygiene.rs000064400000000000000000000002441046102023000142570ustar 00000000000000#![no_std] #![no_implicit_prelude] #[allow(dead_code)] fn hygiene() { ::crokey::key!(M); ::crokey::key!(ctrl-c); ::crokey::key!(alt-shift-ctrl-']'); } crokey-1.0.1/tests/ui/duplicate-modifier.rs000064400000000000000000000002251046102023000170110ustar 00000000000000fn main() { crokey::key!(ctrl-ctrl-5); crokey::key!(alt-alt-5); crokey::key!(shift-shift-5); crokey::key!(shift-alt-shift-ctrl-5); } crokey-1.0.1/tests/ui/duplicate-modifier.stderr000064400000000000000000000011151046102023000176670ustar 00000000000000error: duplicate modifier ctrl --> tests/ui/duplicate-modifier.rs:2:23 | 2 | crokey::key!(ctrl-ctrl-5); | ^^^^ error: duplicate modifier alt --> tests/ui/duplicate-modifier.rs:3:22 | 3 | crokey::key!(alt-alt-5); | ^^^ error: duplicate modifier shift --> tests/ui/duplicate-modifier.rs:4:24 | 4 | crokey::key!(shift-shift-5); | ^^^^^ error: duplicate modifier shift --> tests/ui/duplicate-modifier.rs:5:28 | 5 | crokey::key!(shift-alt-shift-ctrl-5); | ^^^^^ crokey-1.0.1/tests/ui/invalid-key.rs000064400000000000000000000001371046102023000154610ustar 00000000000000fn main() { crokey::key!(10); crokey::key!(ctrl-backpace); crokey::key!(ctrl--); } crokey-1.0.1/tests/ui/invalid-key.stderr000064400000000000000000000006771046102023000163510ustar 00000000000000error: invalid key; must be between 0-9 --> tests/ui/invalid-key.rs:2:18 | 2 | crokey::key!(10); | ^^ error: unrecognized key code "backpace" --> tests/ui/invalid-key.rs:3:23 | 3 | crokey::key!(ctrl-backpace); | ^^^^^^^^ error: expected one of: character literal, integer literal, identifier --> tests/ui/invalid-key.rs:4:23 | 4 | crokey::key!(ctrl--); | ^ crokey-1.0.1/tests/ui/invalid-modifier.rs000064400000000000000000000000531046102023000164640ustar 00000000000000fn main() { crokey::key!(control-c); } crokey-1.0.1/tests/ui/invalid-modifier.stderr000064400000000000000000000002201046102023000173370ustar 00000000000000error: unrecognized key code "control" --> tests/ui/invalid-modifier.rs:2:18 | 2 | crokey::key!(control-c); | ^^^^^^^ crokey-1.0.1/tests/ui/unexpected-eof.rs000064400000000000000000000001231046102023000161530ustar 00000000000000fn main() { crokey::key!(); crokey::key!(ctrl); crokey::key!(ctrl-); } crokey-1.0.1/tests/ui/unexpected-eof.stderr000064400000000000000000000020661046102023000170420ustar 00000000000000error: unexpected end of input, expected one of: character literal, integer literal, identifier --> tests/ui/unexpected-eof.rs:2:5 | 2 | crokey::key!(); | ^^^^^^^^^^^^^^ | = note: this error originates in the macro `$crate::__private::key` which comes from the expansion of the macro `crokey::key` (in Nightly builds, run with -Z macro-backtrace for more info) error: expected `-` --> tests/ui/unexpected-eof.rs:3:5 | 3 | crokey::key!(ctrl); | ^^^^^^^^^^^^^^^^^^ | = note: this error originates in the macro `$crate::__private::key` which comes from the expansion of the macro `crokey::key` (in Nightly builds, run with -Z macro-backtrace for more info) error: unexpected end of input, expected one of: character literal, integer literal, identifier --> tests/ui/unexpected-eof.rs:4:5 | 4 | crokey::key!(ctrl-); | ^^^^^^^^^^^^^^^^^^^ | = note: this error originates in the macro `$crate::__private::key` which comes from the expansion of the macro `crokey::key` (in Nightly builds, run with -Z macro-backtrace for more info)