Repository: ynqa/jnv (language: rust)

Issue:
Feature Request: configure keymap/colors
## Motivation

- Due to the user's terminal conditions, the default colors might be hard to see
- By allowing users to configure colors and keymaps, we aim to improve the user experience

## Configurable items

- Keymap
- Color
- Prompt symbol (such as `❯❯`)

## Config file manager
(Under investigation)
- https://github.com/rust-cli/confy

## Related issues
- #3
- #15


Discussion / hints:

Fix patch:
diff --git a/Cargo.lock b/Cargo.lock
index 41fac3a..a1fff0d 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"
@@ -45,6 +45,21 @@ version = "0.2.18"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f"
 
+[[package]]
+name = "android-tzdata"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
+
+[[package]]
+name = "android_system_properties"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
+dependencies = [
+ "libc",
+]
+
 [[package]]
 name = "anstream"
 version = "0.6.13"
@@ -155,6 +170,12 @@ version = "0.21.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
 
+[[package]]
+name = "base64"
+version = "0.22.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
+
 [[package]]
 name = "bincode"
 version = "1.3.3"
@@ -175,6 +196,9 @@ name = "bitflags"
 version = "2.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1"
+dependencies = [
+ "serde",
+]
 
 [[package]]
 name = "block2"
@@ -185,6 +209,12 @@ dependencies = [
  "objc2",
 ]
 
+[[package]]
+name = "bumpalo"
+version = "3.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
+
 [[package]]
 name = "bytemuck"
 version = "1.16.3"
@@ -218,13 +248,26 @@ version = "1.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
 
+[[package]]
+name = "chrono"
+version = "0.4.39"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825"
+dependencies = [
+ "android-tzdata",
+ "iana-time-zone",
+ "num-traits",
+ "serde",
+ "windows-targets 0.52.6",
+]
+
 [[package]]
 name = "chumsky"
 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 +413,7 @@ dependencies = [
  "mio",
  "parking_lot",
  "rustix",
+ "serde",
  "signal-hook",
  "signal-hook-mio",
  "winapi",
@@ -384,6 +428,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"
@@ -391,6 +470,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4"
 dependencies = [
  "powerfmt",
+ "serde",
+]
+
+[[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]]
@@ -449,7 +550,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "7199d965852c3bac31f779ef99cbb4537f80e952e2d6aa0ffeb30cce00f4f46e"
 dependencies = [
  "libc",
- "thiserror",
+ "thiserror 1.0.64",
  "winapi",
 ]
 
@@ -463,6 +564,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"
@@ -612,6 +719,12 @@ version = "0.29.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd"
 
+[[package]]
+name = "hashbrown"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
+
 [[package]]
 name = "hashbrown"
 version = "0.14.3"
@@ -622,6 +735,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"
@@ -634,12 +753,47 @@ version = "0.3.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
 
+[[package]]
+name = "hex"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
+
 [[package]]
 name = "hifijson"
 version = "0.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "18ae468bcb4dfecf0e4949ee28abbc99076b6a0077f51ddbc94dbfff8e6a870c"
 
+[[package]]
+name = "iana-time-zone"
+version = "0.1.61"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220"
+dependencies = [
+ "android_system_properties",
+ "core-foundation-sys",
+ "iana-time-zone-haiku",
+ "js-sys",
+ "wasm-bindgen",
+ "windows-core",
+]
+
+[[package]]
+name = "iana-time-zone-haiku"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
+dependencies = [
+ "cc",
+]
+
+[[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 +809,24 @@ dependencies = [
 
 [[package]]
 name = "indexmap"
-version = "2.2.6"
+version = "1.9.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26"
+checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
+dependencies = [
+ "autocfg",
+ "hashbrown 0.12.3",
+ "serde",
+]
+
+[[package]]
+name = "indexmap"
+version = "2.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f"
 dependencies = [
  "equivalent",
- "hashbrown",
+ "hashbrown 0.15.2",
+ "serde",
 ]
 
 [[package]]
@@ -676,7 +842,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "03d6a5713b8f33675abfac79d1db0022a3f28764b2a6b96a185c199ad8dab86d"
 dependencies = [
  "aho-corasick",
- "base64",
+ "base64 0.21.7",
  "hifijson",
  "jaq-interpret",
  "libm",
@@ -695,7 +861,7 @@ dependencies = [
  "ahash",
  "dyn-clone",
  "hifijson",
- "indexmap",
+ "indexmap 2.7.0",
  "jaq-syn",
  "once_cell",
  "serde_json",
@@ -740,6 +906,7 @@ dependencies = [
  "async-trait",
  "clap",
  "crossterm",
+ "dirs",
  "futures",
  "futures-timer",
  "jaq-core",
@@ -747,8 +914,11 @@ dependencies = [
  "jaq-parse",
  "jaq-std",
  "promkit",
+ "serde",
+ "serde_with",
  "tokio",
  "tokio-stream",
+ "toml",
 ]
 
 [[package]]
@@ -757,11 +927,21 @@ version = "0.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0"
 
+[[package]]
+name = "js-sys"
+version = "0.3.76"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6717b6b5b077764fb5966237269cb3c64edddde4b14ce42647430a78ced9e7b7"
+dependencies = [
+ "once_cell",
+ "wasm-bindgen",
+]
+
 [[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 +949,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 +977,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 +1148,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 +1210,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",
 ]
@@ -1029,7 +1225,7 @@ checksum = "a8e3e630da08ae2df2829e2ec38a1cc49ea12c3c3ca8143e58d37ab64a6e613e"
 dependencies = [
  "anyhow",
  "crossterm",
- "indexmap",
+ "indexmap 2.7.0",
  "radix_trie",
  "rayon",
  "serde",
@@ -1085,6 +1281,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 +1354,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",
@@ -1171,12 +1378,51 @@ version = "1.0.116"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3e17db7126d17feb94eb3fad46bf1a96b034e8aacbc2e775fe81505f8b0b2813"
 dependencies = [
- "indexmap",
+ "indexmap 2.7.0",
  "itoa",
  "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 = "serde_with"
+version = "3.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6b6f7f2fcb69f747921f79f3926bd1e203fce4fef62c268dd3abfb6d86029aa"
+dependencies = [
+ "base64 0.22.1",
+ "chrono",
+ "hex",
+ "indexmap 1.9.3",
+ "indexmap 2.7.0",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "serde_with_macros",
+ "time",
+]
+
+[[package]]
+name = "serde_with_macros"
+version = "3.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8d00caa5193a3c8362ac2b73be6b9e768aa5a4b2f721d8f4b339600c3cb51f8e"
+dependencies = [
+ "darling",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
 [[package]]
 name = "shlex"
 version = "1.3.0"
@@ -1252,9 +1498,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 +1513,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 +1536,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 +1629,40 @@ dependencies = [
  "tokio",
 ]
 
+[[package]]
+name = "toml"
+version = "0.8.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e"
+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.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5"
+dependencies = [
+ "indexmap 2.7.0",
+ "serde",
+ "serde_spanned",
+ "toml_datetime",
+ "winnow",
+]
+
 [[package]]
 name = "unicode-ident"
 version = "1.0.12"
@@ -1399,6 +1699,60 @@ version = "0.11.0+wasi-snapshot-preview1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
 
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.99"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "wasm-bindgen-macro",
+]
+
+[[package]]
+name = "wasm-bindgen-backend"
+version = "0.2.99"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79"
+dependencies = [
+ "bumpalo",
+ "log",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.99"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.99"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-backend",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.99"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6"
+
 [[package]]
 name = "weezl"
 version = "0.1.8"
@@ -1427,6 +1781,15 @@ version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
 
+[[package]]
+name = "windows-core"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
+dependencies = [
+ "windows-targets 0.52.6",
+]
+
 [[package]]
 name = "windows-sys"
 version = "0.48.0"
@@ -1442,7 +1805,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 +1834,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 +1856,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 +1868,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 +1880,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 +1898,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 +1910,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 +1922,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 +1934,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 = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
+
+[[package]]
+name = "winnow"
+version = "0.6.22"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0"
+checksum = "39281189af81c07ec09db316b302a3e67bf9bd7cbf6c820b50e35fee9c2fa980"
+dependencies = [
+ "memchr",
+]
 
 [[package]]
 name = "x11rb"
diff --git a/Cargo.toml b/Cargo.toml
index 23c6210..05c82e5 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -14,7 +14,8 @@ arboard = "3.4.1"
 async-trait = "0.1.83"
 clap = { version = "4.5.23", features = ["derive"] }
 # 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 +23,11 @@ jaq-interpret = "1.2.1"
 jaq-parse = "1.0.2"
 jaq-std = "1.2.1"
 promkit = "0.6.2"
+serde = "1.0.217"
+serde_with = "3.12.0"
 tokio = { version = "1.42.0", features = ["full"] }
 tokio-stream = "0.1.16"
+toml = "0.8.19"
 
 # The profile that 'cargo dist' will build with
 [profile.dist]
diff --git a/src/config.rs b/src/config.rs
new file mode 100644
index 0000000..9120f52
--- /dev/null
+++ b/src/config.rs
@@ -0,0 +1,611 @@
+use std::collections::HashSet;
+use std::time::Duration;
+
+use crossterm::style::{Attribute, Attributes, Color};
+use promkit::style::StyleBuilder;
+use serde::{Deserialize, Serialize};
+use serde_with::serde_as;
+use serde_with::DurationMilliSeconds;
+
+/// Loads a configuration file and parses its contents into a Config struct.
+///
+/// This function reads the contents of the specified file and parses it into a Config struct.
+/// It returns a Result containing the Config struct if successful, or an error if the file
+/// cannot be read or parsed.
+///
+/// # Arguments
+///
+/// * `filename` - A string slice that holds the name of the file to be loaded.
+///
+/// # Returns
+///
+/// This function returns an `anyhow::Result<Config>` which is `Ok(Config)` if the file is
+/// successfully read and parsed, or an error if something goes wrong during the process.
+pub(crate) fn load_file(filename: &str) -> anyhow::Result<Config> {
+    load_string(&std::fs::read_to_string(filename)?)
+}
+
+fn load_string(content: &str) -> anyhow::Result<Config> {
+    let config_file: ConfigFile = toml::from_str(content)?;
+    Ok(config_file.into())
+}
+
+#[serde_as]
+#[derive(Serialize, Deserialize)]
+#[serde(deny_unknown_fields)]
+pub(crate) struct ConfigFile {
+    /// Duration to debounce query events, in milliseconds.
+    #[serde(default, rename = "query_debounce_duration_ms")]
+    #[serde_as(as = "Option<DurationMilliSeconds<u64>>")]
+    pub query_debounce_duration: Option<Duration>,
+
+    /// Duration to debounce resize events, in milliseconds.
+    #[serde(default, rename = "resize_debounce_duration_ms")]
+    #[serde_as(as = "Option<DurationMilliSeconds<u64>>")]
+    pub resize_debounce_duration: Option<Duration>,
+
+    pub search_result_chunk_size: Option<usize>,
+    pub search_load_chunk_size: Option<usize>,
+
+    pub active_item_style: Option<ContentStyle>,
+    pub inactive_item_style: Option<ContentStyle>,
+
+    pub prefix_style: Option<ContentStyle>,
+    pub active_char_style: Option<ContentStyle>,
+    pub inactive_char_style: Option<ContentStyle>,
+
+    pub focus_prefix: Option<String>,
+    pub focus_prefix_style: Option<ContentStyle>,
+    pub focus_active_char_style: Option<ContentStyle>,
+    pub focus_inactive_char_style: Option<ContentStyle>,
+
+    pub defocus_prefix: Option<String>,
+    pub defocus_prefix_style: Option<ContentStyle>,
+    pub defocus_active_char_style: Option<ContentStyle>,
+    pub defocus_inactive_char_style: Option<ContentStyle>,
+
+    pub curly_brackets_style: Option<ContentStyle>,
+    pub square_brackets_style: Option<ContentStyle>,
+    pub key_style: Option<ContentStyle>,
+    pub string_value_style: Option<ContentStyle>,
+    pub number_value_style: Option<ContentStyle>,
+    pub boolean_value_style: Option<ContentStyle>,
+    pub null_value_style: Option<ContentStyle>,
+
+    pub word_break_chars: Option<Vec<char>>,
+    #[serde(default, rename = "spin_duration_ms")]
+    #[serde_as(as = "Option<DurationMilliSeconds<u64>>")]
+    pub spin_duration: Option<Duration>,
+
+    pub move_to_tail: Option<KeyEvent>,
+    pub move_to_head: Option<KeyEvent>,
+    pub backward: Option<KeyEvent>,
+    pub forward: Option<KeyEvent>,
+    pub completion: Option<KeyEvent>,
+    pub move_to_next_nearest: Option<KeyEvent>,
+    pub move_to_previous_nearest: Option<KeyEvent>,
+    pub erase: Option<KeyEvent>,
+    pub erase_all: Option<KeyEvent>,
+    pub erase_to_previous_nearest: Option<KeyEvent>,
+    pub erase_to_next_nearest: Option<KeyEvent>,
+    pub search_up: Option<KeyEvent>,
+}
+
+impl From<ConfigFile> for Config {
+    fn from(val: ConfigFile) -> Self {
+        let mut config = Config::default();
+        merge(&mut config, val).unwrap();
+        config
+    }
+}
+
+impl From<crossterm::style::ContentStyle> for ContentStyle {
+    fn from(style: crossterm::style::ContentStyle) -> Self {
+        Self {
+            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<crossterm::event::KeyEvent> for KeyEvent {
+    fn from(keybind: crossterm::event::KeyEvent) -> Self {
+        Self {
+            key: keybind.code,
+            modifiers: keybind.modifiers,
+        }
+    }
+}
+
+impl Default for ConfigFile {
+    fn default() -> Self {
+        let default = Config::default();
+        Self {
+            query_debounce_duration: Some(default.query_debounce_duration),
+            resize_debounce_duration: Some(default.resize_debounce_duration),
+            search_result_chunk_size: Some(default.search_result_chunk_size),
+            search_load_chunk_size: Some(default.search_load_chunk_size),
+            active_item_style: Some(default.active_item_style.unwrap().into()),
+            inactive_item_style: Some(default.inactive_item_style.unwrap().into()),
+            prefix_style: Some(default.prefix_style.into()),
+            active_char_style: Some(default.active_char_style.into()),
+            inactive_char_style: Some(default.inactive_char_style.into()),
+            focus_prefix: Some(default.focus_prefix),
+            focus_prefix_style: Some(default.focus_prefix_style.into()),
+            focus_active_char_style: Some(default.focus_active_char_style.into()),
+            focus_inactive_char_style: Some(default.focus_inactive_char_style.into()),
+            defocus_prefix: Some(default.defocus_prefix),
+            defocus_prefix_style: Some(default.defocus_prefix_style.into()),
+            defocus_active_char_style: Some(default.defocus_active_char_style.into()),
+            defocus_inactive_char_style: Some(default.defocus_inactive_char_style.into()),
+            curly_brackets_style: Some(default.curly_brackets_style.into()),
+            square_brackets_style: Some(default.square_brackets_style.into()),
+            key_style: Some(default.key_style.into()),
+            string_value_style: Some(default.string_value_style.into()),
+            number_value_style: Some(default.number_value_style.into()),
+            boolean_value_style: Some(default.boolean_value_style.into()),
+            null_value_style: Some(default.null_value_style.into()),
+            word_break_chars: Some(default.word_break_chars.iter().copied().collect()),
+            spin_duration: Some(default.spin_duration),
+            move_to_tail: Some(default.move_to_tail.into()),
+            move_to_head: Some(default.move_to_head.into()),
+            backward: Some(default.backward.into()),
+            forward: Some(default.forward.into()),
+            completion: Some(default.completion.into()),
+            move_to_next_nearest: Some(default.move_to_next_nearest.into()),
+            move_to_previous_nearest: Some(default.move_to_previous_nearest.into()),
+            erase: Some(default.erase.into()),
+            erase_all: Some(default.erase_all.into()),
+            erase_to_previous_nearest: Some(default.erase_to_previous_nearest.into()),
+            erase_to_next_nearest: Some(default.erase_to_next_nearest.into()),
+            search_up: Some(default.search_up.into()),
+        }
+    }
+}
+
+pub(crate) struct Config {
+    pub query_debounce_duration: Duration,
+    pub resize_debounce_duration: Duration,
+
+    pub search_result_chunk_size: usize,
+    pub search_load_chunk_size: usize,
+
+    pub prefix_style: crossterm::style::ContentStyle,
+    pub active_char_style: crossterm::style::ContentStyle,
+    pub inactive_char_style: crossterm::style::ContentStyle,
+    pub active_item_style: Option<crossterm::style::ContentStyle>,
+    pub inactive_item_style: Option<crossterm::style::ContentStyle>,
+
+    pub curly_brackets_style: crossterm::style::ContentStyle,
+    pub square_brackets_style: crossterm::style::ContentStyle,
+    pub key_style: crossterm::style::ContentStyle,
+    pub string_value_style: crossterm::style::ContentStyle,
+    pub number_value_style: crossterm::style::ContentStyle,
+    pub boolean_value_style: crossterm::style::ContentStyle,
+    pub null_value_style: crossterm::style::ContentStyle,
+
+    pub defocus_prefix: String,
+    pub defocus_prefix_style: crossterm::style::ContentStyle,
+    pub defocus_active_char_style: crossterm::style::ContentStyle,
+    pub defocus_inactive_char_style: crossterm::style::ContentStyle,
+
+    pub focus_prefix: String,
+    pub focus_prefix_style: crossterm::style::ContentStyle,
+    pub focus_active_char_style: crossterm::style::ContentStyle,
+    pub focus_inactive_char_style: crossterm::style::ContentStyle,
+
+    pub spin_duration: Duration,
+    pub word_break_chars: std::collections::HashSet<char>,
+
+    pub move_to_tail: crossterm::event::KeyEvent,
+    pub move_to_head: crossterm::event::KeyEvent,
+    pub move_to_next_nearest: crossterm::event::KeyEvent,
+    pub move_to_previous_nearest: crossterm::event::KeyEvent,
+    pub backward: crossterm::event::KeyEvent,
+    pub forward: crossterm::event::KeyEvent,
+    pub completion: crossterm::event::KeyEvent,
+    pub erase: crossterm::event::KeyEvent,
+    pub erase_all: crossterm::event::KeyEvent,
+    pub erase_to_previous_nearest: crossterm::event::KeyEvent,
+    pub erase_to_next_nearest: crossterm::event::KeyEvent,
+    pub search_up: crossterm::event::KeyEvent,
+    // pub search_down: KeyEvent, TODO: Vec of KeyEvent
+}
+
+/// Merge the ConfigFile into the Config
+///
+/// This function is used to merge the ConfigFile into the Config. It will only update the fields
+/// that are present in the ConfigFile. If a field is not present in the ConfigFile, the Config will
+/// keep its default value.
+///
+/// # Arguments
+///
+/// * `config` - A mutable reference to the Config struct that will be updated.
+/// * `config_file` - The ConfigFile struct containing the new configuration values.
+///
+/// # Returns
+///
+/// This function returns an `anyhow::Result<()>` which is `Ok(())` if the merge is successful,
+/// or an error if something goes wrong during the merge process.
+fn merge(config: &mut Config, config_file: ConfigFile) -> anyhow::Result<()> {
+    if let Some(query_debounce_duration) = config_file.query_debounce_duration {
+        config.query_debounce_duration = query_debounce_duration;
+    }
+
+    if let Some(resize_debounce_duration) = config_file.resize_debounce_duration {
+        config.resize_debounce_duration = resize_debounce_duration;
+    }
+
+    if let Some(active_item_style) = config_file.active_item_style {
+        config.active_item_style = Some(active_item_style.try_into()?);
+    }
+
+    if let Some(inactive_item_style) = config_file.inactive_item_style {
+        config.inactive_item_style = Some(inactive_item_style.try_into()?);
+    }
+
+    if let Some(search_result_chunk_size) = config_file.search_result_chunk_size {
+        config.search_result_chunk_size = search_result_chunk_size;
+    }
+
+    if let Some(search_load_chunk_size) = config_file.search_load_chunk_size {
+        config.search_load_chunk_size = search_load_chunk_size;
+    }
+
+    if let Some(prefix_style) = config_file.prefix_style {
+        config.prefix_style = prefix_style.try_into()?;
+    }
+
+    if let Some(active_char_style) = config_file.active_char_style {
+        config.active_char_style = active_char_style.try_into()?;
+    }
+
+    if let Some(inactive_char_style) = config_file.inactive_char_style {
+        config.inactive_char_style = inactive_char_style.try_into()?;
+    }
+
+    if let Some(curly_brackets_style) = config_file.curly_brackets_style {
+        config.curly_brackets_style = curly_brackets_style.try_into()?;
+    }
+
+    if let Some(square_brackets_style) = config_file.square_brackets_style {
+        config.square_brackets_style = square_brackets_style.try_into()?;
+    }
+
+    if let Some(key_style) = config_file.key_style {
+        config.key_style = key_style.try_into()?;
+    }
+
+    if let Some(string_value_style) = config_file.string_value_style {
+        config.string_value_style = string_value_style.try_into()?;
+    }
+
+    if let Some(number_value_style) = config_file.number_value_style {
+        config.number_value_style = number_value_style.try_into()?;
+    }
+
+    if let Some(boolean_value_style) = config_file.boolean_value_style {
+        config.boolean_value_style = boolean_value_style.try_into()?;
+    }
+
+    if let Some(null_value_style) = config_file.null_value_style {
+        config.null_value_style = null_value_style.try_into()?;
+    }
+
+    if let Some(focus_prefix) = config_file.focus_prefix {
+        config.focus_prefix = focus_prefix;
+    }
+
+    if let Some(focus_prefix_style) = config_file.focus_prefix_style {
+        config.focus_prefix_style = focus_prefix_style.try_into()?;
+    }
+
+    if let Some(focus_active_char_style) = config_file.focus_active_char_style {
+        config.focus_active_char_style = focus_active_char_style.try_into()?;
+    }
+
+    if let Some(focus_inactive_char_style) = config_file.focus_inactive_char_style {
+        config.focus_inactive_char_style = focus_inactive_char_style.try_into()?;
+    }
+
+    if let Some(defocus_prefix) = config_file.defocus_prefix {
+        config.defocus_prefix = defocus_prefix;
+    }
+
+    if let Some(defocus_prefix_style) = config_file.defocus_prefix_style {
+        config.defocus_prefix_style = defocus_prefix_style.try_into()?;
+    }
+
+    if let Some(defocus_active_char_style) = config_file.defocus_active_char_style {
+        config.defocus_active_char_style = defocus_active_char_style.try_into()?;
+    }
+
+    if let Some(defocus_inactive_char_style) = config_file.defocus_inactive_char_style {
+        config.defocus_inactive_char_style = defocus_inactive_char_style.try_into()?;
+    }
+
+    if let Some(spin_duration) = config_file.spin_duration {
+        config.spin_duration = spin_duration;
+    }
+
+    if let Some(word_break_chars) = config_file.word_break_chars {
+        config.word_break_chars = word_break_chars.into_iter().collect();
+    }
+
+    if let Some(backward) = config_file.backward {
+        config.backward = backward.try_into()?;
+    }
+
+    if let Some(forward) = config_file.forward {
+        config.forward = forward.try_into()?;
+    }
+
+    if let Some(move_to_tail) = config_file.move_to_tail {
+        config.move_to_tail = move_to_tail.try_into()?;
+    }
+
+    if let Some(move_to_head) = config_file.move_to_head {
+        config.move_to_head = move_to_head.try_into()?;
+    }
+
+    if let Some(completion) = config_file.completion {
+        config.completion = completion.try_into()?;
+    }
+
+    if let Some(move_to_next_nearest) = config_file.move_to_next_nearest {
+        config.move_to_next_nearest = move_to_next_nearest.try_into()?;
+    }
+
+    if let Some(move_to_previous_nearest) = config_file.move_to_previous_nearest {
+        config.move_to_previous_nearest = move_to_previous_nearest.try_into()?;
+    }
+
+    if let Some(erase) = config_file.erase {
+        config.erase = erase.try_into()?;
+    }
+
+    if let Some(erase_all) = config_file.erase_all {
+        config.erase_all = erase_all.try_into()?;
+    }
+
+    if let Some(erase_to_previous_nearest) = config_file.erase_to_previous_nearest {
+        config.erase_to_previous_nearest = erase_to_previous_nearest.try_into()?;
+    }
+
+    if let Some(erase_to_next_nearest) = config_file.erase_to_next_nearest {
+        config.erase_to_next_nearest = erase_to_next_nearest.try_into()?;
+    }
+
+    if let Some(search_up) = config_file.search_up {
+        config.search_up = search_up.try_into()?;
+    }
+
+    Ok(())
+}
+
+/// A Deserializable struct that represents a ContentStyle in the ConfigFile
+#[derive(Default, Clone, Debug, PartialEq, Deserialize, Serialize)]
+#[serde(deny_unknown_fields)]
+pub(crate) struct ContentStyle {
+    foreground: Option<Color>,
+    background: Option<Color>,
+    underline: Option<Color>,
+    attributes: Option<Vec<Attribute>>,
+}
+
+/// A Deserializable struct that represents a KeyPress in the ConfigFile
+#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
+#[serde(deny_unknown_fields)]
+pub(crate) struct KeyEvent {
+    pub key: crossterm::event::KeyCode,
+    pub modifiers: crossterm::event::KeyModifiers,
+}
+
+impl Default for Config {
+    fn default() -> Self {
+        Self {
+            focus_prefix: String::from("❯❯ "),
+            active_item_style: Some(
+                StyleBuilder::new()
+                    .fgc(Color::Grey)
+                    .bgc(Color::Yellow)
+                    .build(),
+            ),
+            defocus_prefix: String::from("▼"),
+            search_result_chunk_size: 100,
+            query_debounce_duration: Duration::from_millis(600),
+            resize_debounce_duration: Duration::from_millis(200),
+            search_load_chunk_size: 50000,
+            move_to_tail: crossterm::event::KeyEvent::new(
+                crossterm::event::KeyCode::Char('e'),
+                crossterm::event::KeyModifiers::CONTROL,
+            ),
+            move_to_head: crossterm::event::KeyEvent::new(
+                crossterm::event::KeyCode::Char('a'),
+                crossterm::event::KeyModifiers::CONTROL,
+            ),
+            spin_duration: Duration::from_millis(300),
+            word_break_chars: HashSet::from(['.', '|', '(', ')', '[', ']']),
+            backward: crossterm::event::KeyEvent::new(
+                crossterm::event::KeyCode::Left,
+                crossterm::event::KeyModifiers::NONE,
+            ),
+            forward: crossterm::event::KeyEvent::new(
+                crossterm::event::KeyCode::Right,
+                crossterm::event::KeyModifiers::NONE,
+            ),
+            completion: crossterm::event::KeyEvent::new(
+                crossterm::event::KeyCode::Tab,
+                crossterm::event::KeyModifiers::NONE,
+            ),
+            prefix_style: StyleBuilder::new().fgc(Color::Blue).build(),
+            active_char_style: StyleBuilder::new().bgc(Color::Magenta).build(),
+            inactive_char_style: StyleBuilder::new().build(),
+            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(),
+            defocus_prefix_style: StyleBuilder::new()
+                .fgc(Color::Blue)
+                .attrs(Attributes::from(Attribute::Dim))
+                .build(),
+            defocus_active_char_style: StyleBuilder::new()
+                .attrs(Attributes::from(Attribute::Dim))
+                .build(),
+            defocus_inactive_char_style: StyleBuilder::new()
+                .attrs(Attributes::from(Attribute::Dim))
+                .build(),
+            focus_prefix_style: StyleBuilder::new().fgc(Color::Blue).build(),
+            focus_active_char_style: StyleBuilder::new().bgc(Color::Magenta).build(),
+            focus_inactive_char_style: StyleBuilder::new().build(),
+            inactive_item_style: Some(StyleBuilder::new().fgc(Color::Grey).build()),
+            move_to_next_nearest: crossterm::event::KeyEvent::new(
+                crossterm::event::KeyCode::Char('f'),
+                crossterm::event::KeyModifiers::ALT,
+            ),
+            move_to_previous_nearest: crossterm::event::KeyEvent::new(
+                crossterm::event::KeyCode::Char('b'),
+                crossterm::event::KeyModifiers::ALT,
+            ),
+            erase: crossterm::event::KeyEvent::new(
+                crossterm::event::KeyCode::Backspace,
+                crossterm::event::KeyModifiers::NONE,
+            ),
+            erase_all: crossterm::event::KeyEvent::new(
+                crossterm::event::KeyCode::Char('u'),
+                crossterm::event::KeyModifiers::CONTROL,
+            ),
+            erase_to_previous_nearest: crossterm::event::KeyEvent::new(
+                crossterm::event::KeyCode::Char('w'),
+                crossterm::event::KeyModifiers::CONTROL,
+            ),
+            erase_to_next_nearest: crossterm::event::KeyEvent::new(
+                crossterm::event::KeyCode::Char('d'),
+                crossterm::event::KeyModifiers::CONTROL,
+            ),
+            search_up: crossterm::event::KeyEvent::new(
+                crossterm::event::KeyCode::Up,
+                crossterm::event::KeyModifiers::NONE,
+            ),
+            // search_down: KeyEvent::new(KeyCode::Down, KeyModifiers::NONE),
+        }
+    }
+}
+
+impl TryFrom<KeyEvent> for crossterm::event::KeyEvent {
+    type Error = anyhow::Error;
+
+    fn try_from(keybind: KeyEvent) -> Result<Self, Self::Error> {
+        Ok(crossterm::event::KeyEvent::new(
+            keybind.key,
+            keybind.modifiers,
+        ))
+    }
+}
+
+// Convert a ConfigContentStyle into a ContentStyle
+impl TryFrom<ContentStyle> for crossterm::style::ContentStyle {
+    type Error = anyhow::Error;
+
+    fn try_from(config_content_style: ContentStyle) -> Result<Self, Self::Error> {
+        let mut style_builder = StyleBuilder::new();
+
+        if let Some(foreground_color) = config_content_style.foreground {
+            style_builder = style_builder.fgc(foreground_color);
+        }
+
+        if let Some(background_color) = config_content_style.background {
+            style_builder = style_builder.bgc(background_color);
+        }
+
+        if let Some(underline_color) = config_content_style.underline {
+            style_builder = style_builder.ulc(underline_color);
+        }
+
+        if let Some(attributes) = config_content_style.attributes {
+            style_builder = style_builder.attrs(
+                attributes
+                    .into_iter()
+                    .fold(Attributes::default(), |acc, x| acc | x),
+            );
+        }
+
+        Ok(style_builder.build())
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use std::time::Duration;
+
+    #[test]
+    fn test_config_deserialization() {
+        let toml = r#"
+            search_result_chunk_size = 10
+            query_debounce_duration_ms = 1000
+            resize_debounce_duration_ms = 2000
+            search_load_chunk_size = 5
+            focus_prefix = "❯ "
+
+            [active_item_style]
+            foreground = "green"
+
+            [focus_active_char_style]
+            background = "green"
+            underline = "red"
+            attributes = ["Bold", "Underlined"]
+
+            [move_to_tail]
+            key = { Char = "$" }
+            modifiers = "CONTROL"
+        "#;
+
+        let config = load_string(toml).unwrap();
+
+        assert_eq!(config.search_result_chunk_size, 10);
+        assert_eq!(config.query_debounce_duration, Duration::from_millis(1000));
+        assert_eq!(config.resize_debounce_duration, Duration::from_millis(2000));
+        assert_eq!(config.search_load_chunk_size, 5);
+        assert_eq!(
+            config.active_item_style,
+            Some(StyleBuilder::new().fgc(Color::Green).build()),
+        );
+
+        assert_eq!(
+            config.move_to_tail,
+            crossterm::event::KeyEvent::new(
+                crossterm::event::KeyCode::Char('$'),
+                crossterm::event::KeyModifiers::CONTROL
+            )
+        );
+
+        assert_eq!(config.focus_prefix, "❯ ".to_string());
+
+        assert_eq!(
+            config.focus_active_char_style,
+            StyleBuilder::new()
+                .bgc(Color::Green)
+                .ulc(Color::Red)
+                .attrs(Attributes::from(Attribute::Bold) | Attribute::Underlined)
+                .build(),
+        );
+    }
+}
diff --git a/src/editor.rs b/src/editor.rs
index 849cf7b..02acbef 100644
--- a/src/editor.rs
+++ b/src/editor.rs
@@ -15,11 +15,12 @@ pub struct Editor {
     defocus_theme: EditorTheme,
     guide: text::State,
     searcher: IncrementalSearcher,
+    keybinds: Keybinds,
 }
 
 pub struct EditorTheme {
+    // Prefix for the prompt string.
     pub prefix: String,
-
     /// Style applied to the prompt string.
     pub prefix_style: ContentStyle,
     /// Style applied to the currently selected character.
@@ -28,12 +29,28 @@ pub struct EditorTheme {
     pub inactive_char_style: ContentStyle,
 }
 
+pub struct Keybinds {
+    pub move_to_tail: KeyEvent,
+    pub backward: KeyEvent,
+    pub forward: KeyEvent,
+    pub completion: KeyEvent,
+    pub move_to_head: KeyEvent,
+    pub move_to_previous_nearest: KeyEvent,
+    pub move_to_next_nearest: KeyEvent,
+    pub erase: KeyEvent,
+    pub erase_all: KeyEvent,
+    pub erase_to_previous_nearest: KeyEvent,
+    pub erase_to_next_nearest: KeyEvent,
+    pub search_up: KeyEvent,
+}
+
 impl Editor {
     pub fn new(
         state: text_editor::State,
         searcher: IncrementalSearcher,
         focus_theme: EditorTheme,
         defocus_theme: EditorTheme,
+        keybinds: Keybinds,
     ) -> Self {
         Self {
             keybind: BOXED_EDITOR_KEYBIND,
@@ -45,6 +62,7 @@ impl Editor {
                 style: Default::default(),
             },
             searcher,
+            keybinds,
         }
     }
 
@@ -106,12 +124,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 key == &Event::Key(editor.keybinds.completion) => {
             let prefix = editor.state.texteditor.text_without_cursor().to_string();
             match editor.searcher.start_search(&prefix) {
                 Ok(result) => match result.head_item {
@@ -145,58 +158,29 @@ 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 key == &Event::Key(editor.keybinds.backward) => {
             editor.state.texteditor.backward();
         }
-        Event::Key(KeyEvent {
-            code: KeyCode::Right,
-            modifiers: KeyModifiers::NONE,
-            kind: KeyEventKind::Press,
-            state: KeyEventState::NONE,
-        }) => {
+        key if key == &Event::Key(editor.keybinds.forward) => {
             editor.state.texteditor.forward();
         }
-        Event::Key(KeyEvent {
-            code: KeyCode::Char('a'),
-            modifiers: KeyModifiers::CONTROL,
-            kind: KeyEventKind::Press,
-            state: KeyEventState::NONE,
-        }) => {
+        key if key == &Event::Key(editor.keybinds.move_to_head) => {
             editor.state.texteditor.move_to_head();
         }
-        Event::Key(KeyEvent {
-            code: KeyCode::Char('e'),
-            modifiers: KeyModifiers::CONTROL,
-            kind: KeyEventKind::Press,
-            state: KeyEventState::NONE,
-        }) => {
+
+        key if key == &Event::Key(editor.keybinds.move_to_tail) => {
             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 key == &Event::Key(editor.keybinds.move_to_previous_nearest) => {
             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 key == &Event::Key(editor.keybinds.move_to_next_nearest) => {
             editor
                 .state
                 .texteditor
@@ -204,42 +188,22 @@ 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 key == &Event::Key(editor.keybinds.erase) => {
             editor.state.texteditor.erase();
         }
-        Event::Key(KeyEvent {
-            code: KeyCode::Char('u'),
-            modifiers: KeyModifiers::CONTROL,
-            kind: KeyEventKind::Press,
-            state: KeyEventState::NONE,
-        }) => {
+        key if key == &Event::Key(editor.keybinds.erase_all) => {
             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 key == &Event::Key(editor.keybinds.erase_to_previous_nearest) => {
             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 key == &Event::Key(editor.keybinds.erase_to_next_nearest) => {
             editor
                 .state
                 .texteditor
@@ -270,6 +234,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 {
+        // TODO: Implement the search down keybinds as a collection
         Event::Key(KeyEvent {
             code: KeyCode::Tab,
             modifiers: KeyModifiers::NONE,
@@ -289,12 +254,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 key == &Event::Key(editor.keybinds.search_up) => {
             editor.searcher.up();
             editor
                 .state
diff --git a/src/main.rs b/src/main.rs
index 73a7582..3f42ef4 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,23 +1,21 @@
 use std::{
-    collections::HashSet,
     fs::File,
-    io::{self, Read},
-    path::PathBuf,
-    time::Duration,
+    io::{self, Read, Write},
+    path::{Path, PathBuf},
 };
 
 use anyhow::{anyhow, Result};
 use clap::Parser;
-use crossterm::style::{Attribute, Attributes, Color};
+use crossterm::style::Attribute;
 use promkit::{
     jsonz::format::RowFormatter,
     listbox::{self, Listbox},
-    style::StyleBuilder,
     text_editor,
 };
 
 mod editor;
 use editor::{Editor, EditorTheme};
+mod config;
 mod json;
 use json::JsonStreamProvider;
 mod processor;
@@ -31,6 +29,14 @@ use render::{PaneIndex, Renderer, EMPTY_PANE};
 mod search;
 use search::{IncrementalSearcher, SearchProvider};
 
+use std::sync::OnceLock;
+
+static DEFAULT_CONFIG_FILE: OnceLock<PathBuf> = OnceLock::new();
+
+fn get_config_dir() -> &'static PathBuf {
+    DEFAULT_CONFIG_FILE.get_or_init(|| dirs::config_dir().unwrap().join("jnv/config.toml"))
+}
+
 /// JSON navigator and interactive filter leveraging jq
 #[derive(Parser)]
 #[command(
@@ -99,6 +105,17 @@ pub struct Args {
     )]
     pub no_hint: bool,
 
+    #[arg(
+        short = 'c',
+        long = "config-file",
+        help = "Path to the configuration file.",
+        long_help = "
+        Specifies the path to the configuration file.
+        ",
+        default_value = get_config_dir().to_str().unwrap(),
+    )]
+    pub config_file: String,
+
     #[arg(
         long = "max-streams",
         help = "Maximum number of JSON streams to display",
@@ -110,6 +127,12 @@ pub struct Args {
     )]
     pub max_streams: Option<usize>,
 
+    #[arg(
+        long = "write-default-config",
+        help = "Writes the default configuration to the specified file and exits."
+    )]
+    pub write_default_config: bool,
+
     #[arg(
         long = "suggestions",
         default_value = "3",
@@ -123,6 +146,25 @@ pub struct Args {
     pub suggestions: usize,
 }
 
+impl Args {
+    pub fn write_default_config(&self) -> anyhow::Result<()> {
+        let path = PathBuf::from(&self.config_file);
+
+        if path.exists() {
+            return Err(anyhow!("Config file `{}` already exists", path.display()));
+        }
+
+        if let Some(parent) = path.parent() {
+            std::fs::create_dir_all(parent)?;
+        }
+
+        let mut file = std::fs::File::create(path)?;
+        let default_config: config::ConfigFile = Default::default();
+        file.write_all(toml::to_string_pretty(&default_config)?.as_bytes())?;
+        Ok(())
+    }
+}
+
 fn edit_mode_validator(val: &str) -> Result<text_editor::Mode> {
     match val {
         "insert" | "" => Ok(text_editor::Mode::Insert),
@@ -160,77 +202,148 @@ fn parse_input(args: &Args) -> Result<String> {
 #[tokio::main]
 async fn main() -> anyhow::Result<()> {
     let args = Args::parse();
+
+    if args.write_default_config {
+        return args.write_default_config().map_err(Into::into);
+    }
+
     let input = parse_input(&args)?;
 
-    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::<String>::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),
+    let config = if Path::new(&args.config_file).exists() {
+        config::load_file(&args.config_file)?
+    } else {
+        config::Config::default()
+    };
+
+    let config::Config {
+        search_result_chunk_size,
+        query_debounce_duration,
+        resize_debounce_duration,
+        search_load_chunk_size,
+        active_item_style,
+        defocus_prefix,
+        focus_prefix,
+        move_to_tail,
+        word_break_chars,
+        backward,
+        forward,
+        defocus_prefix_style,
+        defocus_active_char_style,
+        defocus_inactive_char_style,
+        focus_prefix_style,
+        focus_active_char_style,
+        focus_inactive_char_style,
+        inactive_item_style,
+        completion,
+        move_to_head,
+        move_to_next_nearest,
+        move_to_previous_nearest,
+        erase,
+        erase_all,
+        erase_to_previous_nearest,
+        erase_to_next_nearest,
+        search_up,
+        spin_duration,
+        prefix_style,
+        active_char_style,
+        inactive_char_style,
+        curly_brackets_style,
+        square_brackets_style,
+        key_style,
+        string_value_style,
+        number_value_style,
+        boolean_value_style,
+        null_value_style,
+    } = config;
+
+    let listbox_state = listbox::State {
+        listbox: Listbox::from_displayable(Vec::<String>::new()),
+        cursor: String::from("❯ "),
+        active_item_style,
+        inactive_item_style,
+        lines: Some(args.suggestions),
+    };
+
+    let searcher = IncrementalSearcher::new(listbox_state, search_result_chunk_size);
+
+    let text_editor_state = text_editor::State {
+        texteditor: Default::default(),
+        history: Default::default(),
+        prefix: focus_prefix.clone(),
+        mask: Default::default(),
+        prefix_style,
+        active_char_style,
+        inactive_char_style,
+        edit_mode: args.edit_mode,
+        word_break_chars,
+        lines: Default::default(),
+    };
+
+    let editor_focus_theme = EditorTheme {
+        prefix: focus_prefix.clone(),
+        prefix_style: focus_prefix_style,
+        active_char_style: focus_active_char_style,
+        inactive_char_style: focus_inactive_char_style,
+    };
+
+    let editor_defocus_theme = EditorTheme {
+        prefix: defocus_prefix,
+        prefix_style: defocus_prefix_style,
+        active_char_style: defocus_active_char_style,
+        inactive_char_style: defocus_inactive_char_style,
+    };
+
+    let provider = &mut JsonStreamProvider::new(
+        RowFormatter {
+            curly_brackets_style,
+            square_brackets_style,
+            key_style,
+            string_value_style,
+            number_value_style,
+            boolean_value_style,
+            null_value_style,
+            active_item_attribute: Attribute::Bold,
+            inactive_item_attribute: Attribute::Dim,
+            indent: args.indent,
         },
-        100,
-        50000,
+        args.max_streams,
+    );
+
+    let item = Box::leak(input.into_boxed_str());
+
+    let loading_suggestions_task = searcher.spawn_load_task(provider, item, search_load_chunk_size);
+
+    let editor_keybinds = editor::Keybinds {
+        move_to_tail,
+        backward,
+        forward,
+        completion,
+        move_to_head,
+        move_to_previous_nearest,
+        move_to_next_nearest,
+        erase,
+        erase_all,
+        erase_to_previous_nearest,
+        erase_to_next_nearest,
+        search_up,
+    };
+
+    let editor = Editor::new(
+        text_editor_state,
+        searcher,
+        editor_focus_theme,
+        editor_defocus_theme,
+        editor_keybinds,
+    );
+
+    prompt::run(
+        item,
+        spin_duration,
+        query_debounce_duration,
+        resize_debounce_duration,
+        provider,
+        editor,
+        loading_suggestions_task,
         args.no_hint,
     )
     .await?;
diff --git a/src/prompt.rs b/src/prompt.rs
index a4037f6..bbde57d 100644
--- a/src/prompt.rs
+++ b/src/prompt.rs
@@ -10,16 +10,15 @@ 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,
+    Context, ContextMonitor, Editor, PaneIndex, Processor, Renderer, SearchProvider,
+    SpinnerSpawner, ViewInitializer, ViewProvider, Visualizer, EMPTY_PANE,
 };
 
 fn spawn_debouncer<T: Send + 'static>(
@@ -85,12 +84,8 @@ pub async fn run<T: ViewProvider + SearchProvider>(
     query_debounce_duration: Duration,
     resize_debounce_duration: Duration,
     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<anyhow::Result<()>>,
     no_hint: bool,
 ) -> anyhow::Result<()> {
     enable_raw_mode()?;
@@ -98,15 +93,6 @@ pub async fn run<T: ViewProvider + SearchProvider>(
 
     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),


Test patch: