diff --git a/Cargo.lock b/Cargo.lock index 06da8a27..7334773a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -35,7 +35,7 @@ dependencies = [ "atspi-common", "serde", "thiserror 1.0.69", - "zvariant 5.10.0", + "zvariant", ] [[package]] @@ -70,14 +70,14 @@ checksum = "301e55b39cfc15d9c48943ce5f572204a551646700d0e8efa424585f94fec528" dependencies = [ "accesskit", "accesskit_atspi_common", - "async-channel", + "async-channel 2.5.0", "async-executor", "async-task", "atspi", "futures-lite", "futures-util", "serde", - "zbus 5.14.0", + "zbus", ] [[package]] @@ -123,17 +123,6 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" -[[package]] -name = "ahash" -version = "0.7.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" -dependencies = [ - "getrandom 0.2.17", - "once_cell", - "version_check", -] - [[package]] name = "ahash" version = "0.8.12" @@ -301,18 +290,47 @@ dependencies = [ "libloading", ] +[[package]] +name = "ashpd" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de3d60bee1a1d38c2077030f4788e1b4e31058d2e79a8cfc8f2b440bd44db290" +dependencies = [ + "async-fs", + "async-net", + "enumflags2", + "futures-channel", + "futures-util", + "rand 0.8.5", + "serde", + "serde_repr", + "url", + "zbus", +] + [[package]] name = "async-broadcast" version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" dependencies = [ - "event-listener", + "event-listener 5.4.1", "event-listener-strategy", "futures-core", "pin-project-lite", ] +[[package]] +name = "async-channel" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" +dependencies = [ + "concurrent-queue", + "event-listener 2.5.3", + "futures-core", +] + [[package]] name = "async-channel" version = "2.5.0" @@ -350,6 +368,21 @@ dependencies = [ "futures-lite", ] +[[package]] +name = "async-global-executor" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c" +dependencies = [ + "async-channel 2.5.0", + "async-executor", + "async-io", + "async-lock", + "blocking", + "futures-lite", + "once_cell", +] + [[package]] name = "async-io" version = "2.6.0" @@ -374,25 +407,36 @@ version = "3.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" dependencies = [ - "event-listener", + "event-listener 5.4.1", "event-listener-strategy", "pin-project-lite", ] +[[package]] +name = "async-net" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b948000fad4873c1c9339d60f2623323a0cfd3816e5181033c6a5cb68b2accf7" +dependencies = [ + "async-io", + "blocking", + "futures-lite", +] + [[package]] name = "async-process" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" dependencies = [ - "async-channel", + "async-channel 2.5.0", "async-io", "async-lock", "async-signal", "async-task", "blocking", "cfg-if", - "event-listener", + "event-listener 5.4.1", "futures-lite", "rustix 1.1.4", ] @@ -426,6 +470,32 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "async-std" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c8e079a4ab67ae52b7403632e4618815d6db36d2a010cfe41b02c1b1578f93b" +dependencies = [ + "async-channel 1.9.0", + "async-global-executor", + "async-io", + "async-lock", + "crossbeam-utils", + "futures-channel", + "futures-core", + "futures-io", + "futures-lite", + "gloo-timers", + "kv-log-macro", + "log", + "memchr", + "once_cell", + "pin-project-lite", + "pin-utils", + "slab", + "wasm-bindgen-futures", +] + [[package]] name = "async-task" version = "4.7.1" @@ -469,11 +539,11 @@ dependencies = [ "enumflags2", "serde", "static_assertions", - "zbus 5.14.0", + "zbus", "zbus-lockstep", "zbus-lockstep-macros", - "zbus_names 4.3.1", - "zvariant 5.10.0", + "zbus_names", + "zvariant", ] [[package]] @@ -485,7 +555,7 @@ dependencies = [ "atspi-common", "atspi-proxies", "futures-lite", - "zbus 5.14.0", + "zbus", ] [[package]] @@ -496,7 +566,7 @@ checksum = "d2eebcb9e7e76f26d0bcfd6f0295e1cd1e6f33bedbc5698a971db8dc43d7751c" dependencies = [ "atspi-common", "serde", - "zbus 5.14.0", + "zbus", ] [[package]] @@ -568,15 +638,6 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - [[package]] name = "block2" version = "0.5.1" @@ -601,7 +662,7 @@ version = "1.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" dependencies = [ - "async-channel", + "async-channel 2.5.0", "async-task", "futures-io", "futures-lite", @@ -964,15 +1025,6 @@ dependencies = [ "libc", ] -[[package]] -name = "cpufeatures" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" -dependencies = [ - "libc", -] - [[package]] name = "crc32fast" version = "1.5.0" @@ -1028,16 +1080,6 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" -[[package]] -name = "crypto-common" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" -dependencies = [ - "generic-array", - "typenum", -] - [[package]] name = "ctor" version = "0.6.3" @@ -1079,7 +1121,7 @@ dependencies = [ name = "dap-gui-config" version = "0.1.0" dependencies = [ - "dirs 6.0.0", + "dirs", "serde", "tempfile", "toml", @@ -1096,7 +1138,7 @@ dependencies = [ "dap-gui-launch-configuration", "dap-gui-server", "dap-gui-types", - "dirs 6.0.0", + "dirs", "eyre", "futures", "serde", @@ -1127,7 +1169,7 @@ dependencies = [ "dap-gui-types", "dap-gui-ui-core", "dark-light", - "dirs 6.0.0", + "dirs", "eframe", "eyre", "sentry", @@ -1183,7 +1225,7 @@ version = "0.1.0" dependencies = [ "color-eyre", "eyre", - "nix 0.30.1", + "nix", "tracing", "tracing-subscriber", "which", @@ -1194,7 +1236,7 @@ name = "dap-gui-state" version = "0.1.0" dependencies = [ "dap-gui-debugger", - "dirs 6.0.0", + "dirs", "eyre", "serde", "serde_json", @@ -1224,7 +1266,7 @@ dependencies = [ "dap-gui-server", "dap-gui-state", "dap-gui-types", - "dirs 6.0.0", + "dirs", "eyre", "serde", "serde_json", @@ -1250,8 +1292,9 @@ dependencies = [ "dap-gui-state", "dap-gui-types", "dap-gui-ui-core", + "dark-light", "data-encoding", - "dirs 6.0.0", + "dirs", "eyre", "insta", "ratatui", @@ -1267,18 +1310,16 @@ dependencies = [ [[package]] name = "dark-light" -version = "1.1.1" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a76fa97167fa740dcdbfe18e8895601e1bc36525f09b044e00916e717c03a3c" +checksum = "18e1a09f280e29a8b00bc7e81eca5ac87dca0575639c9422a5fa25a07bb884b8" dependencies = [ - "dconf_rs", - "detect-desktop-environment", - "dirs 4.0.0", - "objc", - "rust-ini", + "ashpd", + "async-std", + "objc2 0.5.2", + "objc2-foundation 0.2.2", "web-sys", "winreg", - "zbus 4.4.0", ] [[package]] @@ -1321,12 +1362,6 @@ version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" -[[package]] -name = "dconf_rs" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7046468a81e6a002061c01e6a7c83139daf91b11c30e66795b13217c2d885c8b" - [[package]] name = "debugid" version = "0.8.0" @@ -1357,49 +1392,13 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "detect-desktop-environment" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21d8ad60dd5b13a4ee6bd8fa2d5d88965c597c67bce32b5fc49c94f55cb50810" - -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer", - "crypto-common", -] - -[[package]] -name = "dirs" -version = "4.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" -dependencies = [ - "dirs-sys 0.3.7", -] - [[package]] name = "dirs" version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" dependencies = [ - "dirs-sys 0.5.0", -] - -[[package]] -name = "dirs-sys" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" -dependencies = [ - "libc", - "redox_users 0.4.6", - "winapi", + "dirs-sys", ] [[package]] @@ -1410,7 +1409,7 @@ checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" dependencies = [ "libc", "option-ext", - "redox_users 0.5.2", + "redox_users", "windows-sys 0.61.2", ] @@ -1450,12 +1449,6 @@ dependencies = [ "libloading", ] -[[package]] -name = "dlv-list" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0688c2a7f92e427f44895cd63841bff7b29f8d7a1648b9e7e07a4a365b2e1257" - [[package]] name = "document-features" version = "0.2.12" @@ -1508,7 +1501,7 @@ version = "0.33.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "457481173e6db5ca9fa2be93a58df8f4c7be639587aeb4853b526c6cf87db4e6" dependencies = [ - "ahash 0.8.12", + "ahash", "bytemuck", "document-features", "egui", @@ -1544,7 +1537,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a9b567d356674e9a5121ed3fedfb0a7c31e059fe71f6972b691bcd0bfc284e3" dependencies = [ "accesskit", - "ahash 0.8.12", + "ahash", "bitflags 2.11.0", "emath", "epaint", @@ -1561,7 +1554,7 @@ version = "0.33.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e4d209971c84b2352a06174abdba701af1e552ce56b144d96f2bd50a3c91236" dependencies = [ - "ahash 0.8.12", + "ahash", "bytemuck", "document-features", "egui", @@ -1675,7 +1668,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "009d0dd3c2163823a0abdb899451ecbc78798dec545ee91b43aff1fa790bab62" dependencies = [ "ab_glyph", - "ahash 0.8.12", + "ahash", "bytemuck", "ecolor", "emath", @@ -1723,6 +1716,12 @@ dependencies = [ "arrayvec", ] +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + [[package]] name = "event-listener" version = "5.4.1" @@ -1740,7 +1739,7 @@ version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" dependencies = [ - "event-listener", + "event-listener 5.4.1", "pin-project-lite", ] @@ -1978,16 +1977,6 @@ dependencies = [ "slab", ] -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", -] - [[package]] name = "gethostname" version = "1.1.0" @@ -2061,6 +2050,18 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" +[[package]] +name = "gloo-timers" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + [[package]] name = "glow" version = "0.16.0" @@ -2202,15 +2203,6 @@ dependencies = [ "zerocopy", ] -[[package]] -name = "hashbrown" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" -dependencies = [ - "ahash 0.7.8", -] - [[package]] name = "hashbrown" version = "0.15.5" @@ -2657,6 +2649,15 @@ version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" +[[package]] +name = "kv-log-macro" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f" +dependencies = [ + "log", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -2741,6 +2742,9 @@ name = "log" version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +dependencies = [ + "value-bag", +] [[package]] name = "lru" @@ -2902,19 +2906,6 @@ dependencies = [ "jni-sys", ] -[[package]] -name = "nix" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" -dependencies = [ - "bitflags 2.11.0", - "cfg-if", - "cfg_aliases", - "libc", - "memoffset", -] - [[package]] name = "nix" version = "0.30.1" @@ -3436,16 +3427,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "ordered-multimap" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccd746e37177e1711c20dd619a1620f34f5c8b569c53590a72dedd5344d8924a" -dependencies = [ - "dlv-list", - "hashbrown 0.12.3", -] - [[package]] name = "ordered-stream" version = "0.2.0" @@ -3464,7 +3445,7 @@ checksum = "e4022a17595a00d6a369236fdae483f0de7f0a339960a53118b818238e132224" dependencies = [ "android_system_properties", "log", - "nix 0.30.1", + "nix", "objc2 0.6.4", "objc2-foundation 0.3.2", "objc2-ui-kit 0.3.2", @@ -3911,17 +3892,6 @@ dependencies = [ "bitflags 2.11.0", ] -[[package]] -name = "redox_users" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" -dependencies = [ - "getrandom 0.2.17", - "libredox", - "thiserror 1.0.69", -] - [[package]] name = "redox_users" version = "0.5.2" @@ -4057,16 +4027,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "rust-ini" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6d5f2436026b4f6e79dc829837d467cc7e9a55ee40e750d716713540715a2df" -dependencies = [ - "cfg-if", - "ordered-multimap", -] - [[package]] name = "rustc-demangle" version = "0.1.27" @@ -4390,17 +4350,6 @@ dependencies = [ "serde", ] -[[package]] -name = "sha1" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - [[package]] name = "sharded-slab" version = "0.1.7" @@ -5145,12 +5094,6 @@ dependencies = [ "rustc-hash 2.1.1", ] -[[package]] -name = "typenum" -version = "1.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" - [[package]] name = "uds_windows" version = "1.1.0" @@ -5295,6 +5238,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" +[[package]] +name = "value-bag" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ba6f5989077681266825251a52748b8c1d8a4ad098cc37e440103d0ea717fc0" + [[package]] name = "version_check" version = "0.9.5" @@ -5997,6 +5946,15 @@ dependencies = [ "windows-targets 0.42.2", ] +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + [[package]] name = "windows-sys" version = "0.52.0" @@ -6048,6 +6006,21 @@ dependencies = [ "windows_x86_64_msvc 0.42.2", ] +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + [[package]] name = "windows-targets" version = "0.52.6" @@ -6096,6 +6069,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" @@ -6114,6 +6093,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" @@ -6132,6 +6117,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -6162,6 +6153,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + [[package]] name = "windows_i686_msvc" version = "0.52.6" @@ -6180,6 +6177,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" @@ -6198,6 +6201,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" @@ -6216,6 +6225,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" @@ -6234,7 +6249,7 @@ version = "0.30.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c66d4b9ed69c4009f6321f762d6e61ad8a2389cd431b97cb1e146812e9e6c732" dependencies = [ - "ahash 0.8.12", + "ahash", "android-activity", "atomic-waker", "bitflags 2.11.0", @@ -6291,11 +6306,12 @@ dependencies = [ [[package]] name = "winreg" -version = "0.10.1" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" +checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5" dependencies = [ - "winapi", + "cfg-if", + "windows-sys 0.48.0", ] [[package]] @@ -6436,16 +6452,6 @@ version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bec9e4a500ca8864c5b47b8b482a73d62e4237670e5b5f1d6b9e3cae50f28f2b" -[[package]] -name = "xdg-home" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec1cdab258fb55c0da61328dc52c8764709b249011b2cad0454c72f0bf10a1f6" -dependencies = [ - "libc", - "windows-sys 0.59.0", -] - [[package]] name = "xkbcommon-dl" version = "0.4.2" @@ -6501,44 +6507,6 @@ dependencies = [ "synstructure", ] -[[package]] -name = "zbus" -version = "4.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb97012beadd29e654708a0fdb4c84bc046f537aecfde2c3ee0a9e4b4d48c725" -dependencies = [ - "async-broadcast", - "async-executor", - "async-fs", - "async-io", - "async-lock", - "async-process", - "async-recursion", - "async-task", - "async-trait", - "blocking", - "enumflags2", - "event-listener", - "futures-core", - "futures-sink", - "futures-util", - "hex", - "nix 0.29.0", - "ordered-stream", - "rand 0.8.5", - "serde", - "serde_repr", - "sha1", - "static_assertions", - "tracing", - "uds_windows", - "windows-sys 0.52.0", - "xdg-home", - "zbus_macros 4.4.0", - "zbus_names 3.0.0", - "zvariant 4.2.0", -] - [[package]] name = "zbus" version = "5.14.0" @@ -6555,7 +6523,7 @@ dependencies = [ "async-trait", "blocking", "enumflags2", - "event-listener", + "event-listener 5.4.1", "futures-core", "futures-lite", "hex", @@ -6569,9 +6537,9 @@ dependencies = [ "uuid", "windows-sys 0.61.2", "winnow", - "zbus_macros 5.14.0", - "zbus_names 4.3.1", - "zvariant 5.10.0", + "zbus_macros", + "zbus_names", + "zvariant", ] [[package]] @@ -6581,7 +6549,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6998de05217a084b7578728a9443d04ea4cd80f2a0839b8d78770b76ccd45863" dependencies = [ "zbus_xml", - "zvariant 5.10.0", + "zvariant", ] [[package]] @@ -6595,20 +6563,7 @@ dependencies = [ "syn 2.0.117", "zbus-lockstep", "zbus_xml", - "zvariant 5.10.0", -] - -[[package]] -name = "zbus_macros" -version = "4.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "267db9407081e90bbfa46d841d3cbc60f59c0351838c4bc65199ecd79ab1983e" -dependencies = [ - "proc-macro-crate", - "proc-macro2", - "quote", - "syn 2.0.117", - "zvariant_utils 2.1.0", + "zvariant", ] [[package]] @@ -6621,20 +6576,9 @@ dependencies = [ "proc-macro2", "quote", "syn 2.0.117", - "zbus_names 4.3.1", - "zvariant 5.10.0", - "zvariant_utils 3.3.0", -] - -[[package]] -name = "zbus_names" -version = "3.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b9b1fef7d021261cc16cba64c351d291b715febe0fa10dc3a443ac5a5022e6c" -dependencies = [ - "serde", - "static_assertions", - "zvariant 4.2.0", + "zbus_names", + "zvariant", + "zvariant_utils", ] [[package]] @@ -6645,7 +6589,7 @@ checksum = "ffd8af6d5b78619bab301ff3c560a5bd22426150253db278f164d6cf3b72c50f" dependencies = [ "serde", "winnow", - "zvariant 5.10.0", + "zvariant", ] [[package]] @@ -6656,8 +6600,8 @@ checksum = "441a0064125265655bccc3a6af6bef56814d9277ac83fce48b1cd7e160b80eac" dependencies = [ "quick-xml", "serde", - "zbus_names 4.3.1", - "zvariant 5.10.0", + "zbus_names", + "zvariant", ] [[package]] @@ -6761,19 +6705,6 @@ dependencies = [ "zune-core", ] -[[package]] -name = "zvariant" -version = "4.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2084290ab9a1c471c38fc524945837734fbf124487e105daec2bb57fd48c81fe" -dependencies = [ - "endi", - "enumflags2", - "serde", - "static_assertions", - "zvariant_derive 4.2.0", -] - [[package]] name = "zvariant" version = "5.10.0" @@ -6783,22 +6714,10 @@ dependencies = [ "endi", "enumflags2", "serde", + "url", "winnow", - "zvariant_derive 5.10.0", - "zvariant_utils 3.3.0", -] - -[[package]] -name = "zvariant_derive" -version = "4.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73e2ba546bda683a90652bac4a279bc146adad1386f25379cf73200d2002c449" -dependencies = [ - "proc-macro-crate", - "proc-macro2", - "quote", - "syn 2.0.117", - "zvariant_utils 2.1.0", + "zvariant_derive", + "zvariant_utils", ] [[package]] @@ -6811,18 +6730,7 @@ dependencies = [ "proc-macro2", "quote", "syn 2.0.117", - "zvariant_utils 3.3.0", -] - -[[package]] -name = "zvariant_utils" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c51bcff7cc3dbb5055396bcf774748c3dab426b4b8659046963523cee4808340" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", + "zvariant_utils", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index c1ddabd8..75ac521f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,7 @@ tracing-appender = "0.2" tracing-subscriber = { version = "0.3.20", features = ["json", "env-filter"] } dirs = "6.0.0" toml = "0.8" -dark-light = "1.1.1" +dark-light = "2" oneshot = { version = "0.1.11", default-features = false, features = ["std"] } tokio = { version = "1.48", default-features = false } tokio-util = { version = "0.7.17", features = ["codec"] } diff --git a/crates/config/src/lib.rs b/crates/config/src/lib.rs index 28a74894..21a07d38 100644 --- a/crates/config/src/lib.rs +++ b/crates/config/src/lib.rs @@ -4,11 +4,26 @@ use std::path::PathBuf; use serde::{Deserialize, Serialize}; +/// User preference for the color theme. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum ThemePreference { + /// Detect automatically from the system setting and follow changes. + #[default] + Auto, + /// Always use the dark palette. + Dark, + /// Always use the light palette. + Light, +} + /// Top-level application configuration. #[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct Config { #[serde(default)] pub keybindings: keybindings::KeybindingConfig, + #[serde(default)] + pub theme: ThemePreference, } /// Load configuration from the user's config directory. diff --git a/crates/gui/examples/scrolling_textarea.rs b/crates/gui/examples/scrolling_textarea.rs index f907ae96..62e1edb3 100644 --- a/crates/gui/examples/scrolling_textarea.rs +++ b/crates/gui/examples/scrolling_textarea.rs @@ -63,14 +63,18 @@ fn main() { Box::new(|cc| { let style = egui::Style { visuals: match dark_light::detect() { - dark_light::Mode::Dark | dark_light::Mode::Default => { + Ok(dark_light::Mode::Dark) | Ok(dark_light::Mode::Unspecified) => { tracing::debug!("choosing dark mode"); Visuals::dark() } - dark_light::Mode::Light => { + Ok(dark_light::Mode::Light) => { tracing::debug!("choosing light mode"); Visuals::light() } + Err(e) => { + tracing::warn!(error = %e, "error detecting current theme, defaulting to dark"); + Visuals::dark() + } }, ..Default::default() }; diff --git a/crates/gui/src/main.rs b/crates/gui/src/main.rs index d2ae30e1..08cf0c49 100644 --- a/crates/gui/src/main.rs +++ b/crates/gui/src/main.rs @@ -336,8 +336,10 @@ fn main() -> eyre::Result<()> { Box::new(move |cc| { let style = egui::Style { visuals: match dark_light::detect() { - dark_light::Mode::Dark | dark_light::Mode::Default => Visuals::dark(), - dark_light::Mode::Light => Visuals::light(), + Ok(dark_light::Mode::Dark) | Ok(dark_light::Mode::Unspecified) | Err(_) => { + Visuals::dark() + } + Ok(dark_light::Mode::Light) => Visuals::light(), }, ..Default::default() }; diff --git a/crates/tui/Cargo.toml b/crates/tui/Cargo.toml index f395c509..0ffb0d76 100644 --- a/crates/tui/Cargo.toml +++ b/crates/tui/Cargo.toml @@ -21,6 +21,7 @@ fuzzy = { path = "../fuzzy", version = "0.1.0", package = "dap-gui-fuzzy" } async-transport = { path = "../async-transport", version = "0.1.0", package = "dap-gui-async-transport" } ui-core = { path = "../ui-core", version = "0.1.0", package = "dap-gui-ui-core" } config = { path = "../config", version = "0.1.0", package = "dap-gui-config" } +dark-light.workspace = true clap.workspace = true eyre.workspace = true color-eyre.workspace = true diff --git a/crates/tui/src/app.rs b/crates/tui/src/app.rs index 00627afe..471aee79 100644 --- a/crates/tui/src/app.rs +++ b/crates/tui/src/app.rs @@ -7,6 +7,7 @@ use crate::async_bridge::UiCommand; use crate::event::AppEvent; use crate::line_editor::{InputHistory, LineEditor}; use crate::session::Session; +use crate::theme::{Theme, ThemeMode}; use crossterm::event::KeyEvent; use launch_configuration::LaunchConfiguration; use state::StateManager; @@ -207,6 +208,9 @@ pub struct App { pub search_history: InputHistory, pub breakpoint_history: InputHistory, pub file_browser_history: InputHistory, + + // Theme + pub theme: Theme, } impl App { @@ -220,6 +224,7 @@ impl App { wakeup_tx: crossbeam_channel::Sender<()>, initial_breakpoints: Vec, keybindings: config::keybindings::KeybindingConfig, + initial_theme: ThemeMode, ) -> Self { let kill_ring = Rc::new(RefCell::new(String::new())); Self { @@ -275,6 +280,7 @@ impl App { search_history: InputHistory::new(), breakpoint_history: InputHistory::new(), file_browser_history: InputHistory::new(), + theme: Theme::for_mode(initial_theme), } } @@ -286,6 +292,9 @@ impl App { AppEvent::Tick => self.drain_debugger_events(), AppEvent::Mouse(_) => {} AppEvent::Debugger(_) => {} // events arrive via session channel, drained on tick + AppEvent::ThemeChanged(mode) => { + self.theme = Theme::for_mode(mode); + } } } @@ -293,7 +302,6 @@ impl App { crate::input::handle_key(self, key); } - /// Drain all pending debugger events from the session's channel. pub fn drain_debugger_events(&mut self) { // Collect events and errors while briefly borrowing the session let (events, errors) = { @@ -928,6 +936,7 @@ pub(crate) mod test_helpers { wakeup_tx, vec![], // no initial breakpoints Default::default(), + Default::default(), ); f(&mut app); @@ -964,6 +973,7 @@ pub(crate) mod test_helpers { wakeup_tx, vec![], Default::default(), + Default::default(), ); f(&mut app); @@ -1029,6 +1039,7 @@ pub(crate) mod test_helpers { wakeup_tx, vec![], Default::default(), + Default::default(), ); f(&mut app); @@ -1589,6 +1600,7 @@ mod tests { wakeup_tx, vec![], Default::default(), + Default::default(), ); // Add a breakpoint and persist @@ -1612,6 +1624,7 @@ mod tests { wakeup_tx2, vec![], Default::default(), + Default::default(), ); let restored = ui_core::breakpoints::collect_all_breakpoints( @@ -1645,6 +1658,7 @@ mod tests { wakeup_tx, vec![], Default::default(), + Default::default(), ); app1.ui_breakpoints.insert(debugger::Breakpoint { name: None, @@ -1666,6 +1680,7 @@ mod tests { wakeup_tx2, vec![], Default::default(), + Default::default(), ); app2.ui_breakpoints.insert(debugger::Breakpoint { name: None, diff --git a/crates/tui/src/event.rs b/crates/tui/src/event.rs index 1727dd00..17d4c38f 100644 --- a/crates/tui/src/event.rs +++ b/crates/tui/src/event.rs @@ -3,6 +3,8 @@ use std::time::Duration; use crossbeam_channel::{Receiver, Sender}; use crossterm::event::{self, Event, KeyEvent, MouseEvent}; +use crate::theme::ThemeMode; + /// All events the application loop can receive. #[derive(Debug)] #[allow(dead_code)] // Variants/fields used as phases are implemented @@ -17,14 +19,17 @@ pub enum AppEvent { Debugger(debugger::Event), /// Periodic tick for UI refresh (cursor blink, status expiry, etc.). Tick, + /// The system color scheme changed. + ThemeChanged(ThemeMode), } /// Background event handler that multiplexes terminal input, debugger events, /// and periodic ticks into a single channel. pub struct EventHandler { rx: Receiver, - // Keep the handle so the thread is joined on drop. + // Keep handles so threads are joined on drop. _thread: std::thread::JoinHandle<()>, + _theme_thread: Option>, } impl EventHandler { @@ -33,7 +38,16 @@ impl EventHandler { /// `wakeup_rx` receives notifications from the async bridge when debugger /// events are available. This unblocks the poll wait so the TUI redraws /// promptly. - pub fn new(tick_rate: Duration, wakeup_rx: Receiver<()>) -> (Self, Sender) { + /// + /// When `theme_preference` is `Auto`, a background thread periodically + /// polls `dark_light::detect()` and sends `ThemeChanged` events when the + /// system color scheme changes. + pub fn new( + tick_rate: Duration, + wakeup_rx: Receiver<()>, + theme_preference: config::ThemePreference, + initial_mode: ThemeMode, + ) -> (Self, Sender) { let (tx, rx) = crossbeam_channel::unbounded(); let event_tx = tx.clone(); @@ -87,9 +101,40 @@ impl EventHandler { }) .expect("failed to spawn event handler thread"); + // Spawn a separate thread for theme detection so the blocking D-Bus + // call does not interfere with terminal event polling. + tracing::warn!(?theme_preference, "read theme preference"); + let theme_thread = if theme_preference == config::ThemePreference::Auto { + let theme_tx = tx.clone(); + Some( + std::thread::Builder::new() + .name("tui-theme-watcher".into()) + .spawn(move || { + tracing::warn!("spawning theme watcher thread"); + let mut current = initial_mode; + loop { + std::thread::sleep(Duration::from_secs(2)); + let detected = crate::theme::detect_theme_mode(); + tracing::warn!(?detected, "detected current theme"); + if detected != current { + tracing::warn!(?current, ?detected, "switching themes"); + current = detected; + if theme_tx.send(AppEvent::ThemeChanged(detected)).is_err() { + break; + } + } + } + }) + .expect("failed to spawn theme watcher thread"), + ) + } else { + None + }; + let handler = Self { rx, _thread: thread, + _theme_thread: theme_thread, }; (handler, tx) } diff --git a/crates/tui/src/main.rs b/crates/tui/src/main.rs index 45ba21bb..821929d7 100644 --- a/crates/tui/src/main.rs +++ b/crates/tui/src/main.rs @@ -17,6 +17,7 @@ mod input; mod line_editor; mod session; mod syntax; +pub mod theme; mod ui; use app::App; @@ -52,6 +53,13 @@ fn main() -> eyre::Result<()> { // the event handler when debugger events arrive. let (wakeup_tx, wakeup_rx) = crossbeam_channel::unbounded(); + let initial_theme = match boot.theme { + config::ThemePreference::Auto => theme::detect_theme_mode(), + config::ThemePreference::Dark => theme::ThemeMode::Dark, + config::ThemePreference::Light => theme::ThemeMode::Light, + }; + + tracing::warn!(?initial_theme, "got initial theme"); let mut app = App::new( boot.configs, boot.config_names, @@ -62,6 +70,7 @@ fn main() -> eyre::Result<()> { wakeup_tx, boot.initial_breakpoints, boot.keybindings, + initial_theme, ); // Install a panic hook that restores the terminal before printing. @@ -88,7 +97,12 @@ fn main() -> eyre::Result<()> { let mut terminal = Terminal::new(backend).wrap_err("creating terminal")?; // Event loop - let (events, _event_tx) = EventHandler::new(Duration::from_millis(250), wakeup_rx); + let (events, _event_tx) = EventHandler::new( + Duration::from_millis(250), + wakeup_rx, + boot.theme, + initial_theme, + ); let result = run_loop(&mut terminal, &mut app, &events); diff --git a/crates/tui/src/syntax.rs b/crates/tui/src/syntax.rs index 5e7a4596..00320c0b 100644 --- a/crates/tui/src/syntax.rs +++ b/crates/tui/src/syntax.rs @@ -6,6 +6,8 @@ use ratatui::text::{Line, Span}; use syntect::highlighting::{self, HighlightState, Highlighter, ThemeSet}; use syntect::parsing::{ParseState, ScopeStack, SyntaxReference, SyntaxSet}; +use crate::theme::Theme; + static SYNTAX_SET: LazyLock = LazyLock::new(SyntaxSet::load_defaults_newlines); static THEME_SET: LazyLock = LazyLock::new(ThemeSet::load_defaults); @@ -65,6 +67,7 @@ impl SyntaxHighlighter { content: &str, start_line: usize, end_line: usize, + syntect_theme: &str, ) -> Vec> { let syntax = match self.syntax { Some(s) => s, @@ -79,7 +82,7 @@ impl SyntaxHighlighter { } }; - let theme = &THEME_SET.themes["base16-ocean.dark"]; + let theme = &THEME_SET.themes[syntect_theme]; let highlighter = Highlighter::new(theme); // Find the best checkpoint at or before start_line @@ -181,12 +184,13 @@ impl SyntaxHighlighter { breakpoint_lines: &std::collections::HashSet, selection_range: Option<(usize, usize)>, inline_evals: &std::collections::HashMap, + theme: &Theme, ) -> Vec> { - let match_bg = Color::Rgb(100, 100, 0); - let current_match_bg = Color::Rgb(180, 120, 0); - let cursor_bg = Color::Rgb(40, 44, 52); - let exec_bg = Color::Rgb(50, 60, 30); // greenish background for execution line - let selection_bg = Color::Rgb(40, 50, 70); // bluish background for visual selection + let match_bg = theme.search_match_bg; + let current_match_bg = theme.search_current_bg; + let cursor_bg = theme.cursor_line_bg; + let exec_bg = theme.exec_line_bg; + let selection_bg = theme.code_selection_bg; highlighted .iter() @@ -222,17 +226,17 @@ impl SyntaxHighlighter { " " }; let gutter_style = if is_exec { - Style::default().fg(Color::Yellow).bg(exec_bg) + Style::default().fg(theme.accent).bg(exec_bg) } else if has_bp { - Style::default().fg(Color::Red).bg(if is_cursor { + Style::default().fg(theme.error).bg(if is_cursor { cursor_bg } else { Color::Reset }) } else if is_cursor { - Style::default().fg(Color::White).bg(cursor_bg) + Style::default().fg(theme.text).bg(cursor_bg) } else { - Style::default().fg(Color::DarkGray) + Style::default().fg(theme.text_muted) }; let num_str = format!("{gutter_marker}{:>width$} ", line_num, width = gutter_width); @@ -311,10 +315,10 @@ impl SyntaxHighlighter { // Append inline evaluation annotation if present if let Some(eval_text) = inline_evals.get(&line_idx) { let eval_style = if eval_text.starts_with("!!") { - Style::default().fg(Color::Red).add_modifier(Modifier::DIM) + Style::default().fg(theme.error).add_modifier(Modifier::DIM) } else { Style::default() - .fg(Color::Green) + .fg(theme.success) .add_modifier(Modifier::DIM) }; line_spans.push(Span::styled(format!(" {eval_text}"), eval_style)); @@ -373,6 +377,10 @@ mod tests { use super::*; use std::collections::{HashMap, HashSet}; + fn test_theme() -> Theme { + Theme::dark() + } + // ── SyntaxHighlighter::highlight_lines ──────────────────────────── #[test] @@ -381,7 +389,7 @@ mod tests { h.set_file(Path::new("/tmp/file.unknownext")); let content = "line one\nline two\nline three\n"; - let result = h.highlight_lines(content, 0, 3); + let result = h.highlight_lines(content, 0, 3, "base16-ocean.dark"); assert_eq!(result.len(), 3); assert_eq!(result[0].len(), 1); @@ -395,7 +403,7 @@ mod tests { h.set_file(Path::new("/tmp/test.py")); let content = "def hello():\n pass\n"; - let result = h.highlight_lines(content, 0, 2); + let result = h.highlight_lines(content, 0, 2, "base16-ocean.dark"); assert_eq!(result.len(), 2); // Python keywords should produce multiple styled spans @@ -413,7 +421,7 @@ mod tests { h.set_file(Path::new("/tmp/test.py")); let content = "line0\nline1\nline2\nline3\nline4\n"; - let result = h.highlight_lines(content, 2, 4); + let result = h.highlight_lines(content, 2, 4, "base16-ocean.dark"); assert_eq!(result.len(), 2); // lines 2 and 3 } @@ -423,7 +431,7 @@ mod tests { let mut h = SyntaxHighlighter::new(); h.set_file(Path::new("/tmp/test.py")); - let result = h.highlight_lines("", 0, 0); + let result = h.highlight_lines("", 0, 0, "base16-ocean.dark"); assert!(result.is_empty()); } @@ -433,7 +441,7 @@ mod tests { h.set_file(Path::new("/tmp/test.py")); let content = "line0\nline1\n"; - let result = h.highlight_lines(content, 0, 100); + let result = h.highlight_lines(content, 0, 100, "base16-ocean.dark"); assert_eq!(result.len(), 2); // only 2 lines exist } @@ -447,7 +455,7 @@ mod tests { // Generate enough lines to trigger checkpoints let content: String = (0..150).map(|i| format!("x = {i}\n")).collect(); - h.highlight_lines(&content, 0, 150); + h.highlight_lines(&content, 0, 150, "base16-ocean.dark"); assert!(!h.checkpoints.is_empty()); // Switch file: checkpoints cleared @@ -462,11 +470,11 @@ mod tests { let content: String = (0..200).map(|i| format!("x = {i}\n")).collect(); - let r1 = h.highlight_lines(&content, 100, 110); + let r1 = h.highlight_lines(&content, 100, 110, "base16-ocean.dark"); let checkpoint_count = h.checkpoints.len(); // Highlighting again should reuse existing checkpoints - let r2 = h.highlight_lines(&content, 100, 110); + let r2 = h.highlight_lines(&content, 100, 110, "base16-ocean.dark"); assert_eq!(h.checkpoints.len(), checkpoint_count); assert_eq!(r1.len(), r2.len()); } @@ -494,6 +502,7 @@ mod tests { &HashSet::new(), None, // selection_range &HashMap::new(), + &test_theme(), ); assert_eq!(lines.len(), 2); @@ -527,6 +536,7 @@ mod tests { &HashSet::new(), None, &HashMap::new(), + &test_theme(), ); // The code spans on cursor line (index 1) should have a background @@ -561,6 +571,7 @@ mod tests { &bp_lines, None, &HashMap::new(), + &test_theme(), ); // The gutter of line 1 (0-indexed) should have the breakpoint marker ● @@ -586,6 +597,7 @@ mod tests { &HashSet::new(), None, &HashMap::new(), + &test_theme(), ); // Gutter should contain the execution marker ▶ @@ -618,6 +630,7 @@ mod tests { &HashSet::new(), Some((1, 2)), // select lines 1-2 &HashMap::new(), + &test_theme(), ); // Lines 1 and 2 should have selection background @@ -658,6 +671,7 @@ mod tests { &HashSet::new(), None, &evals, + &test_theme(), ); // Last span of line 0 should contain the eval annotation @@ -687,6 +701,7 @@ mod tests { &HashSet::new(), None, &evals, + &test_theme(), ); let last_span = lines[0].spans.last().unwrap(); @@ -715,6 +730,7 @@ mod tests { &HashSet::new(), None, &HashMap::new(), + &test_theme(), ); // Should have at least 3 spans: gutter, matched "hello", remaining " world" @@ -746,6 +762,7 @@ mod tests { &HashSet::new(), None, &HashMap::new(), + &test_theme(), ); assert_eq!(lines.len(), 2); diff --git a/crates/tui/src/theme.rs b/crates/tui/src/theme.rs new file mode 100644 index 00000000..b30243a7 --- /dev/null +++ b/crates/tui/src/theme.rs @@ -0,0 +1,141 @@ +use ratatui::style::Color; + +/// Resolved theme mode. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +pub enum ThemeMode { + #[default] + Dark, + Light, +} + +/// Complete color palette for the TUI. +/// +/// Every `Color` reference in the UI should come from here so that switching +/// between dark and light mode is a single palette swap. +#[derive(Debug, Clone, Copy)] +pub struct Theme { + pub mode: ThemeMode, + + /// Syntect theme name for code syntax highlighting. + pub syntect_theme: &'static str, + + // ── Borders ────────────────────────────────────────────────────── + pub border_focused: Color, + pub border_unfocused: Color, + + // ── Text ───────────────────────────────────────────────────────── + pub text: Color, + pub text_secondary: Color, + pub text_muted: Color, + + // ── Accent ─────────────────────────────────────────────────────── + pub accent: Color, + pub accent_alt: Color, + + // ── Semantic ───────────────────────────────────────────────────── + pub error: Color, + pub success: Color, + pub warning: Color, + + // ── Status bar ─────────────────────────────────────────────────── + pub status_badge_fg: Color, + + // ── Backgrounds ────────────────────────────────────────────────── + pub selection_bg: Color, + pub cursor_line_bg: Color, + pub exec_line_bg: Color, + pub search_match_bg: Color, + pub search_current_bg: Color, + pub code_selection_bg: Color, + + // ── Controls bar key badges ────────────────────────────────────── + pub key_badge_fg: Color, + pub key_badge_bg: Color, +} + +impl Theme { + pub fn dark() -> Self { + Self { + mode: ThemeMode::Dark, + syntect_theme: "base16-ocean.dark", + + border_focused: Color::Cyan, + border_unfocused: Color::DarkGray, + + text: Color::White, + text_secondary: Color::Gray, + text_muted: Color::DarkGray, + + accent: Color::Yellow, + accent_alt: Color::Cyan, + + error: Color::Red, + success: Color::Green, + warning: Color::Yellow, + + status_badge_fg: Color::Black, + + selection_bg: Color::Rgb(50, 50, 80), + cursor_line_bg: Color::Rgb(40, 44, 52), + exec_line_bg: Color::Rgb(50, 60, 30), + search_match_bg: Color::Rgb(100, 100, 0), + search_current_bg: Color::Rgb(180, 120, 0), + code_selection_bg: Color::Rgb(40, 50, 70), + + key_badge_fg: Color::Black, + key_badge_bg: Color::Cyan, + } + } + + pub fn light() -> Self { + Self { + mode: ThemeMode::Light, + syntect_theme: "base16-ocean.light", + + border_focused: Color::Blue, + border_unfocused: Color::Gray, + + text: Color::Black, + text_secondary: Color::DarkGray, + text_muted: Color::Gray, + + accent: Color::Rgb(180, 130, 0), + accent_alt: Color::Blue, + + error: Color::Red, + success: Color::Rgb(0, 140, 0), + warning: Color::Yellow, + + status_badge_fg: Color::White, + + selection_bg: Color::Rgb(200, 210, 235), + cursor_line_bg: Color::Rgb(232, 232, 238), + exec_line_bg: Color::Rgb(215, 240, 195), + search_match_bg: Color::Rgb(255, 255, 120), + search_current_bg: Color::Rgb(255, 190, 70), + code_selection_bg: Color::Rgb(180, 200, 230), + + key_badge_fg: Color::White, + key_badge_bg: Color::Blue, + } + } + + pub fn for_mode(mode: ThemeMode) -> Self { + match mode { + ThemeMode::Dark => Self::dark(), + ThemeMode::Light => Self::light(), + } + } +} + +/// Detect the current system theme preference. +pub fn detect_theme_mode() -> ThemeMode { + match dark_light::detect() { + Ok(dark_light::Mode::Light) => ThemeMode::Light, + Ok(dark_light::Mode::Dark) | Ok(dark_light::Mode::Unspecified) => ThemeMode::Dark, + Err(e) => { + tracing::warn!(error = %e, "could not detect system theme, defaulting to dark"); + ThemeMode::Dark + } + } +} diff --git a/crates/tui/src/ui/breakpoints.rs b/crates/tui/src/ui/breakpoints.rs index e2bc7327..e5662c56 100644 --- a/crates/tui/src/ui/breakpoints.rs +++ b/crates/tui/src/ui/breakpoints.rs @@ -1,7 +1,7 @@ use ratatui::{ Frame, layout::{Constraint, Direction, Layout, Rect}, - style::{Color, Modifier, Style}, + style::{Modifier, Style}, text::{Line, Span}, widgets::{Block, Borders, List, ListItem, Paragraph}, }; @@ -30,9 +30,9 @@ pub fn render(app: &App, frame: &mut Frame, area: Rect) { render_list(app, frame, chunks[0]); let base_style = Style::default() - .fg(Color::White) + .fg(app.theme.text) .add_modifier(Modifier::BOLD); - let mut spans = vec![Span::styled("> ", Style::default().fg(Color::Yellow))]; + let mut spans = vec![Span::styled("> ", Style::default().fg(app.theme.accent))]; spans.extend(app.breakpoint_editor.render_spans(base_style)); let input_line = Line::from(spans); frame.render_widget(Paragraph::new(input_line), chunks[1]); @@ -42,10 +42,12 @@ pub fn render(app: &App, frame: &mut Frame, area: Rect) { } fn render_list(app: &App, frame: &mut Frame, area: Rect) { + let theme = &app.theme; + if app.ui_breakpoints.is_empty() { let empty = Paragraph::new(Span::styled( " (none)", - Style::default().fg(Color::DarkGray), + Style::default().fg(theme.text_muted), )); frame.render_widget(empty, area); return; @@ -68,10 +70,10 @@ fn render_list(app: &App, frame: &mut Frame, area: Rect) { let style = if is_selected { Style::default() - .fg(Color::Red) + .fg(theme.error) .add_modifier(Modifier::BOLD | Modifier::REVERSED) } else { - Style::default().fg(Color::Red) + Style::default().fg(theme.error) }; ListItem::new(Line::from(Span::styled(text, style))) diff --git a/crates/tui/src/ui/call_stack.rs b/crates/tui/src/ui/call_stack.rs index 9142179c..fb4f3e65 100644 --- a/crates/tui/src/ui/call_stack.rs +++ b/crates/tui/src/ui/call_stack.rs @@ -1,7 +1,7 @@ use ratatui::{ Frame, layout::Rect, - style::{Color, Modifier, Style}, + style::{Modifier, Style}, text::{Line, Span}, widgets::{Block, Borders, List, ListItem}, }; @@ -13,6 +13,7 @@ use crate::session::DebuggerState; pub fn render(app: &App, frame: &mut Frame, area: Rect) { let border = super::border_style(app, super::Focus::CallStack); let is_focused = app.focus == super::Focus::CallStack; + let theme = &app.theme; let items: Vec = if let Some(session) = &app.session { if let DebuggerState::Paused { @@ -38,14 +39,14 @@ pub fn render(app: &App, frame: &mut Frame, area: Rect) { let style = if is_selected { Style::default() - .fg(Color::Yellow) + .fg(theme.accent) .add_modifier(Modifier::BOLD | Modifier::REVERSED) } else if is_current { Style::default() - .fg(Color::Yellow) + .fg(theme.accent) .add_modifier(Modifier::BOLD) } else { - Style::default().fg(Color::Gray) + Style::default().fg(theme.text_secondary) }; ListItem::new(Line::from(Span::styled(text, style))) }) @@ -53,13 +54,13 @@ pub fn render(app: &App, frame: &mut Frame, area: Rect) { } else { vec![ListItem::new(Span::styled( " (not paused)", - Style::default().fg(Color::DarkGray), + Style::default().fg(theme.text_muted), ))] } } else { vec![ListItem::new(Span::styled( " (no session)", - Style::default().fg(Color::DarkGray), + Style::default().fg(theme.text_muted), ))] }; diff --git a/crates/tui/src/ui/code_view.rs b/crates/tui/src/ui/code_view.rs index 2d61e91f..439d7989 100644 --- a/crates/tui/src/ui/code_view.rs +++ b/crates/tui/src/ui/code_view.rs @@ -1,7 +1,7 @@ use ratatui::{ Frame, layout::{Alignment, Constraint, Direction, Layout, Rect}, - style::{Color, Modifier, Style}, + style::{Modifier, Style}, text::{Line, Span}, widgets::{Block, Borders, Paragraph}, }; @@ -16,7 +16,7 @@ use super::border_style; // checkpoints across frames. thread_local! { static HIGHLIGHTER: std::cell::RefCell = std::cell::RefCell::new(SyntaxHighlighter::new()); - static LAST_FILE: std::cell::RefCell> = std::cell::RefCell::new(None); + static LAST_FILE: std::cell::RefCell> = std::cell::RefCell::new(None); } pub fn render(app: &mut App, frame: &mut Frame, area: Rect) { @@ -70,17 +70,19 @@ pub fn render(app: &mut App, frame: &mut Frame, area: Rect) { let gutter_width = format!("{}", app.code_view.total_lines).len(); // Syntax highlight the visible lines + let syntect_theme = app.theme.syntect_theme; LAST_FILE.with(|last| { let mut last = last.borrow_mut(); - if last.as_ref() != Some(path) { + let key = (path.clone(), syntect_theme); + if last.as_ref() != Some(&key) { HIGHLIGHTER.with(|h| h.borrow_mut().set_file(path)); - *last = Some(path.clone()); + *last = Some(key); } }); let highlighted = HIGHLIGHTER.with(|h| { h.borrow_mut() - .highlight_lines(&content_owned, start_line, end_line) + .highlight_lines(&content_owned, start_line, end_line, syntect_theme) }); // Determine execution line (0-indexed) if paused at current file @@ -118,6 +120,7 @@ pub fn render(app: &mut App, frame: &mut Frame, area: Rect) { &breakpoint_lines, selection_range, &app.inline_evaluations, + &app.theme, ); let paragraph = Paragraph::new(lines); @@ -155,10 +158,10 @@ fn render_search_bar(app: &App, frame: &mut Frame, area: Rect) { let is_active = app.input_mode == InputMode::Search; let base_style = Style::default() - .fg(Color::White) + .fg(app.theme.text) .add_modifier(Modifier::BOLD); - let mut spans = vec![Span::styled("/", Style::default().fg(Color::Yellow))]; + let mut spans = vec![Span::styled("/", Style::default().fg(app.theme.accent))]; if is_active { spans.extend(app.search_editor.render_spans(base_style)); } else { @@ -166,7 +169,7 @@ fn render_search_bar(app: &App, frame: &mut Frame, area: Rect) { } spans.push(Span::styled( match_info, - Style::default().fg(Color::DarkGray), + Style::default().fg(app.theme.text_muted), )); let line = Line::from(spans); diff --git a/crates/tui/src/ui/controls_bar.rs b/crates/tui/src/ui/controls_bar.rs index ebfa8599..13af568c 100644 --- a/crates/tui/src/ui/controls_bar.rs +++ b/crates/tui/src/ui/controls_bar.rs @@ -1,7 +1,7 @@ use ratatui::{ Frame, layout::Rect, - style::{Color, Modifier, Style}, + style::{Modifier, Style}, text::{Line, Span}, widgets::{Block, Borders, Paragraph}, }; @@ -11,6 +11,7 @@ use crate::app::{App, AppMode}; /// Render the controls bar showing available keybindings for the current state. /// In NoSession/Terminated modes, also shows the config selector with h/l cycling. pub fn render(app: &App, frame: &mut Frame, area: Rect) { + let theme = &app.theme; let mut spans = Vec::new(); // Config selector: shown when no active session @@ -18,18 +19,18 @@ pub fn render(app: &App, frame: &mut Frame, area: Rect) { { spans.push(Span::styled( " \u{25c0} ", - Style::default().fg(Color::DarkGray), + Style::default().fg(theme.text_muted), )); // ◀ spans.push(Span::styled( format!(" {} ", app.config_names[app.selected_config_index]), Style::default() - .fg(Color::White) - .bg(Color::Rgb(50, 50, 80)) + .fg(theme.text) + .bg(theme.selection_bg) .add_modifier(Modifier::BOLD), )); spans.push(Span::styled( " \u{25b6} ", - Style::default().fg(Color::DarkGray), + Style::default().fg(theme.text_muted), )); // ▶ spans.push(Span::raw(" ")); } @@ -72,11 +73,13 @@ pub fn render(app: &App, frame: &mut Frame, area: Rect) { } spans.push(Span::styled( format!(" {key} "), - Style::default().fg(Color::Black).bg(Color::Cyan), + Style::default() + .fg(theme.key_badge_fg) + .bg(theme.key_badge_bg), )); spans.push(Span::styled( format!(" {action}"), - Style::default().fg(Color::Gray), + Style::default().fg(theme.text_secondary), )); } @@ -84,7 +87,7 @@ pub fn render(app: &App, frame: &mut Frame, area: Rect) { let paragraph = Paragraph::new(line).block( Block::default() .borders(Borders::ALL) - .border_style(Style::default().fg(Color::DarkGray)), + .border_style(Style::default().fg(theme.border_unfocused)), ); frame.render_widget(paragraph, area); } diff --git a/crates/tui/src/ui/evaluate_popup.rs b/crates/tui/src/ui/evaluate_popup.rs index 65f8b5cc..a76c44f6 100644 --- a/crates/tui/src/ui/evaluate_popup.rs +++ b/crates/tui/src/ui/evaluate_popup.rs @@ -1,7 +1,7 @@ use ratatui::{ Frame, layout::{Constraint, Direction, Layout, Rect}, - style::{Color, Modifier, Style}, + style::{Modifier, Style}, text::{Line, Span}, widgets::{Block, Borders, Clear, Paragraph, Wrap}, }; @@ -11,6 +11,7 @@ use crate::app::App; /// Render the evaluate expression popup as a floating overlay. pub fn render(app: &App, frame: &mut Frame) { let area = frame.area(); + let theme = &app.theme; let popup_width = ((area.width as f32 * 0.5) as u16).min(60).max(30); let popup_height = 8_u16.min(area.height); @@ -21,7 +22,7 @@ pub fn render(app: &App, frame: &mut Frame) { let block = Block::default() .borders(Borders::ALL) - .border_style(Style::default().fg(Color::Cyan)) + .border_style(Style::default().fg(theme.accent_alt)) .title(" Evaluate Expression (Ctrl+E) "); let inner = block.inner(popup_area); @@ -38,10 +39,8 @@ pub fn render(app: &App, frame: &mut Frame) { .split(inner); // Input line - let base_style = Style::default() - .fg(Color::White) - .add_modifier(Modifier::BOLD); - let mut input_spans = vec![Span::styled("> ", Style::default().fg(Color::Yellow))]; + let base_style = Style::default().fg(theme.text).add_modifier(Modifier::BOLD); + let mut input_spans = vec![Span::styled("> ", Style::default().fg(theme.accent))]; input_spans.extend(app.evaluate_editor.render_spans(base_style)); let input_line = Line::from(input_spans); frame.render_widget(Paragraph::new(input_line), chunks[0]); @@ -49,7 +48,7 @@ pub fn render(app: &App, frame: &mut Frame) { // Separator let sep = Line::from(Span::styled( "\u{2500}".repeat(chunks[1].width as usize), - Style::default().fg(Color::DarkGray), + Style::default().fg(theme.text_muted), )); frame.render_widget(Paragraph::new(sep), chunks[1]); @@ -58,22 +57,22 @@ pub fn render(app: &App, frame: &mut Frame) { Some((text, true)) => { // Error let line = Line::from(vec![ - Span::styled("!! ", Style::default().fg(Color::Red)), - Span::styled(text.clone(), Style::default().fg(Color::Red)), + Span::styled("!! ", Style::default().fg(theme.error)), + Span::styled(text.clone(), Style::default().fg(theme.error)), ]); Paragraph::new(line).wrap(Wrap { trim: false }) } Some((text, false)) => { // Success let line = Line::from(vec![ - Span::styled("=> ", Style::default().fg(Color::Green)), - Span::styled(text.clone(), Style::default().fg(Color::Green)), + Span::styled("=> ", Style::default().fg(theme.success)), + Span::styled(text.clone(), Style::default().fg(theme.success)), ]); Paragraph::new(line).wrap(Wrap { trim: false }) } None => Paragraph::new(Line::from(Span::styled( "(press Enter to evaluate)", - Style::default().fg(Color::DarkGray), + Style::default().fg(theme.text_muted), ))), }; frame.render_widget(result_paragraph, chunks[2]); diff --git a/crates/tui/src/ui/file_browser.rs b/crates/tui/src/ui/file_browser.rs index c8e5d953..4d6d6021 100644 --- a/crates/tui/src/ui/file_browser.rs +++ b/crates/tui/src/ui/file_browser.rs @@ -1,7 +1,7 @@ use ratatui::{ Frame, layout::{Constraint, Direction, Layout, Rect}, - style::{Color, Modifier, Style}, + style::{Modifier, Style}, text::{Line, Span}, widgets::{Block, Borders, List, ListItem, Paragraph}, }; @@ -12,6 +12,7 @@ use crate::app::App; /// Shows fuzzy-filtered project files with matched character highlighting. pub fn render(app: &App, frame: &mut Frame, area: Rect) { let border = super::border_style(app, super::Focus::CallStack); + let theme = &app.theme; let block = Block::default() .borders(Borders::ALL) @@ -32,10 +33,8 @@ pub fn render(app: &App, frame: &mut Frame, area: Rect) { .split(inner); // Search input - let base_style = Style::default() - .fg(Color::White) - .add_modifier(Modifier::BOLD); - let mut input_spans = vec![Span::styled("> ", Style::default().fg(Color::Yellow))]; + let base_style = Style::default().fg(theme.text).add_modifier(Modifier::BOLD); + let mut input_spans = vec![Span::styled("> ", Style::default().fg(theme.accent))]; input_spans.extend(app.file_browser_editor.render_spans(base_style)); let input_line = Line::from(input_spans); frame.render_widget(Paragraph::new(input_line), chunks[0]); @@ -43,7 +42,7 @@ pub fn render(app: &App, frame: &mut Frame, area: Rect) { // Separator let sep = Line::from(Span::styled( "\u{2500}".repeat(chunks[1].width as usize), - Style::default().fg(Color::DarkGray), + Style::default().fg(theme.text_muted), )); frame.render_widget(Paragraph::new(sep), chunks[1]); @@ -60,20 +59,20 @@ pub fn render(app: &App, frame: &mut Frame, area: Rect) { let base_style = if is_selected { Style::default() - .fg(Color::White) - .bg(Color::Rgb(50, 50, 80)) + .fg(theme.text) + .bg(theme.selection_bg) .add_modifier(Modifier::BOLD) } else { - Style::default().fg(Color::Gray) + Style::default().fg(theme.text_secondary) }; let match_style = if is_selected { Style::default() - .fg(Color::Yellow) - .bg(Color::Rgb(50, 50, 80)) + .fg(theme.accent) + .bg(theme.selection_bg) .add_modifier(Modifier::BOLD) } else { Style::default() - .fg(Color::Yellow) + .fg(theme.accent) .add_modifier(Modifier::BOLD) }; @@ -100,7 +99,7 @@ pub fn render(app: &App, frame: &mut Frame, area: Rect) { "No matches" }; frame.render_widget( - Paragraph::new(msg).style(Style::default().fg(Color::DarkGray)), + Paragraph::new(msg).style(Style::default().fg(theme.text_muted)), chunks[2], ); } else { diff --git a/crates/tui/src/ui/file_picker.rs b/crates/tui/src/ui/file_picker.rs index f8e341b8..5879930e 100644 --- a/crates/tui/src/ui/file_picker.rs +++ b/crates/tui/src/ui/file_picker.rs @@ -1,7 +1,7 @@ use ratatui::{ Frame, layout::{Constraint, Direction, Layout, Rect}, - style::{Color, Modifier, Style}, + style::{Modifier, Style}, text::{Line, Span}, widgets::{Block, Borders, Clear, List, ListItem, Paragraph}, }; @@ -11,6 +11,7 @@ use crate::app::App; /// Render the file picker as a floating overlay centred on screen. pub fn render(app: &App, frame: &mut Frame) { let area = frame.area(); + let theme = &app.theme; // Compute popup dimensions: 60% width, capped at 80 cols; 60% height, capped at 20 rows. let popup_width = ((area.width as f32 * 0.6) as u16).min(80).max(30); @@ -23,7 +24,7 @@ pub fn render(app: &App, frame: &mut Frame) { let block = Block::default() .borders(Borders::ALL) - .border_style(Style::default().fg(Color::Cyan)) + .border_style(Style::default().fg(theme.accent_alt)) .title(" Find File (Ctrl+P) "); let inner = block.inner(popup_area); @@ -40,10 +41,8 @@ pub fn render(app: &App, frame: &mut Frame) { .split(inner); // Search input - let base_style = Style::default() - .fg(Color::White) - .add_modifier(Modifier::BOLD); - let mut input_spans = vec![Span::styled("> ", Style::default().fg(Color::Yellow))]; + let base_style = Style::default().fg(theme.text).add_modifier(Modifier::BOLD); + let mut input_spans = vec![Span::styled("> ", Style::default().fg(theme.accent))]; input_spans.extend(app.file_picker_editor.render_spans(base_style)); let input_line = Line::from(input_spans); frame.render_widget(Paragraph::new(input_line), chunks[0]); @@ -51,7 +50,7 @@ pub fn render(app: &App, frame: &mut Frame) { // Separator let sep = Line::from(Span::styled( "─".repeat(chunks[1].width as usize), - Style::default().fg(Color::DarkGray), + Style::default().fg(theme.text_muted), )); frame.render_widget(Paragraph::new(sep), chunks[1]); @@ -71,20 +70,20 @@ pub fn render(app: &App, frame: &mut Frame) { let mut spans: Vec = Vec::new(); let base_style = if is_selected { Style::default() - .fg(Color::White) - .bg(Color::Rgb(50, 50, 80)) + .fg(theme.text) + .bg(theme.selection_bg) .add_modifier(Modifier::BOLD) } else { - Style::default().fg(Color::Gray) + Style::default().fg(theme.text_secondary) }; let match_style = if is_selected { Style::default() - .fg(Color::Yellow) - .bg(Color::Rgb(50, 50, 80)) + .fg(theme.accent) + .bg(theme.selection_bg) .add_modifier(Modifier::BOLD) } else { Style::default() - .fg(Color::Yellow) + .fg(theme.accent) .add_modifier(Modifier::BOLD) }; @@ -112,7 +111,7 @@ pub fn render(app: &App, frame: &mut Frame) { "No matches" }; frame.render_widget( - Paragraph::new(msg).style(Style::default().fg(Color::DarkGray)), + Paragraph::new(msg).style(Style::default().fg(theme.text_muted)), chunks[2], ); } else { diff --git a/crates/tui/src/ui/help.rs b/crates/tui/src/ui/help.rs index 17649004..67fd873f 100644 --- a/crates/tui/src/ui/help.rs +++ b/crates/tui/src/ui/help.rs @@ -1,13 +1,19 @@ use ratatui::{ Frame, layout::Rect, - style::{Color, Modifier, Style}, + style::{Modifier, Style}, text::{Line, Span}, widgets::{Block, Borders, Clear, Paragraph, Wrap}, }; +use crate::theme::Theme; + /// Render the help overlay as a floating popup. -pub fn render(frame: &mut Frame, keybindings: &config::keybindings::KeybindingConfig) { +pub fn render( + frame: &mut Frame, + keybindings: &config::keybindings::KeybindingConfig, + theme: &Theme, +) { let area = frame.area(); let popup_width = ((area.width as f32 * 0.7) as u16).min(70).max(40); @@ -19,18 +25,18 @@ pub fn render(frame: &mut Frame, keybindings: &config::keybindings::KeybindingCo let block = Block::default() .borders(Borders::ALL) - .border_style(Style::default().fg(Color::Yellow)) + .border_style(Style::default().fg(theme.accent)) .title(" Help (? to close) "); let inner = block.inner(popup_area); frame.render_widget(block, popup_area); let key_style = Style::default() - .fg(Color::Yellow) + .fg(theme.accent) .add_modifier(Modifier::BOLD); - let desc_style = Style::default().fg(Color::White); + let desc_style = Style::default().fg(theme.text); let section_style = Style::default() - .fg(Color::Cyan) + .fg(theme.accent_alt) .add_modifier(Modifier::BOLD); use config::keybindings::DebugAction; diff --git a/crates/tui/src/ui/mod.rs b/crates/tui/src/ui/mod.rs index 664c0480..2817083d 100644 --- a/crates/tui/src/ui/mod.rs +++ b/crates/tui/src/ui/mod.rs @@ -15,29 +15,19 @@ pub mod variables; use ratatui::{ Frame, layout::{Constraint, Direction, Layout, Rect}, - style::{Color, Modifier, Style}, + style::{Modifier, Style}, }; use crate::app::{App, AppMode, BottomTab, Focus}; -/// Border style for the currently focused pane. -fn focused_border() -> Style { - Style::default() - .fg(Color::Cyan) - .add_modifier(Modifier::BOLD) -} - -/// Border style for unfocused panes. -fn unfocused_border() -> Style { - Style::default().fg(Color::DarkGray) -} - /// Return the appropriate border style for a pane. pub fn border_style(app: &App, pane: Focus) -> Style { if app.focus == pane { - focused_border() + Style::default() + .fg(app.theme.border_focused) + .add_modifier(Modifier::BOLD) } else { - unfocused_border() + Style::default().fg(app.theme.border_unfocused) } } @@ -115,7 +105,7 @@ pub fn render(app: &mut App, frame: &mut Frame) { evaluate_popup::render(app, frame); } if app.show_help { - help::render(frame, &app.keybindings); + help::render(frame, &app.keybindings, &app.theme); } } @@ -167,13 +157,14 @@ fn render_file_browser_overflow(app: &App, frame: &mut Frame, sidebar: Rect) { } // Use the same styles as the selected item in the file browser + let theme = &app.theme; let base_style = Style::default() - .fg(Color::White) - .bg(Color::Rgb(50, 50, 80)) + .fg(theme.text) + .bg(theme.selection_bg) .add_modifier(Modifier::BOLD); let match_style = Style::default() - .fg(Color::Yellow) - .bg(Color::Rgb(50, 50, 80)) + .fg(theme.accent) + .bg(theme.selection_bg) .add_modifier(Modifier::BOLD); // Build spans for only the overflowing characters @@ -234,6 +225,7 @@ mod snapshot_tests { wakeup_tx, vec![], Default::default(), + Default::default(), ); // Prevent the file browser from loading real git files. app.file_browser_loaded = true; @@ -281,6 +273,7 @@ mod snapshot_tests { wakeup_tx, vec![], Default::default(), + Default::default(), ); // Prevent the file browser from loading real git files. app.file_browser_loaded = true; diff --git a/crates/tui/src/ui/output.rs b/crates/tui/src/ui/output.rs index 35421686..8a600d95 100644 --- a/crates/tui/src/ui/output.rs +++ b/crates/tui/src/ui/output.rs @@ -1,7 +1,7 @@ use ratatui::{ Frame, layout::Rect, - style::{Color, Style}, + style::Style, text::{Line, Span}, widgets::{Block, Borders, Paragraph}, }; @@ -14,6 +14,7 @@ pub const MAX_OUTPUT_LINES: usize = 10_000; /// Render the program output panel with category coloring and auto-scroll. pub fn render(app: &App, frame: &mut Frame, area: Rect) { let border = super::border_style(app, super::Focus::Output); + let theme = &app.theme; let auto_indicator = if app.output_auto_scroll { "auto" @@ -35,7 +36,7 @@ pub fn render(app: &App, frame: &mut Frame, area: Rect) { if app.output_lines.is_empty() { let empty = Paragraph::new(Span::styled( " (no output)", - Style::default().fg(Color::DarkGray), + Style::default().fg(theme.text_muted), )); frame.render_widget(empty, inner); return; @@ -48,16 +49,16 @@ pub fn render(app: &App, frame: &mut Frame, area: Rect) { .filter(|(cat, _)| cat != "telemetry") .map(|(category, text)| { let color = match category.as_str() { - "stderr" => Color::Red, - "console" => Color::DarkGray, - "important" => Color::Yellow, - _ => Color::White, // stdout and others + "stderr" => theme.error, + "console" => theme.text_muted, + "important" => theme.warning, + _ => theme.text, // stdout and others }; let prefix = match category.as_str() { - "stderr" => Span::styled("[err] ", Style::default().fg(Color::Red)), - "console" => Span::styled("[dbg] ", Style::default().fg(Color::DarkGray)), - "important" => Span::styled("[!!!] ", Style::default().fg(Color::Yellow)), + "stderr" => Span::styled("[err] ", Style::default().fg(theme.error)), + "console" => Span::styled("[dbg] ", Style::default().fg(theme.text_muted)), + "important" => Span::styled("[!!!] ", Style::default().fg(theme.warning)), _ => Span::raw(""), }; diff --git a/crates/tui/src/ui/repl.rs b/crates/tui/src/ui/repl.rs index 9b02c466..0d839884 100644 --- a/crates/tui/src/ui/repl.rs +++ b/crates/tui/src/ui/repl.rs @@ -1,7 +1,7 @@ use ratatui::{ Frame, layout::{Constraint, Direction, Layout, Rect}, - style::{Color, Modifier, Style}, + style::{Modifier, Style}, text::{Line, Span}, widgets::{Block, Borders, Paragraph}, }; @@ -11,6 +11,7 @@ use crate::app::{App, AppMode}; /// Render the REPL panel with history and input line. pub fn render(app: &App, frame: &mut Frame, area: Rect) { let border = super::border_style(app, super::Focus::Repl); + let theme = &app.theme; let block = Block::default() .borders(Borders::ALL) @@ -32,12 +33,16 @@ pub fn render(app: &App, frame: &mut Frame, area: Rect) { for (input, output, is_error) in &app.repl_history { lines.push(Line::from(vec![ - Span::styled(">> ", Style::default().fg(Color::Cyan)), - Span::styled(input.as_str(), Style::default().fg(Color::White)), + Span::styled(">> ", Style::default().fg(theme.accent_alt)), + Span::styled(input.as_str(), Style::default().fg(theme.text)), ])); let prefix = if *is_error { "!! " } else { "=> " }; - let color = if *is_error { Color::Red } else { Color::Green }; + let color = if *is_error { + theme.error + } else { + theme.success + }; lines.push(Line::from(vec![ Span::styled(prefix, Style::default().fg(color)), @@ -58,16 +63,14 @@ pub fn render(app: &App, frame: &mut Frame, area: Rect) { // Input line let is_paused = app.mode == AppMode::Paused; let input_line = if is_paused { - let base_style = Style::default() - .fg(Color::White) - .add_modifier(Modifier::BOLD); - let mut spans = vec![Span::styled(">> ", Style::default().fg(Color::Cyan))]; + let base_style = Style::default().fg(theme.text).add_modifier(Modifier::BOLD); + let mut spans = vec![Span::styled(">> ", Style::default().fg(theme.accent_alt))]; spans.extend(app.repl_editor.render_spans(base_style)); Line::from(spans) } else { Line::from(Span::styled( " (pause to evaluate)", - Style::default().fg(Color::DarkGray), + Style::default().fg(theme.text_muted), )) }; diff --git a/crates/tui/src/ui/status_bar.rs b/crates/tui/src/ui/status_bar.rs index fb548e71..06d11323 100644 --- a/crates/tui/src/ui/status_bar.rs +++ b/crates/tui/src/ui/status_bar.rs @@ -1,7 +1,7 @@ use ratatui::{ Frame, layout::Rect, - style::{Color, Modifier, Style}, + style::{Modifier, Style}, text::{Line, Span}, widgets::Paragraph, }; @@ -10,20 +10,21 @@ use crate::app::{App, AppMode}; /// Render the status bar at the bottom showing session state and file info. pub fn render(app: &App, frame: &mut Frame, area: Rect) { + let theme = &app.theme; let mut spans = Vec::new(); // Mode indicator let (mode_text, mode_color) = match app.mode { - AppMode::NoSession => (" NO SESSION ", Color::DarkGray), - AppMode::Initialising => (" STARTING ", Color::Yellow), - AppMode::Running => (" RUNNING ", Color::Green), - AppMode::Paused => (" PAUSED ", Color::Cyan), - AppMode::Terminated => (" TERMINATED ", Color::Red), + AppMode::NoSession => (" NO SESSION ", theme.text_muted), + AppMode::Initialising => (" STARTING ", theme.warning), + AppMode::Running => (" RUNNING ", theme.success), + AppMode::Paused => (" PAUSED ", theme.accent_alt), + AppMode::Terminated => (" TERMINATED ", theme.error), }; spans.push(Span::styled( mode_text, Style::default() - .fg(Color::Black) + .fg(theme.status_badge_fg) .bg(mode_color) .add_modifier(Modifier::BOLD), )); @@ -35,7 +36,7 @@ pub fn render(app: &App, frame: &mut Frame, area: Rect) { spans.push(Span::styled( "[Zen: z] ", Style::default() - .fg(Color::Yellow) + .fg(theme.accent) .add_modifier(Modifier::BOLD), )); } @@ -44,7 +45,7 @@ pub fn render(app: &App, frame: &mut Frame, area: Rect) { if !app.config_names.is_empty() { spans.push(Span::styled( format!("[{}]", app.config_names[app.selected_config_index]), - Style::default().fg(Color::Gray), + Style::default().fg(theme.text_secondary), )); spans.push(Span::raw(" ")); } @@ -55,7 +56,7 @@ pub fn render(app: &App, frame: &mut Frame, area: Rect) { let line = app.code_view.cursor_line + 1; spans.push(Span::styled( format!("{filename}:{line}"), - Style::default().fg(Color::White), + Style::default().fg(theme.text), )); spans.push(Span::raw(" ")); } @@ -64,10 +65,13 @@ pub fn render(app: &App, frame: &mut Frame, area: Rect) { if let Some(err) = &app.status_error { spans.push(Span::styled( format!("ERROR: {err}"), - Style::default().fg(Color::Red), + Style::default().fg(theme.error), )); } else if let Some(msg) = &app.status_message { - spans.push(Span::styled(msg.as_str(), Style::default().fg(Color::Gray))); + spans.push(Span::styled( + msg.as_str(), + Style::default().fg(theme.text_secondary), + )); } let line = Line::from(spans); diff --git a/crates/tui/src/ui/threads.rs b/crates/tui/src/ui/threads.rs index 7a9ece7d..168250ee 100644 --- a/crates/tui/src/ui/threads.rs +++ b/crates/tui/src/ui/threads.rs @@ -1,7 +1,7 @@ use ratatui::{ Frame, layout::Rect, - style::{Color, Style}, + style::Style, text::Span, widgets::{Block, Borders, List, ListItem}, }; @@ -11,18 +11,22 @@ use crate::app::App; /// Render the threads panel showing active threads. pub fn render(app: &App, frame: &mut Frame, area: Rect) { let border = super::border_style(app, super::Focus::CodeView); // threads don't have focus yet + let theme = &app.theme; let items: Vec = if app.threads.is_empty() { vec![ListItem::new(Span::styled( " (none)", - Style::default().fg(Color::DarkGray), + Style::default().fg(theme.text_muted), ))] } else { app.threads .iter() .map(|(id, reason)| { let text = format!(" Thread {id} ({reason})"); - ListItem::new(Span::styled(text, Style::default().fg(Color::Gray))) + ListItem::new(Span::styled( + text, + Style::default().fg(theme.text_secondary), + )) }) .collect() }; diff --git a/crates/tui/src/ui/variables.rs b/crates/tui/src/ui/variables.rs index 98806260..59146812 100644 --- a/crates/tui/src/ui/variables.rs +++ b/crates/tui/src/ui/variables.rs @@ -1,7 +1,7 @@ use ratatui::{ Frame, layout::Rect, - style::{Color, Modifier, Style}, + style::{Modifier, Style}, text::{Line, Span}, widgets::{Block, Borders, List, ListItem}, }; @@ -15,6 +15,7 @@ pub fn render(app: &App, frame: &mut Frame, area: Rect) { let border = super::border_style(app, super::Focus::Variables); let title = " Variables [Alt+1] [y:yank] "; let is_focused = app.focus == super::Focus::Variables; + let theme = &app.theme; let items: Vec = if let Some(session) = &app.session { if let DebuggerState::Paused { paused_frame, .. } = &session.state { @@ -27,6 +28,7 @@ pub fn render(app: &App, frame: &mut Frame, area: Rect) { 0, is_focused && flat_idx == app.variables_cursor, &app.variables_cache, + theme, )); flat_idx += 1; @@ -40,6 +42,7 @@ pub fn render(app: &App, frame: &mut Frame, area: Rect) { 1, is_focused && flat_idx == app.variables_cursor, &app.variables_cache, + theme, )); flat_idx += 1; } @@ -51,13 +54,13 @@ pub fn render(app: &App, frame: &mut Frame, area: Rect) { } else { vec![ListItem::new(Span::styled( " (not paused)", - Style::default().fg(Color::DarkGray), + Style::default().fg(theme.text_muted), ))] } } else { vec![ListItem::new(Span::styled( " (no session)", - Style::default().fg(Color::DarkGray), + Style::default().fg(theme.text_muted), ))] }; @@ -75,6 +78,7 @@ fn make_variable_item<'a>( indent: usize, is_selected: bool, cache: &std::collections::HashMap>, + theme: &crate::theme::Theme, ) -> ListItem<'a> { let indent_str = " ".repeat(indent + 1); @@ -94,21 +98,21 @@ fn make_variable_item<'a>( " " }; - let name_span = Span::styled(var.name.clone(), Style::default().fg(Color::Cyan)); + let name_span = Span::styled(var.name.clone(), Style::default().fg(theme.accent_alt)); let type_span = if let Some(ref ty) = var.r#type { - Span::styled(format!(": {ty}"), Style::default().fg(Color::DarkGray)) + Span::styled(format!(": {ty}"), Style::default().fg(theme.text_muted)) } else { Span::raw("") }; let value_span = if let Some(ref val) = var.value { - Span::styled(format!(" = {val}"), Style::default().fg(Color::White)) + Span::styled(format!(" = {val}"), Style::default().fg(theme.text)) } else { Span::raw("") }; let mut line = Line::from(vec![ Span::raw(indent_str), - Span::styled(tree_marker, Style::default().fg(Color::Yellow)), + Span::styled(tree_marker, Style::default().fg(theme.accent)), name_span, type_span, value_span, diff --git a/crates/ui-core/src/bootstrap.rs b/crates/ui-core/src/bootstrap.rs index ab22a139..175a9977 100644 --- a/crates/ui-core/src/bootstrap.rs +++ b/crates/ui-core/src/bootstrap.rs @@ -71,6 +71,7 @@ pub struct BootstrapResult { pub state_manager: StateManager, pub initial_breakpoints: Vec, pub keybindings: KeybindingConfig, + pub theme: config::ThemePreference, } /// Perform shared application bootstrap: load configurations, set up @@ -144,5 +145,6 @@ pub fn bootstrap(args: &Args) -> eyre::Result { state_manager, initial_breakpoints, keybindings: config.keybindings, + theme: config.theme, }) }