Migrated left overs from github
This commit is contained in:
parent
b53203f8a7
commit
5dc5ffdad8
12 changed files with 1487 additions and 566 deletions
128
Cargo.lock
generated
128
Cargo.lock
generated
|
|
@ -1471,9 +1471,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
|||
|
||||
[[package]]
|
||||
name = "hyper"
|
||||
version = "1.8.1"
|
||||
version = "1.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11"
|
||||
checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca"
|
||||
dependencies = [
|
||||
"atomic-waker",
|
||||
"bytes",
|
||||
|
|
@ -1486,7 +1486,6 @@ dependencies = [
|
|||
"httpdate",
|
||||
"itoa",
|
||||
"pin-project-lite",
|
||||
"pin-utils",
|
||||
"smallvec",
|
||||
"tokio",
|
||||
"want",
|
||||
|
|
@ -1559,12 +1558,13 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "icu_collections"
|
||||
version = "2.1.1"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43"
|
||||
checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"potential_utf",
|
||||
"utf8_iter",
|
||||
"yoke",
|
||||
"zerofrom",
|
||||
"zerovec",
|
||||
|
|
@ -1572,9 +1572,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "icu_locale_core"
|
||||
version = "2.1.1"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6"
|
||||
checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"litemap",
|
||||
|
|
@ -1585,9 +1585,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "icu_normalizer"
|
||||
version = "2.1.1"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599"
|
||||
checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4"
|
||||
dependencies = [
|
||||
"icu_collections",
|
||||
"icu_normalizer_data",
|
||||
|
|
@ -1599,15 +1599,15 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "icu_normalizer_data"
|
||||
version = "2.1.1"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a"
|
||||
checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38"
|
||||
|
||||
[[package]]
|
||||
name = "icu_properties"
|
||||
version = "2.1.2"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec"
|
||||
checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de"
|
||||
dependencies = [
|
||||
"icu_collections",
|
||||
"icu_locale_core",
|
||||
|
|
@ -1619,15 +1619,15 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "icu_properties_data"
|
||||
version = "2.1.2"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af"
|
||||
checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14"
|
||||
|
||||
[[package]]
|
||||
name = "icu_provider"
|
||||
version = "2.1.1"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614"
|
||||
checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"icu_locale_core",
|
||||
|
|
@ -1784,9 +1784,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "iri-string"
|
||||
version = "0.7.11"
|
||||
version = "0.7.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d8e7418f59cc01c88316161279a7f665217ae316b388e58a0d10e29f54f1e5eb"
|
||||
checksum = "25e659a4bb38e810ebc252e53b5814ff908a8c58c2a9ce2fae1bbec24cbf4e20"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"serde",
|
||||
|
|
@ -1882,9 +1882,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.92"
|
||||
version = "0.3.94"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cc4c90f45aa2e6eacbe8645f77fdea542ac97a494bcd117a67df9ff4d611f995"
|
||||
checksum = "2e04e2ef80ce82e13552136fabeef8a5ed1f985a96805761cbb9a2c34e7664d9"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"futures-util",
|
||||
|
|
@ -1941,9 +1941,9 @@ checksum = "2c4a545a15244c7d945065b5d392b2d2d7f21526fba56ce51467b06ed445e8f7"
|
|||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.183"
|
||||
version = "0.2.184"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d"
|
||||
checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af"
|
||||
|
||||
[[package]]
|
||||
name = "libsqlite3-sys"
|
||||
|
|
@ -1972,9 +1972,9 @@ checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
|
|||
|
||||
[[package]]
|
||||
name = "litemap"
|
||||
version = "0.8.1"
|
||||
version = "0.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77"
|
||||
checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0"
|
||||
|
||||
[[package]]
|
||||
name = "litrs"
|
||||
|
|
@ -2509,12 +2509,6 @@ version = "0.2.17"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
||||
|
||||
[[package]]
|
||||
name = "pin-utils"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
||||
|
||||
[[package]]
|
||||
name = "pkg-config"
|
||||
version = "0.3.32"
|
||||
|
|
@ -2632,9 +2626,9 @@ checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49"
|
|||
|
||||
[[package]]
|
||||
name = "potential_utf"
|
||||
version = "0.1.4"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77"
|
||||
checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564"
|
||||
dependencies = [
|
||||
"zerovec",
|
||||
]
|
||||
|
|
@ -3695,9 +3689,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "tinystr"
|
||||
version = "0.8.2"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869"
|
||||
checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"zerovec",
|
||||
|
|
@ -3879,7 +3873,7 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
|||
[[package]]
|
||||
name = "ttp-core"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/Tensamin/TTP.git#23c9e68da6622a0cb3a773881f353ae4489c9743"
|
||||
source = "git+https://github.com/Tensamin/TTP.git#e246d1af6a42c71514d0ccc06fef94d71e4ad167"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"byteorder",
|
||||
|
|
@ -3891,7 +3885,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "ttp-native"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/Tensamin/TTP.git#23c9e68da6622a0cb3a773881f353ae4489c9743"
|
||||
source = "git+https://github.com/Tensamin/TTP.git#e246d1af6a42c71514d0ccc06fef94d71e4ad167"
|
||||
dependencies = [
|
||||
"quinn",
|
||||
"rustls",
|
||||
|
|
@ -4123,9 +4117,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.115"
|
||||
version = "0.2.117"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6523d69017b7633e396a89c5efab138161ed5aafcbc8d3e5c5a42ae38f50495a"
|
||||
checksum = "0551fc1bb415591e3372d0bc4780db7e587d84e2a7e79da121051c5c4b89d0b0"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
|
|
@ -4136,9 +4130,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-futures"
|
||||
version = "0.4.65"
|
||||
version = "0.4.67"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2d1faf851e778dfa54db7cd438b70758eba9755cb47403f3496edd7c8fc212f0"
|
||||
checksum = "03623de6905b7206edd0a75f69f747f134b7f0a2323392d664448bf2d3c5d87e"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
|
|
@ -4146,9 +4140,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.115"
|
||||
version = "0.2.117"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4e3a6c758eb2f701ed3d052ff5737f5bfe6614326ea7f3bbac7156192dc32e67"
|
||||
checksum = "7fbdf9a35adf44786aecd5ff89b4563a90325f9da0923236f6104e603c7e86be"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"wasm-bindgen-macro-support",
|
||||
|
|
@ -4156,9 +4150,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.115"
|
||||
version = "0.2.117"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "921de2737904886b52bcbb237301552d05969a6f9c40d261eb0533c8b055fedf"
|
||||
checksum = "dca9693ef2bab6d4e6707234500350d8dad079eb508dca05530c85dc3a529ff2"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"proc-macro2",
|
||||
|
|
@ -4169,9 +4163,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.115"
|
||||
version = "0.2.117"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a93e946af942b58934c604527337bad9ae33ba1d5c6900bbb41c2c07c2364a93"
|
||||
checksum = "39129a682a6d2d841b6c429d0c51e5cb0ed1a03829d8b3d1e69a011e62cb3d3b"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
|
@ -4212,9 +4206,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "web-sys"
|
||||
version = "0.3.92"
|
||||
version = "0.3.94"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "84cde8507f4d7cfcb1185b8cb5890c494ffea65edbe1ba82cfd63661c805ed94"
|
||||
checksum = "cd70027e39b12f0849461e08ffc50b9cd7688d942c1c8e3c7b22273236b4dd0a"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
|
|
@ -4872,9 +4866,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "yoke"
|
||||
version = "0.8.1"
|
||||
version = "0.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954"
|
||||
checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca"
|
||||
dependencies = [
|
||||
"stable_deref_trait",
|
||||
"yoke-derive",
|
||||
|
|
@ -4883,9 +4877,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "yoke-derive"
|
||||
version = "0.8.1"
|
||||
version = "0.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d"
|
||||
checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
|
@ -4895,18 +4889,18 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.8.47"
|
||||
version = "0.8.48"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "efbb2a062be311f2ba113ce66f697a4dc589f85e78a4aea276200804cea0ed87"
|
||||
checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9"
|
||||
dependencies = [
|
||||
"zerocopy-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy-derive"
|
||||
version = "0.8.47"
|
||||
version = "0.8.48"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0e8bc7269b54418e7aeeef514aa68f8690b8c0489a06b0136e5f57c4c5ccab89"
|
||||
checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
|
@ -4915,18 +4909,18 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "zerofrom"
|
||||
version = "0.1.6"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5"
|
||||
checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df"
|
||||
dependencies = [
|
||||
"zerofrom-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerofrom-derive"
|
||||
version = "0.1.6"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502"
|
||||
checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
|
@ -4956,9 +4950,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "zerotrie"
|
||||
version = "0.2.3"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851"
|
||||
checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"yoke",
|
||||
|
|
@ -4967,9 +4961,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "zerovec"
|
||||
version = "0.11.5"
|
||||
version = "0.11.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002"
|
||||
checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239"
|
||||
dependencies = [
|
||||
"yoke",
|
||||
"zerofrom",
|
||||
|
|
@ -4978,9 +4972,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "zerovec-derive"
|
||||
version = "0.11.2"
|
||||
version = "0.11.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3"
|
||||
checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
|
|
|||
|
|
@ -1,2 +1 @@
|
|||
# Iota
|
||||
Iota is the selfhost server for establishing a connection between clients. It uses its counterpart Omikron to allow a connection between clients.
|
||||
# ⚠️ Moved to [git.methanium.net](https://git.methanium.net/tensamin/iota) ⚠️
|
||||
|
|
|
|||
|
|
@ -17,20 +17,30 @@ use crate::{
|
|||
ui::FPS,
|
||||
util::borders::draw_block_joins,
|
||||
},
|
||||
log, log_cv,
|
||||
log, log_command, log_cv,
|
||||
omikron::omikron_connection::OMIKRON_CONNECTION,
|
||||
users::{user_manager, user_profile::UserProfile},
|
||||
util::file_util,
|
||||
};
|
||||
use std::{any::Any, time::Duration};
|
||||
use std::{
|
||||
any::Any,
|
||||
sync::{Arc, Mutex},
|
||||
time::Duration,
|
||||
};
|
||||
use tokio::time::Instant;
|
||||
|
||||
pub struct ConsoleCard {
|
||||
focused: bool,
|
||||
pub title: String,
|
||||
pub content: String,
|
||||
pub cursor_position: usize,
|
||||
|
||||
borders: Borders,
|
||||
joins: Borders,
|
||||
|
||||
cursor: Arc<Mutex<bool>>,
|
||||
last_swap: Arc<Mutex<Instant>>,
|
||||
tab_index: usize,
|
||||
}
|
||||
|
||||
impl ConsoleCard {
|
||||
|
|
@ -39,10 +49,164 @@ impl ConsoleCard {
|
|||
focused: false,
|
||||
title: title.to_string(),
|
||||
content: content.to_string(),
|
||||
cursor_position: content.chars().count(),
|
||||
borders: Borders::ALL,
|
||||
joins: Borders::NONE,
|
||||
cursor: Arc::new(Mutex::new(true)),
|
||||
last_swap: Arc::new(Mutex::new(Instant::now())),
|
||||
tab_index: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn byte_index(&self) -> usize {
|
||||
self.content
|
||||
.char_indices()
|
||||
.nth(self.cursor_position)
|
||||
.map(|(i, _)| i)
|
||||
.unwrap_or(self.content.len())
|
||||
}
|
||||
|
||||
fn cursor_visible(&self) -> bool {
|
||||
if !self.focused {
|
||||
return false;
|
||||
}
|
||||
|
||||
let mut visible = self.cursor.lock().unwrap();
|
||||
let mut last = self.last_swap.lock().unwrap();
|
||||
let now = Instant::now();
|
||||
|
||||
if now.duration_since(*last) >= Duration::from_millis(500) {
|
||||
*visible = !*visible;
|
||||
*last = now;
|
||||
}
|
||||
|
||||
*visible
|
||||
}
|
||||
|
||||
fn current_prefix(&self) -> Option<&str> {
|
||||
if self.content.starts_with('/') {
|
||||
Some("/")
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn cursor_spans(&self) -> Vec<Span<'static>> {
|
||||
let cursor_visible = self.cursor_visible();
|
||||
let cursor_style = Style::default().fg(Color::White).bg(Color::DarkGray);
|
||||
let mut spans = Vec::new();
|
||||
|
||||
if self.content.is_empty() {
|
||||
if self.focused {
|
||||
if cursor_visible {
|
||||
spans.push(Span::styled(" ", cursor_style));
|
||||
} else {
|
||||
spans.push(Span::styled(" ", Style::default().fg(Color::White)));
|
||||
}
|
||||
spans.push(Span::styled(
|
||||
"send command (<help> for info)",
|
||||
Style::default().fg(Color::DarkGray),
|
||||
));
|
||||
} else {
|
||||
spans.push(Span::styled(
|
||||
" send command (<help> for info)",
|
||||
Style::default().fg(Color::DarkGray),
|
||||
));
|
||||
}
|
||||
return spans;
|
||||
}
|
||||
|
||||
let byte_index = self.byte_index();
|
||||
let before = self.content[..byte_index].to_string();
|
||||
let after = self.content[byte_index..].to_string();
|
||||
|
||||
let prefix_len = self.current_prefix().map(|s| s.len()).unwrap_or(0);
|
||||
|
||||
if prefix_len > 0 && before.len() >= prefix_len {
|
||||
let prefix = &before[..prefix_len];
|
||||
let rest = &before[prefix_len..];
|
||||
spans.push(Span::styled(
|
||||
prefix.to_string(),
|
||||
Self::style_for_part(true, false, false),
|
||||
));
|
||||
if !rest.is_empty() {
|
||||
spans.push(Span::styled(
|
||||
rest.to_string(),
|
||||
Style::default().fg(Color::White),
|
||||
));
|
||||
}
|
||||
} else if !before.is_empty() {
|
||||
spans.push(Span::styled(
|
||||
before.clone(),
|
||||
Style::default().fg(Color::White),
|
||||
));
|
||||
}
|
||||
|
||||
if cursor_visible {
|
||||
spans.push(Span::styled(" ", cursor_style));
|
||||
}
|
||||
|
||||
if !after.is_empty() {
|
||||
spans.push(Span::styled(after, Style::default().fg(Color::White)));
|
||||
}
|
||||
|
||||
spans
|
||||
}
|
||||
|
||||
fn style_for_part(is_prefix: bool, is_hint: bool, is_error: bool) -> Style {
|
||||
if is_error {
|
||||
return Style::default().fg(Color::Red);
|
||||
}
|
||||
|
||||
if is_hint {
|
||||
return Style::default().fg(Color::DarkGray);
|
||||
}
|
||||
|
||||
if is_prefix {
|
||||
return Style::default().fg(Color::DarkGray);
|
||||
}
|
||||
|
||||
Style::default().fg(Color::White)
|
||||
}
|
||||
|
||||
fn render_cursor_spans(&self) -> Vec<Span<'static>> {
|
||||
self.cursor_spans()
|
||||
}
|
||||
|
||||
fn move_cursor_left(&mut self) {
|
||||
if self.cursor_position > 0 {
|
||||
self.cursor_position -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
fn move_cursor_right(&mut self) {
|
||||
let len = self.content.chars().count();
|
||||
if self.cursor_position < len {
|
||||
self.cursor_position += 1;
|
||||
}
|
||||
}
|
||||
|
||||
fn delete_at_cursor(&mut self) {
|
||||
if self.content.is_empty() || self.cursor_position == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
let start = self
|
||||
.content
|
||||
.char_indices()
|
||||
.nth(self.cursor_position.saturating_sub(1))
|
||||
.map(|(i, _)| i)
|
||||
.unwrap_or(0);
|
||||
let end = self.byte_index();
|
||||
self.content.replace_range(start..end, "");
|
||||
self.cursor_position -= 1;
|
||||
}
|
||||
|
||||
fn insert_at_cursor(&mut self, c: char) {
|
||||
let idx = self.byte_index();
|
||||
self.content.insert(idx, c);
|
||||
self.cursor_position += 1;
|
||||
}
|
||||
}
|
||||
|
||||
impl Element for ConsoleCard {
|
||||
|
|
@ -70,7 +234,8 @@ impl Element for ConsoleCard {
|
|||
Style::default()
|
||||
});
|
||||
|
||||
let par = Paragraph::new(Line::from(Span::from(self.content.clone())))
|
||||
let spans = self.render_cursor_spans();
|
||||
let par = Paragraph::new(Line::from(spans))
|
||||
.block(block)
|
||||
.scroll((0, 0));
|
||||
f.render_widget(par, r);
|
||||
|
|
@ -87,11 +252,11 @@ impl JoinableElement for ConsoleCard {
|
|||
self
|
||||
}
|
||||
|
||||
fn as_element(&self) -> &dyn Element {
|
||||
fn as_element(&self) -> &(dyn Element + 'static) {
|
||||
self
|
||||
}
|
||||
|
||||
fn as_element_mut(&mut self) -> &mut dyn Element {
|
||||
fn as_element_mut(&mut self) -> &mut (dyn Element + 'static) {
|
||||
self
|
||||
}
|
||||
|
||||
|
|
@ -113,11 +278,11 @@ impl InteractableElement for ConsoleCard {
|
|||
self
|
||||
}
|
||||
|
||||
fn as_element(&self) -> &dyn Element {
|
||||
fn as_element(&self) -> &(dyn Element + 'static) {
|
||||
self
|
||||
}
|
||||
|
||||
fn as_element_mut(&mut self) -> &mut dyn Element {
|
||||
fn as_element_mut(&mut self) -> &mut (dyn Element + 'static) {
|
||||
self
|
||||
}
|
||||
|
||||
|
|
@ -128,26 +293,71 @@ impl InteractableElement for ConsoleCard {
|
|||
log!("");
|
||||
return InteractionResult::Handled;
|
||||
}
|
||||
|
||||
let command = self.content.clone();
|
||||
let id = Uuid::new_v4();
|
||||
let id = id.to_string();
|
||||
let id = id.split_at(8).0;
|
||||
let task_id = format!("command_{}_{}", command, id);
|
||||
ACTIVE_TASKS.insert(task_id.clone());
|
||||
|
||||
log_command!("{}", command);
|
||||
|
||||
tokio::spawn(async move {
|
||||
run_command(&command).await;
|
||||
ACTIVE_TASKS.remove(&task_id);
|
||||
});
|
||||
self.content = "".to_string();
|
||||
|
||||
self.content.clear();
|
||||
self.cursor_position = 0;
|
||||
self.tab_index = 0;
|
||||
InteractionResult::Handled
|
||||
}
|
||||
KeyCode::Backspace => {
|
||||
self.content.pop();
|
||||
self.delete_at_cursor();
|
||||
InteractionResult::Handled
|
||||
}
|
||||
KeyCode::Delete => {
|
||||
let len = self.content.chars().count();
|
||||
if self.cursor_position < len {
|
||||
let start = self.byte_index();
|
||||
let end = self
|
||||
.content
|
||||
.char_indices()
|
||||
.nth(self.cursor_position + 1)
|
||||
.map(|(i, _)| i)
|
||||
.unwrap_or(self.content.len());
|
||||
self.content.replace_range(start..end, "");
|
||||
}
|
||||
InteractionResult::Handled
|
||||
}
|
||||
KeyCode::Left => {
|
||||
self.move_cursor_left();
|
||||
InteractionResult::Handled
|
||||
}
|
||||
KeyCode::Right => {
|
||||
self.move_cursor_right();
|
||||
InteractionResult::Handled
|
||||
}
|
||||
KeyCode::Home => {
|
||||
self.cursor_position = 0;
|
||||
InteractionResult::Handled
|
||||
}
|
||||
KeyCode::End => {
|
||||
self.cursor_position = self.content.chars().count();
|
||||
InteractionResult::Handled
|
||||
}
|
||||
KeyCode::Tab => {
|
||||
if let Some(prefix) = self.current_prefix() {
|
||||
if prefix == "/" {
|
||||
self.tab_index = self.tab_index.saturating_add(1);
|
||||
}
|
||||
}
|
||||
InteractionResult::Handled
|
||||
}
|
||||
_ => {
|
||||
if let Some(c) = key.code.as_char() {
|
||||
self.content.push(c);
|
||||
self.insert_at_cursor(c);
|
||||
InteractionResult::Handled
|
||||
} else {
|
||||
InteractionResult::Unhandled
|
||||
|
|
@ -168,9 +378,8 @@ impl InteractableElement for ConsoleCard {
|
|||
self.focused = f;
|
||||
}
|
||||
}
|
||||
pub async fn run_command(command: &str) {
|
||||
log!(":{}", command);
|
||||
|
||||
pub async fn run_command(command: &str) {
|
||||
let parts = command.split(" ").collect::<Vec<&str>>();
|
||||
|
||||
match parts.as_slice() {
|
||||
|
|
@ -267,8 +476,6 @@ pub async fn run_command(command: &str) {
|
|||
}
|
||||
|
||||
pub async fn ping(time: u64) {
|
||||
let time = time;
|
||||
|
||||
let conn = OMIKRON_CONNECTION.clone();
|
||||
|
||||
let response_cv = conn
|
||||
|
|
|
|||
|
|
@ -1,27 +1,76 @@
|
|||
use crate::gui::elements::elements::{InteractableElement, JoinableElement};
|
||||
use crate::APP_STATE;
|
||||
use crate::gui::elements::elements::{Element, InteractableElement, JoinableElement};
|
||||
use crate::gui::interaction_result::InteractionResult;
|
||||
use crate::gui::util::borders::draw_block_joins;
|
||||
use crate::{APP_STATE, gui::elements::elements::Element};
|
||||
use crate::util::logger::PrintType;
|
||||
use crossterm::event::{KeyCode, KeyEvent};
|
||||
use ratatui::{
|
||||
Frame,
|
||||
layout::Rect,
|
||||
style::{Color, Style},
|
||||
text::{Line, Span},
|
||||
widgets::{Block, Borders, Paragraph, Wrap},
|
||||
widgets::{Block, Borders, Paragraph},
|
||||
};
|
||||
use std::any::Any;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct UiLogEntry {
|
||||
pub line: String,
|
||||
pub color: Color,
|
||||
pub timestamp_ms: u128,
|
||||
pub sender: PrintType,
|
||||
pub message: String,
|
||||
pub is_error: bool,
|
||||
}
|
||||
|
||||
impl UiLogEntry {
|
||||
pub fn format_timestamp(&self) -> String {
|
||||
let secs = (self.timestamp_ms / 1000) as i64;
|
||||
let hours = (secs / 3600) % 24;
|
||||
let minutes = (secs / 60) % 60;
|
||||
let seconds = secs % 60;
|
||||
format!("{:02}:{:02}:{:02}", hours, minutes, seconds)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<LogEntry> for UiLogEntry {
|
||||
fn from(entry: LogEntry) -> Self {
|
||||
Self {
|
||||
timestamp_ms: entry.timestamp_ms,
|
||||
sender: entry.sender,
|
||||
message: entry.message,
|
||||
is_error: entry.is_error,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct LogEntry {
|
||||
pub timestamp_ms: u128,
|
||||
pub sender: PrintType,
|
||||
pub message: String,
|
||||
pub is_error: bool,
|
||||
}
|
||||
|
||||
impl LogEntry {
|
||||
pub fn new(sender: PrintType, message: String, is_error: bool) -> Self {
|
||||
Self {
|
||||
timestamp_ms: SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_millis(),
|
||||
sender,
|
||||
message,
|
||||
is_error,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct LogCard {
|
||||
focused: bool,
|
||||
scroll: u16,
|
||||
|
||||
selected: bool,
|
||||
scroll_offset: usize,
|
||||
last_total_lines: usize,
|
||||
last_visible_height: usize,
|
||||
pub borders: Borders,
|
||||
pub joins: Borders,
|
||||
}
|
||||
|
|
@ -30,12 +79,213 @@ impl LogCard {
|
|||
pub fn new() -> Self {
|
||||
Self {
|
||||
focused: false,
|
||||
scroll: 1,
|
||||
selected: false,
|
||||
scroll_offset: 0,
|
||||
last_total_lines: 0,
|
||||
last_visible_height: 10,
|
||||
borders: Borders::ALL,
|
||||
joins: Borders::NONE,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_logs(&self) -> Vec<UiLogEntry> {
|
||||
let state = APP_STATE.lock().unwrap();
|
||||
state.get_logs().iter().cloned().collect()
|
||||
}
|
||||
|
||||
fn find_split_point(s: &str, max_width: usize) -> usize {
|
||||
if max_width == 0 {
|
||||
return s.len();
|
||||
}
|
||||
|
||||
let mut current_width = 0usize;
|
||||
let mut last_boundary = 0usize;
|
||||
|
||||
for (idx, ch) in s.char_indices() {
|
||||
let char_width = if ch.is_ascii() { 1 } else { 2 };
|
||||
if current_width + char_width > max_width {
|
||||
if last_boundary == 0 {
|
||||
return idx + ch.len_utf8();
|
||||
}
|
||||
return last_boundary;
|
||||
}
|
||||
current_width += char_width;
|
||||
last_boundary = idx + ch.len_utf8();
|
||||
}
|
||||
|
||||
s.len()
|
||||
}
|
||||
|
||||
fn wrap_entry(entry: &UiLogEntry, available_width: usize) -> Vec<(String, Color, bool)> {
|
||||
let mut result = Vec::new();
|
||||
|
||||
let timestamp = entry.format_timestamp();
|
||||
let first_prefix = "┌ ";
|
||||
let default_prefix = "│ ";
|
||||
let last_prefix = "└ ";
|
||||
|
||||
let prefix_width = 2;
|
||||
let first_line_width = available_width.saturating_sub(prefix_width + 1);
|
||||
let continuation_width = available_width.saturating_sub(prefix_width);
|
||||
|
||||
let segments: Vec<&str> = entry.message.split('\n').collect();
|
||||
|
||||
let mut raw_lines = Vec::new();
|
||||
|
||||
for segment in segments {
|
||||
let mut remaining = segment;
|
||||
if remaining.is_empty() {
|
||||
raw_lines.push(String::new());
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut is_first_part = true;
|
||||
while !remaining.is_empty() {
|
||||
let current_width = if is_first_part {
|
||||
first_line_width
|
||||
} else {
|
||||
continuation_width
|
||||
};
|
||||
|
||||
let split_point = Self::find_split_point(remaining, current_width);
|
||||
let line_content = remaining[..split_point].to_string();
|
||||
raw_lines.push(line_content);
|
||||
remaining = &remaining[split_point..];
|
||||
is_first_part = false;
|
||||
}
|
||||
}
|
||||
|
||||
if raw_lines.is_empty() {
|
||||
raw_lines.push(String::new());
|
||||
}
|
||||
|
||||
for (idx, content) in raw_lines.iter().enumerate() {
|
||||
let is_last = idx + 1 == raw_lines.len();
|
||||
let is_single = raw_lines.len() == 1;
|
||||
let is_first = idx == 0;
|
||||
let prefix = if is_first {
|
||||
first_prefix
|
||||
} else if is_last && !is_single {
|
||||
last_prefix
|
||||
} else {
|
||||
default_prefix
|
||||
};
|
||||
|
||||
let mut line = String::from(prefix);
|
||||
line.push_str(content);
|
||||
|
||||
if is_last && !timestamp.is_empty() {
|
||||
line.push(' ');
|
||||
line.push_str(×tamp);
|
||||
}
|
||||
|
||||
result.push((line, entry.sender.prefix_color(), entry.is_error));
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
fn build_all_lines(
|
||||
&self,
|
||||
entries: Vec<UiLogEntry>,
|
||||
width: usize,
|
||||
) -> Vec<(String, Color, bool)> {
|
||||
let mut lines = Vec::new();
|
||||
|
||||
for entry in entries {
|
||||
let wrapped = Self::wrap_entry(&entry, width);
|
||||
lines.extend(wrapped);
|
||||
}
|
||||
|
||||
lines
|
||||
}
|
||||
|
||||
fn calculate_view_window(&self, total_lines: usize, visible_height: usize) -> (usize, usize) {
|
||||
if total_lines <= visible_height {
|
||||
return (0, total_lines);
|
||||
}
|
||||
|
||||
let max_offset = total_lines - visible_height;
|
||||
let clamped_offset = self.scroll_offset.min(max_offset);
|
||||
|
||||
let end = total_lines - clamped_offset;
|
||||
let start = end.saturating_sub(visible_height);
|
||||
|
||||
(start, end)
|
||||
}
|
||||
|
||||
fn get_title_hints(&self) -> (bool, bool) {
|
||||
if self.last_total_lines == 0 || self.last_total_lines <= self.last_visible_height {
|
||||
return (false, false);
|
||||
}
|
||||
|
||||
let max_offset = self.last_total_lines - self.last_visible_height;
|
||||
let can_scroll_up = self.scroll_offset < max_offset;
|
||||
let can_scroll_down = self.scroll_offset > 0;
|
||||
|
||||
(can_scroll_up, can_scroll_down)
|
||||
}
|
||||
|
||||
fn build_title(&self) -> String {
|
||||
if !self.focused {
|
||||
return "Logs".to_string();
|
||||
}
|
||||
|
||||
let (can_up, can_down) = self.get_title_hints();
|
||||
|
||||
if !can_up && !can_down {
|
||||
return "Logs".to_string();
|
||||
}
|
||||
|
||||
let nav_symbol = if self.selected { "↑" } else { "j" };
|
||||
let down_symbol = if self.selected { "↓" } else { "k" };
|
||||
|
||||
match (can_up, can_down) {
|
||||
(true, true) => format!("Logs ({} older {} newer)", nav_symbol, down_symbol),
|
||||
(true, false) => format!("Logs ({} older)", nav_symbol),
|
||||
(false, true) => format!("Logs ({} newer)", down_symbol),
|
||||
(false, false) => "Logs".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn scroll_up(&mut self) {
|
||||
let max_offset = self
|
||||
.last_total_lines
|
||||
.saturating_sub(self.last_visible_height);
|
||||
self.scroll_offset = (self.scroll_offset + 1).min(max_offset);
|
||||
}
|
||||
|
||||
fn scroll_down(&mut self) {
|
||||
self.scroll_offset = self.scroll_offset.saturating_sub(1);
|
||||
}
|
||||
|
||||
fn split_line_prefix(line: &str) -> (&str, &str) {
|
||||
if let Some(rest) = line.strip_prefix("└ ") {
|
||||
("└ ", rest)
|
||||
} else if let Some(rest) = line.strip_prefix("│ ") {
|
||||
("│ ", rest)
|
||||
} else if let Some(rest) = line.strip_prefix("┌ ") {
|
||||
("┌ ", rest)
|
||||
} else {
|
||||
("", line)
|
||||
}
|
||||
}
|
||||
|
||||
fn split_timestamp_suffix(line: &str) -> (&str, &str) {
|
||||
if let Some(idx) = line.rfind(' ') {
|
||||
let possible_timestamp = &line[idx + 1..];
|
||||
if possible_timestamp.len() == 8
|
||||
&& possible_timestamp.as_bytes()[2] == b':'
|
||||
&& possible_timestamp.as_bytes()[5] == b':'
|
||||
{
|
||||
let (content, timestamp_with_space) = line.split_at(idx);
|
||||
return (content.trim_end(), timestamp_with_space);
|
||||
}
|
||||
}
|
||||
(line, "")
|
||||
}
|
||||
}
|
||||
|
||||
impl Element for LogCard {
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
|
|
@ -46,22 +296,10 @@ impl Element for LogCard {
|
|||
}
|
||||
|
||||
fn render(&self, f: &mut Frame, area: Rect) {
|
||||
let state = APP_STATE.lock().unwrap();
|
||||
let logs = state.get_logs();
|
||||
|
||||
let lines: Vec<Line> = logs
|
||||
.iter()
|
||||
.map(|log| {
|
||||
Line::from(Span::raw(log.line.clone())).style(Style::default().fg(log.color))
|
||||
})
|
||||
.collect();
|
||||
let entries = self.get_logs();
|
||||
|
||||
let block = Block::default()
|
||||
.title(if self.focused {
|
||||
"Logs J/K to scroll"
|
||||
} else {
|
||||
"Logs"
|
||||
})
|
||||
.title(self.build_title())
|
||||
.borders(self.borders)
|
||||
.border_style(if self.focused {
|
||||
Style::default().fg(Color::Yellow)
|
||||
|
|
@ -69,22 +307,70 @@ impl Element for LogCard {
|
|||
Style::default()
|
||||
});
|
||||
|
||||
let inner_height = area.height.saturating_sub(2) as usize;
|
||||
let inner_area = block.inner(area);
|
||||
f.render_widget(block, area);
|
||||
|
||||
let total_lines = lines.len();
|
||||
let base_scroll = total_lines.saturating_sub(inner_height) as u16;
|
||||
if inner_area.width == 0 || inner_area.height == 0 {
|
||||
draw_block_joins(f, area, self.borders, self.joins);
|
||||
return;
|
||||
}
|
||||
|
||||
let scroll = base_scroll.saturating_sub(self.scroll);
|
||||
let all_lines = self.build_all_lines(entries, inner_area.width as usize);
|
||||
let total_lines = all_lines.len();
|
||||
let visible_height = inner_area.height as usize;
|
||||
|
||||
let paragraph = Paragraph::new(lines)
|
||||
.block(block)
|
||||
.wrap(Wrap { trim: false })
|
||||
.scroll((scroll, 0));
|
||||
let (start, end) = self.calculate_view_window(total_lines, visible_height);
|
||||
let visible_lines = &all_lines[start..end];
|
||||
|
||||
let rendered_lines: Vec<Line> = visible_lines
|
||||
.iter()
|
||||
.map(|(line, prefix_color, is_error)| {
|
||||
let mut spans = Vec::new();
|
||||
|
||||
let (prefix, rest) = Self::split_line_prefix(line);
|
||||
|
||||
if !prefix.is_empty() {
|
||||
spans.push(Span::styled(
|
||||
prefix.to_string(),
|
||||
Style::default().fg(*prefix_color),
|
||||
));
|
||||
}
|
||||
|
||||
let (content, timestamp) = Self::split_timestamp_suffix(rest);
|
||||
let text_color = if *is_error { Color::Red } else { Color::White };
|
||||
|
||||
if !content.is_empty() {
|
||||
spans.push(Span::styled(
|
||||
content.to_string(),
|
||||
Style::default().fg(text_color),
|
||||
));
|
||||
}
|
||||
|
||||
if !timestamp.is_empty() {
|
||||
spans.push(Span::styled(
|
||||
timestamp.to_string(),
|
||||
Style::default().fg(Color::DarkGray),
|
||||
));
|
||||
}
|
||||
|
||||
Line::from(spans)
|
||||
})
|
||||
.collect();
|
||||
|
||||
for (idx, line) in rendered_lines.iter().enumerate() {
|
||||
let line_area = Rect {
|
||||
x: inner_area.x,
|
||||
y: inner_area.y + idx as u16,
|
||||
width: inner_area.width,
|
||||
height: 1,
|
||||
};
|
||||
f.render_widget(Paragraph::new(line.clone()), line_area);
|
||||
}
|
||||
|
||||
f.render_widget(paragraph, area);
|
||||
draw_block_joins(f, area, self.borders, self.joins);
|
||||
}
|
||||
}
|
||||
|
||||
impl JoinableElement for LogCard {
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
|
|
@ -94,12 +380,12 @@ impl JoinableElement for LogCard {
|
|||
self
|
||||
}
|
||||
|
||||
fn as_element(&self) -> &dyn Element {
|
||||
self
|
||||
fn as_element(&self) -> &(dyn Element + 'static) {
|
||||
&*self
|
||||
}
|
||||
|
||||
fn as_element_mut(&mut self) -> &mut dyn Element {
|
||||
self
|
||||
fn as_element_mut(&mut self) -> &mut (dyn Element + 'static) {
|
||||
&mut *self
|
||||
}
|
||||
|
||||
fn set_borders(&mut self, borders: Borders) {
|
||||
|
|
@ -110,6 +396,7 @@ impl JoinableElement for LogCard {
|
|||
self.joins = joins;
|
||||
}
|
||||
}
|
||||
|
||||
impl InteractableElement for LogCard {
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
|
|
@ -119,30 +406,65 @@ impl InteractableElement for LogCard {
|
|||
self
|
||||
}
|
||||
|
||||
fn as_element(&self) -> &dyn Element {
|
||||
self
|
||||
fn as_element(&self) -> &(dyn Element + 'static) {
|
||||
&*self
|
||||
}
|
||||
|
||||
fn as_element_mut(&mut self) -> &mut dyn Element {
|
||||
self
|
||||
fn as_element_mut(&mut self) -> &mut (dyn Element + 'static) {
|
||||
&mut *self
|
||||
}
|
||||
|
||||
fn interact(&mut self, key: KeyEvent) -> InteractionResult {
|
||||
let entries = self.get_logs();
|
||||
let estimated_width = 80usize;
|
||||
let all_lines = self.build_all_lines(entries, estimated_width);
|
||||
|
||||
self.last_total_lines = all_lines.len();
|
||||
let visible_height = self.last_visible_height.max(1);
|
||||
|
||||
match key.code {
|
||||
KeyCode::Char('J') | KeyCode::Char('j') => {
|
||||
let state = APP_STATE.lock().unwrap();
|
||||
let logs = state.get_logs();
|
||||
if self.scroll < logs.len() as u16 {
|
||||
self.scroll += 1;
|
||||
KeyCode::Enter | KeyCode::Char(' ') => {
|
||||
self.selected = !self.selected;
|
||||
InteractionResult::Handled
|
||||
}
|
||||
KeyCode::Char('j') | KeyCode::Char('J') => {
|
||||
let (can_up, _) = self.get_title_hints();
|
||||
if can_up {
|
||||
self.scroll_up();
|
||||
}
|
||||
InteractionResult::Handled
|
||||
}
|
||||
KeyCode::Char('K') | KeyCode::Char('k') => {
|
||||
if self.scroll > 1 {
|
||||
self.scroll -= 1;
|
||||
KeyCode::Char('k') | KeyCode::Char('K') => {
|
||||
let (_, can_down) = self.get_title_hints();
|
||||
if can_down {
|
||||
self.scroll_down();
|
||||
}
|
||||
InteractionResult::Handled
|
||||
}
|
||||
KeyCode::Up if self.selected => {
|
||||
let (can_up, _) = self.get_title_hints();
|
||||
if can_up {
|
||||
self.scroll_up();
|
||||
}
|
||||
InteractionResult::Handled
|
||||
}
|
||||
KeyCode::Down if self.selected => {
|
||||
let (_, can_down) = self.get_title_hints();
|
||||
if can_down {
|
||||
self.scroll_down();
|
||||
}
|
||||
InteractionResult::Handled
|
||||
}
|
||||
KeyCode::Home => {
|
||||
if self.last_total_lines > visible_height {
|
||||
self.scroll_offset = self.last_total_lines - visible_height;
|
||||
}
|
||||
InteractionResult::Handled
|
||||
}
|
||||
KeyCode::End => {
|
||||
self.scroll_offset = 0;
|
||||
InteractionResult::Handled
|
||||
}
|
||||
_ => InteractionResult::Unhandled,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,59 +1,51 @@
|
|||
use crate::ACTIVE_TASKS;
|
||||
use crate::gui::ui::{UI, UNIQUE};
|
||||
use crate::{RELOAD, SHUTDOWN};
|
||||
use crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers, poll, read};
|
||||
use crossterm::event::{Event, KeyEvent, KeyEventKind, KeyModifiers, poll, read};
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::time::Duration;
|
||||
|
||||
pub fn setup_input_handler(ui: Arc<UI>) {
|
||||
tokio::spawn(async move {
|
||||
ACTIVE_TASKS.insert("Input Handler".to_string());
|
||||
|
||||
loop {
|
||||
{
|
||||
let should_shutdown = *SHUTDOWN.read().await;
|
||||
if should_shutdown {
|
||||
if *SHUTDOWN.read().await {
|
||||
break;
|
||||
}
|
||||
|
||||
let event_result = tokio::task::spawn_blocking(|| {
|
||||
if let Ok(true) = poll(Duration::from_millis(100)) {
|
||||
read().ok().and_then(|ev| match ev {
|
||||
Event::Key(key) if key.kind == KeyEventKind::Press => Some(key),
|
||||
_ => None,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
let has_event = match poll(Duration::from_millis(100)) {
|
||||
Ok(true) => true,
|
||||
Ok(false) => false,
|
||||
Err(_) => false,
|
||||
};
|
||||
|
||||
if has_event {
|
||||
match read() {
|
||||
Ok(event) => {
|
||||
if let Event::Key(key_event) = event {
|
||||
if key_event.kind == KeyEventKind::Press {
|
||||
let uic = ui.clone();
|
||||
handle_input(key_event, uic).await;
|
||||
match event_result {
|
||||
Ok(Some(key_event)) => {
|
||||
handle_input(key_event, ui.clone()).await;
|
||||
UNIQUE.store(true, Ordering::Relaxed);
|
||||
}
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
eprintln!("Input task error: {}", e);
|
||||
tokio::time::sleep(Duration::from_millis(10)).await;
|
||||
}
|
||||
}
|
||||
Err(_) => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
{
|
||||
ACTIVE_TASKS.remove("Input Handler");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub async fn handle_input(key: KeyEvent, ui: Arc<UI>) {
|
||||
match (key.code, key.modifiers) {
|
||||
(KeyCode::Char('q'), KeyModifiers::CONTROL) => {
|
||||
(crossterm::event::KeyCode::Char('q'), KeyModifiers::CONTROL)
|
||||
| (crossterm::event::KeyCode::Char('c'), KeyModifiers::CONTROL) => {
|
||||
*SHUTDOWN.write().await = true;
|
||||
}
|
||||
(KeyCode::Char('c'), KeyModifiers::CONTROL) => {
|
||||
*SHUTDOWN.write().await = true;
|
||||
}
|
||||
(KeyCode::Char('r'), KeyModifiers::CONTROL) => {
|
||||
(crossterm::event::KeyCode::Char('r'), KeyModifiers::CONTROL) => {
|
||||
*RELOAD.write().await = true;
|
||||
*SHUTDOWN.write().await = true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
use crate::users::contact::Contact;
|
||||
use crate::users::user_community_util::UserCommunityUtil;
|
||||
use crate::util::chat_files::{MessageState, change_message_state};
|
||||
use crate::util::chats_util::{get_user, mod_user};
|
||||
use crate::util::communities_util::CommunitiesUtil;
|
||||
use crate::util::crypto_util::{DataFormat, SecurePayload};
|
||||
use crate::util::file_util::{get_children, load_file, save_file};
|
||||
use crate::util::{chat_files, chats_util};
|
||||
|
|
@ -283,11 +283,16 @@ impl OmikronConnection {
|
|||
|
||||
let key_pair = crypto_helper::generate_keypair();
|
||||
let public_key_base64 = crypto_helper::public_key_to_base64(&key_pair.public);
|
||||
let private_key_base64 = crypto_helper::secret_key_to_base64(&key_pair.secret);
|
||||
let _private_key_base64 = crypto_helper::secret_key_to_base64(&key_pair.secret);
|
||||
|
||||
let mut conf_write = CONFIG.write().await;
|
||||
/*conf_write.change("public_key", DataValue::Str(public_key_base64.clone()));
|
||||
conf_write.change("private_key", DataValue::Str(private_key_base64));*/
|
||||
// NOTE:
|
||||
// Intentionally not storing the generated private/public keys directly into the
|
||||
// config file here to avoid persisting sensitive material in plaintext. If you
|
||||
// want to persist them, uncomment the two lines below and accept the security
|
||||
// implications (they will be saved by `conf_write.update()`).
|
||||
// conf_write.change("public_key", DataValue::Str(public_key_base64.clone()));
|
||||
// conf_write.change("private_key", DataValue::Str(private_key_base64));
|
||||
conf_write.update();
|
||||
drop(conf_write);
|
||||
|
||||
|
|
@ -418,7 +423,7 @@ impl OmikronConnection {
|
|||
}
|
||||
|
||||
if cv.is_type(CommunicationType::identification_response) {
|
||||
if let Some(accepted) = cv.get_data(DataTypes::accepted).as_bool() {
|
||||
if let Some(_accepted) = cv.get_data(DataTypes::accepted).as_bool() {
|
||||
let mut state = self.state.write().await;
|
||||
if let ConnectionState::Connected { identified: _ } = *state {
|
||||
*state = ConnectionState::Connected { identified: true };
|
||||
|
|
@ -430,6 +435,7 @@ impl OmikronConnection {
|
|||
// ************************************************ //
|
||||
// Direct messages //
|
||||
// ************************************************ //
|
||||
|
||||
if cv.is_type(CommunicationType::message_state) {
|
||||
let sender_id = &cv.get_sender();
|
||||
let receiver_id = &cv.get_receiver();
|
||||
|
|
@ -458,6 +464,160 @@ impl OmikronConnection {
|
|||
);
|
||||
}
|
||||
|
||||
// Incoming stored message: store for the recipient, attempt local delivery, notify sender.
|
||||
if cv.is_type(CommunicationType::message_send) {
|
||||
let sender_id: i64 = if let Some(n) = cv.get_data(DataTypes::sender_id).as_number() {
|
||||
n as i64
|
||||
} else if let Some(s) = cv.get_data(DataTypes::sender_id).as_str() {
|
||||
s.parse::<i64>().unwrap_or(0)
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
// parse receiver_id (the storage owner for this incoming message)
|
||||
let receiver_id: i64 = if let Some(n) = cv.get_data(DataTypes::receiver_id).as_number()
|
||||
{
|
||||
n as i64
|
||||
} else if let Some(s) = cv.get_data(DataTypes::receiver_id).as_str() {
|
||||
s.parse::<i64>().unwrap_or(0)
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
// parse send_time robustly (number or string), fallback to now
|
||||
let send_time_val = cv.get_data(DataTypes::send_time);
|
||||
let now_i64 = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_millis() as i64;
|
||||
let timestamp_i64 = if let Some(n) = send_time_val.as_number() {
|
||||
n as i64
|
||||
} else if let Some(s) = send_time_val.as_str() {
|
||||
s.parse::<i64>().unwrap_or(now_i64)
|
||||
} else {
|
||||
now_i64
|
||||
};
|
||||
let timestamp_u128 = timestamp_i64 as u128;
|
||||
|
||||
// content may be missing; default to empty string
|
||||
let content = cv
|
||||
.get_data(DataTypes::content)
|
||||
.as_str()
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
|
||||
let height = cv.get_data(DataTypes::height).as_number().unwrap_or(0) as i64;
|
||||
|
||||
// persist message for the receiver (storage_owner = receiver_id)
|
||||
chat_files::add_message(
|
||||
timestamp_u128,
|
||||
false,
|
||||
receiver_id as i64,
|
||||
sender_id as i64,
|
||||
&content,
|
||||
height,
|
||||
);
|
||||
|
||||
// persist message for the sender (storage_owner = sender_id)
|
||||
chat_files::add_message(
|
||||
timestamp_u128,
|
||||
true,
|
||||
sender_id as i64,
|
||||
receiver_id as i64,
|
||||
&content,
|
||||
height,
|
||||
);
|
||||
|
||||
// send confirmation back to sender
|
||||
let conf_msg = CommunicationValue::new(CommunicationType::message_send)
|
||||
.with_id(cv.get_id())
|
||||
.with_receiver(sender_id as u64);
|
||||
self.send_message(&conf_msg).await;
|
||||
|
||||
// Build a live-delivery message for the local client (recipient)
|
||||
let user_forward = CommunicationValue::new(CommunicationType::message_live)
|
||||
.with_id(cv.get_id())
|
||||
.with_receiver(receiver_id as u64)
|
||||
.add_data(DataTypes::send_time, DataValue::Number(timestamp_i64))
|
||||
.add_data(DataTypes::content, DataValue::Str(content.clone()))
|
||||
.add_data(DataTypes::sender_id, DataValue::Number(sender_id as i64))
|
||||
.add_data(DataTypes::height, DataValue::Number(height));
|
||||
|
||||
// Attempt delivery and await a response from the local client
|
||||
let user_resp = self
|
||||
.clone()
|
||||
.await_response(&user_forward, Some(Duration::from_secs(10)))
|
||||
.await;
|
||||
|
||||
if let Ok(user_resp) = user_resp {
|
||||
let ms_raw = user_resp
|
||||
.get_data(DataTypes::message_state)
|
||||
.as_string()
|
||||
.unwrap_or_else(|| "".to_string());
|
||||
let ms = MessageState::from_str(&ms_raw).upgrade(MessageState::Received);
|
||||
|
||||
// update stored message state for receiver
|
||||
let _ = chat_files::change_message_state(
|
||||
timestamp_i64,
|
||||
receiver_id as i64,
|
||||
sender_id as i64,
|
||||
ms.clone(),
|
||||
);
|
||||
|
||||
// update stored message state for sender
|
||||
let _ = chat_files::change_message_state(
|
||||
timestamp_i64,
|
||||
sender_id as i64,
|
||||
receiver_id as i64,
|
||||
ms.clone(),
|
||||
);
|
||||
|
||||
// notify original sender about the delivered/read state
|
||||
self.send_message(
|
||||
&CommunicationValue::new(CommunicationType::message_state)
|
||||
.with_id(cv.get_id())
|
||||
.with_receiver(sender_id as u64)
|
||||
.with_sender(receiver_id as u64)
|
||||
.add_data(DataTypes::send_time, DataValue::Number(timestamp_i64))
|
||||
.add_data(
|
||||
DataTypes::message_state,
|
||||
DataValue::Str(ms.as_str().to_string()),
|
||||
),
|
||||
)
|
||||
.await;
|
||||
} else {
|
||||
// Delivery failed or timed out; mark as Sent
|
||||
let _ = chat_files::change_message_state(
|
||||
timestamp_i64,
|
||||
receiver_id as i64,
|
||||
sender_id as i64,
|
||||
MessageState::Sent,
|
||||
);
|
||||
|
||||
let _ = chat_files::change_message_state(
|
||||
timestamp_i64,
|
||||
sender_id as i64,
|
||||
receiver_id as i64,
|
||||
MessageState::Sent,
|
||||
);
|
||||
|
||||
// notify sender
|
||||
self.send_message(
|
||||
&CommunicationValue::new(CommunicationType::message_state)
|
||||
.with_id(cv.get_id())
|
||||
.with_receiver(sender_id as u64)
|
||||
.with_sender(receiver_id as u64)
|
||||
.add_data(DataTypes::send_time, DataValue::Number(timestamp_i64))
|
||||
.add_data(
|
||||
DataTypes::message_state,
|
||||
DataValue::Str(MessageState::Sent.as_str().to_string()),
|
||||
),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if cv.is_type(CommunicationType::message_other_iota) {
|
||||
let sender_id = &cv.get_sender();
|
||||
let receiver_id = &cv.get_receiver();
|
||||
|
|
@ -483,26 +643,25 @@ impl OmikronConnection {
|
|||
.unwrap_or("")
|
||||
.to_string();
|
||||
|
||||
let height = cv.get_data(DataTypes::height).as_number().unwrap_or(0) as i64;
|
||||
|
||||
chat_files::add_message(
|
||||
timestamp as u128,
|
||||
false,
|
||||
*receiver_id as i64,
|
||||
*sender_id as i64,
|
||||
&content,
|
||||
height,
|
||||
);
|
||||
|
||||
// Build user_forward using the parsed numeric timestamp and safe content string
|
||||
let user_forward = CommunicationValue::new(CommunicationType::message_live)
|
||||
.with_id(cv.get_id())
|
||||
.with_receiver(*receiver_id)
|
||||
.add_data(
|
||||
DataTypes::send_time,
|
||||
cv.get_data(DataTypes::send_time).clone(),
|
||||
)
|
||||
.add_data(DataTypes::message, cv.get_data(DataTypes::content).clone())
|
||||
.add_data(
|
||||
DataTypes::sender_id,
|
||||
DataValue::Number(cv.get_sender() as i64),
|
||||
);
|
||||
.add_data(DataTypes::send_time, DataValue::Number(timestamp))
|
||||
.add_data(DataTypes::content, DataValue::Str(content.clone()))
|
||||
.add_data(DataTypes::sender_id, DataValue::Number(*sender_id as i64))
|
||||
.add_data(DataTypes::height, DataValue::Number(height));
|
||||
|
||||
let user_resp = self
|
||||
.clone()
|
||||
|
|
@ -528,10 +687,7 @@ impl OmikronConnection {
|
|||
.with_id(cv.get_id())
|
||||
.with_receiver(*sender_id)
|
||||
.with_sender(*receiver_id)
|
||||
.add_data(
|
||||
DataTypes::send_time,
|
||||
cv.get_data(DataTypes::send_time).clone(),
|
||||
)
|
||||
.add_data(DataTypes::send_time, DataValue::Number(timestamp))
|
||||
.add_data(
|
||||
DataTypes::message_state,
|
||||
DataValue::Str(ms.as_str().to_string()),
|
||||
|
|
@ -539,15 +695,20 @@ impl OmikronConnection {
|
|||
)
|
||||
.await;
|
||||
} else {
|
||||
// Delivery timed out/failed — update stored state and notify sender with numeric timestamp
|
||||
let _ = chat_files::change_message_state(
|
||||
timestamp,
|
||||
*receiver_id as i64,
|
||||
*sender_id as i64,
|
||||
MessageState::Sent,
|
||||
);
|
||||
|
||||
self.send_message(
|
||||
&CommunicationValue::new(CommunicationType::message_state)
|
||||
.with_id(cv.get_id())
|
||||
.with_receiver(*sender_id)
|
||||
.with_sender(*receiver_id)
|
||||
.add_data(
|
||||
DataTypes::send_time,
|
||||
cv.get_data(DataTypes::send_time).clone(),
|
||||
)
|
||||
.add_data(DataTypes::send_time, DataValue::Number(timestamp))
|
||||
.add_data(
|
||||
DataTypes::message_state,
|
||||
DataValue::Str(MessageState::Sent.as_str().to_string()),
|
||||
|
|
@ -558,90 +719,21 @@ impl OmikronConnection {
|
|||
return;
|
||||
}
|
||||
|
||||
if cv.is_type(CommunicationType::message_send) {
|
||||
let my_id = cv.get_sender();
|
||||
|
||||
// parse other id robustly (number or string)
|
||||
let other_id = if let Some(n) = cv.get_data(DataTypes::receiver_id).as_number() {
|
||||
n as i64
|
||||
} else if let Some(s) = cv.get_data(DataTypes::receiver_id).as_str() {
|
||||
s.parse::<i64>().unwrap_or(0)
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
let now_ms_u128 = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_millis() as u128;
|
||||
// derive an i64 timestamp for protocol fields; fall back to current time if out of range
|
||||
let now_ms_i64: i64 = match i64::try_from(now_ms_u128) {
|
||||
Ok(v) => v,
|
||||
Err(_) => SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_millis() as i64,
|
||||
};
|
||||
|
||||
// safe content extraction
|
||||
let content = cv
|
||||
.get_data(DataTypes::content)
|
||||
.as_str()
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
|
||||
chat_files::add_message(now_ms_u128, true, my_id as i64, other_id, &content);
|
||||
|
||||
let ack = CommunicationValue::new(CommunicationType::success)
|
||||
.with_id(cv.get_id())
|
||||
.with_receiver(my_id);
|
||||
self.send_message(&ack).await;
|
||||
|
||||
let forward = CommunicationValue::new(CommunicationType::message_other_iota)
|
||||
.with_id(cv.get_id())
|
||||
.with_receiver(other_id as u64)
|
||||
.add_data(DataTypes::receiver_id, DataValue::Number(other_id))
|
||||
.with_sender(my_id)
|
||||
.add_data(DataTypes::send_time, DataValue::Number(now_ms_i64))
|
||||
.add_data(DataTypes::sender_id, DataValue::Number(my_id as i64))
|
||||
.add_data(DataTypes::content, DataValue::Str(content));
|
||||
if let Err(err) = self.send_message_result(&forward).await {
|
||||
// sending failed - record via existing logging path
|
||||
log_t!("send_message_failed", err);
|
||||
} else {
|
||||
// forwarding succeeded -> update stored message state to Sent
|
||||
let _ = chat_files::change_message_state(
|
||||
now_ms_i64,
|
||||
my_id as i64,
|
||||
other_id,
|
||||
MessageState::Sent,
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if cv.is_type(CommunicationType::messages_get) {
|
||||
let my_id = cv.get_sender();
|
||||
let partner_id = cv.get_data(DataTypes::user_id).as_number().unwrap_or(0);
|
||||
let offset = cv.get_data(DataTypes::offset).as_number().unwrap_or(0);
|
||||
let amount = cv.get_data(DataTypes::amount).as_number().unwrap_or(0);
|
||||
// retrieve raw JSON messages
|
||||
let messages = chat_files::get_messages(my_id as i64, partner_id, offset, amount);
|
||||
// convert JSON array -> protocol Array of Containers (send_time, content, sender_id, message_state)
|
||||
let mut msg_array: Vec<DataValue> = Vec::new();
|
||||
for m in messages.members() {
|
||||
// extract fields defensively
|
||||
let message_time: i64 = m["message_time"].as_i64().unwrap_or(0);
|
||||
let content: String = m["content"].as_str().unwrap_or("").to_string();
|
||||
let sent_by_self: bool = m["sent_by_self"].as_bool().unwrap_or(false);
|
||||
// determine sender id:
|
||||
// - if sent_by_self => sender is the requester (my_id)
|
||||
// - otherwise prefer an explicit chat_partner_id if present on the request,
|
||||
// fallback to the partner_id parameter
|
||||
let height: i64 = m["height"].as_i64().unwrap_or(0);
|
||||
let sender_id: i64 = if sent_by_self {
|
||||
my_id as i64
|
||||
} else {
|
||||
// check for chat_partner_id in the incoming request (accept number or string)
|
||||
if let Some(n) = cv.get_data(DataTypes::chat_partner_id).as_number() {
|
||||
n as i64
|
||||
} else if let Some(s) = cv.get_data(DataTypes::chat_partner_id).as_str() {
|
||||
|
|
@ -654,9 +746,11 @@ impl OmikronConnection {
|
|||
|
||||
let mut container = Vec::new();
|
||||
container.push((DataTypes::send_time, DataValue::Number(message_time)));
|
||||
container.push((DataTypes::message, DataValue::Str(content)));
|
||||
container.push((DataTypes::content, DataValue::Str(content)));
|
||||
container.push((DataTypes::sender_id, DataValue::Number(sender_id)));
|
||||
container.push((DataTypes::message_state, DataValue::Str(message_state)));
|
||||
container.push((DataTypes::height, DataValue::Number(height)));
|
||||
container.push((DataTypes::sent_by_self, DataValue::Bool(sent_by_self)));
|
||||
msg_array.push(DataValue::Container(container));
|
||||
}
|
||||
|
||||
|
|
@ -724,7 +818,7 @@ impl OmikronConnection {
|
|||
}
|
||||
|
||||
if cv.is_type(CommunicationType::add_community) {
|
||||
UserCommunityUtil::add_community(
|
||||
CommunitiesUtil::add_community(
|
||||
cv.get_sender() as i64,
|
||||
cv.get_data(DataTypes::community_address)
|
||||
.as_str()
|
||||
|
|
@ -747,19 +841,37 @@ impl OmikronConnection {
|
|||
}
|
||||
|
||||
if cv.is_type(CommunicationType::get_communities) {
|
||||
let mut comm_array = Vec::new();
|
||||
for c in CommunitiesUtil::get_communities(cv.get_sender() as i64) {
|
||||
let mut container: Vec<(DataTypes, DataValue)> = Vec::new();
|
||||
if let Some(address) = c["address"].as_str() {
|
||||
container.push((
|
||||
DataTypes::community_address,
|
||||
DataValue::Str(address.to_string()),
|
||||
));
|
||||
}
|
||||
if let Some(title) = c["title"].as_str() {
|
||||
container.push((
|
||||
DataTypes::community_title,
|
||||
DataValue::Str(title.to_string()),
|
||||
));
|
||||
}
|
||||
if let Some(position) = c["position"].as_str() {
|
||||
container.push((DataTypes::position, DataValue::Str(position.to_string())));
|
||||
}
|
||||
comm_array.push(DataValue::Container(container));
|
||||
}
|
||||
|
||||
let resp = CommunicationValue::new(CommunicationType::get_communities)
|
||||
.with_id(cv.get_id())
|
||||
.with_receiver(cv.get_sender())
|
||||
/*.add_data(
|
||||
DataTypes::communities,
|
||||
DataValue::Array(UserCommunityUtil::get_communities(cv.get_sender() as i64)),
|
||||
) */;
|
||||
.add_data(DataTypes::communities, DataValue::Array(comm_array));
|
||||
self.send_message(&resp).await;
|
||||
return;
|
||||
}
|
||||
|
||||
if cv.is_type(CommunicationType::remove_community) {
|
||||
UserCommunityUtil::remove_community(
|
||||
CommunitiesUtil::remove_community(
|
||||
cv.get_sender() as i64,
|
||||
cv.get_data(DataTypes::community_address)
|
||||
.as_str()
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
use crate::log;
|
||||
use crate::util::file_util::get_directory;
|
||||
use crate::util::db;
|
||||
use json::{JsonValue, array, object};
|
||||
use rusqlite::{Connection, params};
|
||||
use rusqlite::params;
|
||||
use std::io;
|
||||
use std::sync::{LazyLock, Mutex};
|
||||
use std::sync::{Arc, LazyLock, Mutex};
|
||||
|
||||
#[derive(PartialEq, Debug, Clone)]
|
||||
pub enum MessageState {
|
||||
|
|
@ -45,30 +45,10 @@ impl MessageState {
|
|||
}
|
||||
}
|
||||
|
||||
static DB_CONN: LazyLock<Mutex<Connection>> = LazyLock::new(|| {
|
||||
let conn = Connection::open(format!("{}/messages.sqlite3", get_directory()))
|
||||
.expect("Failed to open messages sqlite DB");
|
||||
conn.execute_batch(
|
||||
r#"
|
||||
PRAGMA journal_mode = WAL;
|
||||
PRAGMA synchronous = NORMAL;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS messages (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
storage_owner INTEGER NOT NULL,
|
||||
external_user INTEGER NOT NULL,
|
||||
message_time INTEGER NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
sent_by_self INTEGER NOT NULL,
|
||||
message_state TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_messages_lookup
|
||||
ON messages (storage_owner, external_user, message_time DESC);
|
||||
"#,
|
||||
)
|
||||
.expect("Failed to initialize messages DB");
|
||||
Mutex::new(conn)
|
||||
// Shared DB created via helper.
|
||||
// The db helper constructs the messages sqlite file and ensures PRAGMAs and schema exist.
|
||||
static MESSAGES_DB: LazyLock<Arc<Mutex<rusqlite::Connection>>> = LazyLock::new(|| {
|
||||
db::create_general_messages_db().expect("Failed to create or initialize general messages DB")
|
||||
});
|
||||
|
||||
pub fn add_message(
|
||||
|
|
@ -77,6 +57,7 @@ pub fn add_message(
|
|||
storage_owner: i64,
|
||||
external_user: i64,
|
||||
message: &str,
|
||||
height: i64,
|
||||
) {
|
||||
let message_time = match i64::try_from(send_time) {
|
||||
Ok(v) => v,
|
||||
|
|
@ -86,15 +67,9 @@ pub fn add_message(
|
|||
}
|
||||
};
|
||||
|
||||
let conn = match DB_CONN.lock() {
|
||||
Ok(g) => g,
|
||||
Err(e) => {
|
||||
log!("Failed to lock messages DB mutex for add_message: {:?}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(e) = conn.execute(
|
||||
// Insert the message into the DB
|
||||
let insert_result = db::with_conn(&MESSAGES_DB, |conn| {
|
||||
conn.execute(
|
||||
r#"
|
||||
INSERT INTO messages (
|
||||
storage_owner,
|
||||
|
|
@ -102,8 +77,9 @@ pub fn add_message(
|
|||
message_time,
|
||||
content,
|
||||
sent_by_self,
|
||||
message_state
|
||||
) VALUES (?1, ?2, ?3, ?4, ?5, ?6)
|
||||
message_state,
|
||||
height
|
||||
) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)
|
||||
"#,
|
||||
params![
|
||||
storage_owner,
|
||||
|
|
@ -116,10 +92,23 @@ pub fn add_message(
|
|||
0_i64
|
||||
},
|
||||
MessageState::Sending.as_str(),
|
||||
height,
|
||||
],
|
||||
) {
|
||||
)?;
|
||||
Ok(())
|
||||
});
|
||||
|
||||
if let Err(e) = insert_result {
|
||||
log!("Failed to insert message into sqlite: {}", e);
|
||||
return;
|
||||
}
|
||||
|
||||
// Update contacts table to reflect that this conversation exists and has a recent message.
|
||||
// Use the Contact helper to set last_message_at to the message timestamp.
|
||||
let mut contact = crate::users::contact::Contact::new(external_user);
|
||||
contact.set_last_message_at(message_time);
|
||||
// This will insert or update the contact for the storage owner.
|
||||
crate::util::chats_util::mod_user(storage_owner, &contact);
|
||||
}
|
||||
|
||||
pub fn change_message_state(
|
||||
|
|
@ -128,10 +117,8 @@ pub fn change_message_state(
|
|||
external_user: i64,
|
||||
new_state: MessageState,
|
||||
) -> io::Result<()> {
|
||||
let conn = DB_CONN
|
||||
.lock()
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::Other, format!("Mutex lock error: {:?}", e)))?;
|
||||
|
||||
// Run the SELECT and UPDATE inside with_conn to centralize connection access.
|
||||
let res: Result<(), String> = db::with_conn(&MESSAGES_DB, |conn| {
|
||||
let current: Option<String> = match conn.query_row(
|
||||
r#"
|
||||
SELECT message_state
|
||||
|
|
@ -147,7 +134,7 @@ pub fn change_message_state(
|
|||
) {
|
||||
Ok(state) => Some(state),
|
||||
Err(rusqlite::Error::QueryReturnedNoRows) => None,
|
||||
Err(e) => return Err(io::Error::new(io::ErrorKind::Other, e.to_string())),
|
||||
Err(e) => return Err(e),
|
||||
};
|
||||
|
||||
let Some(current_state_raw) = current else {
|
||||
|
|
@ -174,10 +161,14 @@ pub fn change_message_state(
|
|||
)
|
||||
"#,
|
||||
params![upgraded, storage_owner, external_user, timestamp],
|
||||
)
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?;
|
||||
|
||||
)?;
|
||||
Ok(())
|
||||
});
|
||||
|
||||
match res {
|
||||
Ok(_) => Ok(()),
|
||||
Err(e) => Err(io::Error::new(io::ErrorKind::Other, e)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_messages(
|
||||
|
|
@ -186,40 +177,28 @@ pub fn get_messages(
|
|||
loaded_messages: i64,
|
||||
amount: i64,
|
||||
) -> JsonValue {
|
||||
let mut messages = array![];
|
||||
let messages = array![];
|
||||
|
||||
if amount <= 0 || loaded_messages < 0 {
|
||||
return messages;
|
||||
}
|
||||
|
||||
let conn = match DB_CONN.lock() {
|
||||
Ok(g) => g,
|
||||
Err(e) => {
|
||||
log!("Failed to lock messages DB mutex for get_messages: {:?}", e);
|
||||
return messages;
|
||||
}
|
||||
};
|
||||
|
||||
let mut stmt = match conn.prepare(
|
||||
let res: Result<JsonValue, String> = db::with_conn(&MESSAGES_DB, |conn| {
|
||||
let mut stmt = conn.prepare(
|
||||
r#"
|
||||
SELECT
|
||||
message_time,
|
||||
content,
|
||||
sent_by_self,
|
||||
message_state
|
||||
message_state,
|
||||
height
|
||||
FROM messages
|
||||
WHERE storage_owner = ?1
|
||||
AND external_user = ?2
|
||||
ORDER BY message_time DESC, id DESC
|
||||
LIMIT ?3 OFFSET ?4
|
||||
"#,
|
||||
) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
log!("Failed to prepare get_messages query: {}", e);
|
||||
return messages;
|
||||
}
|
||||
};
|
||||
)?;
|
||||
|
||||
let rows = stmt.query_map(
|
||||
params![storage_owner, external_user, amount, loaded_messages],
|
||||
|
|
@ -228,29 +207,26 @@ pub fn get_messages(
|
|||
let content: String = row.get(1)?;
|
||||
let sent_by_self: i64 = row.get(2)?;
|
||||
let message_state: String = row.get(3)?;
|
||||
Ok((message_time, content, sent_by_self, message_state))
|
||||
let height: i64 = row.get(4).unwrap_or(0);
|
||||
Ok((message_time, content, sent_by_self, message_state, height))
|
||||
},
|
||||
);
|
||||
|
||||
let Ok(rows) = rows else {
|
||||
if let Err(e) = rows {
|
||||
log!("Failed to query messages: {}", e);
|
||||
}
|
||||
return messages;
|
||||
};
|
||||
)?;
|
||||
|
||||
let mut out = array![];
|
||||
for row in rows {
|
||||
match row {
|
||||
Ok((message_time, content, sent_by_self, message_state)) => {
|
||||
Ok((message_time, content, sent_by_self, message_state, height)) => {
|
||||
let msg = object! {
|
||||
"message_time" => message_time,
|
||||
"content" => content,
|
||||
"sent_by_self" => (sent_by_self != 0),
|
||||
"message_state" => message_state
|
||||
"message_state" => message_state,
|
||||
"height" => height
|
||||
};
|
||||
|
||||
if let Err(e) = messages.push(msg) {
|
||||
log!("Failed to append message to output array: {}", e);
|
||||
if let Err(e) = out.push(msg) {
|
||||
// out.push returns a JsonError; log it instead of using `?` to avoid
|
||||
// incompatible error conversions inside the DB closure.
|
||||
log!("Failed to append message to output array: {:?}", e);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
|
|
@ -258,8 +234,16 @@ pub fn get_messages(
|
|||
}
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
});
|
||||
|
||||
match res {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
log!("Failed to query messages: {}", e);
|
||||
messages
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
|
|||
|
|
@ -1,37 +1,17 @@
|
|||
use crate::users::contact::Contact;
|
||||
use crate::util::file_util::get_directory;
|
||||
use rusqlite::{Connection, params};
|
||||
use std::sync::{LazyLock, Mutex};
|
||||
use crate::util::db;
|
||||
use rusqlite::params;
|
||||
use std::sync::{Arc, LazyLock, Mutex};
|
||||
|
||||
static DB_CONN: LazyLock<Mutex<Connection>> = LazyLock::new(|| {
|
||||
let conn = Connection::open(format!("{}/messages.sqlite3", get_directory()))
|
||||
.expect("Failed to open DB");
|
||||
conn.execute_batch(
|
||||
r#"
|
||||
PRAGMA journal_mode = WAL;
|
||||
PRAGMA synchronous = NORMAL;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS contacts (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
storage_owner INTEGER NOT NULL,
|
||||
user_id INTEGER NOT NULL,
|
||||
user_name TEXT,
|
||||
last_message_at INTEGER,
|
||||
UNIQUE(storage_owner, user_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_contacts_owner
|
||||
ON contacts (storage_owner, last_message_at DESC, user_id ASC);
|
||||
"#,
|
||||
)
|
||||
.expect("Failed to initialize DB");
|
||||
Mutex::new(conn)
|
||||
/// Shared DB connection for contacts/messages (created by db helper).
|
||||
static MESSAGES_DB: LazyLock<Arc<Mutex<rusqlite::Connection>>> = LazyLock::new(|| {
|
||||
db::create_general_messages_db().expect("Failed to create or initialize general messages DB")
|
||||
});
|
||||
|
||||
/// Insert or update a contact for the given storage owner.
|
||||
pub fn mod_user(storage_owner: i64, contact: &Contact) {
|
||||
let conn = DB_CONN.lock().unwrap();
|
||||
|
||||
let _ = conn.execute(
|
||||
if let Err(e) = db::with_conn(&MESSAGES_DB, |conn| {
|
||||
conn.execute(
|
||||
r#"
|
||||
INSERT INTO contacts (
|
||||
storage_owner,
|
||||
|
|
@ -49,13 +29,17 @@ pub fn mod_user(storage_owner: i64, contact: &Contact) {
|
|||
contact.user_name.clone(),
|
||||
contact.last_message_at
|
||||
],
|
||||
);
|
||||
)?;
|
||||
Ok(())
|
||||
}) {
|
||||
eprintln!("Failed to mod_user: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
/// Retrieve a single contact for storage_owner/user_id.
|
||||
pub fn get_user(storage_owner: i64, user_id: i64) -> Option<Contact> {
|
||||
let conn = DB_CONN.lock().unwrap();
|
||||
|
||||
let row = conn.query_row(
|
||||
let res: Result<Option<Contact>, String> = db::with_conn(&MESSAGES_DB, |conn| {
|
||||
match conn.query_row(
|
||||
r#"
|
||||
SELECT user_id, user_name, last_message_at
|
||||
FROM contacts
|
||||
|
|
@ -73,11 +57,15 @@ pub fn get_user(storage_owner: i64, user_id: i64) -> Option<Contact> {
|
|||
last_message_at,
|
||||
})
|
||||
},
|
||||
);
|
||||
) {
|
||||
Ok(c) => Ok(Some(c)),
|
||||
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
});
|
||||
|
||||
match row {
|
||||
Ok(contact) => Some(contact),
|
||||
Err(rusqlite::Error::QueryReturnedNoRows) => None,
|
||||
match res {
|
||||
Ok(opt) => opt,
|
||||
Err(e) => {
|
||||
eprintln!("Error querying user in get_user: {}", e);
|
||||
None
|
||||
|
|
@ -85,12 +73,12 @@ pub fn get_user(storage_owner: i64, user_id: i64) -> Option<Contact> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Retrieve all contacts for a storage owner, ordered by last_message_at desc / user_id asc.
|
||||
pub fn get_users(storage_owner: i64) -> Vec<Contact> {
|
||||
let mut contacts_out = Vec::new();
|
||||
let contacts_out = Vec::new();
|
||||
|
||||
let conn = DB_CONN.lock().unwrap();
|
||||
|
||||
let mut stmt = match conn.prepare(
|
||||
let res: Result<Vec<Contact>, String> = db::with_conn(&MESSAGES_DB, |conn| {
|
||||
let mut stmt = conn.prepare(
|
||||
r#"
|
||||
SELECT user_id, user_name, last_message_at
|
||||
FROM contacts
|
||||
|
|
@ -100,15 +88,9 @@ pub fn get_users(storage_owner: i64) -> Vec<Contact> {
|
|||
last_message_at DESC,
|
||||
user_id ASC
|
||||
"#,
|
||||
) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
eprintln!("Failed to prepare statement in get_users: {}", e);
|
||||
return contacts_out;
|
||||
}
|
||||
};
|
||||
)?;
|
||||
|
||||
let rows = match stmt.query_map(params![storage_owner], |r| {
|
||||
let rows = stmt.query_map(params![storage_owner], |r| {
|
||||
let user_id: i64 = r.get(0)?;
|
||||
let user_name: Option<String> = r.get(1)?;
|
||||
let last_message_at: Option<i64> = r.get(2)?;
|
||||
|
|
@ -117,19 +99,23 @@ pub fn get_users(storage_owner: i64) -> Vec<Contact> {
|
|||
user_name,
|
||||
last_message_at,
|
||||
})
|
||||
}) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
eprintln!("Failed to query map in get_users: {}", e);
|
||||
return contacts_out;
|
||||
}
|
||||
};
|
||||
})?;
|
||||
|
||||
let mut out = Vec::new();
|
||||
for row in rows {
|
||||
if let Ok(contact) = row {
|
||||
contacts_out.push(contact);
|
||||
match row {
|
||||
Ok(contact) => out.push(contact),
|
||||
Err(e) => eprintln!("Failed to read contact row: {}", e),
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
});
|
||||
|
||||
match res {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
eprintln!("Failed to query contacts in get_users: {}", e);
|
||||
contacts_out
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
90
src/util/communities_util.rs
Normal file
90
src/util/communities_util.rs
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
use crate::util::db;
|
||||
use json::Array;
|
||||
use rusqlite::params;
|
||||
use std::sync::{Arc, LazyLock, Mutex};
|
||||
|
||||
static MESSAGES_DB: LazyLock<Arc<Mutex<rusqlite::Connection>>> = LazyLock::new(|| {
|
||||
db::create_general_messages_db().expect("Failed to create or initialize general messages DB")
|
||||
});
|
||||
|
||||
pub struct CommunitiesUtil;
|
||||
|
||||
impl CommunitiesUtil {
|
||||
pub fn add_community(storage_owner: i64, address: String, title: String, position: String) {
|
||||
if let Err(e) = db::with_conn(&MESSAGES_DB, |conn| {
|
||||
conn.execute(
|
||||
r#"
|
||||
INSERT INTO communities (
|
||||
storage_owner,
|
||||
address,
|
||||
title,
|
||||
position
|
||||
) VALUES (?1, ?2, ?3, ?4)
|
||||
ON CONFLICT(storage_owner, address) DO UPDATE SET
|
||||
title = excluded.title,
|
||||
position = excluded.position
|
||||
"#,
|
||||
params![storage_owner, address, title, position],
|
||||
)?;
|
||||
Ok(())
|
||||
}) {
|
||||
eprintln!("Failed to add_community: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove_community(storage_owner: i64, community_address: String) {
|
||||
if let Err(e) = db::with_conn(&MESSAGES_DB, |conn| {
|
||||
conn.execute(
|
||||
"DELETE FROM communities WHERE storage_owner = ?1 AND address = ?2",
|
||||
params![storage_owner, community_address],
|
||||
)?;
|
||||
Ok(())
|
||||
}) {
|
||||
eprintln!("Failed to remove_community: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_communities(storage_owner: i64) -> Array {
|
||||
let communities_out = Array::new();
|
||||
|
||||
let res: Result<Array, String> = db::with_conn(&MESSAGES_DB, |conn| {
|
||||
let mut stmt = conn.prepare(
|
||||
r#"
|
||||
SELECT address, title, position
|
||||
FROM communities
|
||||
WHERE storage_owner = ?1
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let rows = stmt.query_map(params![storage_owner], |r| {
|
||||
let address: String = r.get(0)?;
|
||||
let title: String = r.get(1)?;
|
||||
let position: String = r.get(2)?;
|
||||
Ok((address, title, position))
|
||||
})?;
|
||||
|
||||
let mut out = Array::new();
|
||||
for row in rows {
|
||||
match row {
|
||||
Ok((address, title, position)) => {
|
||||
let mut community = json::JsonValue::new_object();
|
||||
community["title"] = json::JsonValue::String(title);
|
||||
community["address"] = json::JsonValue::String(address);
|
||||
community["position"] = json::JsonValue::String(position);
|
||||
out.push(community);
|
||||
}
|
||||
Err(e) => eprintln!("Failed to read community row: {}", e),
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
});
|
||||
|
||||
match res {
|
||||
Ok(arr) => arr,
|
||||
Err(e) => {
|
||||
eprintln!("Failed to query communities in get_communities: {}", e);
|
||||
communities_out
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
185
src/util/db.rs
Normal file
185
src/util/db.rs
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
//! Database helper utilities.
|
||||
//!
|
||||
//! This module provides small helpers to open/init sqlite databases and to
|
||||
//! create a shared (Arc<Mutex<Connection>>) connection wrapper callers can
|
||||
//! reuse. The goal is to centralize the "open and initialize" logic and
|
||||
//! provide small convenience helpers used by other util modules.
|
||||
|
||||
use crate::util::file_util::get_directory;
|
||||
use rusqlite::{Connection, Error as RusqliteError};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Duration;
|
||||
|
||||
/// Returns the file path for a named DB inside the application's data directory.
|
||||
///
|
||||
/// Arguments:
|
||||
/// - `db_name` : name of the DB (without extension). Example: `"messages"`.
|
||||
pub fn db_file_path(db_name: &str) -> String {
|
||||
let mut p = PathBuf::from(get_directory());
|
||||
p.push(format!("{db_name}.sqlite3"));
|
||||
p.to_string_lossy().to_string()
|
||||
}
|
||||
|
||||
/// Open a sqlite connection to the named DB file (no initialization).
|
||||
///
|
||||
/// Arguments:
|
||||
/// - `db_name`: name of the DB (without extension).
|
||||
pub fn open_connection(db_name: &str) -> Result<Connection, RusqliteError> {
|
||||
let path = db_file_path(db_name);
|
||||
Connection::open(path)
|
||||
}
|
||||
|
||||
/// Open a connection and immediately run `init_sql` via `execute_batch`.
|
||||
///
|
||||
/// Arguments:
|
||||
/// - `db_name`: name of the DB (without extension).
|
||||
/// - `init_sql`: SQL statements to initialize schema & PRAGMAs (can be multiple).
|
||||
pub fn open_and_init(db_name: &str, init_sql: &str) -> Result<Connection, RusqliteError> {
|
||||
let conn = open_connection(db_name)?;
|
||||
conn.execute_batch(init_sql)?;
|
||||
Ok(conn)
|
||||
}
|
||||
|
||||
/// Create a shared, Arc<Mutex<Connection>> initialized with the given SQL.
|
||||
///
|
||||
/// This is a convenience wrapper that returns an owned Arc<Mutex<Connection>>
|
||||
/// so caller modules can store it in a `static` or pass it around.
|
||||
///
|
||||
/// Arguments:
|
||||
/// - `db_name`: DB name (without extension).
|
||||
/// - `init_sql`: init SQL (eg PRAGMA + CREATE TABLE statements).
|
||||
pub fn create_shared_connection(
|
||||
db_name: &str,
|
||||
init_sql: &str,
|
||||
) -> Result<Arc<Mutex<Connection>>, String> {
|
||||
match open_and_init(db_name, init_sql) {
|
||||
Ok(conn) => {
|
||||
// Configure some sensible defaults for concurrency
|
||||
// Attempt to set a busy timeout to reduce SQLITE_BUSY failures.
|
||||
let _ = conn.busy_timeout(Duration::from_millis(250));
|
||||
Ok(Arc::new(Mutex::new(conn)))
|
||||
}
|
||||
Err(e) => Err(format!("Failed to open/init DB '{}': {}", db_name, e)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Acquire the Connection from an Arc<Mutex<Connection>> and run the provided
|
||||
/// closure. Converts rusqlite::Error into a String on error.
|
||||
///
|
||||
/// Arguments:
|
||||
/// - `shared`: Arc<Mutex<Connection>>
|
||||
/// - `f`: closure that receives &Connection and returns Result<T, RusqliteError>
|
||||
///
|
||||
/// Returns Ok(T) or Err(String).
|
||||
pub fn with_conn<T, F>(shared: &Arc<Mutex<Connection>>, f: F) -> Result<T, String>
|
||||
where
|
||||
F: FnOnce(&Connection) -> Result<T, RusqliteError>,
|
||||
{
|
||||
// When invoked from within an async runtime (such as Tokio), taking a blocking
|
||||
// std::sync::Mutex lock on the runtime thread can cause deadlocks or permanent
|
||||
// awaits. Detect whether we're running inside a Tokio runtime and, if so,
|
||||
// execute the blocking lock + database closure using Tokio's blocking helper.
|
||||
//
|
||||
// The blocking section returns Result<T, String> so we can propagate errors
|
||||
// in the same form as before.
|
||||
if tokio::runtime::Handle::try_current().is_ok() {
|
||||
tokio::task::block_in_place(|| {
|
||||
let guard = shared
|
||||
.lock()
|
||||
.map_err(|e| format!("DB mutex poisoned: {:?}", e))?;
|
||||
f(&*guard).map_err(|e| e.to_string())
|
||||
})
|
||||
} else {
|
||||
let guard = shared
|
||||
.lock()
|
||||
.map_err(|e| format!("DB mutex poisoned: {:?}", e))?;
|
||||
f(&*guard).map_err(|e| e.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// Initialize a general-purpose messages+contacts DB and return a shared
|
||||
/// connection. This helper creates a single DB file that can contain multiple
|
||||
/// tables (messages, contacts, ...). The SQL here is conservative and intended
|
||||
/// to be safe if called multiple times.
|
||||
///
|
||||
/// Callers may prefer to call `create_shared_connection("messages", INIT_SQL)`
|
||||
/// directly, but this convenience is useful for code that expects both tables.
|
||||
pub fn create_general_messages_db() -> Result<Arc<Mutex<Connection>>, String> {
|
||||
// Keep PRAGMA and schema in one multi-statement string so callers only
|
||||
// need to call a single execute_batch.
|
||||
const INIT_SQL: &str = r#"
|
||||
PRAGMA journal_mode = WAL;
|
||||
PRAGMA synchronous = NORMAL;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS messages (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
storage_owner INTEGER NOT NULL,
|
||||
external_user INTEGER NOT NULL,
|
||||
message_time INTEGER NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
sent_by_self INTEGER NOT NULL,
|
||||
message_state TEXT NOT NULL,
|
||||
height INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_messages_lookup
|
||||
ON messages (storage_owner, external_user, message_time DESC);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS contacts (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
storage_owner INTEGER NOT NULL,
|
||||
user_id INTEGER NOT NULL,
|
||||
user_name TEXT,
|
||||
last_message_at INTEGER,
|
||||
UNIQUE(storage_owner, user_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_contacts_owner
|
||||
ON contacts (storage_owner, last_message_at DESC, user_id ASC);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS communities (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
storage_owner INTEGER NOT NULL,
|
||||
address TEXT NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
position TEXT NOT NULL,
|
||||
UNIQUE(storage_owner, address)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_communities_owner
|
||||
ON communities (storage_owner);
|
||||
"#;
|
||||
|
||||
match create_shared_connection("messages", INIT_SQL) {
|
||||
Ok(shared_conn) => {
|
||||
// Attempt to add the height column for backwards compatibility.
|
||||
// This will fail if the column already exists, which is expected.
|
||||
let _ = with_conn(&shared_conn, |conn| {
|
||||
let _ = conn.execute(
|
||||
"ALTER TABLE messages ADD COLUMN height INTEGER NOT NULL DEFAULT 0",
|
||||
[],
|
||||
);
|
||||
Ok(())
|
||||
});
|
||||
Ok(shared_conn)
|
||||
}
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Example usage:
|
||||
|
||||
// In some util module (at init time, e.g. lazy_static or LazyLock)
|
||||
static MESSAGES_DB: LazyLock<Arc<Mutex<Connection>>> = LazyLock::new(|| {
|
||||
create_general_messages_db().expect("failed to create messages DB")
|
||||
});
|
||||
|
||||
// Later, to run a query:
|
||||
let res: Result<Vec<MyRow>, String> = with_conn(&MESSAGES_DB, |conn| {
|
||||
let mut stmt = conn.prepare("SELECT ...")?;
|
||||
let rows = stmt.query_map(...)?;
|
||||
// collect and return Ok(...)
|
||||
});
|
||||
*/
|
||||
|
|
@ -13,13 +13,13 @@ use ttp_core::{CommunicationValue, DataTypes, DataValue};
|
|||
|
||||
use crate::{
|
||||
APP_STATE,
|
||||
gui::{elements::log_card::UiLogEntry, ui::UNIQUE},
|
||||
gui::{elements::log_card::LogEntry, ui::UNIQUE},
|
||||
langu::language_manager,
|
||||
};
|
||||
|
||||
static LOGGER: OnceLock<mpsc::Sender<LogMessage>> = OnceLock::new();
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||
#[allow(unused)]
|
||||
pub enum PrintType {
|
||||
Call,
|
||||
|
|
@ -28,6 +28,20 @@ pub enum PrintType {
|
|||
Omikron,
|
||||
Omega,
|
||||
General,
|
||||
Command,
|
||||
}
|
||||
impl PrintType {
|
||||
pub fn prefix_color(self) -> Color {
|
||||
match self {
|
||||
PrintType::Call => Color::Magenta,
|
||||
PrintType::Client => Color::Green,
|
||||
PrintType::Iota => Color::Yellow,
|
||||
PrintType::Omikron => Color::Blue,
|
||||
PrintType::Omega => Color::Cyan,
|
||||
PrintType::General => Color::LightCyan,
|
||||
PrintType::Command => Color::LightGreen,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct LogMessage {
|
||||
|
|
@ -35,10 +49,8 @@ struct LogMessage {
|
|||
prefix: String,
|
||||
kind: PrintType,
|
||||
is_error: bool,
|
||||
|
||||
translation_key: Option<String>,
|
||||
format_args: Vec<String>,
|
||||
|
||||
message: Option<String>,
|
||||
}
|
||||
|
||||
|
|
@ -70,27 +82,49 @@ pub fn startup() {
|
|||
msg.message.unwrap_or_default()
|
||||
};
|
||||
|
||||
let ts = fixed_box(&msg.timestamp_ms.to_string(), 13);
|
||||
let timestamp = format_timestamp_inline(msg.timestamp_ms);
|
||||
let entry = LogEntry::new(msg.kind, resolved_message, msg.is_error);
|
||||
|
||||
let line = format!("{} {}", msg.prefix, resolved_message);
|
||||
|
||||
let _ = writeln!(file, "{} {}", ts, line);
|
||||
|
||||
let color = colorize(msg.kind, msg.is_error);
|
||||
|
||||
let ui_entry = UiLogEntry {
|
||||
line: line.clone(),
|
||||
color,
|
||||
let prefix = if msg.prefix.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!("{} ", msg.prefix)
|
||||
};
|
||||
|
||||
{
|
||||
let _ = writeln!(
|
||||
file,
|
||||
"{} {}{}",
|
||||
fixed_box(&msg.timestamp_ms.to_string(), 13),
|
||||
prefix,
|
||||
entry.message
|
||||
);
|
||||
|
||||
let _ = writeln!(file, " {}", timestamp);
|
||||
|
||||
let mut state = APP_STATE.lock().unwrap();
|
||||
state.push_log(ui_entry);
|
||||
}
|
||||
state.push_log(entry.into());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn format_timestamp_inline(timestamp_ms: u128) -> String {
|
||||
let secs = (timestamp_ms / 1000) as i64;
|
||||
let hours = (secs / 3600) % 24;
|
||||
let minutes = (secs / 60) % 60;
|
||||
let seconds = secs % 60;
|
||||
format!("[{:02}:{:02}:{:02}]", hours, minutes, seconds)
|
||||
}
|
||||
|
||||
fn fixed_box(content: &str, width: usize) -> String {
|
||||
let s: String = content.chars().take(width).collect();
|
||||
let len = s.chars().count();
|
||||
if len < width {
|
||||
format!("[{}{}]", " ".repeat(width - len), s)
|
||||
} else {
|
||||
s
|
||||
}
|
||||
}
|
||||
|
||||
fn colorize(kind: PrintType, is_error: bool) -> Color {
|
||||
if is_error {
|
||||
return Color::Red;
|
||||
|
|
@ -102,17 +136,8 @@ fn colorize(kind: PrintType, is_error: bool) -> Color {
|
|||
PrintType::Iota => Color::Yellow,
|
||||
PrintType::Omikron => Color::Blue,
|
||||
PrintType::Omega => Color::Cyan,
|
||||
PrintType::General => Color::White,
|
||||
}
|
||||
}
|
||||
|
||||
fn fixed_box(content: &str, width: usize) -> String {
|
||||
let s: String = content.chars().take(width).collect();
|
||||
let len = s.chars().count();
|
||||
if len < width {
|
||||
format!("[{}{}]", " ".repeat(width - len), s)
|
||||
} else {
|
||||
s
|
||||
PrintType::General => Color::LightCyan,
|
||||
PrintType::Command => Color::LightGreen,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -180,6 +205,7 @@ macro_rules! log_t {
|
|||
)
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! log_t_err {
|
||||
($key:expr) => {
|
||||
|
|
@ -203,13 +229,32 @@ macro_rules! log_t_err {
|
|||
};
|
||||
}
|
||||
|
||||
/// Log a command message.
|
||||
#[macro_export]
|
||||
macro_rules! log_command {
|
||||
($($arg:tt)*) => {
|
||||
$crate::util::logger::log_internal(
|
||||
$crate::util::logger::PrintType::Command,
|
||||
"".to_string(),
|
||||
false,
|
||||
format!($($arg)*)
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
/// Log a general informational message.
|
||||
#[macro_export]
|
||||
macro_rules! log {
|
||||
($($arg:tt)*) => {
|
||||
$crate::util::logger::log_internal($crate::util::logger::PrintType::General, "".to_string(), false, format!($($arg)*))
|
||||
$crate::util::logger::log_internal(
|
||||
$crate::util::logger::PrintType::General,
|
||||
"".to_string(),
|
||||
false,
|
||||
format!($($arg)*)
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
/// Log an inbound message (`>`).
|
||||
#[macro_export]
|
||||
macro_rules! log_in {
|
||||
|
|
@ -222,6 +267,7 @@ macro_rules! log_in {
|
|||
)
|
||||
};
|
||||
}
|
||||
|
||||
/// Log an outbound message (`<`).
|
||||
#[macro_export]
|
||||
macro_rules! log_out {
|
||||
|
|
@ -234,6 +280,7 @@ macro_rules! log_out {
|
|||
)
|
||||
};
|
||||
}
|
||||
|
||||
/// Log an error message (`>>`).
|
||||
#[macro_export]
|
||||
macro_rules! log_err {
|
||||
|
|
@ -373,6 +420,7 @@ macro_rules! log_cv_in {
|
|||
$crate::util::logger::log_cv_internal("> ", &$cv, None)
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! log_cv_out {
|
||||
($kind:expr, $cv:expr) => {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
pub mod chat_files;
|
||||
pub mod chats_util;
|
||||
pub mod communities_util;
|
||||
pub mod config_util;
|
||||
pub mod crypto_helper;
|
||||
pub mod crypto_util;
|
||||
pub mod db;
|
||||
pub mod file_util;
|
||||
pub mod logger;
|
||||
|
|
|
|||
Loading…
Reference in a new issue