diff --git a/Cargo.lock b/Cargo.lock index 41fac3a..8c4cbcd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "addr2line" @@ -175,6 +175,9 @@ name = "bitflags" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" +dependencies = [ + "serde", +] [[package]] name = "block2" @@ -224,7 +227,7 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8eebd66744a15ded14960ab4ccdbfb51ad3b81f51f3f04a80adac98c985396c9" dependencies = [ - "hashbrown", + "hashbrown 0.14.3", ] [[package]] @@ -370,6 +373,7 @@ dependencies = [ "mio", "parking_lot", "rustix", + "serde", "signal-hook", "signal-hook-mio", "winapi", @@ -384,6 +388,41 @@ dependencies = [ "winapi", ] +[[package]] +name = "darling" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" +dependencies = [ + "darling_core", + "quote", + "syn", +] + [[package]] name = "deranged" version = "0.3.11" @@ -393,6 +432,67 @@ dependencies = [ "powerfmt", ] +[[package]] +name = "derive_builder" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" +dependencies = [ + "derive_builder_macro", +] + +[[package]] +name = "derive_builder_core" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "derive_builder_macro" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" +dependencies = [ + "derive_builder_core", + "syn", +] + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.59.0", +] + +[[package]] +name = "duration-string" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04782251e09dc67c90d694d89e9a3e5fc6cfe883df1b203202de672d812fb299" +dependencies = [ + "serde", +] + [[package]] name = "dyn-clone" version = "1.0.17" @@ -449,7 +549,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7199d965852c3bac31f779ef99cbb4537f80e952e2d6aa0ffeb30cce00f4f46e" dependencies = [ "libc", - "thiserror", + "thiserror 1.0.64", "winapi", ] @@ -463,6 +563,12 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "foreign-types" version = "0.5.0" @@ -622,6 +728,12 @@ dependencies = [ "allocator-api2", ] +[[package]] +name = "hashbrown" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" + [[package]] name = "heck" version = "0.5.0" @@ -640,6 +752,12 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "18ae468bcb4dfecf0e4949ee28abbc99076b6a0077f51ddbc94dbfff8e6a870c" +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "image" version = "0.25.2" @@ -655,12 +773,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.2.6" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" +checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.15.2", ] [[package]] @@ -740,6 +858,9 @@ dependencies = [ "async-trait", "clap", "crossterm", + "derive_builder", + "dirs", + "duration-string", "futures", "futures-timer", "jaq-core", @@ -747,8 +868,10 @@ dependencies = [ "jaq-parse", "jaq-std", "promkit", + "serde", "tokio", "tokio-stream", + "toml", ] [[package]] @@ -759,9 +882,9 @@ checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0" [[package]] name = "libc" -version = "0.2.153" +version = "0.2.169" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" +checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" [[package]] name = "libm" @@ -769,6 +892,16 @@ version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" +[[package]] +name = "libredox" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +dependencies = [ + "bitflags 2.5.0", + "libc", +] + [[package]] name = "linux-raw-sys" version = "0.4.14" @@ -787,9 +920,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.21" +version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" [[package]] name = "memchr" @@ -958,6 +1091,12 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + [[package]] name = "parking_lot" version = "0.12.1" @@ -1014,9 +1153,9 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "proc-macro2" -version = "1.0.81" +version = "1.0.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba" +checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" dependencies = [ "unicode-ident", ] @@ -1039,9 +1178,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.36" +version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" dependencies = [ "proc-macro2", ] @@ -1085,6 +1224,17 @@ dependencies = [ "bitflags 1.3.2", ] +[[package]] +name = "redox_users" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b" +dependencies = [ + "getrandom", + "libredox", + "thiserror 2.0.11", +] + [[package]] name = "regex" version = "1.10.4" @@ -1147,18 +1297,18 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "serde" -version = "1.0.198" +version = "1.0.217" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9846a40c979031340571da2545a4e5b7c4163bdae79b301d5f86d03979451fcc" +checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.198" +version = "1.0.217" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e88edab869b01783ba905e7d0153f9fc1a6505a96e4ad3018011eedb838566d9" +checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" dependencies = [ "proc-macro2", "quote", @@ -1167,16 +1317,26 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.116" +version = "1.0.140" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e17db7126d17feb94eb3fad46bf1a96b034e8aacbc2e775fe81505f8b0b2813" +checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" dependencies = [ "indexmap", "itoa", + "memchr", "ryu", "serde", ] +[[package]] +name = "serde_spanned" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +dependencies = [ + "serde", +] + [[package]] name = "shlex" version = "1.3.0" @@ -1252,9 +1412,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "syn" -version = "2.0.60" +version = "2.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "909518bc7b1c9b779f1bbf07f2929d35af9f0f37e47c6e9ef7f9dddc1e1821f3" +checksum = "46f71c0377baf4ef1cc3e3402ded576dccc315800fbc62dfc7fe04b009773b4a" dependencies = [ "proc-macro2", "quote", @@ -1267,7 +1427,16 @@ version = "1.0.64" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.64", +] + +[[package]] +name = "thiserror" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc" +dependencies = [ + "thiserror-impl 2.0.11", ] [[package]] @@ -1281,6 +1450,17 @@ dependencies = [ "syn", ] +[[package]] +name = "thiserror-impl" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "tiff" version = "0.9.1" @@ -1363,6 +1543,40 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml" +version = "0.8.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + [[package]] name = "unicode-ident" version = "1.0.12" @@ -1442,7 +1656,16 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.5", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", ] [[package]] @@ -1462,18 +1685,18 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm 0.52.5", - "windows_aarch64_msvc 0.52.5", - "windows_i686_gnu 0.52.5", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", "windows_i686_gnullvm", - "windows_i686_msvc 0.52.5", - "windows_x86_64_gnu 0.52.5", - "windows_x86_64_gnullvm 0.52.5", - "windows_x86_64_msvc 0.52.5", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", ] [[package]] @@ -1484,9 +1707,9 @@ checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_msvc" @@ -1496,9 +1719,9 @@ checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_i686_gnu" @@ -1508,15 +1731,15 @@ checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnullvm" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_msvc" @@ -1526,9 +1749,9 @@ checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_x86_64_gnu" @@ -1538,9 +1761,9 @@ checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnullvm" @@ -1550,9 +1773,9 @@ checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_msvc" @@ -1562,9 +1785,18 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e97b544156e9bebe1a0ffbc03484fc1ffe3100cbce3ffb17eac35f7cdd7ab36" +dependencies = [ + "memchr", +] [[package]] name = "x11rb" diff --git a/Cargo.toml b/Cargo.toml index 23c6210..9f38f12 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,8 +13,11 @@ anyhow = "1.0.95" arboard = "3.4.1" async-trait = "0.1.83" clap = { version = "4.5.23", features = ["derive"] } +duration-string = { version = "0.5.2", features = ["serde"] } +derive_builder = "0.20.2" # See https://github.com/crossterm-rs/crossterm/issues/935 -crossterm = { version = "0.28.1", features = ["use-dev-tty", "event-stream", "libc"] } +crossterm = { version = "0.28.1", features = ["use-dev-tty", "event-stream", "libc", "serde"] } +dirs = "6.0.0" futures = "0.3.30" futures-timer = "3.0.3" jaq-core = "1.2.1" @@ -22,8 +25,10 @@ jaq-interpret = "1.2.1" jaq-parse = "1.0.2" jaq-std = "1.2.1" promkit = "0.6.2" +serde = "1.0.217" tokio = { version = "1.42.0", features = ["full"] } tokio-stream = "0.1.16" +toml = "0.8.20" # The profile that 'cargo dist' will build with [profile.dist] diff --git a/default.toml b/default.toml new file mode 100644 index 0000000..b0b2327 --- /dev/null +++ b/default.toml @@ -0,0 +1,222 @@ +# Whether to hide hint messages +no_hint = false + +# Editor settings +[editor] +# Editor mode +# "Insert": Insert characters at the cursor position +# "Overwrite": Replace characters at the cursor position with new ones +mode = "Insert" + +# Characters considered as word boundaries +# These are used to define word movement and deletion behavior in the editor +word_break_chars = [".", "|", "(", ")", "[", "]"] + +# How to configure colors and text attributes +# +# Color specification methods: +# 1. By name: "black", "red", etc. +# 2. By RGB value: "rgb_(255,0,0)" or "#ff0000" +# 3. By ANSI value: "ansi_(16)" +# +# Text attribute specification: +# attributes = ["Bold"], etc. +# +# Configuration example: +# style = { foreground = "blue", background = "magenta", attributes = ["Bold"] } +# +# Detailed information: +# - Color: https://docs.rs/crossterm/0.28.1/crossterm/style/enum.Color.html +# - Attribute: https://docs.rs/crossterm/0.28.1/crossterm/style/enum.Attribute.html + +# Theme settings when the editor is focused +[editor.theme_on_focus] +# Prefix shown before the cursor +prefix = "❯❯ " +# Style for the prefix +prefix_style = { foreground = "blue" } +# Style for the character under the cursor +active_char_style = { background = "magenta" } +# Style for all other characters +inactive_char_style = {} + +# Theme settings when the editor is unfocused +[editor.theme_on_defocus] +# Prefix shown when focus is lost +prefix = "▼ " +# Style for the prefix when unfocused +prefix_style = { foreground = "blue", attributes = ["Dim"] } +# Style for the character under the cursor when unfocused +active_char_style = { attributes = ["Dim"] } +# Style for all other characters when unfocused +inactive_char_style = { attributes = ["Dim"] } + +# JSON display settings +[json] +# Maximum number of JSON objects to read from streams (e.g., JSON Lines format) +# Limits how many objects are processed to reduce memory usage when handling large data streams +# No limit if unset +# max_streams = + +# JSON display theme +[json.theme] +# Number of spaces to use for indentation +indent = 2 +# Style for curly brackets {} +curly_brackets_style = { attributes = ["Bold"] } +# Style for square brackets [] +square_brackets_style = { attributes = ["Bold"] } +# Style for JSON keys +key_style = { foreground = "cyan" } +# Style for string values +string_value_style = { foreground = "green" } +# Style for number values +number_value_style = {} +# Style for boolean values +boolean_value_style = {} +# Style for null values +null_value_style = { foreground = "grey" } + +# Completion feature settings +[completion] +# Number of lines to display for completion candidates +lines = 3 +# Cursor character shown before the selected candidate +cursor = "❯ " +# Style for the selected candidate +active_item_style = { foreground = "grey", background = "yellow" } +# Style for unselected candidates +inactive_item_style = { foreground = "grey" } + +# Settings for background loading of completion candidates +# +# Number of candidates loaded per chunk for search results +# A larger value displays results faster but uses more memory +search_result_chunk_size = 100 + +# Number of items loaded per batch during background loading +# A larger value finishes loading sooner but uses more memory temporarily +search_load_chunk_size = 50000 + +# Keybinding settings +[keybinds] +# Key to exit the application +exit = [ + { Key = { modifiers = "CONTROL", code = { Char = "c" } } } +] +# Key to copy the query to the clipboard +copy_query = [ + { Key = { modifiers = "CONTROL", code = { Char = "q" } } } +] +# Key to copy the result to the clipboard +copy_result = [ + { Key = { modifiers = "CONTROL", code = { Char = "o" } } } +] +# Keys to switch focus between editor and JSON viewer +switch_mode = [ + { Key = { code = "Down", modifiers = "SHIFT" } }, + { Key = { code = "Up", modifiers = "SHIFT" } } +] + +# Keybindings for editor operations +[keybinds.on_editor] +# Move cursor left +backward = [ + { Key = { code = "Left", modifiers = "" } } +] +# Move cursor right +forward = [ + { Key = { code = "Right", modifiers = "" } } +] +# Move cursor to beginning of line +move_to_head = [ + { Key = { modifiers = "CONTROL", code = { Char = "a" } } } +] +# Move cursor to end of line +move_to_tail = [ + { Key = { modifiers = "CONTROL", code = { Char = "e" } } } +] +# Move cursor to previous word boundary +move_to_previous_nearest = [ + { Key = { modifiers = "ALT", code = { Char = "b" } } } +] +# Move cursor to next word boundary +move_to_next_nearest = [ + { Key = { modifiers = "ALT", code = { Char = "f" } } } +] +# Delete character at the cursor +erase = [ + { Key = { code = "Backspace", modifiers = "" } } +] +# Delete all input +erase_all = [ + { Key = { modifiers = "CONTROL", code = { Char = "u" } } } +] +# Delete from cursor to previous word boundary +erase_to_previous_nearest = [ + { Key = { modifiers = "CONTROL", code = { Char = "w" } } } +] +# Delete from cursor to next word boundary +erase_to_next_nearest = [ + { Key = { modifiers = "ALT", code = { Char = "d" } } } +] +# Trigger completion +completion = [ + { Key = { code = "Tab", modifiers = "" } } +] +# Move up in the completion list +on_completion.up = [ + { Key = { code = "Up", modifiers = "" } } +] +# Move down in the completion list +on_completion.down = [ + { Key = { code = "Down", modifiers = "" } }, + { Key = { code = "Tab", modifiers = "" } } +] + +# Keybindings for JSON viewer operations +[keybinds.on_json_viewer] +# Move up in JSON viewer +up = [ + { Key = { code = "Up", modifiers = "" } }, + { Key = { modifiers = "CONTROL", code = { Char = "k" } } } +] +# Move down in JSON viewer +down = [ + { Key = { modifiers = "CONTROL", code = { Char = "j" } } }, + { Key = { code = "Down", modifiers = "" } } +] +# Move to the top of JSON viewer +move_to_head = [ + { Key = { modifiers = "CONTROL", code = { Char = "l" } } } +] +# Move to the bottom of JSON viewer +move_to_tail = [ + { Key = { modifiers = "CONTROL", code = { Char = "h" } } } +] +# Toggle expand/collapse of JSON nodes +toggle = [ + { Key = { code = "Enter", modifiers = "" } } +] +# Expand all JSON nodes +expand = [ + { Key = { modifiers = "CONTROL", code = { Char = "p" } } } +] +# Collapse all JSON nodes +collapse = [ + { Key = { modifiers = "CONTROL", code = { Char = "n" } } } +] + +# Application reactivity settings +[reactivity_control] +# Delay before processing query input +# Prevents excessive updates while user is typing +query_debounce_duration = "600ms" + +# Delay before redrawing after window resize +# Prevents frequent redraws during continuous resizing +resize_debounce_duration = "200ms" + +# Interval for spinner animation updates +# Controls the speed of the loading spinner +spin_duration = "300ms" diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..e722373 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,339 @@ +use std::collections::HashSet; + +use crossterm::{ + event::{KeyCode, KeyModifiers}, + style::{Attribute, Attributes, Color, ContentStyle}, +}; +use promkit::{style::StyleBuilder, text_editor::Mode}; +use serde::{Deserialize, Serialize}; +use tokio::time::Duration; + +mod content_style; +use content_style::content_style_serde; +mod duration; +use duration::duration_serde; +pub mod event; +use event::{EventDef, EventDefSet, KeyEventDef}; +mod text_editor; +use text_editor::text_editor_mode_serde; + +#[derive(Serialize, Deserialize)] +pub struct EditorConfig { + pub theme_on_focus: EditorTheme, + pub theme_on_defocus: EditorTheme, + #[serde(with = "text_editor_mode_serde")] + pub mode: Mode, + pub word_break_chars: HashSet, +} + +#[derive(Serialize, Deserialize)] +pub struct EditorTheme { + pub prefix: String, + + #[serde(with = "content_style_serde")] + pub prefix_style: ContentStyle, + + #[serde(with = "content_style_serde")] + pub active_char_style: ContentStyle, + + #[serde(with = "content_style_serde")] + pub inactive_char_style: ContentStyle, +} + +impl Default for EditorConfig { + fn default() -> Self { + Self { + theme_on_focus: EditorTheme { + prefix: String::from("❯❯ "), + prefix_style: StyleBuilder::new().fgc(Color::Blue).build(), + active_char_style: StyleBuilder::new().bgc(Color::Magenta).build(), + inactive_char_style: StyleBuilder::new().build(), + }, + theme_on_defocus: EditorTheme { + prefix: String::from("▼ "), + prefix_style: StyleBuilder::new() + .fgc(Color::Blue) + .attrs(Attributes::from(Attribute::Dim)) + .build(), + active_char_style: StyleBuilder::new() + .attrs(Attributes::from(Attribute::Dim)) + .build(), + inactive_char_style: StyleBuilder::new() + .attrs(Attributes::from(Attribute::Dim)) + .build(), + }, + mode: Mode::Insert, + word_break_chars: HashSet::from(['.', '|', '(', ')', '[', ']']), + } + } +} + +#[derive(Serialize, Deserialize)] +pub struct JsonConfig { + pub max_streams: Option, + pub theme: JsonTheme, +} + +#[derive(Serialize, Deserialize)] +pub struct JsonTheme { + pub indent: usize, + + #[serde(with = "content_style_serde")] + pub curly_brackets_style: ContentStyle, + + #[serde(with = "content_style_serde")] + pub square_brackets_style: ContentStyle, + + #[serde(with = "content_style_serde")] + pub key_style: ContentStyle, + + #[serde(with = "content_style_serde")] + pub string_value_style: ContentStyle, + + #[serde(with = "content_style_serde")] + pub number_value_style: ContentStyle, + + #[serde(with = "content_style_serde")] + pub boolean_value_style: ContentStyle, + + #[serde(with = "content_style_serde")] + pub null_value_style: ContentStyle, +} + +impl Default for JsonConfig { + fn default() -> Self { + Self { + max_streams: None, + theme: JsonTheme { + indent: 2, + curly_brackets_style: StyleBuilder::new() + .attrs(Attributes::from(Attribute::Bold)) + .build(), + square_brackets_style: StyleBuilder::new() + .attrs(Attributes::from(Attribute::Bold)) + .build(), + key_style: StyleBuilder::new().fgc(Color::Cyan).build(), + string_value_style: StyleBuilder::new().fgc(Color::Green).build(), + number_value_style: StyleBuilder::new().build(), + boolean_value_style: StyleBuilder::new().build(), + null_value_style: StyleBuilder::new().fgc(Color::Grey).build(), + }, + } + } +} + +#[derive(Serialize, Deserialize)] +pub struct CompletionConfig { + pub lines: Option, + pub cursor: String, + pub search_result_chunk_size: usize, + pub search_load_chunk_size: usize, + + #[serde(with = "content_style_serde")] + pub active_item_style: ContentStyle, + + #[serde(with = "content_style_serde")] + pub inactive_item_style: ContentStyle, +} + +impl Default for CompletionConfig { + fn default() -> Self { + Self { + lines: Some(3), + cursor: String::from("❯ "), + search_result_chunk_size: 100, + search_load_chunk_size: 50000, + active_item_style: StyleBuilder::new() + .fgc(Color::Grey) + .bgc(Color::Yellow) + .build(), + inactive_item_style: StyleBuilder::new().fgc(Color::Grey).build(), + } + } +} + +// TODO: remove Clone derive +#[derive(Clone, Serialize, Deserialize)] +pub struct Keybinds { + pub exit: EventDefSet, + pub copy_query: EventDefSet, + pub copy_result: EventDefSet, + pub switch_mode: EventDefSet, + pub on_editor: EditorKeybinds, + pub on_json_viewer: JsonViewerKeybinds, +} + +#[derive(Clone, Serialize, Deserialize)] +pub struct EditorKeybinds { + pub backward: EventDefSet, + pub forward: EventDefSet, + pub move_to_head: EventDefSet, + pub move_to_tail: EventDefSet, + pub move_to_previous_nearest: EventDefSet, + pub move_to_next_nearest: EventDefSet, + pub erase: EventDefSet, + pub erase_all: EventDefSet, + pub erase_to_previous_nearest: EventDefSet, + pub erase_to_next_nearest: EventDefSet, + pub completion: EventDefSet, + pub on_completion: CompletionKeybinds, +} + +#[derive(Clone, Serialize, Deserialize)] +pub struct CompletionKeybinds { + pub up: EventDefSet, + pub down: EventDefSet, +} + +#[derive(Clone, Serialize, Deserialize)] +pub struct JsonViewerKeybinds { + pub up: EventDefSet, + pub down: EventDefSet, + pub move_to_head: EventDefSet, + pub move_to_tail: EventDefSet, + pub toggle: EventDefSet, + pub expand: EventDefSet, + pub collapse: EventDefSet, +} + +impl Default for Keybinds { + fn default() -> Self { + Self { + exit: EventDefSet::from(KeyEventDef::new(KeyCode::Char('c'), KeyModifiers::CONTROL)), + copy_query: EventDefSet::from(KeyEventDef::new( + KeyCode::Char('q'), + KeyModifiers::CONTROL, + )), + copy_result: EventDefSet::from(KeyEventDef::new( + KeyCode::Char('o'), + KeyModifiers::CONTROL, + )), + switch_mode: EventDefSet::from_iter([ + EventDef::Key(KeyEventDef::new(KeyCode::Down, KeyModifiers::SHIFT)), + EventDef::Key(KeyEventDef::new(KeyCode::Up, KeyModifiers::SHIFT)), + ]), + on_editor: EditorKeybinds { + backward: EventDefSet::from(KeyEventDef::new(KeyCode::Left, KeyModifiers::NONE)), + forward: EventDefSet::from(KeyEventDef::new(KeyCode::Right, KeyModifiers::NONE)), + move_to_head: EventDefSet::from(KeyEventDef::new( + KeyCode::Char('a'), + KeyModifiers::CONTROL, + )), + move_to_tail: EventDefSet::from(KeyEventDef::new( + KeyCode::Char('e'), + KeyModifiers::CONTROL, + )), + move_to_next_nearest: EventDefSet::from(KeyEventDef::new( + KeyCode::Char('f'), + KeyModifiers::ALT, + )), + move_to_previous_nearest: EventDefSet::from(KeyEventDef::new( + KeyCode::Char('b'), + KeyModifiers::ALT, + )), + erase: EventDefSet::from(KeyEventDef::new(KeyCode::Backspace, KeyModifiers::NONE)), + erase_all: EventDefSet::from(KeyEventDef::new( + KeyCode::Char('u'), + KeyModifiers::CONTROL, + )), + erase_to_previous_nearest: EventDefSet::from(KeyEventDef::new( + KeyCode::Char('w'), + KeyModifiers::CONTROL, + )), + erase_to_next_nearest: EventDefSet::from(KeyEventDef::new( + KeyCode::Char('d'), + KeyModifiers::ALT, + )), + completion: EventDefSet::from(KeyEventDef::new(KeyCode::Tab, KeyModifiers::NONE)), + on_completion: CompletionKeybinds { + up: EventDefSet::from(KeyEventDef::new(KeyCode::Up, KeyModifiers::NONE)), + down: EventDefSet::from_iter([ + EventDef::Key(KeyEventDef::new(KeyCode::Tab, KeyModifiers::NONE)), + EventDef::Key(KeyEventDef::new(KeyCode::Down, KeyModifiers::NONE)), + ]), + }, + }, + on_json_viewer: JsonViewerKeybinds { + up: EventDefSet::from_iter([ + EventDef::Key(KeyEventDef::new(KeyCode::Char('k'), KeyModifiers::CONTROL)), + EventDef::Key(KeyEventDef::new(KeyCode::Up, KeyModifiers::NONE)), + ]), + down: EventDefSet::from_iter([ + EventDef::Key(KeyEventDef::new(KeyCode::Char('j'), KeyModifiers::CONTROL)), + EventDef::Key(KeyEventDef::new(KeyCode::Down, KeyModifiers::NONE)), + ]), + move_to_head: EventDefSet::from(KeyEventDef::new( + KeyCode::Char('l'), + KeyModifiers::CONTROL, + )), + move_to_tail: EventDefSet::from(KeyEventDef::new( + KeyCode::Char('h'), + KeyModifiers::CONTROL, + )), + toggle: EventDefSet::from(KeyEventDef::new(KeyCode::Enter, KeyModifiers::NONE)), + expand: EventDefSet::from(KeyEventDef::new( + KeyCode::Char('p'), + KeyModifiers::CONTROL, + )), + collapse: EventDefSet::from(KeyEventDef::new( + KeyCode::Char('n'), + KeyModifiers::CONTROL, + )), + }, + } + } +} + +#[derive(Serialize, Deserialize)] +pub struct ReactivityControl { + #[serde(with = "duration_serde")] + pub query_debounce_duration: Duration, + + #[serde(with = "duration_serde")] + pub resize_debounce_duration: Duration, + + #[serde(with = "duration_serde")] + pub spin_duration: Duration, +} + +impl Default for ReactivityControl { + fn default() -> Self { + Self { + query_debounce_duration: Duration::from_millis(600), + resize_debounce_duration: Duration::from_millis(200), + spin_duration: Duration::from_millis(300), + } + } +} + +/// Note that the config struct and the `.toml` configuration file are +/// managed separately because the current toml crate +/// does not readily support the following features: +/// +/// - Preserve docstrings as comments in the `.toml` file +/// - https://github.com/toml-rs/toml/issues/376 +/// - Output inline tables +/// - https://github.com/toml-rs/toml/issues/592 +/// +/// Also difficult to patch `Config` using only the items specified in the configuration file +/// (Premise: To address the complexity of configurations, +/// it assumes using a macro to avoid managing Option-wrapped structures on our side).s +/// +/// The main challenge is that, for nested structs, +/// it is not able to wrap every leaf field with Option<>. +/// https://github.com/colin-kiegel/rust-derive-builder/issues/254 +#[derive(Default, Serialize, Deserialize)] +pub struct Config { + pub no_hint: bool, + pub reactivity_control: ReactivityControl, + pub editor: EditorConfig, + pub json: JsonConfig, + pub completion: CompletionConfig, + pub keybinds: Keybinds, +} + +impl Config { + pub fn load_from(content: &str) -> anyhow::Result { + toml::from_str(content).map_err(Into::into) + } +} diff --git a/src/config/content_style.rs b/src/config/content_style.rs new file mode 100644 index 0000000..03294de --- /dev/null +++ b/src/config/content_style.rs @@ -0,0 +1,65 @@ +use crossterm::style::{Attribute, Attributes, Color, ContentStyle}; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize)] +struct ContentStyleDef { + foreground: Option, + background: Option, + underline: Option, + attributes: Option>, +} + +impl From<&ContentStyle> for ContentStyleDef { + fn from(style: &ContentStyle) -> Self { + ContentStyleDef { + foreground: style.foreground_color, + background: style.background_color, + underline: style.underline_color, + attributes: if style.attributes.is_empty() { + None + } else { + Some( + Attribute::iterator() + .filter(|x| style.attributes.has(*x)) + .collect(), + ) + }, + } + } +} + +impl From for ContentStyle { + fn from(style_def: ContentStyleDef) -> Self { + let mut style = ContentStyle::new(); + style.foreground_color = style_def.foreground; + style.background_color = style_def.background; + style.underline_color = style_def.underline; + if let Some(attributes) = style_def.attributes { + style.attributes = attributes + .into_iter() + .fold(Attributes::default(), |acc, x| acc | x); + } + style + } +} + +pub mod content_style_serde { + use super::*; + use serde::{Deserializer, Serializer}; + + pub fn serialize(style: &ContentStyle, serializer: S) -> Result + where + S: Serializer, + { + let style_def = ContentStyleDef::from(style); + style_def.serialize(serializer) + } + + pub fn deserialize<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let style_def = ContentStyleDef::deserialize(deserializer)?; + Ok(ContentStyle::from(style_def)) + } +} diff --git a/src/config/duration.rs b/src/config/duration.rs new file mode 100644 index 0000000..6f5955a --- /dev/null +++ b/src/config/duration.rs @@ -0,0 +1,22 @@ +use duration_string::DurationString; +use serde::Deserialize; +use tokio::time::Duration; + +pub mod duration_serde { + use super::*; + use serde::{Deserializer, Serializer}; + + pub fn serialize(duration: &Duration, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&DurationString::from(*duration).to_string()) + } + + pub fn deserialize<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + Ok(DurationString::deserialize(deserializer)?.into()) + } +} diff --git a/src/config/event.rs b/src/config/event.rs new file mode 100644 index 0000000..4e50423 --- /dev/null +++ b/src/config/event.rs @@ -0,0 +1,102 @@ +use std::collections::HashSet; + +use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers, MouseEvent, MouseEventKind}; +use serde::{Deserialize, Serialize}; + +pub trait Matcher { + fn matches(&self, other: &T) -> bool; +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct EventDefSet(HashSet); + +impl Matcher for EventDefSet { + fn matches(&self, other: &Event) -> bool { + self.0.iter().any(|event_def| event_def.matches(other)) + } +} + +impl FromIterator for EventDefSet { + fn from_iter>(iter: I) -> Self { + EventDefSet(iter.into_iter().collect()) + } +} + +impl From for EventDefSet { + fn from(key_event_def: KeyEventDef) -> Self { + EventDefSet(HashSet::from_iter([EventDef::Key(key_event_def)])) + } +} + +impl From for EventDefSet { + fn from(mouse_event_def: MouseEventDef) -> Self { + EventDefSet(HashSet::from_iter([EventDef::Mouse(mouse_event_def)])) + } +} + +/// A part of `crossterm::event::Event`. +/// It is used for parsing from a config file or +/// for comparison with crossterm::event::Event. +/// https://docs.rs/crossterm/0.28.1/crossterm/event/enum.Event.html +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)] +pub enum EventDef { + Key(KeyEventDef), + Mouse(MouseEventDef), +} + +impl Matcher for EventDef { + fn matches(&self, other: &Event) -> bool { + match (self, other) { + (EventDef::Key(key_def), Event::Key(key_event)) => key_def.matches(key_event), + (EventDef::Mouse(mouse_def), Event::Mouse(mouse_event)) => { + mouse_def.matches(mouse_event) + } + _ => false, + } + } +} + +/// A part of `crossterm::event::KeyEvent`. +/// It is used for parsing from a config file or +/// for comparison with crossterm::event::KeyEvent. +/// https://docs.rs/crossterm/0.28.1/crossterm/event/struct.KeyEvent.html +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)] +pub struct KeyEventDef { + code: KeyCode, + modifiers: KeyModifiers, +} + +impl KeyEventDef { + pub fn new(code: KeyCode, modifiers: KeyModifiers) -> Self { + KeyEventDef { code, modifiers } + } +} + +impl Matcher for KeyEventDef { + fn matches(&self, other: &KeyEvent) -> bool { + self.code == other.code && self.modifiers == other.modifiers + } +} + +/// A part of `crossterm::event::MouseEvent`. +/// It is used for parsing from a config file or +/// for comparison with crossterm::event::MouseEvent. +/// https://docs.rs/crossterm/0.28.1/crossterm/event/struct.MouseEvent.html +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)] +pub struct MouseEventDef { + kind: MouseEventKind, + modifiers: KeyModifiers, +} + +impl MouseEventDef { + #[allow(dead_code)] + pub fn new(kind: MouseEventKind, modifiers: KeyModifiers) -> Self { + MouseEventDef { kind, modifiers } + } +} + +impl Matcher for MouseEventDef { + fn matches(&self, other: &MouseEvent) -> bool { + self.kind == other.kind && self.modifiers == other.modifiers + } +} diff --git a/src/config/text_editor.rs b/src/config/text_editor.rs new file mode 100644 index 0000000..318e372 --- /dev/null +++ b/src/config/text_editor.rs @@ -0,0 +1,34 @@ +use promkit::text_editor::Mode; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; + +pub mod text_editor_mode_serde { + use super::*; + + pub fn serialize(mode: &Mode, serializer: S) -> Result + where + S: Serializer, + { + let mode_str = match mode { + Mode::Insert => "Insert", + Mode::Overwrite => "Overwrite", + // Add other variants if they exist + }; + mode_str.serialize(serializer) + } + + pub fn deserialize<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let mode_str = String::deserialize(deserializer)?; + match mode_str.as_str() { + "Insert" => Ok(Mode::Insert), + "Overwrite" => Ok(Mode::Overwrite), + // Add other variants if they exist + _ => Err(serde::de::Error::custom(format!( + "Unknown Mode variant: {}", + mode_str + ))), + } + } +} diff --git a/src/editor.rs b/src/editor.rs index 849cf7b..8085dcd 100644 --- a/src/editor.rs +++ b/src/editor.rs @@ -2,30 +2,23 @@ use std::{future::Future, pin::Pin}; use crossterm::{ event::{Event, KeyCode, KeyEvent, KeyEventKind, KeyEventState, KeyModifiers}, - style::{Color, ContentStyle}, + style::Color, }; use promkit::{pane::Pane, style::StyleBuilder, text, text_editor, PaneFactory}; -use crate::search::IncrementalSearcher; +use crate::{ + config::{event::Matcher, EditorKeybinds, EditorTheme}, + search::IncrementalSearcher, +}; pub struct Editor { - keybind: Keybind, + handler: Handler, state: text_editor::State, focus_theme: EditorTheme, defocus_theme: EditorTheme, guide: text::State, searcher: IncrementalSearcher, -} - -pub struct EditorTheme { - pub prefix: String, - - /// Style applied to the prompt string. - pub prefix_style: ContentStyle, - /// Style applied to the currently selected character. - pub active_char_style: ContentStyle, - /// Style applied to characters that are not currently selected. - pub inactive_char_style: ContentStyle, + editor_keybinds: EditorKeybinds, } impl Editor { @@ -34,9 +27,10 @@ impl Editor { searcher: IncrementalSearcher, focus_theme: EditorTheme, defocus_theme: EditorTheme, + editor_keybinds: EditorKeybinds, ) -> Self { Self { - keybind: BOXED_EDITOR_KEYBIND, + handler: BOXED_EDITOR_HANDLER, state, focus_theme, defocus_theme, @@ -45,6 +39,7 @@ impl Editor { style: Default::default(), }, searcher, + editor_keybinds, } } @@ -62,7 +57,7 @@ impl Editor { self.state.active_char_style = self.defocus_theme.active_char_style; self.searcher.leave_search(); - self.keybind = BOXED_EDITOR_KEYBIND; + self.handler = BOXED_EDITOR_HANDLER; self.guide.text = Default::default(); } @@ -84,20 +79,20 @@ impl Editor { } pub async fn operate(&mut self, event: &Event) -> anyhow::Result<()> { - (self.keybind)(event, self).await + (self.handler)(event, self).await } } -pub type Keybind = for<'a> fn( +pub type Handler = for<'a> fn( &'a Event, &'a mut Editor, ) -> Pin> + Send + 'a>>; -const BOXED_EDITOR_KEYBIND: Keybind = +const BOXED_EDITOR_HANDLER: Handler = |event, editor| -> Pin> + Send + '_>> { Box::pin(edit(event, editor)) }; -const BOXED_SEARCHER_KEYBIND: Keybind = +const BOXED_SEARCHER_HANDLER: Handler = |event, editor| -> Pin> + Send + '_>> { Box::pin(search(event, editor)) }; @@ -106,12 +101,7 @@ pub async fn edit<'a>(event: &'a Event, editor: &'a mut Editor) -> anyhow::Resul editor.guide.text = Default::default(); match event { - Event::Key(KeyEvent { - code: KeyCode::Tab, - modifiers: KeyModifiers::NONE, - kind: KeyEventKind::Press, - state: KeyEventState::NONE, - }) => { + key if editor.editor_keybinds.completion.matches(key) => { let prefix = editor.state.texteditor.text_without_cursor().to_string(); match editor.searcher.start_search(&prefix) { Ok(result) => match result.head_item { @@ -130,7 +120,7 @@ pub async fn edit<'a>(event: &'a Event, editor: &'a mut Editor) -> anyhow::Resul editor.guide.style = StyleBuilder::new().fgc(Color::Green).build(); } editor.state.texteditor.replace(&head); - editor.keybind = BOXED_SEARCHER_KEYBIND; + editor.handler = BOXED_SEARCHER_HANDLER; } None => { editor.guide.text = format!("No suggestion found for '{}'", prefix); @@ -145,58 +135,27 @@ pub async fn edit<'a>(event: &'a Event, editor: &'a mut Editor) -> anyhow::Resul } // Move cursor. - Event::Key(KeyEvent { - code: KeyCode::Left, - modifiers: KeyModifiers::NONE, - kind: KeyEventKind::Press, - state: KeyEventState::NONE, - }) => { + key if editor.editor_keybinds.backward.matches(key) => { editor.state.texteditor.backward(); } - Event::Key(KeyEvent { - code: KeyCode::Right, - modifiers: KeyModifiers::NONE, - kind: KeyEventKind::Press, - state: KeyEventState::NONE, - }) => { + key if editor.editor_keybinds.forward.matches(key) => { editor.state.texteditor.forward(); } - Event::Key(KeyEvent { - code: KeyCode::Char('a'), - modifiers: KeyModifiers::CONTROL, - kind: KeyEventKind::Press, - state: KeyEventState::NONE, - }) => { + key if editor.editor_keybinds.move_to_head.matches(key) => { editor.state.texteditor.move_to_head(); } - Event::Key(KeyEvent { - code: KeyCode::Char('e'), - modifiers: KeyModifiers::CONTROL, - kind: KeyEventKind::Press, - state: KeyEventState::NONE, - }) => { + key if editor.editor_keybinds.move_to_tail.matches(key) => { editor.state.texteditor.move_to_tail(); } // Move cursor to the nearest character. - Event::Key(KeyEvent { - code: KeyCode::Char('b'), - modifiers: KeyModifiers::ALT, - kind: KeyEventKind::Press, - state: KeyEventState::NONE, - }) => { + key if editor.editor_keybinds.move_to_previous_nearest.matches(key) => { editor .state .texteditor .move_to_previous_nearest(&editor.state.word_break_chars); } - - Event::Key(KeyEvent { - code: KeyCode::Char('f'), - modifiers: KeyModifiers::ALT, - kind: KeyEventKind::Press, - state: KeyEventState::NONE, - }) => { + key if editor.editor_keybinds.move_to_next_nearest.matches(key) => { editor .state .texteditor @@ -204,42 +163,25 @@ pub async fn edit<'a>(event: &'a Event, editor: &'a mut Editor) -> anyhow::Resul } // Erase char(s). - Event::Key(KeyEvent { - code: KeyCode::Backspace, - modifiers: KeyModifiers::NONE, - kind: KeyEventKind::Press, - state: KeyEventState::NONE, - }) => { + key if editor.editor_keybinds.erase.matches(key) => { editor.state.texteditor.erase(); } - Event::Key(KeyEvent { - code: KeyCode::Char('u'), - modifiers: KeyModifiers::CONTROL, - kind: KeyEventKind::Press, - state: KeyEventState::NONE, - }) => { + key if editor.editor_keybinds.erase_all.matches(key) => { editor.state.texteditor.erase_all(); } // Erase to the nearest character. - Event::Key(KeyEvent { - code: KeyCode::Char('w'), - modifiers: KeyModifiers::CONTROL, - kind: KeyEventKind::Press, - state: KeyEventState::NONE, - }) => { + key if editor + .editor_keybinds + .erase_to_previous_nearest + .matches(key) => + { editor .state .texteditor .erase_to_previous_nearest(&editor.state.word_break_chars); } - - Event::Key(KeyEvent { - code: KeyCode::Char('d'), - modifiers: KeyModifiers::ALT, - kind: KeyEventKind::Press, - state: KeyEventState::NONE, - }) => { + key if editor.editor_keybinds.erase_to_next_nearest.matches(key) => { editor .state .texteditor @@ -270,18 +212,7 @@ pub async fn edit<'a>(event: &'a Event, editor: &'a mut Editor) -> anyhow::Resul pub async fn search<'a>(event: &'a Event, editor: &'a mut Editor) -> anyhow::Result<()> { match event { - Event::Key(KeyEvent { - code: KeyCode::Tab, - modifiers: KeyModifiers::NONE, - kind: KeyEventKind::Press, - state: KeyEventState::NONE, - }) - | Event::Key(KeyEvent { - code: KeyCode::Down, - modifiers: KeyModifiers::NONE, - kind: KeyEventKind::Press, - state: KeyEventState::NONE, - }) => { + key if editor.editor_keybinds.on_completion.down.matches(key) => { editor.searcher.down_with_load(); editor .state @@ -289,12 +220,7 @@ pub async fn search<'a>(event: &'a Event, editor: &'a mut Editor) -> anyhow::Res .replace(&editor.searcher.get_current_item()); } - Event::Key(KeyEvent { - code: KeyCode::Up, - modifiers: KeyModifiers::NONE, - kind: KeyEventKind::Press, - state: KeyEventState::NONE, - }) => { + key if editor.editor_keybinds.on_completion.up.matches(key) => { editor.searcher.up(); editor .state @@ -304,7 +230,7 @@ pub async fn search<'a>(event: &'a Event, editor: &'a mut Editor) -> anyhow::Res _ => { editor.searcher.leave_search(); - editor.keybind = BOXED_EDITOR_KEYBIND; + editor.handler = BOXED_EDITOR_HANDLER; return edit(event, editor).await; } } diff --git a/src/json.rs b/src/json.rs index fa54118..4422fa5 100644 --- a/src/json.rs +++ b/src/json.rs @@ -1,5 +1,5 @@ use crossterm::{ - event::{Event, KeyCode, KeyEvent, KeyEventKind, KeyEventState, KeyModifiers}, + event::Event, style::{Attribute, Attributes}, }; use jaq_interpret::{Ctx, FilterT, ParseCtx, RcIter, Val}; @@ -14,20 +14,23 @@ use promkit::{ }; use crate::{ + config::{event::Matcher, JsonViewerKeybinds}, processor::{ViewProvider, Visualizer}, search::SearchProvider, }; -#[derive(Clone)] +// #[derive(Clone)] pub struct Json { state: jsonstream::State, json: &'static [serde_json::Value], + keybinds: JsonViewerKeybinds, } impl Json { pub fn new( formatter: RowFormatter, input_stream: &'static [serde_json::Value], + keybinds: JsonViewerKeybinds, ) -> anyhow::Result { Ok(Self { json: input_stream, @@ -36,88 +39,42 @@ impl Json { formatter, lines: Default::default(), }, + keybinds, }) } fn operate(&mut self, event: &Event) { match event { // Move up. - Event::Key(KeyEvent { - code: KeyCode::Up, - modifiers: KeyModifiers::NONE, - kind: KeyEventKind::Press, - state: KeyEventState::NONE, - }) - | Event::Key(KeyEvent { - code: KeyCode::Char('k'), - modifiers: KeyModifiers::CONTROL, - kind: KeyEventKind::Press, - state: KeyEventState::NONE, - }) => { + event if self.keybinds.up.matches(event) => { self.state.stream.up(); } // Move down. - Event::Key(KeyEvent { - code: KeyCode::Down, - modifiers: KeyModifiers::NONE, - kind: KeyEventKind::Press, - state: KeyEventState::NONE, - }) - | Event::Key(KeyEvent { - code: KeyCode::Char('j'), - modifiers: KeyModifiers::CONTROL, - kind: KeyEventKind::Press, - state: KeyEventState::NONE, - }) => { + event if self.keybinds.down.matches(event) => { self.state.stream.down(); } - // Move to tail - Event::Key(KeyEvent { - code: KeyCode::Char('h'), - modifiers: KeyModifiers::CONTROL, - kind: KeyEventKind::Press, - state: KeyEventState::NONE, - }) => { - self.state.stream.tail(); - } - // Move to head - Event::Key(KeyEvent { - code: KeyCode::Char('l'), - modifiers: KeyModifiers::CONTROL, - kind: KeyEventKind::Press, - state: KeyEventState::NONE, - }) => { + event if self.keybinds.move_to_head.matches(event) => { self.state.stream.head(); } + // Move to tail + event if self.keybinds.move_to_tail.matches(event) => { + self.state.stream.tail(); + } + // Toggle collapse/expand - Event::Key(KeyEvent { - code: KeyCode::Enter, - modifiers: KeyModifiers::NONE, - kind: KeyEventKind::Press, - state: KeyEventState::NONE, - }) => { + event if self.keybinds.toggle.matches(event) => { self.state.stream.toggle(); } - Event::Key(KeyEvent { - code: KeyCode::Char('p'), - modifiers: KeyModifiers::CONTROL, - kind: KeyEventKind::Press, - state: KeyEventState::NONE, - }) => { + event if self.keybinds.expand.matches(event) => { self.state.stream.set_nodes_visibility(false); } - Event::Key(KeyEvent { - code: KeyCode::Char('n'), - modifiers: KeyModifiers::CONTROL, - kind: KeyEventKind::Press, - state: KeyEventState::NONE, - }) => { + event if self.keybinds.collapse.matches(event) => { self.state.stream.set_nodes_visibility(true); } @@ -242,10 +199,14 @@ impl JsonStreamProvider { #[async_trait::async_trait] impl ViewProvider for JsonStreamProvider { - async fn provide(&mut self, item: &'static str) -> anyhow::Result { + async fn provide( + &mut self, + item: &'static str, + keybinds: JsonViewerKeybinds, + ) -> anyhow::Result { let stream = self.deserialize_json(item)?; let static_stream = Box::leak(stream.into_boxed_slice()); - Json::new(std::mem::take(&mut self.formatter), static_stream) + Json::new(std::mem::take(&mut self.formatter), static_stream, keybinds) } } diff --git a/src/main.rs b/src/main.rs index 73a7582..2832689 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,23 +1,22 @@ use std::{ - collections::HashSet, fs::File, - io::{self, Read}, + io::{self, Read, Write}, path::PathBuf, - time::Duration, }; -use anyhow::{anyhow, Result}; +use anyhow::anyhow; use clap::Parser; -use crossterm::style::{Attribute, Attributes, Color}; +use config::Config; +use crossterm::style::Attribute; use promkit::{ jsonz::format::RowFormatter, listbox::{self, Listbox}, - style::StyleBuilder, text_editor, }; mod editor; -use editor::{Editor, EditorTheme}; +use editor::Editor; +mod config; mod json; use json::JsonStreamProvider; mod processor; @@ -31,6 +30,8 @@ use render::{PaneIndex, Renderer, EMPTY_PANE}; mod search; use search::{IncrementalSearcher, SearchProvider}; +static DEFAULT_CONFIG: &str = include_str!("../default.toml"); + /// JSON navigator and interactive filter leveraging jq #[derive(Parser)] #[command( @@ -61,74 +62,8 @@ pub struct Args { /// reads from standard input. pub input: Option, - #[arg( - short = 'e', - long = "edit-mode", - default_value = "insert", - value_parser = edit_mode_validator, - help = "Edit mode for the interface ('insert' or 'overwrite').", - long_help = r#" - Specifies the edit mode for the interface. - Acceptable values are "insert" or "overwrite". - - "insert" inserts a new input at the cursor's position. - - "overwrite" mode replaces existing characters with new input at the cursor's position. - "#, - )] - pub edit_mode: text_editor::Mode, - - #[arg( - short = 'i', - long = "indent", - default_value = "2", - help = "Number of spaces used for indentation in the visualized data.", - long_help = " - Affect the formatting of the displayed JSON, - making it more readable by adjusting the indentation level. - " - )] - pub indent: usize, - - #[arg( - short = 'n', - long = "no-hint", - help = "Disables the display of hints.", - long_help = " - When this option is enabled, it prevents the display of - hints that typically guide or offer suggestions to the user. - " - )] - pub no_hint: bool, - - #[arg( - long = "max-streams", - help = "Maximum number of JSON streams to display", - long_help = " - Sets the maximum number of JSON streams to load and display. - Limiting this value improves performance for large datasets. - If not set, all streams will be displayed. - " - )] - pub max_streams: Option, - - #[arg( - long = "suggestions", - default_value = "3", - help = "Number of autocomplete suggestions to show", - long_help = " - Sets the number of autocomplete suggestions displayed during incremental search. - Higher values show more suggestions but may occupy more screen space. - Adjust this value based on your screen size and preference. - " - )] - pub suggestions: usize, -} - -fn edit_mode_validator(val: &str) -> Result { - match val { - "insert" | "" => Ok(text_editor::Mode::Insert), - "overwrite" => Ok(text_editor::Mode::Overwrite), - _ => Err(anyhow!("edit-mode must be 'insert' or 'overwrite'")), - } + #[arg(short = 'c', long = "config", help = "Path to the configuration file.")] + pub config_file: Option, } /// Parses the input based on the provided arguments. @@ -138,7 +73,7 @@ fn edit_mode_validator(val: &str) -> Result { /// that equals "-", data is read from standard input. /// Otherwise, the function attempts to open and /// read from the file specified in the `input` argument. -fn parse_input(args: &Args) -> Result { +fn parse_input(args: &Args) -> anyhow::Result { let mut ret = String::new(); match &args.input { @@ -157,81 +92,131 @@ fn parse_input(args: &Args) -> Result { Ok(ret) } +/// Ensures the configuration file exists, creating it with default settings if it doesn't +/// +/// If the file already exists, returns Ok. +/// If the file doesn't exist, writes the default configuration in TOML format. +/// Returns an error if file creation fails. +fn ensure_file_exists(path: &PathBuf) -> anyhow::Result<()> { + if path.exists() { + return Ok(()); + } + + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent) + .map_err(|e| anyhow!("Failed to create directory: {}", e))?; + } + + std::fs::File::create(path)?.write_all(DEFAULT_CONFIG.as_bytes())?; + + Ok(()) +} + +/// Determines the configuration file path with the following precedence: +/// 1. The provided `config_path` argument, if it exists. +/// 2. The default configuration file path in the user's configuration directory. +/// +/// If the configuration file does not exist, it will be created. +/// Returns an error if the file creation fails. +fn determine_config_file(config_path: Option) -> anyhow::Result { + // If a custom path is provided + if let Some(path) = config_path { + ensure_file_exists(&path)?; + return Ok(path); + } + + // Use the default path + let default_path = dirs::config_dir() + .ok_or_else(|| anyhow!("Failed to determine the configuration directory"))? + // TODO: need versions...? + .join("jnv") + .join("config.toml"); + + ensure_file_exists(&default_path)?; + Ok(default_path) +} + #[tokio::main] async fn main() -> anyhow::Result<()> { let args = Args::parse(); let input = parse_input(&args)?; + let mut config = Config::default(); + if let Ok(config_file) = determine_config_file(args.config_file) { + // Note that the configuration file absolutely exists. + let content = std::fs::read_to_string(&config_file) + // TODO: output the message as the initial guide pane. + .map_err(|e| anyhow!("Failed to read configuration file: {}", e))?; + config = Config::load_from(&content) + .map_err(|e| anyhow!("Failed to deserialize configuration file: {}", e))?; + } + + let listbox_state = listbox::State { + listbox: Listbox::default(), + cursor: config.completion.cursor, + active_item_style: Some(config.completion.active_item_style), + inactive_item_style: Some(config.completion.inactive_item_style), + lines: config.completion.lines, + }; + + let searcher = + IncrementalSearcher::new(listbox_state, config.completion.search_result_chunk_size); + + let text_editor_state = text_editor::State { + texteditor: Default::default(), + history: Default::default(), + prefix: config.editor.theme_on_focus.prefix.clone(), + mask: Default::default(), + prefix_style: config.editor.theme_on_focus.prefix_style, + active_char_style: config.editor.theme_on_focus.active_char_style, + inactive_char_style: config.editor.theme_on_focus.inactive_char_style, + edit_mode: config.editor.mode, + word_break_chars: config.editor.word_break_chars, + lines: Default::default(), + }; + + let provider = &mut JsonStreamProvider::new( + RowFormatter { + curly_brackets_style: config.json.theme.curly_brackets_style, + square_brackets_style: config.json.theme.square_brackets_style, + key_style: config.json.theme.key_style, + string_value_style: config.json.theme.string_value_style, + number_value_style: config.json.theme.number_value_style, + boolean_value_style: config.json.theme.boolean_value_style, + null_value_style: config.json.theme.null_value_style, + active_item_attribute: Attribute::Bold, + inactive_item_attribute: Attribute::Dim, + indent: config.json.theme.indent, + }, + config.json.max_streams, + ); + + let item = Box::leak(input.into_boxed_str()); + + let loading_suggestions_task = + searcher.spawn_load_task(provider, item, config.completion.search_load_chunk_size); + + // TODO: re-consider put editor_task of prompt::run into Editor construction time. + // Overall, there are several cases where it would be sufficient to + // launch a background thread during construction. + let editor = Editor::new( + text_editor_state, + searcher, + config.editor.theme_on_focus, + config.editor.theme_on_defocus, + // TODO: remove clones + config.keybinds.on_editor.clone(), + ); + + // TODO: put all logics here. prompt::run( - Box::leak(input.into_boxed_str()), - Duration::from_millis(300), - Duration::from_millis(600), - Duration::from_millis(200), - &mut JsonStreamProvider::new( - RowFormatter { - curly_brackets_style: StyleBuilder::new() - .attrs(Attributes::from(Attribute::Bold)) - .build(), - square_brackets_style: StyleBuilder::new() - .attrs(Attributes::from(Attribute::Bold)) - .build(), - key_style: StyleBuilder::new().fgc(Color::Cyan).build(), - string_value_style: StyleBuilder::new().fgc(Color::Green).build(), - number_value_style: StyleBuilder::new().build(), - boolean_value_style: StyleBuilder::new().build(), - null_value_style: StyleBuilder::new().fgc(Color::Grey).build(), - active_item_attribute: Attribute::Bold, - inactive_item_attribute: Attribute::Dim, - indent: args.indent, - }, - args.max_streams, - ), - text_editor::State { - texteditor: Default::default(), - history: Default::default(), - prefix: String::from("❯❯ "), - mask: Default::default(), - prefix_style: StyleBuilder::new().fgc(Color::Blue).build(), - active_char_style: StyleBuilder::new().bgc(Color::Magenta).build(), - inactive_char_style: StyleBuilder::new().build(), - edit_mode: args.edit_mode, - word_break_chars: HashSet::from(['.', '|', '(', ')', '[', ']']), - lines: Default::default(), - }, - EditorTheme { - prefix: String::from("❯❯ "), - prefix_style: StyleBuilder::new().fgc(Color::Blue).build(), - active_char_style: StyleBuilder::new().bgc(Color::Magenta).build(), - inactive_char_style: StyleBuilder::new().build(), - }, - EditorTheme { - prefix: String::from("▼"), - prefix_style: StyleBuilder::new() - .fgc(Color::Blue) - .attrs(Attributes::from(Attribute::Dim)) - .build(), - active_char_style: StyleBuilder::new() - .attrs(Attributes::from(Attribute::Dim)) - .build(), - inactive_char_style: StyleBuilder::new() - .attrs(Attributes::from(Attribute::Dim)) - .build(), - }, - listbox::State { - listbox: Listbox::from_displayable(Vec::::new()), - cursor: String::from("❯ "), - active_item_style: Some( - StyleBuilder::new() - .fgc(Color::Grey) - .bgc(Color::Yellow) - .build(), - ), - inactive_item_style: Some(StyleBuilder::new().fgc(Color::Grey).build()), - lines: Some(args.suggestions), - }, - 100, - 50000, - args.no_hint, + item, + config.reactivity_control, + provider, + editor, + loading_suggestions_task, + config.no_hint, + config.keybinds, ) .await?; diff --git a/src/processor/init.rs b/src/processor/init.rs index 6c570ca..cd5a29e 100644 --- a/src/processor/init.rs +++ b/src/processor/init.rs @@ -4,11 +4,15 @@ use async_trait::async_trait; use tokio::sync::Mutex; use super::{Context, State, Visualizer}; -use crate::{PaneIndex, Renderer}; +use crate::{config::JsonViewerKeybinds, PaneIndex, Renderer}; #[async_trait] pub trait ViewProvider { - async fn provide(&mut self, item: &'static str) -> anyhow::Result; + async fn provide( + &mut self, + item: &'static str, + keybinds: JsonViewerKeybinds, + ) -> anyhow::Result; } pub struct ViewInitializer { @@ -26,6 +30,7 @@ impl ViewInitializer { item: &'static str, area: (u16, u16), shared_renderer: Arc>, + keybinds: JsonViewerKeybinds, ) -> anyhow::Result { { let mut shared_state = self.shared.lock().await; @@ -35,7 +40,7 @@ impl ViewInitializer { shared_state.state = State::Loading; } - let mut visualizer = provider.provide(item).await?; + let mut visualizer = provider.provide(item, keybinds).await?; let pane = visualizer.create_init_pane(area).await; // Set state to Idle to prevent overwriting by spinner frames in terminal. diff --git a/src/prompt.rs b/src/prompt.rs index a4037f6..470028b 100644 --- a/src/prompt.rs +++ b/src/prompt.rs @@ -10,16 +10,16 @@ use crossterm::{ }; use futures::StreamExt; use futures_timer::Delay; -use promkit::{listbox, style::StyleBuilder, text, text_editor, PaneFactory}; +use promkit::{style::StyleBuilder, text, PaneFactory}; use tokio::{ sync::{mpsc, Mutex, RwLock}, task::JoinHandle, }; use crate::{ - Context, ContextMonitor, Editor, EditorTheme, IncrementalSearcher, PaneIndex, Processor, - Renderer, SearchProvider, SpinnerSpawner, ViewInitializer, ViewProvider, Visualizer, - EMPTY_PANE, + config::{event::Matcher, Keybinds, ReactivityControl}, + Context, ContextMonitor, Editor, PaneIndex, Processor, Renderer, SearchProvider, + SpinnerSpawner, ViewInitializer, ViewProvider, Visualizer, EMPTY_PANE, }; fn spawn_debouncer( @@ -81,32 +81,18 @@ enum Focus { #[allow(clippy::too_many_arguments)] pub async fn run( item: &'static str, - spin_duration: Duration, - query_debounce_duration: Duration, - resize_debounce_duration: Duration, + reactivity_control: ReactivityControl, provider: &mut T, - text_editor_state: text_editor::State, - editor_focus_theme: EditorTheme, - editor_defocus_theme: EditorTheme, - listbox_state: listbox::State, - search_result_chunk_size: usize, - search_load_chunk_size: usize, + editor: Editor, + loading_suggestions_task: JoinHandle>, no_hint: bool, + keybinds: Keybinds, ) -> anyhow::Result<()> { enable_raw_mode()?; execute!(io::stdout(), cursor::Hide)?; let size = terminal::size()?; - let searcher = IncrementalSearcher::new(listbox_state, search_result_chunk_size); - let loading_suggestions_task = searcher.spawn_load_task(provider, item, search_load_chunk_size); - let editor = Editor::new( - text_editor_state, - searcher, - editor_focus_theme, - editor_defocus_theme, - ); - let shared_renderer = Arc::new(Mutex::new(Renderer::try_init_draw( [ editor.create_editor_pane(size.0, size.1), @@ -122,16 +108,23 @@ pub async fn run( let (last_query_tx, mut last_query_rx) = mpsc::channel(1); let (debounce_query_tx, debounce_query_rx) = mpsc::channel(1); - let query_debouncer = - spawn_debouncer(debounce_query_rx, last_query_tx, query_debounce_duration); + let query_debouncer = spawn_debouncer( + debounce_query_rx, + last_query_tx, + reactivity_control.query_debounce_duration, + ); let (last_resize_tx, mut last_resize_rx) = mpsc::channel::<(u16, u16)>(1); let (debounce_resize_tx, debounce_resize_rx) = mpsc::channel(1); - let resize_debouncer = - spawn_debouncer(debounce_resize_rx, last_resize_tx, resize_debounce_duration); + let resize_debouncer = spawn_debouncer( + debounce_resize_rx, + last_resize_tx, + reactivity_control.resize_debounce_duration, + ); let spinner_spawner = SpinnerSpawner::new(ctx.clone()); - let spinning = spinner_spawner.spawn_spin_task(shared_renderer.clone(), spin_duration); + let spinning = + spinner_spawner.spawn_spin_task(shared_renderer.clone(), reactivity_control.spin_duration); let mut focus = Focus::Editor; let (editor_event_tx, mut editor_event_rx) = mpsc::channel::(1); @@ -147,7 +140,13 @@ pub async fn run( let processor = Processor::new(ctx.clone()); let context_monitor = ContextMonitor::new(ctx.clone()); let initializer = ViewInitializer::new(ctx.clone()); - let initializing = initializer.initialize(provider, item, size, shared_renderer.clone()); + let initializing = initializer.initialize( + provider, + item, + size, + shared_renderer.clone(), + keybinds.on_json_viewer, + ); let main_task: JoinHandle> = { let mut stream = EventStream::new(); @@ -160,12 +159,7 @@ pub async fn run( Event::Resize(width, height) => { debounce_resize_tx.send((width, height)).await?; }, - Event::Key(KeyEvent { - code: KeyCode::Char('c'), - modifiers: KeyModifiers::CONTROL, - kind: KeyEventKind::Press, - state: KeyEventState::NONE, - }) => { + event if keybinds.exit.matches(&event) => { break 'main }, Event::Key(KeyEvent { diff --git a/src/render.rs b/src/render.rs index 71af51c..c1713ed 100644 --- a/src/render.rs +++ b/src/render.rs @@ -3,6 +3,7 @@ use std::sync::LazyLock; use crossterm::{self, cursor}; use promkit::{pane::Pane, terminal::Terminal}; +// TODO: One Guide is sufficient. #[derive(Debug, PartialEq)] pub enum PaneIndex { Editor = 0,