Compare commits
No commits in common. "2c7f5dc0cc5c405ea7228a226869a36027292406" and "78cb3767911b5e5ba84c6eba5a977ce9deb2da68" have entirely different histories.
2c7f5dc0cc
...
78cb376791
12 changed files with 5 additions and 1077 deletions
476
Cargo.lock
generated
476
Cargo.lock
generated
|
|
@ -32,56 +32,6 @@ version = "0.2.21"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
|
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "anstream"
|
|
||||||
version = "0.6.19"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933"
|
|
||||||
dependencies = [
|
|
||||||
"anstyle",
|
|
||||||
"anstyle-parse",
|
|
||||||
"anstyle-query",
|
|
||||||
"anstyle-wincon",
|
|
||||||
"colorchoice",
|
|
||||||
"is_terminal_polyfill",
|
|
||||||
"utf8parse",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "anstyle"
|
|
||||||
version = "1.0.11"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "anstyle-parse"
|
|
||||||
version = "0.2.7"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2"
|
|
||||||
dependencies = [
|
|
||||||
"utf8parse",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "anstyle-query"
|
|
||||||
version = "1.1.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9"
|
|
||||||
dependencies = [
|
|
||||||
"windows-sys 0.59.0",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "anstyle-wincon"
|
|
||||||
version = "3.0.9"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882"
|
|
||||||
dependencies = [
|
|
||||||
"anstyle",
|
|
||||||
"once_cell_polyfill",
|
|
||||||
"windows-sys 0.59.0",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anyhow"
|
name = "anyhow"
|
||||||
version = "1.0.98"
|
version = "1.0.98"
|
||||||
|
|
@ -252,9 +202,9 @@ checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitflags"
|
name = "bitflags"
|
||||||
version = "2.9.1"
|
version = "2.8.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
|
checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
@ -286,21 +236,6 @@ version = "2.0.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a3c8f83209414aacf0eeae3cf730b18d6981697fba62f200fcfb92b9f082acba"
|
checksum = "a3c8f83209414aacf0eeae3cf730b18d6981697fba62f200fcfb92b9f082acba"
|
||||||
|
|
||||||
[[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.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "0abae9be0aaf9ea96a3b1b8b1b55c602ca751eba1b1500220cea4ecbafe7c0d5"
|
|
||||||
dependencies = [
|
|
||||||
"rustversion",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cc"
|
name = "cc"
|
||||||
version = "1.2.15"
|
version = "1.2.15"
|
||||||
|
|
@ -316,66 +251,6 @@ version = "1.0.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "clap"
|
|
||||||
version = "4.5.40"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "40b6887a1d8685cebccf115538db5c0efe625ccac9696ad45c409d96566e910f"
|
|
||||||
dependencies = [
|
|
||||||
"clap_builder",
|
|
||||||
"clap_derive",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "clap_builder"
|
|
||||||
version = "4.5.40"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e0c66c08ce9f0c698cbce5c0279d0bb6ac936d8674174fe48f736533b964f59e"
|
|
||||||
dependencies = [
|
|
||||||
"anstream",
|
|
||||||
"anstyle",
|
|
||||||
"clap_lex",
|
|
||||||
"strsim",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "clap_derive"
|
|
||||||
version = "4.5.40"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d2c7947ae4cc3d851207c1adb5b5e260ff0cca11446b1d6d1423788e442257ce"
|
|
||||||
dependencies = [
|
|
||||||
"heck",
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "clap_lex"
|
|
||||||
version = "0.7.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "colorchoice"
|
|
||||||
version = "1.0.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
|
|
||||||
|
|
||||||
[[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]]
|
[[package]]
|
||||||
name = "concurrent-queue"
|
name = "concurrent-queue"
|
||||||
version = "2.5.0"
|
version = "2.5.0"
|
||||||
|
|
@ -440,48 +315,6 @@ version = "0.8.21"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
|
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",
|
|
||||||
"document-features",
|
|
||||||
"futures-core",
|
|
||||||
"mio",
|
|
||||||
"parking_lot",
|
|
||||||
"rustix 1.0.7",
|
|
||||||
"serde",
|
|
||||||
"signal-hook",
|
|
||||||
"signal-hook-mio",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "crossterm_winapi"
|
|
||||||
version = "0.9.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b"
|
|
||||||
dependencies = [
|
|
||||||
"winapi",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crypto-common"
|
name = "crypto-common"
|
||||||
version = "0.1.6"
|
version = "0.1.6"
|
||||||
|
|
@ -492,41 +325,6 @@ dependencies = [
|
||||||
"typenum",
|
"typenum",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "darling"
|
|
||||||
version = "0.20.11"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee"
|
|
||||||
dependencies = [
|
|
||||||
"darling_core",
|
|
||||||
"darling_macro",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "darling_core"
|
|
||||||
version = "0.20.11"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e"
|
|
||||||
dependencies = [
|
|
||||||
"fnv",
|
|
||||||
"ident_case",
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"strsim",
|
|
||||||
"syn",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "darling_macro"
|
|
||||||
version = "0.20.11"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead"
|
|
||||||
dependencies = [
|
|
||||||
"darling_core",
|
|
||||||
"quote",
|
|
||||||
"syn",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "der"
|
name = "der"
|
||||||
version = "0.7.9"
|
version = "0.7.9"
|
||||||
|
|
@ -576,15 +374,6 @@ dependencies = [
|
||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "document-features"
|
|
||||||
version = "0.2.11"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "95249b50c6c185bee49034bcb378a49dc2b5dff0be90ff6616d31d64febab05d"
|
|
||||||
dependencies = [
|
|
||||||
"litrs",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dotenvy"
|
name = "dotenvy"
|
||||||
version = "0.15.7"
|
version = "0.15.7"
|
||||||
|
|
@ -606,16 +395,6 @@ version = "1.0.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "errno"
|
|
||||||
version = "0.3.12"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18"
|
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
"windows-sys 0.59.0",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "etcetera"
|
name = "etcetera"
|
||||||
version = "0.8.0"
|
version = "0.8.0"
|
||||||
|
|
@ -933,17 +712,12 @@ dependencies = [
|
||||||
"axum",
|
"axum",
|
||||||
"axum-msgpack",
|
"axum-msgpack",
|
||||||
"axum-test",
|
"axum-test",
|
||||||
"clap",
|
|
||||||
"crossterm 0.29.0",
|
|
||||||
"dotenvy",
|
"dotenvy",
|
||||||
"futures",
|
"futures",
|
||||||
"itertools 0.14.0",
|
|
||||||
"ratatui",
|
|
||||||
"serde",
|
"serde",
|
||||||
"sqlx",
|
"sqlx",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-util",
|
|
||||||
"tower-http",
|
"tower-http",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
|
|
@ -1107,12 +881,6 @@ dependencies = [
|
||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "ident_case"
|
|
||||||
version = "1.0.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "idna"
|
name = "idna"
|
||||||
version = "1.0.3"
|
version = "1.0.3"
|
||||||
|
|
@ -1144,49 +912,6 @@ dependencies = [
|
||||||
"hashbrown",
|
"hashbrown",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "indoc"
|
|
||||||
version = "2.0.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "instability"
|
|
||||||
version = "0.3.7"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "0bf9fed6d91cfb734e7476a06bde8300a1b94e217e1b523b6f0cd1a01998c71d"
|
|
||||||
dependencies = [
|
|
||||||
"darling",
|
|
||||||
"indoc",
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "is_terminal_polyfill"
|
|
||||||
version = "1.70.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "itertools"
|
|
||||||
version = "0.13.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
|
|
||||||
dependencies = [
|
|
||||||
"either",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "itertools"
|
|
||||||
version = "0.14.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
|
|
||||||
dependencies = [
|
|
||||||
"either",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itoa"
|
name = "itoa"
|
||||||
version = "1.0.14"
|
version = "1.0.14"
|
||||||
|
|
@ -1225,30 +950,12 @@ dependencies = [
|
||||||
"vcpkg",
|
"vcpkg",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[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.9.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "litemap"
|
name = "litemap"
|
||||||
version = "0.7.4"
|
version = "0.7.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104"
|
checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "litrs"
|
|
||||||
version = "0.4.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lock_api"
|
name = "lock_api"
|
||||||
version = "0.4.12"
|
version = "0.4.12"
|
||||||
|
|
@ -1265,15 +972,6 @@ version = "0.4.26"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e"
|
checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "lru"
|
|
||||||
version = "0.12.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38"
|
|
||||||
dependencies = [
|
|
||||||
"hashbrown",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "matchers"
|
name = "matchers"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|
@ -1337,7 +1035,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd"
|
checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"log",
|
|
||||||
"wasi 0.11.0+wasi-snapshot-preview1",
|
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
@ -1410,12 +1107,6 @@ version = "1.20.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e"
|
checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "once_cell_polyfill"
|
|
||||||
version = "1.70.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "parking"
|
name = "parking"
|
||||||
version = "2.2.1"
|
version = "2.2.1"
|
||||||
|
|
@ -1609,27 +1300,6 @@ dependencies = [
|
||||||
"zerocopy 0.8.20",
|
"zerocopy 0.8.20",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[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]]
|
[[package]]
|
||||||
name = "redox_syscall"
|
name = "redox_syscall"
|
||||||
version = "0.5.9"
|
version = "0.5.9"
|
||||||
|
|
@ -1769,32 +1439,6 @@ version = "0.1.24"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
|
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
|
||||||
|
|
||||||
[[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.0.7"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266"
|
|
||||||
dependencies = [
|
|
||||||
"bitflags",
|
|
||||||
"errno",
|
|
||||||
"libc",
|
|
||||||
"linux-raw-sys 0.9.4",
|
|
||||||
"windows-sys 0.59.0",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustls"
|
name = "rustls"
|
||||||
version = "0.23.23"
|
version = "0.23.23"
|
||||||
|
|
@ -1935,27 +1579,6 @@ version = "1.3.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
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.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd"
|
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
"mio",
|
|
||||||
"signal-hook",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "signal-hook-registry"
|
name = "signal-hook-registry"
|
||||||
version = "1.4.2"
|
version = "1.4.2"
|
||||||
|
|
@ -2215,12 +1838,6 @@ version = "1.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
|
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "static_assertions"
|
|
||||||
version = "1.1.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "stringprep"
|
name = "stringprep"
|
||||||
version = "0.1.5"
|
version = "0.1.5"
|
||||||
|
|
@ -2232,34 +1849,6 @@ dependencies = [
|
||||||
"unicode-properties",
|
"unicode-properties",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "strsim"
|
|
||||||
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",
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"rustversion",
|
|
||||||
"syn",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "subtle"
|
name = "subtle"
|
||||||
version = "2.6.1"
|
version = "2.6.1"
|
||||||
|
|
@ -2421,9 +2010,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio-util"
|
name = "tokio-util"
|
||||||
version = "0.7.15"
|
version = "0.7.13"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df"
|
checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
|
|
@ -2591,35 +2180,6 @@ version = "0.1.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0"
|
checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "unicode-segmentation"
|
|
||||||
version = "1.12.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
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]]
|
[[package]]
|
||||||
name = "untrusted"
|
name = "untrusted"
|
||||||
version = "0.9.0"
|
version = "0.9.0"
|
||||||
|
|
@ -2649,12 +2209,6 @@ version = "1.0.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
|
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "utf8parse"
|
|
||||||
version = "0.2.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "valuable"
|
name = "valuable"
|
||||||
version = "0.1.1"
|
version = "0.1.1"
|
||||||
|
|
@ -2722,28 +2276,6 @@ dependencies = [
|
||||||
"wasite",
|
"wasite",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "winapi"
|
|
||||||
version = "0.3.9"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
|
|
||||||
dependencies = [
|
|
||||||
"winapi-i686-pc-windows-gnu",
|
|
||||||
"winapi-x86_64-pc-windows-gnu",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "winapi-i686-pc-windows-gnu"
|
|
||||||
version = "0.4.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "winapi-x86_64-pc-windows-gnu"
|
|
||||||
version = "0.4.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-sys"
|
name = "windows-sys"
|
||||||
version = "0.48.0"
|
version = "0.48.0"
|
||||||
|
|
|
||||||
11
Cargo.toml
11
Cargo.toml
|
|
@ -13,18 +13,8 @@ axum = { version = "0.8.4", default-features = false, features = [
|
||||||
"http2",
|
"http2",
|
||||||
] }
|
] }
|
||||||
axum-msgpack = "0.5.0"
|
axum-msgpack = "0.5.0"
|
||||||
clap = { version = "4.5.40", features = ["derive"] }
|
|
||||||
crossterm = { version = "0.29.0", default-features = false, features = [
|
|
||||||
"bracketed-paste",
|
|
||||||
"event-stream",
|
|
||||||
"serde",
|
|
||||||
] }
|
|
||||||
dotenvy = "0.15.7"
|
dotenvy = "0.15.7"
|
||||||
itertools = "0.14.0"
|
|
||||||
futures = { version = "0.3.31", default-features = false }
|
futures = { version = "0.3.31", default-features = false }
|
||||||
ratatui = { version = "0.29.0", default-features = false, features = [
|
|
||||||
"crossterm",
|
|
||||||
] }
|
|
||||||
serde = { version = "1.0.219", features = ["derive"] }
|
serde = { version = "1.0.219", features = ["derive"] }
|
||||||
sqlx = { version = "0.8.6", default-features = false, features = [
|
sqlx = { version = "0.8.6", default-features = false, features = [
|
||||||
"macros",
|
"macros",
|
||||||
|
|
@ -38,7 +28,6 @@ tokio = { version = "1.45.1", default-features = false, features = [
|
||||||
"rt-multi-thread",
|
"rt-multi-thread",
|
||||||
"signal",
|
"signal",
|
||||||
] }
|
] }
|
||||||
tokio-util = "0.7.15"
|
|
||||||
thiserror = "2.0.12"
|
thiserror = "2.0.12"
|
||||||
tower-http = { version = "0.6.6", default-features = false, features = ["fs"] }
|
tower-http = { version = "0.6.6", default-features = false, features = ["fs"] }
|
||||||
tracing = "0.1.41"
|
tracing = "0.1.41"
|
||||||
|
|
|
||||||
|
|
@ -61,4 +61,4 @@ COPY --from=builder /usr/src/huellas/ts-client/build/client.js /usr/local/bin/st
|
||||||
|
|
||||||
# Run the application
|
# Run the application
|
||||||
WORKDIR /usr/local/bin
|
WORKDIR /usr/local/bin
|
||||||
CMD ["/usr/local/bin/huellas", "server"]
|
CMD ["/usr/local/bin/huellas"]
|
||||||
|
|
|
||||||
20
src/cli.rs
20
src/cli.rs
|
|
@ -1,20 +0,0 @@
|
||||||
//! Cli Parameters
|
|
||||||
|
|
||||||
use clap::{Parser, Subcommand};
|
|
||||||
|
|
||||||
/// Server for saving places in a map
|
|
||||||
#[derive(Parser)]
|
|
||||||
#[command(version, about, long_about = None)]
|
|
||||||
pub struct CliArgs {
|
|
||||||
/// Application mode
|
|
||||||
#[command(subcommand)]
|
|
||||||
pub mode: Mode,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Subcommand)]
|
|
||||||
pub enum Mode {
|
|
||||||
/// Spins up the server
|
|
||||||
Server,
|
|
||||||
/// Fires up a TUI
|
|
||||||
Tui,
|
|
||||||
}
|
|
||||||
21
src/main.rs
21
src/main.rs
|
|
@ -1,29 +1,15 @@
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use clap::Parser;
|
|
||||||
|
|
||||||
mod cli;
|
|
||||||
mod db;
|
mod db;
|
||||||
mod logging;
|
mod logging;
|
||||||
mod places;
|
mod places;
|
||||||
mod server;
|
mod server;
|
||||||
mod tui;
|
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<()> {
|
async fn main() -> Result<()> {
|
||||||
dotenvy::dotenv().unwrap_or_default();
|
dotenvy::dotenv().unwrap_or_default();
|
||||||
logging::setup()?;
|
logging::setup()?;
|
||||||
|
|
||||||
let args = cli::CliArgs::parse();
|
|
||||||
|
|
||||||
match args.mode {
|
|
||||||
cli::Mode::Server => server_mode().await?,
|
|
||||||
cli::Mode::Tui => tui_mode().await?,
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn server_mode() -> Result<()> {
|
|
||||||
let pool = db::pool().await?;
|
let pool = db::pool().await?;
|
||||||
db::run_migrations(&pool).await?;
|
db::run_migrations(&pool).await?;
|
||||||
|
|
||||||
|
|
@ -31,13 +17,6 @@ async fn server_mode() -> Result<()> {
|
||||||
let places_routes = places::routes::places_routes(places_repository);
|
let places_routes = places::routes::places_routes(places_repository);
|
||||||
|
|
||||||
server::serve(places_routes).await?;
|
server::serve(places_routes).await?;
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn tui_mode() -> Result<()> {
|
|
||||||
let pool = db::pool().await?;
|
|
||||||
let places_repository = places::db_repository::DbPlacesRepository::new(pool);
|
|
||||||
|
|
||||||
tui::tui(places_repository).await?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -44,39 +44,6 @@ impl PlacesRepository for DbPlacesRepository {
|
||||||
.map_err(|err| PlacesError::FailToGet(err.to_string()))
|
.map_err(|err| PlacesError::FailToGet(err.to_string()))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_places_paginated(
|
|
||||||
&self,
|
|
||||||
offset: u32,
|
|
||||||
limit: u8,
|
|
||||||
) -> Result<Vec<super::models::Place>, PlacesError> {
|
|
||||||
sqlx::query!(
|
|
||||||
r#"SELECT id, name, address, open_hours, icon, description, url,
|
|
||||||
longitude as "longitude: f64", latitude as "latitude: f64"
|
|
||||||
FROM places
|
|
||||||
WHERE active = TRUE
|
|
||||||
ORDER BY id
|
|
||||||
LIMIT ?
|
|
||||||
OFFSET ?"#,
|
|
||||||
limit,
|
|
||||||
offset,
|
|
||||||
)
|
|
||||||
.fetch(&self.db_pool)
|
|
||||||
.map_ok(|p| Place {
|
|
||||||
id: p.id,
|
|
||||||
name: p.name,
|
|
||||||
address: p.address,
|
|
||||||
open_hours: p.open_hours,
|
|
||||||
icon: p.icon,
|
|
||||||
description: p.description,
|
|
||||||
latitude: p.latitude,
|
|
||||||
longitude: p.longitude,
|
|
||||||
url: p.url,
|
|
||||||
})
|
|
||||||
.try_collect::<Vec<_>>()
|
|
||||||
.await
|
|
||||||
.map_err(|err| PlacesError::FailToGet(err.to_string()))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn insert_place(&self, place: PlaceInsert) -> Result<Place, PlacesError> {
|
async fn insert_place(&self, place: PlaceInsert) -> Result<Place, PlacesError> {
|
||||||
let id = sqlx::query_scalar!(
|
let id = sqlx::query_scalar!(
|
||||||
r#"INSERT INTO places
|
r#"INSERT INTO places
|
||||||
|
|
|
||||||
|
|
@ -13,13 +13,6 @@ pub trait PlacesRepository: Clone + Send + Sync + 'static {
|
||||||
/// Get all of the Places
|
/// Get all of the Places
|
||||||
fn get_places(&self) -> impl Future<Output = Result<Vec<Place>, PlacesError>> + Send;
|
fn get_places(&self) -> impl Future<Output = Result<Vec<Place>, PlacesError>> + Send;
|
||||||
|
|
||||||
/// Get all of the Places
|
|
||||||
fn get_places_paginated(
|
|
||||||
&self,
|
|
||||||
offset: u32,
|
|
||||||
limit: u8,
|
|
||||||
) -> impl Future<Output = Result<Vec<Place>, PlacesError>> + Send;
|
|
||||||
|
|
||||||
/// Inserts a Place.
|
/// Inserts a Place.
|
||||||
fn insert_place(
|
fn insert_place(
|
||||||
&self,
|
&self,
|
||||||
|
|
@ -50,7 +43,6 @@ pub enum PlacesError {
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct MockPlacesRepository {
|
pub struct MockPlacesRepository {
|
||||||
get_places_count: Arc<RwLock<usize>>,
|
get_places_count: Arc<RwLock<usize>>,
|
||||||
get_places_paginated_count: Arc<RwLock<usize>>,
|
|
||||||
insert_place_count: Arc<RwLock<usize>>,
|
insert_place_count: Arc<RwLock<usize>>,
|
||||||
update_place_count: Arc<RwLock<usize>>,
|
update_place_count: Arc<RwLock<usize>>,
|
||||||
delete_place_count: Arc<RwLock<usize>>,
|
delete_place_count: Arc<RwLock<usize>>,
|
||||||
|
|
@ -61,7 +53,6 @@ impl MockPlacesRepository {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
get_places_count: Arc::new(RwLock::new(0)),
|
get_places_count: Arc::new(RwLock::new(0)),
|
||||||
get_places_paginated_count: Arc::new(RwLock::new(0)),
|
|
||||||
insert_place_count: Arc::new(RwLock::new(0)),
|
insert_place_count: Arc::new(RwLock::new(0)),
|
||||||
update_place_count: Arc::new(RwLock::new(0)),
|
update_place_count: Arc::new(RwLock::new(0)),
|
||||||
delete_place_count: Arc::new(RwLock::new(0)),
|
delete_place_count: Arc::new(RwLock::new(0)),
|
||||||
|
|
@ -72,11 +63,6 @@ impl MockPlacesRepository {
|
||||||
*self.get_places_count.read().await
|
*self.get_places_count.read().await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[expect(dead_code)]
|
|
||||||
pub async fn get_places_paginated_count(&self) -> usize {
|
|
||||||
*self.get_places_paginated_count.read().await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn insert_place_count(&self) -> usize {
|
pub async fn insert_place_count(&self) -> usize {
|
||||||
*self.insert_place_count.read().await
|
*self.insert_place_count.read().await
|
||||||
}
|
}
|
||||||
|
|
@ -98,16 +84,6 @@ impl PlacesRepository for MockPlacesRepository {
|
||||||
Ok(Vec::new())
|
Ok(Vec::new())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_places_paginated(
|
|
||||||
&self,
|
|
||||||
_offset: u32,
|
|
||||||
_limit: u8,
|
|
||||||
) -> Result<Vec<Place>, PlacesError> {
|
|
||||||
let mut get_places_paginated_count = self.get_places_paginated_count.write().await;
|
|
||||||
*get_places_paginated_count += 1;
|
|
||||||
Ok(Vec::new())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn insert_place(&self, place: super::models::PlaceInsert) -> Result<Place, PlacesError> {
|
async fn insert_place(&self, place: super::models::PlaceInsert) -> Result<Place, PlacesError> {
|
||||||
let mut insert_place_count = self.insert_place_count.write().await;
|
let mut insert_place_count = self.insert_place_count.write().await;
|
||||||
*insert_place_count += 1;
|
*insert_place_count += 1;
|
||||||
|
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
//! Keyboard handling
|
|
||||||
|
|
||||||
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
|
||||||
|
|
||||||
use super::state::{Mode, State};
|
|
||||||
|
|
||||||
/// Event handling
|
|
||||||
pub async fn handle_key(state: &mut State, key_event: KeyEvent) {
|
|
||||||
match state.mode {
|
|
||||||
Mode::List => match key_event.code {
|
|
||||||
KeyCode::Char('e') => state.mode = Mode::Edit,
|
|
||||||
KeyCode::Home => state.selected_place.select_first(),
|
|
||||||
KeyCode::End => state.selected_place.select_last(),
|
|
||||||
KeyCode::PageUp => state.prev_page(),
|
|
||||||
KeyCode::PageDown => state.next_page(),
|
|
||||||
KeyCode::Up | KeyCode::Char('k') => state.selected_place.select_previous(),
|
|
||||||
KeyCode::Down | KeyCode::Char('j') => state.selected_place.select_next(),
|
|
||||||
KeyCode::Esc | KeyCode::Char('q') => state.quit = true,
|
|
||||||
_ => {}
|
|
||||||
},
|
|
||||||
Mode::Edit => match (key_event.modifiers, key_event.code) {
|
|
||||||
(KeyModifiers::NONE, KeyCode::Esc) => state.mode = Mode::List,
|
|
||||||
(KeyModifiers::NONE, KeyCode::Tab) => {}
|
|
||||||
(KeyModifiers::SHIFT, KeyCode::Tab) => {}
|
|
||||||
_ => {}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,45 +0,0 @@
|
||||||
//! TUI
|
|
||||||
|
|
||||||
pub mod keys;
|
|
||||||
pub mod state;
|
|
||||||
pub mod terminal;
|
|
||||||
pub mod ui;
|
|
||||||
|
|
||||||
use anyhow::Result;
|
|
||||||
|
|
||||||
use state::State;
|
|
||||||
use terminal::Event;
|
|
||||||
|
|
||||||
use crate::places::db_repository::DbPlacesRepository;
|
|
||||||
|
|
||||||
/// Fires up the UI
|
|
||||||
pub async fn tui(places_repository: DbPlacesRepository) -> Result<()> {
|
|
||||||
let (_, terminal_height) = crossterm::terminal::size()?;
|
|
||||||
let mut state = State::new(places_repository, terminal_height);
|
|
||||||
let mut tui = terminal::Tui::new()?;
|
|
||||||
|
|
||||||
let result = loop {
|
|
||||||
match tui.next().await? {
|
|
||||||
Event::Key(key_event) => keys::handle_key(&mut state, key_event).await,
|
|
||||||
Event::Render => {
|
|
||||||
tui.draw(|frame| ui::ui_draw(&mut state, frame))?;
|
|
||||||
}
|
|
||||||
Event::Tick => {
|
|
||||||
state.fetch_places().await;
|
|
||||||
}
|
|
||||||
Event::SlowTick => {
|
|
||||||
// state.cleanup_finished_tasks();
|
|
||||||
}
|
|
||||||
Event::Resize(_, h) => state.height = h,
|
|
||||||
Event::Quit => state.quit = true,
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
if state.quit {
|
|
||||||
break Ok(());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
tui.stop()?;
|
|
||||||
|
|
||||||
result
|
|
||||||
}
|
|
||||||
|
|
@ -1,77 +0,0 @@
|
||||||
//! TUI state
|
|
||||||
|
|
||||||
use ratatui::widgets::TableState;
|
|
||||||
|
|
||||||
use crate::places::{
|
|
||||||
db_repository::DbPlacesRepository, models::Place, repository::PlacesRepository,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub struct State {
|
|
||||||
pub height: u16,
|
|
||||||
pub page: u32,
|
|
||||||
pub mode: Mode,
|
|
||||||
places_repository: DbPlacesRepository,
|
|
||||||
pub places: Vec<Place>,
|
|
||||||
places_status: DataStatus,
|
|
||||||
pub selected_place: TableState,
|
|
||||||
pub quit: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum Mode {
|
|
||||||
List,
|
|
||||||
Edit,
|
|
||||||
}
|
|
||||||
|
|
||||||
enum DataStatus {
|
|
||||||
Fresh,
|
|
||||||
Old,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl State {
|
|
||||||
pub fn new(places_repository: DbPlacesRepository, height: u16) -> Self {
|
|
||||||
Self {
|
|
||||||
height,
|
|
||||||
page: 0,
|
|
||||||
mode: Mode::List,
|
|
||||||
places_repository,
|
|
||||||
places_status: DataStatus::Old,
|
|
||||||
places: vec![],
|
|
||||||
selected_place: TableState::default(),
|
|
||||||
quit: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn next_page(&mut self) {
|
|
||||||
self.page += 1;
|
|
||||||
self.places_status = DataStatus::Old;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn prev_page(&mut self) {
|
|
||||||
self.page = self.page.saturating_sub(1);
|
|
||||||
self.places_status = DataStatus::Old;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn fetch_places(&mut self) {
|
|
||||||
if let DataStatus::Fresh = self.places_status {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let limit = (self.height as u8).saturating_sub(3);
|
|
||||||
let offset = (limit as u32) * self.page;
|
|
||||||
match self
|
|
||||||
.places_repository
|
|
||||||
.get_places_paginated(offset, limit)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(places) => {
|
|
||||||
self.places = places;
|
|
||||||
if !self.places.is_empty() {
|
|
||||||
self.selected_place.select(Some(0));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
tracing::error!("{err}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.places_status = DataStatus::Fresh;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,198 +0,0 @@
|
||||||
use std::ops::{Deref, DerefMut};
|
|
||||||
|
|
||||||
use anyhow::{Result, anyhow};
|
|
||||||
use crossterm::event::{Event as CrosstermEvent, KeyEvent, KeyEventKind, MouseEvent};
|
|
||||||
use crossterm::terminal::{EnterAlternateScreen, LeaveAlternateScreen};
|
|
||||||
use futures::{FutureExt, StreamExt};
|
|
||||||
use ratatui::Terminal;
|
|
||||||
use ratatui::prelude::CrosstermBackend;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use tokio::sync::mpsc::{UnboundedReceiver, unbounded_channel};
|
|
||||||
use tokio::task::JoinHandle;
|
|
||||||
use tokio_util::sync::CancellationToken;
|
|
||||||
|
|
||||||
/// Terminal events.
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
|
||||||
pub enum Event {
|
|
||||||
Init,
|
|
||||||
Quit,
|
|
||||||
Error,
|
|
||||||
Closed,
|
|
||||||
/// Triggers background actions
|
|
||||||
Tick,
|
|
||||||
/// Triggers less frequent background actions
|
|
||||||
SlowTick,
|
|
||||||
/// UI Render
|
|
||||||
Render,
|
|
||||||
FocusGained,
|
|
||||||
FocusLost,
|
|
||||||
Paste(String),
|
|
||||||
/// Key Press
|
|
||||||
Key(KeyEvent),
|
|
||||||
Mouse(MouseEvent),
|
|
||||||
/// Terminal Resize
|
|
||||||
Resize(u16, u16),
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Terminal event handler.
|
|
||||||
pub struct Tui {
|
|
||||||
/// Terminal backend
|
|
||||||
pub terminal: ratatui::Terminal<CrosstermBackend<std::io::Stderr>>,
|
|
||||||
/// Event handler task.
|
|
||||||
pub task: JoinHandle<Result<()>>,
|
|
||||||
/// Cancelation token
|
|
||||||
pub cancellation_token: CancellationToken,
|
|
||||||
/// Event receiver channel.
|
|
||||||
receiver: UnboundedReceiver<Event>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Tui {
|
|
||||||
pub fn new() -> Result<Self> {
|
|
||||||
let tick_rate = 4.0;
|
|
||||||
let slow_tick_rate = 0.25;
|
|
||||||
let frame_rate = 30.0;
|
|
||||||
let terminal = Terminal::new(CrosstermBackend::new(std::io::stderr()))?;
|
|
||||||
let cancellation_token = CancellationToken::new();
|
|
||||||
|
|
||||||
// setup the event handling task
|
|
||||||
let (sender, receiver) = unbounded_channel();
|
|
||||||
let task = {
|
|
||||||
let cancellation_token = cancellation_token.clone();
|
|
||||||
let sender = sender.clone();
|
|
||||||
let tick_delay = std::time::Duration::from_secs_f64(1.0 / tick_rate);
|
|
||||||
let slow_tick_delay = std::time::Duration::from_secs_f64(1.0 / slow_tick_rate);
|
|
||||||
let render_delay = std::time::Duration::from_secs_f64(1.0 / frame_rate);
|
|
||||||
tokio::spawn(async move {
|
|
||||||
let mut reader = crossterm::event::EventStream::new();
|
|
||||||
let mut tick_interval = tokio::time::interval(tick_delay);
|
|
||||||
let mut slow_tick_interval = tokio::time::interval(slow_tick_delay);
|
|
||||||
let mut render_interval = tokio::time::interval(render_delay);
|
|
||||||
sender.send(Event::Init)?;
|
|
||||||
loop {
|
|
||||||
let tick_delay = tick_interval.tick();
|
|
||||||
let slow_tick_delay = slow_tick_interval.tick();
|
|
||||||
let render_delay = render_interval.tick();
|
|
||||||
let crossterm_event = reader.next().fuse();
|
|
||||||
tokio::select! {
|
|
||||||
_ = cancellation_token.cancelled() => {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
maybe_event = crossterm_event => {
|
|
||||||
match maybe_event {
|
|
||||||
Some(Ok(ev)) => {
|
|
||||||
match ev {
|
|
||||||
CrosstermEvent::Key(key) => {
|
|
||||||
if key.kind == KeyEventKind::Press {
|
|
||||||
sender.send(Event::Key(key))?
|
|
||||||
}
|
|
||||||
},
|
|
||||||
CrosstermEvent::Mouse(mouse) => {
|
|
||||||
sender.send(Event::Mouse(mouse))?
|
|
||||||
},
|
|
||||||
CrosstermEvent::Resize(x, y)=> {
|
|
||||||
sender.send(Event::Resize(x,y))?
|
|
||||||
},
|
|
||||||
CrosstermEvent::FocusLost => sender.send(Event::FocusLost)?,
|
|
||||||
CrosstermEvent::FocusGained => sender.send(Event::FocusGained)?,
|
|
||||||
CrosstermEvent::Paste(s) => sender.send(Event::Paste(s))?,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Some(Err(_)) => sender.send(Event::Error)?,
|
|
||||||
None => {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
_ = tick_delay => {
|
|
||||||
sender.send(Event::Tick)?;
|
|
||||||
},
|
|
||||||
_ = slow_tick_delay => {
|
|
||||||
sender.send(Event::SlowTick)?;
|
|
||||||
},
|
|
||||||
_ = render_delay => {
|
|
||||||
sender.send(Event::Render)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
// Setup the terminal
|
|
||||||
crossterm::terminal::enable_raw_mode()?;
|
|
||||||
crossterm::execute!(
|
|
||||||
std::io::stderr(),
|
|
||||||
EnterAlternateScreen,
|
|
||||||
crossterm::cursor::Hide
|
|
||||||
)?;
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
terminal,
|
|
||||||
cancellation_token,
|
|
||||||
task,
|
|
||||||
receiver,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn cancel(&self) {
|
|
||||||
self.cancellation_token.cancel();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn stop(&self) -> Result<()> {
|
|
||||||
self.cancel();
|
|
||||||
let mut counter = 0;
|
|
||||||
while !self.task.is_finished() {
|
|
||||||
std::thread::sleep(std::time::Duration::from_millis(10));
|
|
||||||
counter += 1;
|
|
||||||
if counter > 5 {
|
|
||||||
self.task.abort();
|
|
||||||
}
|
|
||||||
if counter > 10 {
|
|
||||||
return Err(anyhow!(
|
|
||||||
"Failed to abort task in 100 milliseconds for unknown reason"
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn exit(&mut self) -> Result<()> {
|
|
||||||
self.stop()?;
|
|
||||||
if crossterm::terminal::is_raw_mode_enabled()? {
|
|
||||||
self.flush()?;
|
|
||||||
crossterm::execute!(
|
|
||||||
std::io::stderr(),
|
|
||||||
LeaveAlternateScreen,
|
|
||||||
crossterm::cursor::Show
|
|
||||||
)?;
|
|
||||||
crossterm::terminal::disable_raw_mode()?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Receive the next event from the handler task.
|
|
||||||
pub async fn next(&mut self) -> Result<Event> {
|
|
||||||
self.receiver
|
|
||||||
.recv()
|
|
||||||
.await
|
|
||||||
.ok_or_else(|| anyhow!("Events channel was closed"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Deref for Tui {
|
|
||||||
type Target = ratatui::Terminal<CrosstermBackend<std::io::Stderr>>;
|
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
&self.terminal
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DerefMut for Tui {
|
|
||||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
|
||||||
&mut self.terminal
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Drop for Tui {
|
|
||||||
fn drop(&mut self) {
|
|
||||||
self.exit().unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
147
src/tui/ui.rs
147
src/tui/ui.rs
|
|
@ -1,147 +0,0 @@
|
||||||
//! UI definition and drawing
|
|
||||||
|
|
||||||
use itertools::Itertools;
|
|
||||||
use ratatui::Frame;
|
|
||||||
use ratatui::layout::{Constraint, Direction, Layout, Rect};
|
|
||||||
use ratatui::style::{Color, Style, Stylize};
|
|
||||||
use ratatui::text::{Line, Span, ToSpan};
|
|
||||||
use ratatui::widgets::{Paragraph, Row, Table};
|
|
||||||
|
|
||||||
use super::state::{Mode, State};
|
|
||||||
|
|
||||||
/// UI drawing
|
|
||||||
pub fn ui_draw(state: &mut State, f: &mut Frame<'_>) {
|
|
||||||
let main_split = Layout::default()
|
|
||||||
.direction(Direction::Vertical)
|
|
||||||
.constraints([
|
|
||||||
Constraint::Length(1),
|
|
||||||
Constraint::Fill(1),
|
|
||||||
Constraint::Length(1),
|
|
||||||
])
|
|
||||||
.split(f.area());
|
|
||||||
|
|
||||||
header_draw(state, f, main_split[0]);
|
|
||||||
main_draw(state, f, main_split[1]);
|
|
||||||
footer_draw(state, f, main_split[2]);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn header_draw(state: &mut State, f: &mut Frame<'_>, area: Rect) {
|
|
||||||
let split = Layout::default()
|
|
||||||
.direction(Direction::Horizontal)
|
|
||||||
.constraints([
|
|
||||||
Constraint::Length(9),
|
|
||||||
Constraint::Fill(1),
|
|
||||||
Constraint::Length(6),
|
|
||||||
])
|
|
||||||
.split(area);
|
|
||||||
let app_name = Span::styled(
|
|
||||||
" huellas ",
|
|
||||||
Style::new().bg(Color::Gray).fg(Color::Black).bold(),
|
|
||||||
);
|
|
||||||
let page =
|
|
||||||
Line::from_iter(["Page: ".to_span(), state.page.to_span(), " ".to_span()]).right_aligned();
|
|
||||||
let app_mode = match state.mode {
|
|
||||||
Mode::List => Span::styled(" LIST ", Style::new().black().on_light_green().bold()),
|
|
||||||
Mode::Edit => Span::styled(" EDIT ", Style::new().black().on_light_blue().bold()),
|
|
||||||
};
|
|
||||||
f.render_widget(app_name, split[0]);
|
|
||||||
f.render_widget(page, split[1]);
|
|
||||||
f.render_widget(app_mode, split[2]);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main_draw(state: &mut State, f: &mut Frame<'_>, area: Rect) {
|
|
||||||
match state.mode {
|
|
||||||
Mode::List => list_draw(state, f, area),
|
|
||||||
Mode::Edit => edit_draw(state, f, area),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn list_draw(state: &mut State, f: &mut Frame<'_>, area: Rect) {
|
|
||||||
let places = state.places.iter().map(|p| {
|
|
||||||
Row::new([
|
|
||||||
p.id.to_string(),
|
|
||||||
p.name.clone(),
|
|
||||||
p.latitude.to_string(),
|
|
||||||
p.longitude.to_string(),
|
|
||||||
p.icon.clone(),
|
|
||||||
p.open_hours.clone(),
|
|
||||||
p.description.clone(),
|
|
||||||
p.url.clone().unwrap_or_default(),
|
|
||||||
])
|
|
||||||
});
|
|
||||||
|
|
||||||
let widths = [
|
|
||||||
Constraint::Length(3),
|
|
||||||
Constraint::Fill(1),
|
|
||||||
Constraint::Length(7),
|
|
||||||
Constraint::Length(7),
|
|
||||||
Constraint::Length(8),
|
|
||||||
Constraint::Fill(1),
|
|
||||||
Constraint::Fill(1),
|
|
||||||
Constraint::Fill(1),
|
|
||||||
];
|
|
||||||
|
|
||||||
let places_table = Table::new(places, widths)
|
|
||||||
.header(
|
|
||||||
Row::new([
|
|
||||||
"Id",
|
|
||||||
"Name",
|
|
||||||
"Lat",
|
|
||||||
"Long",
|
|
||||||
"Icon",
|
|
||||||
"Open Hours",
|
|
||||||
"Description",
|
|
||||||
"URL",
|
|
||||||
])
|
|
||||||
.style(Style::new().white().on_dark_gray().bold()),
|
|
||||||
)
|
|
||||||
.row_highlight_style(Style::new().reversed());
|
|
||||||
|
|
||||||
f.render_stateful_widget(places_table, area, &mut state.selected_place);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn edit_draw(_state: &mut State, _f: &mut Frame<'_>, _area: Rect) {}
|
|
||||||
|
|
||||||
#[expect(unstable_name_collisions)]
|
|
||||||
fn footer_draw(state: &mut State, f: &mut Frame<'_>, area: Rect) {
|
|
||||||
let separator = Span::styled(" ", Style::new().black().on_black());
|
|
||||||
let keybindings = match state.mode {
|
|
||||||
Mode::List => {
|
|
||||||
let keybindings = [
|
|
||||||
("j", "Next"),
|
|
||||||
("k", "Previous"),
|
|
||||||
("Home", "First"),
|
|
||||||
("End", "Last"),
|
|
||||||
("PgUp", "Prev Page"),
|
|
||||||
("PgDown", "Next Page"),
|
|
||||||
("e", "Edit"),
|
|
||||||
]
|
|
||||||
.map(|(key, action)| keybinding(key, action).to_vec())
|
|
||||||
.into_iter()
|
|
||||||
.intersperse(vec![separator])
|
|
||||||
.flatten();
|
|
||||||
Paragraph::new(Line::from_iter(keybindings))
|
|
||||||
}
|
|
||||||
Mode::Edit => {
|
|
||||||
let keybindings = [("Esc", "Close w/o saving")]
|
|
||||||
.map(|(key, action)| keybinding(key, action).to_vec())
|
|
||||||
.into_iter()
|
|
||||||
.intersperse(vec![separator])
|
|
||||||
.flatten();
|
|
||||||
Paragraph::new(Line::from_iter(keybindings))
|
|
||||||
}
|
|
||||||
};
|
|
||||||
f.render_widget(keybindings, area);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn keybinding(key: &'static str, action: &'static str) -> [Span<'static>; 5] {
|
|
||||||
let black_bold = Style::new().black().on_gray().bold();
|
|
||||||
let red_bold = Style::new().red().on_gray().bold();
|
|
||||||
let black = Style::new().black().on_gray();
|
|
||||||
[
|
|
||||||
Span::styled(" <", black_bold),
|
|
||||||
Span::styled(key, red_bold),
|
|
||||||
Span::styled("> ", black_bold),
|
|
||||||
Span::styled(action, black),
|
|
||||||
Span::styled(" ", black),
|
|
||||||
]
|
|
||||||
}
|
|
||||||
Loading…
Add table
Reference in a new issue