feat(deps): add desktop UI support and update dependencies
Added new dependencies for desktop UI support including color-eyre, crossterm, and ratatui. Updated existing dependencies and modified Cargo.toml to include a new 'desktop' feature flag. Also cleaned up the contributors list and modified the add-req.sh script to focus on core bot functionality. The desktop UI support enables better terminal-based interfaces while the dependency updates ensure compatibility and security. The script changes reflect a shift in focus areas for the project.
This commit is contained in:
parent
5379e21bfe
commit
7fa6ea9f6a
10 changed files with 1480 additions and 115 deletions
358
Cargo.lock
generated
358
Cargo.lock
generated
|
|
@ -252,6 +252,15 @@ dependencies = [
|
|||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "addr2line"
|
||||
version = "0.25.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b"
|
||||
dependencies = [
|
||||
"gimli",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "adler2"
|
||||
version = "2.0.1"
|
||||
|
|
@ -423,7 +432,7 @@ version = "0.2.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0c269894b6fe5e9d7ada0cf69b5bf847ff35bc25fc271f08e1d080fce80339a"
|
||||
dependencies = [
|
||||
"object",
|
||||
"object 0.32.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -991,6 +1000,21 @@ dependencies = [
|
|||
"tower-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "backtrace"
|
||||
version = "0.3.76"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6"
|
||||
dependencies = [
|
||||
"addr2line",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"miniz_oxide",
|
||||
"object 0.37.3",
|
||||
"rustc-demangle",
|
||||
"windows-link 0.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "base16ct"
|
||||
version = "0.1.1"
|
||||
|
|
@ -1106,7 +1130,9 @@ dependencies = [
|
|||
"base64 0.22.1",
|
||||
"bytes",
|
||||
"chrono",
|
||||
"color-eyre",
|
||||
"cron",
|
||||
"crossterm 0.29.0",
|
||||
"csv",
|
||||
"diesel",
|
||||
"dotenvy",
|
||||
|
|
@ -1129,6 +1155,7 @@ dependencies = [
|
|||
"pdf-extract",
|
||||
"qdrant-client",
|
||||
"rand 0.9.2",
|
||||
"ratatui",
|
||||
"redis",
|
||||
"regex",
|
||||
"reqwest",
|
||||
|
|
@ -1250,6 +1277,21 @@ dependencies = [
|
|||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cassowary"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53"
|
||||
|
||||
[[package]]
|
||||
name = "castaway"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a"
|
||||
dependencies = [
|
||||
"rustversion",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cbc"
|
||||
version = "0.1.2"
|
||||
|
|
@ -1402,7 +1444,34 @@ checksum = "af491d569909a7e4dee0ad7db7f5341fef5c614d5b8ec8cf765732aba3cff681"
|
|||
dependencies = [
|
||||
"serde",
|
||||
"termcolor",
|
||||
"unicode-width",
|
||||
"unicode-width 0.1.14",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "color-eyre"
|
||||
version = "0.6.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e5920befb47832a6d61ee3a3a846565cfa39b331331e68a3b1d1116630f2f26d"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
"color-spantrace",
|
||||
"eyre",
|
||||
"indenter",
|
||||
"once_cell",
|
||||
"owo-colors",
|
||||
"tracing-error",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "color-spantrace"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8b88ea9df13354b55bc7234ebcce36e6ef896aca2e42a15de9e10edce01b427"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"owo-colors",
|
||||
"tracing-core",
|
||||
"tracing-error",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1434,6 +1503,20 @@ dependencies = [
|
|||
"tokio-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "compact_str"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b79c4069c6cad78e2e0cdfcbd26275770669fb39fd308a752dc110e83b9af32"
|
||||
dependencies = [
|
||||
"castaway",
|
||||
"cfg-if",
|
||||
"itoa",
|
||||
"rustversion",
|
||||
"ryu",
|
||||
"static_assertions",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "console"
|
||||
version = "0.16.1"
|
||||
|
|
@ -1443,7 +1526,7 @@ dependencies = [
|
|||
"encode_unicode",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"unicode-width",
|
||||
"unicode-width 0.2.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
|
|
@ -1491,6 +1574,15 @@ version = "0.4.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e"
|
||||
|
||||
[[package]]
|
||||
name = "convert_case"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bb402b8d4c85569410425650ce3eddc7d698ed96d39a73f941b08fb63082f1e7"
|
||||
dependencies = [
|
||||
"unicode-segmentation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cookie"
|
||||
version = "0.16.2"
|
||||
|
|
@ -1591,6 +1683,49 @@ version = "0.8.21"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
|
||||
|
||||
[[package]]
|
||||
name = "crossterm"
|
||||
version = "0.28.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"crossterm_winapi",
|
||||
"mio",
|
||||
"parking_lot",
|
||||
"rustix 0.38.44",
|
||||
"signal-hook",
|
||||
"signal-hook-mio",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossterm"
|
||||
version = "0.29.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"crossterm_winapi",
|
||||
"derive_more 2.0.1",
|
||||
"document-features",
|
||||
"mio",
|
||||
"parking_lot",
|
||||
"rustix 1.1.2",
|
||||
"signal-hook",
|
||||
"signal-hook-mio",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossterm_winapi"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b"
|
||||
dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crunchy"
|
||||
version = "0.2.4"
|
||||
|
|
@ -1871,7 +2006,7 @@ version = "0.99.20"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f"
|
||||
dependencies = [
|
||||
"convert_case",
|
||||
"convert_case 0.4.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rustc_version",
|
||||
|
|
@ -1893,6 +2028,7 @@ version = "2.0.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3"
|
||||
dependencies = [
|
||||
"convert_case 0.7.1",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
|
|
@ -1960,6 +2096,15 @@ dependencies = [
|
|||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "document-features"
|
||||
version = "0.2.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61"
|
||||
dependencies = [
|
||||
"litrs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dotenvy"
|
||||
version = "0.15.7"
|
||||
|
|
@ -2131,6 +2276,16 @@ dependencies = [
|
|||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "eyre"
|
||||
version = "0.6.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec"
|
||||
dependencies = [
|
||||
"indenter",
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "2.3.0"
|
||||
|
|
@ -2363,6 +2518,12 @@ dependencies = [
|
|||
"polyval",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gimli"
|
||||
version = "0.32.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7"
|
||||
|
||||
[[package]]
|
||||
name = "glob"
|
||||
version = "0.3.3"
|
||||
|
|
@ -2881,6 +3042,12 @@ dependencies = [
|
|||
"quote",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indenter"
|
||||
version = "0.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "964de6e86d545b246d84badc0fef527924ace5134f30641c203ef52ba83f58d5"
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "1.9.3"
|
||||
|
|
@ -2909,11 +3076,20 @@ checksum = "ade6dfcba0dfb62ad59e59e7241ec8912af34fd29e0e743e3db992bd278e8b65"
|
|||
dependencies = [
|
||||
"console",
|
||||
"portable-atomic",
|
||||
"unicode-width",
|
||||
"unicode-width 0.2.0",
|
||||
"unit-prefix",
|
||||
"web-time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indoc"
|
||||
version = "2.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706"
|
||||
dependencies = [
|
||||
"rustversion",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inout"
|
||||
version = "0.1.4"
|
||||
|
|
@ -2924,6 +3100,19 @@ dependencies = [
|
|||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "instability"
|
||||
version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "435d80800b936787d62688c927b6490e887c7ef5ff9ce922c6c6050fca75eb9a"
|
||||
dependencies = [
|
||||
"darling 0.20.11",
|
||||
"indoc",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ipnet"
|
||||
version = "2.11.0"
|
||||
|
|
@ -3156,6 +3345,12 @@ dependencies = [
|
|||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.4.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.11.0"
|
||||
|
|
@ -3168,6 +3363,12 @@ version = "0.8.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956"
|
||||
|
||||
[[package]]
|
||||
name = "litrs"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092"
|
||||
|
||||
[[package]]
|
||||
name = "livekit"
|
||||
version = "0.7.24"
|
||||
|
|
@ -3590,6 +3791,15 @@ dependencies = [
|
|||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "object"
|
||||
version = "0.37.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.3"
|
||||
|
|
@ -3685,6 +3895,12 @@ version = "0.5.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e"
|
||||
|
||||
[[package]]
|
||||
name = "owo-colors"
|
||||
version = "4.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c6901729fa79e91a0913333229e9ca5dc725089d1c363b2f4b4760709dc4a52"
|
||||
|
||||
[[package]]
|
||||
name = "p256"
|
||||
version = "0.11.1"
|
||||
|
|
@ -3747,6 +3963,12 @@ dependencies = [
|
|||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "paste"
|
||||
version = "1.0.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
|
||||
|
||||
[[package]]
|
||||
name = "pbjson"
|
||||
version = "0.6.0"
|
||||
|
|
@ -4250,6 +4472,27 @@ version = "1.6.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f93e7e49bb0bf967717f7bd674458b3d6b0c5f48ec7e3038166026a69fc22223"
|
||||
|
||||
[[package]]
|
||||
name = "ratatui"
|
||||
version = "0.29.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"cassowary",
|
||||
"compact_str",
|
||||
"crossterm 0.28.1",
|
||||
"indoc",
|
||||
"instability",
|
||||
"itertools 0.13.0",
|
||||
"lru",
|
||||
"paste",
|
||||
"strum",
|
||||
"unicode-segmentation",
|
||||
"unicode-truncate",
|
||||
"unicode-width 0.2.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redis"
|
||||
version = "0.27.6"
|
||||
|
|
@ -4419,6 +4662,12 @@ dependencies = [
|
|||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc-demangle"
|
||||
version = "0.1.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace"
|
||||
|
||||
[[package]]
|
||||
name = "rustc-hash"
|
||||
version = "2.1.1"
|
||||
|
|
@ -4434,6 +4683,19 @@ dependencies = [
|
|||
"semver",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "0.38.44"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys 0.4.15",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "1.1.2"
|
||||
|
|
@ -4443,7 +4705,7 @@ dependencies = [
|
|||
"bitflags",
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"linux-raw-sys 0.11.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
|
|
@ -4764,6 +5026,27 @@ version = "1.3.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook"
|
||||
version = "0.3.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"signal-hook-registry",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook-mio"
|
||||
version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"mio",
|
||||
"signal-hook",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook-registry"
|
||||
version = "1.4.6"
|
||||
|
|
@ -4890,6 +5173,28 @@ version = "0.11.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "strum"
|
||||
version = "0.26.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06"
|
||||
dependencies = [
|
||||
"strum_macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strum_macros"
|
||||
version = "0.26.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be"
|
||||
dependencies = [
|
||||
"heck 0.5.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rustversion",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "2.6.1"
|
||||
|
|
@ -4971,7 +5276,7 @@ dependencies = [
|
|||
"fastrand",
|
||||
"getrandom 0.3.4",
|
||||
"once_cell",
|
||||
"rustix",
|
||||
"rustix 1.1.2",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
|
|
@ -5352,6 +5657,16 @@ dependencies = [
|
|||
"valuable",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-error"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b1581020d7a273442f5b45074a6a57d5757ad0a47dac0e9f0bd57b81936f3db"
|
||||
dependencies = [
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-log"
|
||||
version = "0.2.0"
|
||||
|
|
@ -5455,10 +5770,33 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
version = "0.2.2"
|
||||
name = "unicode-segmentation"
|
||||
version = "1.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
|
||||
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-truncate"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf"
|
||||
dependencies = [
|
||||
"itertools 0.13.0",
|
||||
"unicode-segmentation",
|
||||
"unicode-width 0.1.14",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
version = "0.1.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-xid"
|
||||
|
|
|
|||
16
Cargo.toml
16
Cargo.toml
|
|
@ -37,12 +37,15 @@ license = "AGPL-3.0"
|
|||
repository = "https://github.com/GeneralBots/BotServer"
|
||||
|
||||
[features]
|
||||
default = [ "vectordb"]
|
||||
default = [ "vectordb", "desktop"]
|
||||
vectordb = ["qdrant-client"]
|
||||
email = ["imap"]
|
||||
desktop = []
|
||||
|
||||
[dependencies]
|
||||
color-eyre = "0.6.5"
|
||||
crossterm = "0.29.0"
|
||||
ratatui = "0.29.0"
|
||||
scopeguard = "1.2.0"
|
||||
once_cell = "1.18.0"
|
||||
actix-cors = "0.7"
|
||||
|
|
@ -101,10 +104,9 @@ urlencoding = "2.1"
|
|||
uuid = { version = "1.11", features = ["serde", "v4"] }
|
||||
zip = "2.2"
|
||||
|
||||
|
||||
[profile.release]
|
||||
lto = true # Enables Link-Time Optimization
|
||||
opt-level = "z" # Optimizes for size instead of speed
|
||||
strip = true # Strips debug symbols
|
||||
panic = "abort" # Reduces size by removing panic unwinding
|
||||
codegen-units = 1 # More aggressive optimization
|
||||
lto = true
|
||||
opt-level = "z"
|
||||
strip = true
|
||||
panic = "abort"
|
||||
codegen-units = 1
|
||||
|
|
|
|||
38
add-req.sh
38
add-req.sh
|
|
@ -21,31 +21,31 @@ for file in "${prompts[@]}"; do
|
|||
done
|
||||
|
||||
dirs=(
|
||||
"auth"
|
||||
"automation"
|
||||
"basic"
|
||||
"bootstrap"
|
||||
#"auth"
|
||||
#"automation"
|
||||
#"basic"
|
||||
#"bootstrap"
|
||||
"bot"
|
||||
"channels"
|
||||
"config"
|
||||
"context"
|
||||
#"channels"
|
||||
#"config"
|
||||
#"context"
|
||||
"drive_monitor"
|
||||
"email"
|
||||
#"email"
|
||||
"file"
|
||||
# "kb"
|
||||
"llm"
|
||||
"llm_models"
|
||||
"org"
|
||||
"package"
|
||||
"package_manager"
|
||||
"riot_compiler"
|
||||
"session"
|
||||
#"llm_models"
|
||||
#"org"
|
||||
#"package_manager"
|
||||
#"riot_compiler"
|
||||
#"session"
|
||||
"shared"
|
||||
"tests"
|
||||
"tools"
|
||||
"ui"
|
||||
"web_server"
|
||||
"web_automation"
|
||||
#"tests"
|
||||
#"tools"
|
||||
#"ui"
|
||||
"ui_tree"
|
||||
#"web_server"
|
||||
#"web_automation"
|
||||
)
|
||||
|
||||
filter_rust_file() {
|
||||
|
|
|
|||
|
|
@ -16,6 +16,6 @@ impl ModelHandler for GptOss20bHandler {
|
|||
}
|
||||
|
||||
fn has_analysis_markers(&self, buffer: &str) -> bool {
|
||||
buffer.contains("**")
|
||||
buffer.contains("analysis<|message|>")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
86
src/main.rs
86
src/main.rs
|
|
@ -27,10 +27,8 @@ mod package_manager;
|
|||
mod session;
|
||||
mod shared;
|
||||
pub mod tests;
|
||||
#[cfg(feature = "desktop")]
|
||||
mod ui;
|
||||
mod ui_tree;
|
||||
mod web_server;
|
||||
|
||||
use crate::auth::auth_handler;
|
||||
use crate::automation::AutomationService;
|
||||
use crate::bootstrap::BootstrapManager;
|
||||
|
|
@ -49,14 +47,12 @@ use crate::session::{create_session, get_session_history, get_sessions, start_se
|
|||
use crate::shared::state::AppState;
|
||||
use crate::web_server::{bot_index, index, static_files};
|
||||
|
||||
#[cfg(not(feature = "desktop"))]
|
||||
#[tokio::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
use botserver::config::ConfigManager;
|
||||
|
||||
use crate::llm::local::ensure_llama_servers_running;
|
||||
|
||||
use botserver::config::ConfigManager;
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
let no_ui = args.contains(&"--noui".to_string());
|
||||
if args.len() > 1 {
|
||||
let command = &args[1];
|
||||
match command.as_str() {
|
||||
|
|
@ -71,6 +67,7 @@ async fn main() -> std::io::Result<()> {
|
|||
));
|
||||
}
|
||||
},
|
||||
"--noui" => {}
|
||||
_ => {
|
||||
eprintln!("Unknown command: {}", command);
|
||||
eprintln!("Run 'botserver --help' for usage information");
|
||||
|
|
@ -81,34 +78,49 @@ async fn main() -> std::io::Result<()> {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Rest of the original main function remains unchanged...
|
||||
dotenv().ok();
|
||||
let ui_handle = if !no_ui {
|
||||
let (ui_tx, mut ui_rx) = tokio::sync::mpsc::channel::<Arc<AppState>>(1);
|
||||
let handle = std::thread::Builder::new()
|
||||
.name("ui-thread".to_string())
|
||||
.spawn(move || {
|
||||
let mut ui = crate::ui_tree::XtreeUI::new();
|
||||
let rt = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.expect("Failed to create UI runtime");
|
||||
rt.block_on(async {
|
||||
if let Some(app_state) = ui_rx.recv().await {
|
||||
ui.set_app_state(app_state);
|
||||
}
|
||||
});
|
||||
if let Err(e) = ui.start_ui() {
|
||||
eprintln!("UI error: {}", e);
|
||||
}
|
||||
})
|
||||
.expect("Failed to spawn UI thread");
|
||||
Some((handle, ui_tx))
|
||||
} else {
|
||||
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info"))
|
||||
.write_style(env_logger::WriteStyle::Always)
|
||||
.init();
|
||||
|
||||
None
|
||||
};
|
||||
let install_mode = if args.contains(&"--container".to_string()) {
|
||||
InstallMode::Container
|
||||
} else {
|
||||
InstallMode::Local
|
||||
};
|
||||
|
||||
let tenant = if let Some(idx) = args.iter().position(|a| a == "--tenant") {
|
||||
args.get(idx + 1).cloned()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let mut bootstrap = BootstrapManager::new(install_mode.clone(), tenant.clone()).await;
|
||||
|
||||
// Prevent double bootstrap: skip if environment already initialized
|
||||
let env_path = std::env::current_dir()?
|
||||
.join("botserver-stack")
|
||||
.join(".env");
|
||||
let cfg = if env_path.exists() {
|
||||
info!("Environment already initialized, skipping bootstrap");
|
||||
|
||||
match diesel::Connection::establish(&std::env::var("DATABASE_URL").unwrap()) {
|
||||
Ok(mut conn) => {
|
||||
AppConfig::from_database(&mut conn).expect("Failed to load config from DB")
|
||||
|
|
@ -117,16 +129,11 @@ async fn main() -> std::io::Result<()> {
|
|||
}
|
||||
} else {
|
||||
match bootstrap.bootstrap().await {
|
||||
Ok(config) => {
|
||||
info!("Bootstrap completed successfully");
|
||||
config
|
||||
}
|
||||
Ok(config) => config,
|
||||
Err(e) => {
|
||||
log::error!("Bootstrap failed: {}", e);
|
||||
match diesel::Connection::establish(
|
||||
&std::env::var("DATABASE_URL").unwrap_or_else(|_| {
|
||||
"postgres://gbuser:@localhost:5432/botserver".to_string()
|
||||
}),
|
||||
&std::env::var("DATABASE_URL").unwrap()
|
||||
) {
|
||||
Ok(mut conn) => {
|
||||
AppConfig::from_database(&mut conn).expect("Failed to load config from DB")
|
||||
|
|
@ -136,18 +143,12 @@ async fn main() -> std::io::Result<()> {
|
|||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Start all services (synchronous)
|
||||
if let Err(e) = bootstrap.start_all() {
|
||||
log::warn!("Failed to start all services: {}", e);
|
||||
}
|
||||
|
||||
// Upload templates (asynchronous)
|
||||
if let Err(e) = futures::executor::block_on(bootstrap.upload_templates_to_drive(&cfg)) {
|
||||
log::warn!("Failed to upload templates to MinIO: {}", e);
|
||||
}
|
||||
|
||||
// Refresh configuration from environment to ensure latest DATABASE_URL and credentials
|
||||
dotenv().ok();
|
||||
let refreshed_cfg = AppConfig::from_env().expect("Failed to load config from env");
|
||||
let config = std::sync::Arc::new(refreshed_cfg.clone());
|
||||
|
|
@ -161,31 +162,26 @@ async fn main() -> std::io::Result<()> {
|
|||
));
|
||||
}
|
||||
};
|
||||
|
||||
let cache_url = std::env::var("CACHE_URL")
|
||||
.or_else(|_| std::env::var("REDIS_URL"))
|
||||
.unwrap_or_else(|_| "redis://localhost:6379".to_string());
|
||||
let redis_client = match redis::Client::open(cache_url.as_str()) {
|
||||
Ok(client) => Some(Arc::new(client)),
|
||||
Err(e) => {
|
||||
log::warn!("Failed to connect to Redis: Redis URL did not parse- {}", e);
|
||||
log::warn!("Failed to connect to Redis: {}", e);
|
||||
None
|
||||
}
|
||||
};
|
||||
let web_adapter = Arc::new(WebChannelAdapter::new());
|
||||
let voice_adapter = Arc::new(VoiceAdapter::new());
|
||||
|
||||
let drive = init_drive(&config.drive)
|
||||
.await
|
||||
.expect("Failed to initialize Drive");
|
||||
|
||||
let session_manager = Arc::new(tokio::sync::Mutex::new(session::SessionManager::new(
|
||||
diesel::Connection::establish(&cfg.database_url()).unwrap(),
|
||||
redis_client.clone(),
|
||||
)));
|
||||
|
||||
let auth_service = Arc::new(tokio::sync::Mutex::new(auth::AuthService::new()));
|
||||
|
||||
let conn = diesel::Connection::establish(&cfg.database_url()).unwrap();
|
||||
let config_manager = ConfigManager::new(Arc::new(Mutex::new(conn)));
|
||||
let mut bot_conn = diesel::Connection::establish(&cfg.database_url()).unwrap();
|
||||
|
|
@ -193,17 +189,15 @@ async fn main() -> std::io::Result<()> {
|
|||
let llm_url = config_manager
|
||||
.get_config(&default_bot_id, "llm-url", Some("http://localhost:8081"))
|
||||
.unwrap_or_else(|_| "http://localhost:8081".to_string());
|
||||
|
||||
let llm_provider = Arc::new(crate::llm::OpenAIClient::new(
|
||||
"empty".to_string(),
|
||||
Some(llm_url.clone()),
|
||||
));
|
||||
|
||||
let app_state = Arc::new(AppState {
|
||||
drive: Some(drive),
|
||||
config: Some(cfg.clone()),
|
||||
conn: db_pool.clone(),
|
||||
bucket_name: "default.gbai".to_string(), // Default bucket name
|
||||
bucket_name: "default.gbai".to_string(),
|
||||
cache: redis_client.clone(),
|
||||
session_manager: session_manager.clone(),
|
||||
llm_provider: llm_provider.clone(),
|
||||
|
|
@ -220,29 +214,25 @@ async fn main() -> std::io::Result<()> {
|
|||
web_adapter: web_adapter.clone(),
|
||||
voice_adapter: voice_adapter.clone(),
|
||||
});
|
||||
|
||||
if let Some((_, ui_tx)) = &ui_handle {
|
||||
ui_tx.send(app_state.clone()).await.ok();
|
||||
}
|
||||
info!(
|
||||
"Starting HTTP server on {}:{}",
|
||||
config.server.host, config.server.port
|
||||
);
|
||||
|
||||
let worker_count = std::thread::available_parallelism()
|
||||
.map(|n| n.get())
|
||||
.unwrap_or(4);
|
||||
|
||||
// Initialize bot orchestrator and mount all bots
|
||||
let bot_orchestrator = BotOrchestrator::new(app_state.clone());
|
||||
|
||||
// Mount all active bots from database
|
||||
if let Err(e) = bot_orchestrator.mount_all_bots().await {
|
||||
log::error!("Failed to mount bots: {}", e);
|
||||
// Use BotOrchestrator::send_warning to notify system admins
|
||||
let msg = format!("Bot mount failure: {}", e);
|
||||
let _ = bot_orchestrator
|
||||
.send_warning("System", "AdminBot", msg.as_str())
|
||||
.await;
|
||||
} else {
|
||||
let _sessions = get_sessions;
|
||||
log::info!("Session handler registered successfully");
|
||||
}
|
||||
|
||||
let automation_state = app_state.clone();
|
||||
|
|
@ -259,7 +249,7 @@ async fn main() -> std::io::Result<()> {
|
|||
});
|
||||
|
||||
if let Err(e) = ensure_llama_servers_running(&app_state).await {
|
||||
error!("Failed to stat LLM servers: {}", e);
|
||||
error!("Failed to start LLM servers: {}", e);
|
||||
}
|
||||
|
||||
HttpServer::new(move || {
|
||||
|
|
@ -270,6 +260,7 @@ async fn main() -> std::io::Result<()> {
|
|||
.max_age(3600);
|
||||
|
||||
let app_state_clone = app_state.clone();
|
||||
|
||||
let mut app = App::new()
|
||||
.wrap(cors)
|
||||
.wrap(Logger::default())
|
||||
|
|
@ -301,6 +292,7 @@ async fn main() -> std::io::Result<()> {
|
|||
.service(save_draft)
|
||||
.service(save_click);
|
||||
}
|
||||
|
||||
app = app.service(static_files);
|
||||
app = app.service(bot_index);
|
||||
app
|
||||
|
|
|
|||
153
src/ui_tree/editor.rs
Normal file
153
src/ui_tree/editor.rs
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
use color_eyre::Result;
|
||||
use std::sync::Arc;
|
||||
use crate::shared::state::AppState;
|
||||
|
||||
pub struct Editor {
|
||||
file_path: String,
|
||||
bucket: String,
|
||||
key: String,
|
||||
content: String,
|
||||
cursor_pos: usize,
|
||||
scroll_offset: usize,
|
||||
modified: bool,
|
||||
}
|
||||
|
||||
impl Editor {
|
||||
pub async fn load(app_state: &Arc<AppState>, bucket: &str, path: &str) -> Result<Self> {
|
||||
let content = if let Some(drive) = &app_state.drive {
|
||||
match drive.get_object().bucket(bucket).key(path).send().await {
|
||||
Ok(response) => {
|
||||
let bytes = response.body.collect().await?.into_bytes();
|
||||
String::from_utf8_lossy(&bytes).to_string()
|
||||
}
|
||||
Err(_) => String::new(),
|
||||
}
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
file_path: format!("{}/{}", bucket, path),
|
||||
bucket: bucket.to_string(),
|
||||
key: path.to_string(),
|
||||
content,
|
||||
cursor_pos: 0,
|
||||
scroll_offset: 0,
|
||||
modified: false,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn save(&mut self, app_state: &Arc<AppState>) -> Result<()> {
|
||||
if let Some(drive) = &app_state.drive {
|
||||
drive.put_object()
|
||||
.bucket(&self.bucket)
|
||||
.key(&self.key)
|
||||
.body(self.content.as_bytes().to_vec().into())
|
||||
.send()
|
||||
.await?;
|
||||
self.modified = false;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn file_path(&self) -> &str {
|
||||
&self.file_path
|
||||
}
|
||||
|
||||
pub fn render(&self) -> String {
|
||||
let lines: Vec<&str> = self.content.lines().collect();
|
||||
let total_lines = lines.len().max(1);
|
||||
let visible_lines = 25;
|
||||
let cursor_line = self.content[..self.cursor_pos].lines().count();
|
||||
let cursor_col = self.content[..self.cursor_pos]
|
||||
.lines()
|
||||
.last()
|
||||
.map(|line| line.len())
|
||||
.unwrap_or(0);
|
||||
|
||||
let start = self.scroll_offset;
|
||||
let end = (start + visible_lines).min(total_lines);
|
||||
let mut display_lines = Vec::new();
|
||||
|
||||
for i in start..end {
|
||||
let line_num = i + 1;
|
||||
let line_content = if i < lines.len() { lines[i] } else { "" };
|
||||
let is_cursor_line = i == cursor_line;
|
||||
let line_marker = if is_cursor_line { "▶" } else { " " };
|
||||
display_lines.push(format!("{} {:4} │ {}", line_marker, line_num, line_content));
|
||||
}
|
||||
|
||||
if display_lines.is_empty() {
|
||||
display_lines.push(" ▶ 1 │ ".to_string());
|
||||
}
|
||||
|
||||
display_lines.push("".to_string());
|
||||
display_lines.push("─────────────────────────────────────────────────────────────".to_string());
|
||||
let status = if self.modified { "●" } else { "✓" };
|
||||
display_lines.push(format!(" {} {} │ Line: {}, Col: {}",
|
||||
status, self.file_path, cursor_line + 1, cursor_col + 1));
|
||||
display_lines.push(" Ctrl+S: Save │ Ctrl+W: Close │ Esc: Close without saving".to_string());
|
||||
display_lines.join("\n")
|
||||
}
|
||||
|
||||
pub fn move_up(&mut self) {
|
||||
if let Some(prev_line_end) = self.content[..self.cursor_pos].rfind('\n') {
|
||||
if let Some(prev_prev_line_end) = self.content[..prev_line_end].rfind('\n') {
|
||||
let target_pos = prev_prev_line_end + 1 + (self.cursor_pos - prev_line_end - 1).min(
|
||||
self.content[prev_prev_line_end + 1..prev_line_end].len()
|
||||
);
|
||||
self.cursor_pos = target_pos;
|
||||
} else {
|
||||
self.cursor_pos = (self.cursor_pos - prev_line_end - 1).min(prev_line_end);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn move_down(&mut self) {
|
||||
if let Some(next_line_start) = self.content[self.cursor_pos..].find('\n') {
|
||||
let current_line_start = self.content[..self.cursor_pos].rfind('\n').map(|pos| pos + 1).unwrap_or(0);
|
||||
let next_line_absolute = self.cursor_pos + next_line_start + 1;
|
||||
if let Some(next_next_line_start) = self.content[next_line_absolute..].find('\n') {
|
||||
let target_pos = next_line_absolute + (self.cursor_pos - current_line_start).min(next_next_line_start);
|
||||
self.cursor_pos = target_pos;
|
||||
} else {
|
||||
let target_pos = next_line_absolute + (self.cursor_pos - current_line_start).min(
|
||||
self.content[next_line_absolute..].len()
|
||||
);
|
||||
self.cursor_pos = target_pos;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn move_left(&mut self) {
|
||||
if self.cursor_pos > 0 {
|
||||
self.cursor_pos -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn move_right(&mut self) {
|
||||
if self.cursor_pos < self.content.len() {
|
||||
self.cursor_pos += 1;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert_char(&mut self, c: char) {
|
||||
self.modified = true;
|
||||
self.content.insert(self.cursor_pos, c);
|
||||
self.cursor_pos += 1;
|
||||
}
|
||||
|
||||
pub fn backspace(&mut self) {
|
||||
if self.cursor_pos > 0 {
|
||||
self.modified = true;
|
||||
self.content.remove(self.cursor_pos - 1);
|
||||
self.cursor_pos -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert_newline(&mut self) {
|
||||
self.modified = true;
|
||||
self.content.insert(self.cursor_pos, '\n');
|
||||
self.cursor_pos += 1;
|
||||
}
|
||||
}
|
||||
240
src/ui_tree/file_tree.rs
Normal file
240
src/ui_tree/file_tree.rs
Normal file
|
|
@ -0,0 +1,240 @@
|
|||
use color_eyre::Result;
|
||||
use std::sync::Arc;
|
||||
use crate::shared::state::AppState;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum TreeNode {
|
||||
Bucket { name: String },
|
||||
Folder { bucket: String, path: String, name: String },
|
||||
File { bucket: String, path: String, name: String },
|
||||
}
|
||||
|
||||
pub struct FileTree {
|
||||
app_state: Arc<AppState>,
|
||||
items: Vec<(String, TreeNode)>,
|
||||
selected: usize,
|
||||
current_bucket: Option<String>,
|
||||
current_path: Vec<String>,
|
||||
}
|
||||
|
||||
impl FileTree {
|
||||
pub fn new(app_state: Arc<AppState>) -> Self {
|
||||
Self {
|
||||
app_state,
|
||||
items: Vec::new(),
|
||||
selected: 0,
|
||||
current_bucket: None,
|
||||
current_path: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn load_root(&mut self) -> Result<()> {
|
||||
self.items.clear();
|
||||
self.current_bucket = None;
|
||||
self.current_path.clear();
|
||||
if let Some(drive) = &self.app_state.drive {
|
||||
let result = drive.list_buckets().send().await;
|
||||
match result {
|
||||
Ok(response) => {
|
||||
let buckets = response.buckets();
|
||||
for bucket in buckets {
|
||||
if let Some(name) = bucket.name() {
|
||||
let icon = if name.ends_with(".gbai") { "🤖" } else { "📦" };
|
||||
let display = format!("{} {}", icon, name);
|
||||
self.items.push((display, TreeNode::Bucket { name: name.to_string() }));
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
self.items.push((format!("✗ Error: {}", e), TreeNode::Bucket { name: String::new() }));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.items.push(("✗ Drive not connected".to_string(), TreeNode::Bucket { name: String::new() }));
|
||||
}
|
||||
if self.items.is_empty() {
|
||||
self.items.push(("(no buckets found)".to_string(), TreeNode::Bucket { name: String::new() }));
|
||||
}
|
||||
self.selected = 0;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn enter_bucket(&mut self, bucket: String) -> Result<()> {
|
||||
self.current_bucket = Some(bucket.clone());
|
||||
self.current_path.clear();
|
||||
self.load_bucket_contents(&bucket, "").await
|
||||
}
|
||||
|
||||
pub async fn enter_folder(&mut self, bucket: String, path: String) -> Result<()> {
|
||||
self.current_bucket = Some(bucket.clone());
|
||||
let parts: Vec<&str> = path.trim_matches('/').split('/').filter(|s| !s.is_empty()).collect();
|
||||
self.current_path = parts.iter().map(|s| s.to_string()).collect();
|
||||
self.load_bucket_contents(&bucket, &path).await
|
||||
}
|
||||
|
||||
pub fn go_up(&mut self) -> bool {
|
||||
if self.current_path.is_empty() {
|
||||
if self.current_bucket.is_some() {
|
||||
self.current_bucket = None;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
self.current_path.pop();
|
||||
true
|
||||
}
|
||||
|
||||
pub async fn refresh_current(&mut self) -> Result<()> {
|
||||
if let Some(bucket) = &self.current_bucket.clone() {
|
||||
let path = self.current_path.join("/");
|
||||
self.load_bucket_contents(bucket, &path).await
|
||||
} else {
|
||||
self.load_root().await
|
||||
}
|
||||
}
|
||||
|
||||
async fn load_bucket_contents(&mut self, bucket: &str, prefix: &str) -> Result<()> {
|
||||
self.items.clear();
|
||||
self.items.push(("⬆️ .. (go back)".to_string(), TreeNode::Folder {
|
||||
bucket: bucket.to_string(),
|
||||
path: "..".to_string(),
|
||||
name: "..".to_string(),
|
||||
}));
|
||||
|
||||
if let Some(drive) = &self.app_state.drive {
|
||||
let normalized_prefix = if prefix.is_empty() {
|
||||
String::new()
|
||||
} else if prefix.ends_with('/') {
|
||||
prefix.to_string()
|
||||
} else {
|
||||
format!("{}/", prefix)
|
||||
};
|
||||
|
||||
let mut continuation_token = None;
|
||||
let mut all_keys = Vec::new();
|
||||
|
||||
loop {
|
||||
let mut request = drive.list_objects_v2().bucket(bucket);
|
||||
if !normalized_prefix.is_empty() {
|
||||
request = request.prefix(&normalized_prefix);
|
||||
}
|
||||
if let Some(token) = continuation_token {
|
||||
request = request.continuation_token(token);
|
||||
}
|
||||
let result = request.send().await?;
|
||||
|
||||
for obj in result.contents() {
|
||||
if let Some(key) = obj.key() {
|
||||
all_keys.push(key.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
if !result.is_truncated.unwrap_or(false) {
|
||||
break;
|
||||
}
|
||||
continuation_token = result.next_continuation_token;
|
||||
}
|
||||
|
||||
let mut folders = std::collections::HashSet::new();
|
||||
let mut files = Vec::new();
|
||||
|
||||
for key in all_keys {
|
||||
if key == normalized_prefix {
|
||||
continue;
|
||||
}
|
||||
let relative = if !normalized_prefix.is_empty() && key.starts_with(&normalized_prefix) {
|
||||
&key[normalized_prefix.len()..]
|
||||
} else {
|
||||
&key
|
||||
};
|
||||
if relative.is_empty() {
|
||||
continue;
|
||||
}
|
||||
if let Some(slash_pos) = relative.find('/') {
|
||||
let folder_name = &relative[..slash_pos];
|
||||
if !folder_name.is_empty() {
|
||||
folders.insert(folder_name.to_string());
|
||||
}
|
||||
} else {
|
||||
files.push((relative.to_string(), key.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
let mut folder_vec: Vec<String> = folders.into_iter().collect();
|
||||
folder_vec.sort();
|
||||
|
||||
for folder_name in folder_vec {
|
||||
let full_path = if normalized_prefix.is_empty() {
|
||||
folder_name.clone()
|
||||
} else {
|
||||
format!("{}{}", normalized_prefix, folder_name)
|
||||
};
|
||||
let display = format!("📁 {}/", folder_name);
|
||||
self.items.push((display, TreeNode::Folder {
|
||||
bucket: bucket.to_string(),
|
||||
path: full_path,
|
||||
name: folder_name,
|
||||
}));
|
||||
}
|
||||
|
||||
files.sort_by(|(a, _), (b, _)| a.cmp(b));
|
||||
|
||||
for (name, full_path) in files {
|
||||
let icon = if name.ends_with(".bas") {
|
||||
"⚙️"
|
||||
} else if name.ends_with(".ast") {
|
||||
"🔧"
|
||||
} else if name.ends_with(".csv") {
|
||||
"📊"
|
||||
} else if name.ends_with(".gbkb") {
|
||||
"📚"
|
||||
} else if name.ends_with(".json") {
|
||||
"🔖"
|
||||
} else {
|
||||
"📄"
|
||||
};
|
||||
let display = format!("{} {}", icon, name);
|
||||
self.items.push((display, TreeNode::File {
|
||||
bucket: bucket.to_string(),
|
||||
path: full_path,
|
||||
name,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
if self.items.len() == 1 {
|
||||
self.items.push(("(empty folder)".to_string(), TreeNode::Folder {
|
||||
bucket: bucket.to_string(),
|
||||
path: String::new(),
|
||||
name: String::new(),
|
||||
}));
|
||||
}
|
||||
|
||||
self.selected = 0;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn render_items(&self) -> &[(String, TreeNode)] {
|
||||
&self.items
|
||||
}
|
||||
|
||||
pub fn selected_index(&self) -> usize {
|
||||
self.selected
|
||||
}
|
||||
|
||||
pub fn get_selected_node(&self) -> Option<&TreeNode> {
|
||||
self.items.get(self.selected).map(|(_, node)| node)
|
||||
}
|
||||
|
||||
pub fn move_up(&mut self) {
|
||||
if self.selected > 0 {
|
||||
self.selected -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn move_down(&mut self) {
|
||||
if self.selected < self.items.len().saturating_sub(1) {
|
||||
self.selected += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
73
src/ui_tree/log_panel.rs
Normal file
73
src/ui_tree/log_panel.rs
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
use std::sync::{Arc, Mutex};
|
||||
use log::{Log, Metadata, LevelFilter, Record, SetLoggerError};
|
||||
use chrono::Local;
|
||||
|
||||
pub struct LogPanel {
|
||||
logs: Vec<String>,
|
||||
max_logs: usize,
|
||||
}
|
||||
|
||||
impl LogPanel {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
logs: Vec::with_capacity(1000),
|
||||
max_logs: 1000,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_log(&mut self, entry: &str) {
|
||||
if self.logs.len() >= self.max_logs {
|
||||
self.logs.remove(0);
|
||||
}
|
||||
self.logs.push(entry.to_string());
|
||||
}
|
||||
|
||||
pub fn render(&self) -> String {
|
||||
let visible_logs = if self.logs.len() > 10 {
|
||||
&self.logs[self.logs.len() - 10..]
|
||||
} else {
|
||||
&self.logs[..]
|
||||
};
|
||||
visible_logs.join("\n")
|
||||
}
|
||||
}
|
||||
|
||||
pub struct UiLogger {
|
||||
log_panel: Arc<Mutex<LogPanel>>,
|
||||
filter: LevelFilter,
|
||||
}
|
||||
|
||||
impl Log for UiLogger {
|
||||
fn enabled(&self, metadata: &Metadata) -> bool {
|
||||
metadata.level() <= self.filter
|
||||
}
|
||||
|
||||
fn log(&self, record: &Record) {
|
||||
if self.enabled(record.metadata()) {
|
||||
let timestamp = Local::now().format("%H:%M:%S");
|
||||
let level_icon = match record.level() {
|
||||
log::Level::Error => "❌",
|
||||
log::Level::Warn => "⚠️",
|
||||
log::Level::Info => "ℹ️",
|
||||
log::Level::Debug => "🔍",
|
||||
log::Level::Trace => "📝",
|
||||
};
|
||||
let log_entry = format!("[{}] {} {}", timestamp, level_icon, record.args());
|
||||
if let Ok(mut panel) = self.log_panel.lock() {
|
||||
panel.add_log(&log_entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn flush(&self) {}
|
||||
}
|
||||
|
||||
pub fn init_logger(log_panel: Arc<Mutex<LogPanel>>) -> Result<(), SetLoggerError> {
|
||||
let logger = Box::new(UiLogger {
|
||||
log_panel,
|
||||
filter: LevelFilter::Info,
|
||||
});
|
||||
log::set_boxed_logger(logger)?;
|
||||
log::set_max_level(LevelFilter::Trace);
|
||||
Ok(())
|
||||
}
|
||||
462
src/ui_tree/mod.rs
Normal file
462
src/ui_tree/mod.rs
Normal file
|
|
@ -0,0 +1,462 @@
|
|||
use crate::shared::state::AppState;
|
||||
use color_eyre::Result;
|
||||
use crossterm::{
|
||||
event::{self, Event, KeyCode, KeyModifiers},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use log::LevelFilter;
|
||||
use ratatui::{
|
||||
backend::CrosstermBackend,
|
||||
layout::{Constraint, Direction, Layout, Rect},
|
||||
style::{Color, Modifier, Style},
|
||||
text::{Line, Span},
|
||||
widgets::{Block, Borders, List, ListItem, Paragraph, Wrap},
|
||||
Frame, Terminal,
|
||||
};
|
||||
use std::io;
|
||||
use std::sync::Arc;
|
||||
use std::sync::Mutex;
|
||||
mod editor;
|
||||
mod file_tree;
|
||||
mod log_panel;
|
||||
mod status_panel;
|
||||
use editor::Editor;
|
||||
use file_tree::{FileTree, TreeNode};
|
||||
use log_panel::{init_logger, LogPanel};
|
||||
use status_panel::StatusPanel;
|
||||
|
||||
pub struct XtreeUI {
|
||||
app_state: Option<Arc<AppState>>,
|
||||
file_tree: Option<FileTree>,
|
||||
status_panel: Option<StatusPanel>,
|
||||
log_panel: Arc<Mutex<LogPanel>>,
|
||||
editor: Option<Editor>,
|
||||
active_panel: ActivePanel,
|
||||
should_quit: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
enum ActivePanel {
|
||||
FileTree,
|
||||
Editor,
|
||||
Status,
|
||||
Logs,
|
||||
}
|
||||
|
||||
impl XtreeUI {
|
||||
pub fn new() -> Self {
|
||||
let log_panel = Arc::new(Mutex::new(LogPanel::new()));
|
||||
Self {
|
||||
app_state: None,
|
||||
file_tree: None,
|
||||
status_panel: None,
|
||||
log_panel: log_panel.clone(),
|
||||
editor: None,
|
||||
active_panel: ActivePanel::Logs,
|
||||
should_quit: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_app_state(&mut self, app_state: Arc<AppState>) {
|
||||
self.file_tree = Some(FileTree::new(app_state.clone()));
|
||||
self.status_panel = Some(StatusPanel::new(app_state.clone()));
|
||||
self.app_state = Some(app_state);
|
||||
self.active_panel = ActivePanel::FileTree;
|
||||
}
|
||||
|
||||
pub fn start_ui(&mut self) -> Result<()> {
|
||||
color_eyre::install()?;
|
||||
if !std::io::IsTerminal::is_terminal(&std::io::stdout()) {
|
||||
return Ok(());
|
||||
}
|
||||
enable_raw_mode()?;
|
||||
let mut stdout = io::stdout();
|
||||
execute!(stdout, EnterAlternateScreen)?;
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
init_logger(self.log_panel.clone())?;
|
||||
log::set_max_level(LevelFilter::Trace);
|
||||
let result = self.run_event_loop(&mut terminal);
|
||||
disable_raw_mode()?;
|
||||
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
|
||||
terminal.show_cursor()?;
|
||||
result
|
||||
}
|
||||
|
||||
fn run_event_loop(&mut self, terminal: &mut Terminal<CrosstermBackend<io::Stdout>>) -> Result<()> {
|
||||
let mut last_update = std::time::Instant::now();
|
||||
let update_interval = std::time::Duration::from_millis(500);
|
||||
let rt = tokio::runtime::Runtime::new()?;
|
||||
loop {
|
||||
terminal.draw(|f| self.render(f))?;
|
||||
if self.app_state.is_some() && last_update.elapsed() >= update_interval {
|
||||
if let Err(e) = rt.block_on(self.update_data()) {
|
||||
let mut log_panel = self.log_panel.lock().unwrap();
|
||||
log_panel.add_log(&format!("Update error: {}", e));
|
||||
}
|
||||
last_update = std::time::Instant::now();
|
||||
}
|
||||
if event::poll(std::time::Duration::from_millis(50))? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if let Err(e) = rt.block_on(self.handle_input(key.code, key.modifiers)) {
|
||||
let mut log_panel = self.log_panel.lock().unwrap();
|
||||
log_panel.add_log(&format!("Input error: {}", e));
|
||||
}
|
||||
if self.should_quit {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn render(&self, f: &mut Frame) {
|
||||
let bg = Color::Rgb(15, 15, 25);
|
||||
let border_active = Color::Rgb(120, 220, 255);
|
||||
let border_inactive = Color::Rgb(70, 70, 90);
|
||||
let text = Color::Rgb(240, 240, 245);
|
||||
let highlight = Color::Rgb(90, 180, 255);
|
||||
let title = Color::Rgb(255, 230, 140);
|
||||
if self.app_state.is_none() {
|
||||
self.render_loading(f, bg, text, border_active, title);
|
||||
return;
|
||||
}
|
||||
let main_chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([Constraint::Min(0), Constraint::Length(12)])
|
||||
.split(f.area());
|
||||
if self.editor.is_some() {
|
||||
let editor_chunks = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Percentage(20), Constraint::Percentage(80)])
|
||||
.split(main_chunks[0]);
|
||||
self.render_file_tree(f, editor_chunks[0], bg, text, border_active, border_inactive, highlight, title);
|
||||
if let Some(editor) = &self.editor {
|
||||
self.render_editor(f, editor_chunks[1], editor, bg, text, border_active, border_inactive, highlight, title);
|
||||
}
|
||||
} else {
|
||||
let top_chunks = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Percentage(30), Constraint::Percentage(70)])
|
||||
.split(main_chunks[0]);
|
||||
self.render_file_tree(f, top_chunks[0], bg, text, border_active, border_inactive, highlight, title);
|
||||
self.render_status(f, top_chunks[1], bg, text, border_active, border_inactive, highlight, title);
|
||||
}
|
||||
self.render_logs(f, main_chunks[1], bg, text, border_active, border_inactive, highlight, title);
|
||||
}
|
||||
|
||||
fn render_loading(&self, f: &mut Frame, bg: Color, text: Color, border: Color, title: Color) {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([Constraint::Percentage(40), Constraint::Percentage(20), Constraint::Percentage(40)])
|
||||
.split(f.area());
|
||||
let center = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Percentage(30), Constraint::Percentage(40), Constraint::Percentage(30)])
|
||||
.split(chunks[1])[1];
|
||||
let block = Block::default()
|
||||
.title(Span::styled(" 🚀 BOTSERVER ", Style::default().fg(title).add_modifier(Modifier::BOLD)))
|
||||
.borders(Borders::ALL)
|
||||
.border_style(Style::default().fg(border))
|
||||
.style(Style::default().bg(bg));
|
||||
let loading_text = vec![
|
||||
"",
|
||||
" ╔════════════════════════════════╗",
|
||||
" ║ ║",
|
||||
" ║ ⚡ Initializing System... ║",
|
||||
" ║ ║",
|
||||
" ║ Loading components... ║",
|
||||
" ║ Connecting to services... ║",
|
||||
" ║ Preparing interface... ║",
|
||||
" ║ ║",
|
||||
" ╚════════════════════════════════╝",
|
||||
"",
|
||||
].join("\n");
|
||||
let paragraph = Paragraph::new(loading_text)
|
||||
.block(block)
|
||||
.style(Style::default().fg(text))
|
||||
.wrap(Wrap { trim: false });
|
||||
f.render_widget(paragraph, center);
|
||||
}
|
||||
|
||||
fn render_file_tree(&self, f: &mut Frame, area: Rect, bg: Color, text: Color, border_active: Color, border_inactive: Color, highlight: Color, title: Color) {
|
||||
if let Some(file_tree) = &self.file_tree {
|
||||
let items = file_tree.render_items();
|
||||
let selected = file_tree.selected_index();
|
||||
let list_items: Vec<ListItem> = items.iter().enumerate().map(|(idx, (display, _))| {
|
||||
let style = if idx == selected {
|
||||
Style::default().bg(highlight).fg(Color::Black).add_modifier(Modifier::BOLD)
|
||||
} else {
|
||||
Style::default().fg(text)
|
||||
};
|
||||
ListItem::new(Line::from(Span::styled(display.clone(), style)))
|
||||
}).collect();
|
||||
let is_active = self.active_panel == ActivePanel::FileTree;
|
||||
let border_color = if is_active { border_active } else { border_inactive };
|
||||
let title_style = if is_active {
|
||||
Style::default().fg(title).add_modifier(Modifier::BOLD)
|
||||
} else {
|
||||
Style::default().fg(text)
|
||||
};
|
||||
let block = Block::default()
|
||||
.title(Span::styled(" 📁 FILE EXPLORER ", title_style))
|
||||
.borders(Borders::ALL)
|
||||
.border_style(Style::default().fg(border_color))
|
||||
.style(Style::default().bg(bg));
|
||||
let list = List::new(list_items).block(block);
|
||||
f.render_widget(list, area);
|
||||
} else {
|
||||
let block = Block::default()
|
||||
.title(Span::styled(" 📁 FILE EXPLORER ", Style::default().fg(text)))
|
||||
.borders(Borders::ALL)
|
||||
.border_style(Style::default().fg(border_inactive))
|
||||
.style(Style::default().bg(bg));
|
||||
f.render_widget(block, area);
|
||||
}
|
||||
}
|
||||
|
||||
fn render_status(&self, f: &mut Frame, area: Rect, bg: Color, text: Color, border_active: Color, border_inactive: Color, _highlight: Color, title: Color) {
|
||||
let status_text = if let Some(status_panel) = &self.status_panel {
|
||||
status_panel.render()
|
||||
} else {
|
||||
"Waiting for initialization...".to_string()
|
||||
};
|
||||
let is_active = self.active_panel == ActivePanel::Status;
|
||||
let border_color = if is_active { border_active } else { border_inactive };
|
||||
let title_style = if is_active {
|
||||
Style::default().fg(title).add_modifier(Modifier::BOLD)
|
||||
} else {
|
||||
Style::default().fg(text)
|
||||
};
|
||||
let block = Block::default()
|
||||
.title(Span::styled(" 📊 SYSTEM STATUS ", title_style))
|
||||
.borders(Borders::ALL)
|
||||
.border_style(Style::default().fg(border_color))
|
||||
.style(Style::default().bg(bg));
|
||||
let paragraph = Paragraph::new(status_text)
|
||||
.block(block)
|
||||
.style(Style::default().fg(text))
|
||||
.wrap(Wrap { trim: false });
|
||||
f.render_widget(paragraph, area);
|
||||
}
|
||||
|
||||
fn render_editor(&self, f: &mut Frame, area: Rect, editor: &Editor, bg: Color, text: Color, border_active: Color, border_inactive: Color, _highlight: Color, title: Color) {
|
||||
let is_active = self.active_panel == ActivePanel::Editor;
|
||||
let border_color = if is_active { border_active } else { border_inactive };
|
||||
let title_style = if is_active {
|
||||
Style::default().fg(title).add_modifier(Modifier::BOLD)
|
||||
} else {
|
||||
Style::default().fg(text)
|
||||
};
|
||||
let title_text = format!(" ✏️ EDITOR: {} ", editor.file_path());
|
||||
let block = Block::default()
|
||||
.title(Span::styled(title_text, title_style))
|
||||
.borders(Borders::ALL)
|
||||
.border_style(Style::default().fg(border_color))
|
||||
.style(Style::default().bg(bg));
|
||||
let content = editor.render();
|
||||
let paragraph = Paragraph::new(content)
|
||||
.block(block)
|
||||
.style(Style::default().fg(text))
|
||||
.wrap(Wrap { trim: false });
|
||||
f.render_widget(paragraph, area);
|
||||
}
|
||||
|
||||
fn render_logs(&self, f: &mut Frame, area: Rect, bg: Color, text: Color, border_active: Color, border_inactive: Color, _highlight: Color, title: Color) {
|
||||
let log_panel = self.log_panel.try_lock();
|
||||
let log_lines = if let Ok(panel) = log_panel {
|
||||
panel.render()
|
||||
} else {
|
||||
"Loading logs...".to_string()
|
||||
};
|
||||
let is_active = self.active_panel == ActivePanel::Logs;
|
||||
let border_color = if is_active { border_active } else { border_inactive };
|
||||
let title_style = if is_active {
|
||||
Style::default().fg(title).add_modifier(Modifier::BOLD)
|
||||
} else {
|
||||
Style::default().fg(text)
|
||||
};
|
||||
let block = Block::default()
|
||||
.title(Span::styled(" 📜 SYSTEM LOGS ", title_style))
|
||||
.borders(Borders::ALL)
|
||||
.border_style(Style::default().fg(border_color))
|
||||
.style(Style::default().bg(bg));
|
||||
let paragraph = Paragraph::new(log_lines)
|
||||
.block(block)
|
||||
.style(Style::default().fg(text))
|
||||
.wrap(Wrap { trim: false });
|
||||
f.render_widget(paragraph, area);
|
||||
}
|
||||
|
||||
async fn handle_input(&mut self, key: KeyCode, modifiers: KeyModifiers) -> Result<()> {
|
||||
if modifiers.contains(KeyModifiers::CONTROL) {
|
||||
match key {
|
||||
KeyCode::Char('c') | KeyCode::Char('q') => {
|
||||
self.should_quit = true;
|
||||
return Ok(());
|
||||
}
|
||||
KeyCode::Char('s') => {
|
||||
if let Some(editor) = &mut self.editor {
|
||||
if let Some(app_state) = &self.app_state {
|
||||
if let Err(e) = editor.save(app_state).await {
|
||||
let mut log_panel = self.log_panel.lock().unwrap();
|
||||
log_panel.add_log(&format!("Save failed: {}", e));
|
||||
} else {
|
||||
let mut log_panel = self.log_panel.lock().unwrap();
|
||||
log_panel.add_log(&format!("✓ Saved: {}", editor.file_path()));
|
||||
}
|
||||
}
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
KeyCode::Char('w') => {
|
||||
if self.editor.is_some() {
|
||||
self.editor = None;
|
||||
self.active_panel = ActivePanel::FileTree;
|
||||
let mut log_panel = self.log_panel.lock().unwrap();
|
||||
log_panel.add_log("✓ Closed editor");
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
if self.app_state.is_none() {
|
||||
return Ok(());
|
||||
}
|
||||
match self.active_panel {
|
||||
ActivePanel::FileTree => match key {
|
||||
KeyCode::Up => {
|
||||
if let Some(file_tree) = &mut self.file_tree {
|
||||
file_tree.move_up();
|
||||
}
|
||||
}
|
||||
KeyCode::Down => {
|
||||
if let Some(file_tree) = &mut self.file_tree {
|
||||
file_tree.move_down();
|
||||
}
|
||||
}
|
||||
KeyCode::Enter => {
|
||||
if let Err(e) = self.handle_tree_enter().await {
|
||||
let mut log_panel = self.log_panel.lock().unwrap();
|
||||
log_panel.add_log(&format!("✗ Enter error: {}", e));
|
||||
}
|
||||
}
|
||||
KeyCode::Backspace => {
|
||||
if let Some(file_tree) = &mut self.file_tree {
|
||||
if file_tree.go_up() {
|
||||
if let Err(e) = file_tree.refresh_current().await {
|
||||
let mut log_panel = self.log_panel.lock().unwrap();
|
||||
log_panel.add_log(&format!("✗ Navigation error: {}", e));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
KeyCode::Tab => {
|
||||
self.active_panel = ActivePanel::Status;
|
||||
}
|
||||
KeyCode::Char('q') => {
|
||||
self.should_quit = true;
|
||||
}
|
||||
KeyCode::F(5) => {
|
||||
if let Some(file_tree) = &mut self.file_tree {
|
||||
if let Err(e) = file_tree.refresh_current().await {
|
||||
let mut log_panel = self.log_panel.lock().unwrap();
|
||||
log_panel.add_log(&format!("✗ Refresh failed: {}", e));
|
||||
} else {
|
||||
let mut log_panel = self.log_panel.lock().unwrap();
|
||||
log_panel.add_log("✓ Refreshed");
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
ActivePanel::Editor => {
|
||||
if let Some(editor) = &mut self.editor {
|
||||
match key {
|
||||
KeyCode::Up => editor.move_up(),
|
||||
KeyCode::Down => editor.move_down(),
|
||||
KeyCode::Left => editor.move_left(),
|
||||
KeyCode::Right => editor.move_right(),
|
||||
KeyCode::Char(c) => editor.insert_char(c),
|
||||
KeyCode::Backspace => editor.backspace(),
|
||||
KeyCode::Enter => editor.insert_newline(),
|
||||
KeyCode::Tab => {
|
||||
self.active_panel = ActivePanel::FileTree;
|
||||
}
|
||||
KeyCode::Esc => {
|
||||
self.editor = None;
|
||||
self.active_panel = ActivePanel::FileTree;
|
||||
let mut log_panel = self.log_panel.lock().unwrap();
|
||||
log_panel.add_log("✓ Closed editor");
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
ActivePanel::Status => match key {
|
||||
KeyCode::Tab => {
|
||||
self.active_panel = ActivePanel::Logs;
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
ActivePanel::Logs => match key {
|
||||
KeyCode::Tab => {
|
||||
self.active_panel = ActivePanel::FileTree;
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_tree_enter(&mut self) -> Result<()> {
|
||||
if let (Some(file_tree), Some(app_state)) = (&mut self.file_tree, &self.app_state) {
|
||||
if let Some(node) = file_tree.get_selected_node().cloned() {
|
||||
match node {
|
||||
TreeNode::Bucket { name, .. } => {
|
||||
file_tree.enter_bucket(name.clone()).await?;
|
||||
let mut log_panel = self.log_panel.lock().unwrap();
|
||||
log_panel.add_log(&format!("📂 Opened bucket: {}", name));
|
||||
}
|
||||
TreeNode::Folder { bucket, path, .. } => {
|
||||
file_tree.enter_folder(bucket.clone(), path.clone()).await?;
|
||||
let mut log_panel = self.log_panel.lock().unwrap();
|
||||
log_panel.add_log(&format!("📂 Opened folder: {}", path));
|
||||
}
|
||||
TreeNode::File { bucket, path, .. } => {
|
||||
match Editor::load(app_state, &bucket, &path).await {
|
||||
Ok(editor) => {
|
||||
self.editor = Some(editor);
|
||||
self.active_panel = ActivePanel::Editor;
|
||||
let mut log_panel = self.log_panel.lock().unwrap();
|
||||
log_panel.add_log(&format!("✏️ Editing: {}", path));
|
||||
}
|
||||
Err(e) => {
|
||||
let mut log_panel = self.log_panel.lock().unwrap();
|
||||
log_panel.add_log(&format!("✗ Failed to load file: {}", e));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn update_data(&mut self) -> Result<()> {
|
||||
if let Some(status_panel) = &mut self.status_panel {
|
||||
status_panel.update().await?;
|
||||
}
|
||||
if let Some(file_tree) = &self.file_tree {
|
||||
if file_tree.render_items().is_empty() {
|
||||
if let Some(file_tree) = &mut self.file_tree {
|
||||
file_tree.load_root().await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
105
src/ui_tree/status_panel.rs
Normal file
105
src/ui_tree/status_panel.rs
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
use std::sync::Arc;
|
||||
use crate::shared::state::AppState;
|
||||
use crate::shared::models::schema::bots::dsl::*;
|
||||
use diesel::prelude::*;
|
||||
|
||||
pub struct StatusPanel {
|
||||
app_state: Arc<AppState>,
|
||||
last_update: std::time::Instant,
|
||||
cached_content: String,
|
||||
}
|
||||
|
||||
impl StatusPanel {
|
||||
pub fn new(app_state: Arc<AppState>) -> Self {
|
||||
Self {
|
||||
app_state,
|
||||
last_update: std::time::Instant::now(),
|
||||
cached_content: String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn update(&mut self) -> Result<(), std::io::Error> {
|
||||
if self.last_update.elapsed() < std::time::Duration::from_secs(2) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut lines = Vec::new();
|
||||
lines.push("═══════════════════════════════════════".to_string());
|
||||
lines.push(" COMPONENT STATUS".to_string());
|
||||
lines.push("═══════════════════════════════════════".to_string());
|
||||
lines.push("".to_string());
|
||||
|
||||
let db_status = if self.app_state.conn.try_lock().is_ok() {
|
||||
"🟢 ONLINE"
|
||||
} else {
|
||||
"🔴 OFFLINE"
|
||||
};
|
||||
lines.push(format!(" Database: {}", db_status));
|
||||
|
||||
let cache_status = if self.app_state.cache.is_some() {
|
||||
"🟢 ONLINE"
|
||||
} else {
|
||||
"🟡 DISABLED"
|
||||
};
|
||||
lines.push(format!(" Cache: {}", cache_status));
|
||||
|
||||
let drive_status = if self.app_state.drive.is_some() {
|
||||
"🟢 ONLINE"
|
||||
} else {
|
||||
"🔴 OFFLINE"
|
||||
};
|
||||
lines.push(format!(" Drive: {}", drive_status));
|
||||
|
||||
let llm_status = "🟢 ONLINE";
|
||||
lines.push(format!(" LLM: {}", llm_status));
|
||||
|
||||
lines.push("".to_string());
|
||||
lines.push("───────────────────────────────────────".to_string());
|
||||
lines.push(" ACTIVE BOTS".to_string());
|
||||
lines.push("───────────────────────────────────────".to_string());
|
||||
|
||||
if let Ok(mut conn) = self.app_state.conn.try_lock() {
|
||||
match bots
|
||||
.filter(is_active.eq(true))
|
||||
.select((name, id))
|
||||
.load::<(String, uuid::Uuid)>(&mut *conn)
|
||||
{
|
||||
Ok(bot_list) => {
|
||||
if bot_list.is_empty() {
|
||||
lines.push(" No active bots".to_string());
|
||||
} else {
|
||||
for (bot_name, _bot_id) in bot_list {
|
||||
lines.push(format!(" 🤖 {}", bot_name));
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
lines.push(" Error loading bots".to_string());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
lines.push(" Database locked".to_string());
|
||||
}
|
||||
|
||||
lines.push("".to_string());
|
||||
lines.push("───────────────────────────────────────".to_string());
|
||||
lines.push(" SESSIONS".to_string());
|
||||
lines.push("───────────────────────────────────────".to_string());
|
||||
|
||||
let session_count = self.app_state.response_channels.try_lock()
|
||||
.map(|channels| channels.len())
|
||||
.unwrap_or(0);
|
||||
lines.push(format!(" Active: {}", session_count));
|
||||
|
||||
lines.push("".to_string());
|
||||
lines.push("═══════════════════════════════════════".to_string());
|
||||
|
||||
self.cached_content = lines.join("\n");
|
||||
self.last_update = std::time::Instant::now();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn render(&self) -> String {
|
||||
self.cached_content.clone()
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue