mirror of
https://github.com/logos-messaging/libchat.git
synced 2026-02-10 17:03:12 +00:00
multiple chats support
This commit is contained in:
parent
1aefd7eb0f
commit
537d48d5fd
1
.gitignore
vendored
1
.gitignore
vendored
@ -29,4 +29,5 @@ target
|
|||||||
|
|
||||||
# Temporary data folder
|
# Temporary data folder
|
||||||
tmp
|
tmp
|
||||||
|
chat-cli-data
|
||||||
|
|
||||||
|
|||||||
267
Cargo.lock
generated
267
Cargo.lock
generated
@ -30,12 +30,6 @@ version = "1.8.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
|
checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "bitflags"
|
|
||||||
version = "1.3.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitflags"
|
name = "bitflags"
|
||||||
version = "2.10.0"
|
version = "2.10.0"
|
||||||
@ -127,13 +121,10 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"crossterm 0.29.0",
|
"crossterm 0.29.0",
|
||||||
"dirs",
|
|
||||||
"logos-chat",
|
"logos-chat",
|
||||||
"notify",
|
|
||||||
"ratatui",
|
"ratatui",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"tempfile",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -199,7 +190,7 @@ version = "0.28.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6"
|
checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.10.0",
|
"bitflags",
|
||||||
"crossterm_winapi",
|
"crossterm_winapi",
|
||||||
"mio",
|
"mio",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
@ -215,7 +206,7 @@ version = "0.29.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b"
|
checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.10.0",
|
"bitflags",
|
||||||
"crossterm_winapi",
|
"crossterm_winapi",
|
||||||
"derive_more 2.1.1",
|
"derive_more 2.1.1",
|
||||||
"document-features",
|
"document-features",
|
||||||
@ -376,27 +367,6 @@ dependencies = [
|
|||||||
"subtle",
|
"subtle",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "dirs"
|
|
||||||
version = "6.0.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e"
|
|
||||||
dependencies = [
|
|
||||||
"dirs-sys",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "dirs-sys"
|
|
||||||
version = "0.5.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab"
|
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
"option-ext",
|
|
||||||
"redox_users",
|
|
||||||
"windows-sys 0.61.2",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "document-features"
|
name = "document-features"
|
||||||
version = "0.2.12"
|
version = "0.2.12"
|
||||||
@ -541,15 +511,6 @@ version = "0.1.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
|
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "fsevent-sys"
|
|
||||||
version = "4.1.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2"
|
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "generic-array"
|
name = "generic-array"
|
||||||
version = "0.14.7"
|
version = "0.14.7"
|
||||||
@ -674,26 +635,6 @@ dependencies = [
|
|||||||
"rustversion",
|
"rustversion",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "inotify"
|
|
||||||
version = "0.11.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3"
|
|
||||||
dependencies = [
|
|
||||||
"bitflags 2.10.0",
|
|
||||||
"inotify-sys",
|
|
||||||
"libc",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "inotify-sys"
|
|
||||||
version = "0.1.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb"
|
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "inout"
|
name = "inout"
|
||||||
version = "0.1.4"
|
version = "0.1.4"
|
||||||
@ -749,42 +690,12 @@ version = "1.0.17"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
|
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "kqueue"
|
|
||||||
version = "1.1.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "eac30106d7dce88daf4a3fcb4879ea939476d5074a9b7ddd0fb97fa4bed5596a"
|
|
||||||
dependencies = [
|
|
||||||
"kqueue-sys",
|
|
||||||
"libc",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "kqueue-sys"
|
|
||||||
version = "1.0.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b"
|
|
||||||
dependencies = [
|
|
||||||
"bitflags 1.3.2",
|
|
||||||
"libc",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.180"
|
version = "0.2.180"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc"
|
checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "libredox"
|
|
||||||
version = "0.1.12"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616"
|
|
||||||
dependencies = [
|
|
||||||
"bitflags 2.10.0",
|
|
||||||
"libc",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libsqlite3-sys"
|
name = "libsqlite3-sys"
|
||||||
version = "0.33.0"
|
version = "0.33.0"
|
||||||
@ -891,33 +802,6 @@ dependencies = [
|
|||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "notify"
|
|
||||||
version = "8.2.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3"
|
|
||||||
dependencies = [
|
|
||||||
"bitflags 2.10.0",
|
|
||||||
"fsevent-sys",
|
|
||||||
"inotify",
|
|
||||||
"kqueue",
|
|
||||||
"libc",
|
|
||||||
"log",
|
|
||||||
"mio",
|
|
||||||
"notify-types",
|
|
||||||
"walkdir",
|
|
||||||
"windows-sys 0.60.2",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "notify-types"
|
|
||||||
version = "2.1.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "42b8cfee0e339a0337359f3c88165702ac6e600dc01c0cc9579a92d62b08477a"
|
|
||||||
dependencies = [
|
|
||||||
"bitflags 2.10.0",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "once_cell"
|
name = "once_cell"
|
||||||
version = "1.21.3"
|
version = "1.21.3"
|
||||||
@ -952,12 +836,6 @@ dependencies = [
|
|||||||
"vcpkg",
|
"vcpkg",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "option-ext"
|
|
||||||
version = "0.2.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "parking_lot"
|
name = "parking_lot"
|
||||||
version = "0.12.5"
|
version = "0.12.5"
|
||||||
@ -1125,7 +1003,7 @@ version = "0.29.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b"
|
checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.10.0",
|
"bitflags",
|
||||||
"cassowary",
|
"cassowary",
|
||||||
"compact_str",
|
"compact_str",
|
||||||
"crossterm 0.28.1",
|
"crossterm 0.28.1",
|
||||||
@ -1146,18 +1024,7 @@ version = "0.5.18"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
|
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.10.0",
|
"bitflags",
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "redox_users"
|
|
||||||
version = "0.5.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac"
|
|
||||||
dependencies = [
|
|
||||||
"getrandom 0.2.17",
|
|
||||||
"libredox",
|
|
||||||
"thiserror",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1166,7 +1033,7 @@ version = "0.35.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a22715a5d6deef63c637207afbe68d0c72c3f8d0022d7cf9714c442d6157606b"
|
checksum = "a22715a5d6deef63c637207afbe68d0c72c3f8d0022d7cf9714c442d6157606b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.10.0",
|
"bitflags",
|
||||||
"fallible-iterator",
|
"fallible-iterator",
|
||||||
"fallible-streaming-iterator",
|
"fallible-streaming-iterator",
|
||||||
"hashlink",
|
"hashlink",
|
||||||
@ -1189,7 +1056,7 @@ version = "0.38.44"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
|
checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.10.0",
|
"bitflags",
|
||||||
"errno",
|
"errno",
|
||||||
"libc",
|
"libc",
|
||||||
"linux-raw-sys 0.4.15",
|
"linux-raw-sys 0.4.15",
|
||||||
@ -1202,7 +1069,7 @@ version = "1.1.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34"
|
checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.10.0",
|
"bitflags",
|
||||||
"errno",
|
"errno",
|
||||||
"libc",
|
"libc",
|
||||||
"linux-raw-sys 0.11.0",
|
"linux-raw-sys 0.11.0",
|
||||||
@ -1253,15 +1120,6 @@ dependencies = [
|
|||||||
"syn 1.0.109",
|
"syn 1.0.109",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "same-file"
|
|
||||||
version = "1.0.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
|
|
||||||
dependencies = [
|
|
||||||
"winapi-util",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "scopeguard"
|
name = "scopeguard"
|
||||||
version = "1.2.0"
|
version = "1.2.0"
|
||||||
@ -1642,16 +1500,6 @@ version = "0.9.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "walkdir"
|
|
||||||
version = "2.5.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
|
|
||||||
dependencies = [
|
|
||||||
"same-file",
|
|
||||||
"winapi-util",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasi"
|
name = "wasi"
|
||||||
version = "0.11.1+wasi-snapshot-preview1"
|
version = "0.11.1+wasi-snapshot-preview1"
|
||||||
@ -1683,15 +1531,6 @@ version = "0.4.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "winapi-util"
|
|
||||||
version = "0.1.11"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
|
||||||
dependencies = [
|
|
||||||
"windows-sys 0.61.2",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winapi-x86_64-pc-windows-gnu"
|
name = "winapi-x86_64-pc-windows-gnu"
|
||||||
version = "0.4.0"
|
version = "0.4.0"
|
||||||
@ -1710,16 +1549,7 @@ version = "0.59.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
|
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-targets 0.52.6",
|
"windows-targets",
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows-sys"
|
|
||||||
version = "0.60.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
|
|
||||||
dependencies = [
|
|
||||||
"windows-targets 0.53.5",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1737,31 +1567,14 @@ version = "0.52.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
|
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows_aarch64_gnullvm 0.52.6",
|
"windows_aarch64_gnullvm",
|
||||||
"windows_aarch64_msvc 0.52.6",
|
"windows_aarch64_msvc",
|
||||||
"windows_i686_gnu 0.52.6",
|
"windows_i686_gnu",
|
||||||
"windows_i686_gnullvm 0.52.6",
|
"windows_i686_gnullvm",
|
||||||
"windows_i686_msvc 0.52.6",
|
"windows_i686_msvc",
|
||||||
"windows_x86_64_gnu 0.52.6",
|
"windows_x86_64_gnu",
|
||||||
"windows_x86_64_gnullvm 0.52.6",
|
"windows_x86_64_gnullvm",
|
||||||
"windows_x86_64_msvc 0.52.6",
|
"windows_x86_64_msvc",
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows-targets"
|
|
||||||
version = "0.53.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
|
|
||||||
dependencies = [
|
|
||||||
"windows-link",
|
|
||||||
"windows_aarch64_gnullvm 0.53.1",
|
|
||||||
"windows_aarch64_msvc 0.53.1",
|
|
||||||
"windows_i686_gnu 0.53.1",
|
|
||||||
"windows_i686_gnullvm 0.53.1",
|
|
||||||
"windows_i686_msvc 0.53.1",
|
|
||||||
"windows_x86_64_gnu 0.53.1",
|
|
||||||
"windows_x86_64_gnullvm 0.53.1",
|
|
||||||
"windows_x86_64_msvc 0.53.1",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1770,96 +1583,48 @@ version = "0.52.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_aarch64_gnullvm"
|
|
||||||
version = "0.53.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_aarch64_msvc"
|
name = "windows_aarch64_msvc"
|
||||||
version = "0.52.6"
|
version = "0.52.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_aarch64_msvc"
|
|
||||||
version = "0.53.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_i686_gnu"
|
name = "windows_i686_gnu"
|
||||||
version = "0.52.6"
|
version = "0.52.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
|
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_i686_gnu"
|
|
||||||
version = "0.53.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_i686_gnullvm"
|
name = "windows_i686_gnullvm"
|
||||||
version = "0.52.6"
|
version = "0.52.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_i686_gnullvm"
|
|
||||||
version = "0.53.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_i686_msvc"
|
name = "windows_i686_msvc"
|
||||||
version = "0.52.6"
|
version = "0.52.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_i686_msvc"
|
|
||||||
version = "0.53.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_x86_64_gnu"
|
name = "windows_x86_64_gnu"
|
||||||
version = "0.52.6"
|
version = "0.52.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_x86_64_gnu"
|
|
||||||
version = "0.53.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_x86_64_gnullvm"
|
name = "windows_x86_64_gnullvm"
|
||||||
version = "0.52.6"
|
version = "0.52.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_x86_64_gnullvm"
|
|
||||||
version = "0.53.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_x86_64_msvc"
|
name = "windows_x86_64_msvc"
|
||||||
version = "0.52.6"
|
version = "0.52.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_x86_64_msvc"
|
|
||||||
version = "0.53.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winnow"
|
name = "winnow"
|
||||||
version = "0.7.14"
|
version = "0.7.14"
|
||||||
|
|||||||
@ -14,6 +14,3 @@ crossterm = "0.29"
|
|||||||
anyhow = "1.0"
|
anyhow = "1.0"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
notify = "8.0"
|
|
||||||
tempfile = "3"
|
|
||||||
dirs = "6.0"
|
|
||||||
|
|||||||
@ -40,21 +40,22 @@ cargo run -p chat-cli -- bob
|
|||||||
| `/help` | Show available commands |
|
| `/help` | Show available commands |
|
||||||
| `/intro` | Generate and display your introduction bundle |
|
| `/intro` | Generate and display your introduction bundle |
|
||||||
| `/connect <user> <bundle>` | Connect to a user using their introduction bundle |
|
| `/connect <user> <bundle>` | Connect to a user using their introduction bundle |
|
||||||
|
| `/peers` | List available peers |
|
||||||
| `/status` | Show connection status and your address |
|
| `/status` | Show connection status and your address |
|
||||||
| `/clear` | Clear message history |
|
| `/clear` | Clear message history |
|
||||||
| `/quit` or `Esc` | Exit the application |
|
| `/quit` or `Esc` or `Ctrl+C` | Exit the application |
|
||||||
|
|
||||||
### Sending Messages
|
### Sending Messages
|
||||||
|
|
||||||
Simply type your message and press Enter. Messages are automatically encrypted and delivered via the file-based transport.
|
Simply type your message and press Enter. Messages are automatically encrypted and delivered via file-based transport.
|
||||||
|
|
||||||
## How It Works
|
## How It Works
|
||||||
|
|
||||||
### File-Based Transport
|
### File-Based Transport
|
||||||
|
|
||||||
Since this is a local demo without a real network, messages are passed between users via files:
|
Messages are passed between users via files in a shared directory:
|
||||||
|
|
||||||
1. Each user has an "inbox" directory at `~/.local/share/chat-cli/transport/<username>/`
|
1. Each user has an "inbox" directory at `chat-cli-data/transport/<username>/`
|
||||||
2. When Alice sends a message to Bob, it's written as a JSON file in Bob's inbox
|
2. When Alice sends a message to Bob, it's written as a JSON file in Bob's inbox
|
||||||
3. Bob's client watches for new files and processes incoming messages
|
3. Bob's client watches for new files and processes incoming messages
|
||||||
4. Files are deleted after processing
|
4. Files are deleted after processing
|
||||||
@ -62,7 +63,7 @@ Since this is a local demo without a real network, messages are passed between u
|
|||||||
### Storage
|
### Storage
|
||||||
|
|
||||||
User data (identity keys, chat state) is stored in SQLite databases at:
|
User data (identity keys, chat state) is stored in SQLite databases at:
|
||||||
- `~/.local/share/chat-cli/data/<username>.db`
|
- `chat-cli-data/<username>.db`
|
||||||
|
|
||||||
### Encryption
|
### Encryption
|
||||||
|
|
||||||
|
|||||||
@ -1,21 +1,40 @@
|
|||||||
//! Chat application logic.
|
//! Chat application logic.
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::fs;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use logos_chat::{ChatManager, Introduction, StorageConfig};
|
use logos_chat::{ChatManager, Introduction, StorageConfig};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::transport::FileTransport;
|
use crate::transport::FileTransport;
|
||||||
|
|
||||||
/// A chat message for display.
|
/// A chat message for display.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
#[allow(dead_code)]
|
|
||||||
pub struct DisplayMessage {
|
pub struct DisplayMessage {
|
||||||
pub from_self: bool,
|
pub from_self: bool,
|
||||||
pub content: String,
|
pub content: String,
|
||||||
pub timestamp: u64,
|
pub timestamp: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Metadata for a chat session (persisted).
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct ChatSession {
|
||||||
|
pub chat_id: String,
|
||||||
|
pub remote_user: String,
|
||||||
|
pub messages: Vec<DisplayMessage>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// App state that gets persisted.
|
||||||
|
#[derive(Debug, Default, Serialize, Deserialize)]
|
||||||
|
pub struct AppState {
|
||||||
|
/// Map from remote username to chat session.
|
||||||
|
pub sessions: HashMap<String, ChatSession>,
|
||||||
|
/// Currently active chat (remote username).
|
||||||
|
pub active_chat: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
/// The chat application state.
|
/// The chat application state.
|
||||||
pub struct ChatApp {
|
pub struct ChatApp {
|
||||||
/// The logos-chat manager.
|
/// The logos-chat manager.
|
||||||
@ -24,50 +43,103 @@ pub struct ChatApp {
|
|||||||
pub transport: FileTransport,
|
pub transport: FileTransport,
|
||||||
/// Our introduction bundle (to share with others).
|
/// Our introduction bundle (to share with others).
|
||||||
pub intro_bundle: Option<Introduction>,
|
pub intro_bundle: Option<Introduction>,
|
||||||
/// Current chat ID (if in a conversation).
|
/// Persisted app state.
|
||||||
pub current_chat_id: Option<String>,
|
pub state: AppState,
|
||||||
/// Remote user name (for transport).
|
/// Global messages (shown when no active chat).
|
||||||
pub remote_user: Option<String>,
|
pub global_messages: Vec<DisplayMessage>,
|
||||||
/// Messages to display.
|
|
||||||
pub messages: Vec<DisplayMessage>,
|
|
||||||
/// Input buffer.
|
/// Input buffer.
|
||||||
pub input: String,
|
pub input: String,
|
||||||
/// Status message.
|
/// Status message.
|
||||||
pub status: String,
|
pub status: String,
|
||||||
/// Our user name.
|
/// Our user name.
|
||||||
pub user_name: String,
|
pub user_name: String,
|
||||||
|
/// Path to state file.
|
||||||
|
state_path: PathBuf,
|
||||||
|
/// Data directory.
|
||||||
|
data_dir: PathBuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ChatApp {
|
impl ChatApp {
|
||||||
/// Create a new chat application.
|
/// Create a new chat application.
|
||||||
pub fn new(user_name: &str, data_dir: &PathBuf, transport_dir: &PathBuf) -> Result<Self> {
|
pub fn new(user_name: &str, data_dir: &PathBuf) -> Result<Self> {
|
||||||
// Create database path
|
// Create database path
|
||||||
let db_path = data_dir.join(format!("{}.db", user_name));
|
let db_path = data_dir.join(format!("{}.db", user_name));
|
||||||
std::fs::create_dir_all(data_dir)?;
|
std::fs::create_dir_all(data_dir)?;
|
||||||
|
|
||||||
// Open or create the chat manager with file-based storage
|
// Open or create the chat manager with file-based storage
|
||||||
let manager = ChatManager::open(StorageConfig::File(
|
let manager = ChatManager::open(StorageConfig::File(db_path.to_string_lossy().to_string()))
|
||||||
db_path.to_string_lossy().to_string(),
|
.context("Failed to open ChatManager")?;
|
||||||
))
|
|
||||||
.context("Failed to open ChatManager")?;
|
|
||||||
|
|
||||||
// Create file transport
|
// Create file-based transport
|
||||||
let transport = FileTransport::new(user_name, transport_dir)
|
let transport =
|
||||||
.context("Failed to create transport")?;
|
FileTransport::new(user_name, data_dir).context("Failed to create file transport")?;
|
||||||
|
|
||||||
|
// Load persisted state
|
||||||
|
let state_path = data_dir.join(format!("{}_state.json", user_name));
|
||||||
|
let state = Self::load_state(&state_path);
|
||||||
|
|
||||||
|
// Count existing chats
|
||||||
|
let chat_count = state.sessions.len();
|
||||||
|
let status = if chat_count > 0 {
|
||||||
|
format!(
|
||||||
|
"Welcome back, {}! {} chat(s) loaded. Type /help for commands.",
|
||||||
|
user_name, chat_count
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
format!("Welcome, {}! Type /help for commands.", user_name)
|
||||||
|
};
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
manager,
|
manager,
|
||||||
transport,
|
transport,
|
||||||
intro_bundle: None,
|
intro_bundle: None,
|
||||||
current_chat_id: None,
|
state,
|
||||||
remote_user: None,
|
global_messages: Vec::new(),
|
||||||
messages: Vec::new(),
|
|
||||||
input: String::new(),
|
input: String::new(),
|
||||||
status: format!("Welcome, {}! Type /help for commands.", user_name),
|
status,
|
||||||
user_name: user_name.to_string(),
|
user_name: user_name.to_string(),
|
||||||
|
state_path,
|
||||||
|
data_dir: data_dir.clone(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Load state from file.
|
||||||
|
fn load_state(path: &PathBuf) -> AppState {
|
||||||
|
if path.exists() {
|
||||||
|
if let Ok(contents) = fs::read_to_string(path) {
|
||||||
|
if let Ok(state) = serde_json::from_str(&contents) {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AppState::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Save state to file.
|
||||||
|
fn save_state(&self) -> Result<()> {
|
||||||
|
let json = serde_json::to_string_pretty(&self.state)?;
|
||||||
|
fs::write(&self.state_path, json)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the current chat session (if any).
|
||||||
|
pub fn current_session(&self) -> Option<&ChatSession> {
|
||||||
|
self.state
|
||||||
|
.active_chat
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|name| self.state.sessions.get(name))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the current messages to display.
|
||||||
|
pub fn messages(&self) -> Vec<&DisplayMessage> {
|
||||||
|
if let Some(session) = self.current_session() {
|
||||||
|
session.messages.iter().collect()
|
||||||
|
} else {
|
||||||
|
// Show global messages when no active chat
|
||||||
|
self.global_messages.iter().collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Create and display our introduction bundle.
|
/// Create and display our introduction bundle.
|
||||||
pub fn create_intro(&mut self) -> Result<String> {
|
pub fn create_intro(&mut self) -> Result<String> {
|
||||||
let intro = self.manager.create_intro_bundle()?;
|
let intro = self.manager.create_intro_bundle()?;
|
||||||
@ -80,96 +152,165 @@ impl ChatApp {
|
|||||||
|
|
||||||
/// Connect to another user using their introduction bundle.
|
/// Connect to another user using their introduction bundle.
|
||||||
pub fn connect(&mut self, remote_user: &str, bundle_str: &str) -> Result<()> {
|
pub fn connect(&mut self, remote_user: &str, bundle_str: &str) -> Result<()> {
|
||||||
|
// Check if we already have a chat with this user
|
||||||
|
if self.state.sessions.contains_key(remote_user) {
|
||||||
|
return Err(anyhow::anyhow!(
|
||||||
|
"Already have a chat with {}. Use /switch {} to switch to it.",
|
||||||
|
remote_user,
|
||||||
|
remote_user
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
let intro = Introduction::try_from(bundle_str.as_bytes())
|
let intro = Introduction::try_from(bundle_str.as_bytes())
|
||||||
.map_err(|e| anyhow::anyhow!("Invalid bundle: {:?}", e))?;
|
.map_err(|e| anyhow::anyhow!("Invalid bundle: {:?}", e))?;
|
||||||
|
|
||||||
let (chat_id, envelopes) = self.manager.start_private_chat(&intro, "👋 Hello!")?;
|
let (chat_id, envelopes) = self.manager.start_private_chat(&intro, "👋 Hello!")?;
|
||||||
|
|
||||||
self.current_chat_id = Some(chat_id.clone());
|
|
||||||
self.remote_user = Some(remote_user.to_string());
|
|
||||||
|
|
||||||
// Send the envelopes via file transport
|
// Send the envelopes via file transport
|
||||||
for envelope in envelopes {
|
for envelope in envelopes {
|
||||||
self.transport.send(remote_user, envelope.data)?;
|
self.transport.send(remote_user, envelope.data)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.messages.push(DisplayMessage {
|
// Create new session
|
||||||
|
let mut session = ChatSession {
|
||||||
|
chat_id: chat_id.clone(),
|
||||||
|
remote_user: remote_user.to_string(),
|
||||||
|
messages: Vec::new(),
|
||||||
|
};
|
||||||
|
session.messages.push(DisplayMessage {
|
||||||
from_self: true,
|
from_self: true,
|
||||||
content: "👋 Hello!".to_string(),
|
content: "👋 Hello!".to_string(),
|
||||||
timestamp: now(),
|
timestamp: now(),
|
||||||
});
|
});
|
||||||
|
|
||||||
self.status = format!("Connected to {}! Chat ID: {}", remote_user, &chat_id[..8]);
|
self.state.sessions.insert(remote_user.to_string(), session);
|
||||||
|
self.state.active_chat = Some(remote_user.to_string());
|
||||||
|
self.save_state()?;
|
||||||
|
|
||||||
|
self.status = format!("Connected to {}!", remote_user);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Switch to a different chat.
|
||||||
|
pub fn switch_chat(&mut self, remote_user: &str) -> Result<()> {
|
||||||
|
if self.state.sessions.contains_key(remote_user) {
|
||||||
|
self.state.active_chat = Some(remote_user.to_string());
|
||||||
|
self.save_state()?;
|
||||||
|
self.status = format!("Switched to chat with {}", remote_user);
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(anyhow::anyhow!(
|
||||||
|
"No chat with {}. Use /chats to list available chats.",
|
||||||
|
remote_user
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Send a message in the current chat.
|
/// Send a message in the current chat.
|
||||||
pub fn send_message(&mut self, content: &str) -> Result<()> {
|
pub fn send_message(&mut self, content: &str) -> Result<()> {
|
||||||
let chat_id = self.current_chat_id.as_ref()
|
let active = self
|
||||||
.ok_or_else(|| anyhow::anyhow!("No active chat"))?;
|
.state
|
||||||
let remote_user = self.remote_user.as_ref()
|
.active_chat
|
||||||
.ok_or_else(|| anyhow::anyhow!("No remote user"))?;
|
.clone()
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("No active chat. Use /connect or /switch first."))?;
|
||||||
|
|
||||||
let envelopes = self.manager.send_message(chat_id, content.as_bytes())?;
|
let session = self
|
||||||
|
.state
|
||||||
|
.sessions
|
||||||
|
.get(&active)
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("Chat session not found"))?;
|
||||||
|
|
||||||
|
let chat_id = session.chat_id.clone();
|
||||||
|
let remote_user = session.remote_user.clone();
|
||||||
|
|
||||||
|
let envelopes = self.manager.send_message(&chat_id, content.as_bytes())?;
|
||||||
|
|
||||||
for envelope in envelopes {
|
for envelope in envelopes {
|
||||||
self.transport.send(remote_user, envelope.data)?;
|
self.transport.send(&remote_user, envelope.data)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.messages.push(DisplayMessage {
|
// Update messages
|
||||||
from_self: true,
|
if let Some(session) = self.state.sessions.get_mut(&active) {
|
||||||
content: content.to_string(),
|
session.messages.push(DisplayMessage {
|
||||||
timestamp: now(),
|
from_self: true,
|
||||||
});
|
content: content.to_string(),
|
||||||
|
timestamp: now(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
self.save_state()?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Process incoming messages from transport.
|
/// Process incoming messages from transport.
|
||||||
pub fn process_incoming(&mut self) -> Result<()> {
|
pub fn process_incoming(&mut self) -> Result<()> {
|
||||||
// Check for new messages
|
|
||||||
while let Some(envelope) = self.transport.try_recv() {
|
while let Some(envelope) = self.transport.try_recv() {
|
||||||
self.handle_incoming_envelope(&envelope)?;
|
self.handle_incoming_envelope(&envelope)?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Process existing messages on startup.
|
|
||||||
pub fn process_existing(&mut self) -> Result<()> {
|
|
||||||
let messages = self.transport.process_existing_messages();
|
|
||||||
for envelope in messages {
|
|
||||||
self.handle_incoming_envelope(&envelope)?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Handle an incoming envelope.
|
/// Handle an incoming envelope.
|
||||||
fn handle_incoming_envelope(&mut self, envelope: &crate::transport::FileEnvelope) -> Result<()> {
|
fn handle_incoming_envelope(
|
||||||
|
&mut self,
|
||||||
|
envelope: &crate::transport::MessageEnvelope,
|
||||||
|
) -> Result<()> {
|
||||||
match self.manager.handle_incoming(&envelope.data) {
|
match self.manager.handle_incoming(&envelope.data) {
|
||||||
Ok(content) => {
|
Ok(content) => {
|
||||||
// Update chat state if this is a new chat
|
let from_user = &envelope.from;
|
||||||
if self.current_chat_id.is_none() {
|
let chat_id = content.conversation_id.clone();
|
||||||
self.current_chat_id = Some(content.conversation_id.clone());
|
|
||||||
self.remote_user = Some(envelope.from.clone());
|
// Find or create session for this user
|
||||||
self.status = format!("New chat from {}!", envelope.from);
|
if !self.state.sessions.contains_key(from_user) {
|
||||||
|
// New chat from someone
|
||||||
|
let session = ChatSession {
|
||||||
|
chat_id: chat_id.clone(),
|
||||||
|
remote_user: from_user.clone(),
|
||||||
|
messages: Vec::new(),
|
||||||
|
};
|
||||||
|
self.state.sessions.insert(from_user.clone(), session);
|
||||||
|
self.state.active_chat = Some(from_user.clone());
|
||||||
|
self.status = format!("New chat from {}!", from_user);
|
||||||
}
|
}
|
||||||
|
|
||||||
let message = String::from_utf8_lossy(&content.data).to_string();
|
let message = String::from_utf8_lossy(&content.data).to_string();
|
||||||
if !message.is_empty() {
|
if !message.is_empty() {
|
||||||
self.messages.push(DisplayMessage {
|
if let Some(session) = self.state.sessions.get_mut(from_user) {
|
||||||
from_self: false,
|
session.messages.push(DisplayMessage {
|
||||||
content: message,
|
from_self: false,
|
||||||
timestamp: envelope.timestamp,
|
content: message,
|
||||||
});
|
timestamp: envelope.timestamp,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.save_state()?;
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
self.status = format!("Error handling message: {}", e);
|
self.status = format!("Error: {}", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Add a system message to the current chat (for display only).
|
||||||
|
fn add_system_message(&mut self, content: &str) {
|
||||||
|
let msg = DisplayMessage {
|
||||||
|
from_self: true,
|
||||||
|
content: content.to_string(),
|
||||||
|
timestamp: now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(active) = &self.state.active_chat.clone() {
|
||||||
|
if let Some(session) = self.state.sessions.get_mut(active) {
|
||||||
|
session.messages.push(msg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// No active chat - add to global messages
|
||||||
|
self.global_messages.push(msg);
|
||||||
|
}
|
||||||
|
|
||||||
/// Handle a command (starts with /).
|
/// Handle a command (starts with /).
|
||||||
pub fn handle_command(&mut self, cmd: &str) -> Result<Option<String>> {
|
pub fn handle_command(&mut self, cmd: &str) -> Result<Option<String>> {
|
||||||
let parts: Vec<&str> = cmd.splitn(2, ' ').collect();
|
let parts: Vec<&str> = cmd.splitn(2, ' ').collect();
|
||||||
@ -178,18 +319,22 @@ impl ChatApp {
|
|||||||
|
|
||||||
match command {
|
match command {
|
||||||
"/help" => {
|
"/help" => {
|
||||||
Ok(Some(
|
self.add_system_message("── Commands ──");
|
||||||
"Commands:\n\
|
self.add_system_message("/intro - Show your introduction bundle");
|
||||||
/intro - Show your introduction bundle\n\
|
self.add_system_message("/connect <user> <bundle> - Connect to a user");
|
||||||
/connect <user> <bundle> - Connect to a user\n\
|
self.add_system_message("/chats - List all chats");
|
||||||
/status - Show connection status\n\
|
self.add_system_message("/switch <user> - Switch to chat with user");
|
||||||
/clear - Clear messages\n\
|
self.add_system_message("/status - Show connection status");
|
||||||
/quit - Exit".to_string()
|
self.add_system_message("/clear - Clear current chat messages");
|
||||||
))
|
self.add_system_message("/quit or Esc or Ctrl+C - Exit");
|
||||||
|
Ok(Some("Help displayed".to_string()))
|
||||||
}
|
}
|
||||||
"/intro" => {
|
"/intro" => {
|
||||||
let bundle = self.create_intro()?;
|
let bundle = self.create_intro()?;
|
||||||
Ok(Some(format!("Your bundle:\n{}", bundle)))
|
self.add_system_message("── Your Introduction Bundle ──");
|
||||||
|
self.add_system_message(&bundle);
|
||||||
|
self.add_system_message("Share this bundle with others to connect!");
|
||||||
|
Ok(Some("Bundle created".to_string()))
|
||||||
}
|
}
|
||||||
"/connect" => {
|
"/connect" => {
|
||||||
let connect_parts: Vec<&str> = args.splitn(2, ' ').collect();
|
let connect_parts: Vec<&str> = args.splitn(2, ' ').collect();
|
||||||
@ -201,29 +346,70 @@ impl ChatApp {
|
|||||||
self.connect(remote_user, bundle)?;
|
self.connect(remote_user, bundle)?;
|
||||||
Ok(Some(format!("Connected to {}", remote_user)))
|
Ok(Some(format!("Connected to {}", remote_user)))
|
||||||
}
|
}
|
||||||
|
"/chats" => {
|
||||||
|
let sessions: Vec<_> = self.state.sessions.keys().cloned().collect();
|
||||||
|
if sessions.is_empty() {
|
||||||
|
Ok(Some("No chats yet. Use /connect to start one.".to_string()))
|
||||||
|
} else {
|
||||||
|
self.add_system_message(&format!("── Your Chats ({}) ──", sessions.len()));
|
||||||
|
for name in &sessions {
|
||||||
|
let marker = if Some(name) == self.state.active_chat.as_ref() {
|
||||||
|
" (active)"
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
};
|
||||||
|
self.add_system_message(&format!(" • {}{}", name, marker));
|
||||||
|
}
|
||||||
|
Ok(Some(format!("{} chat(s)", sessions.len())))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"/switch" => {
|
||||||
|
if args.is_empty() {
|
||||||
|
return Ok(Some("Usage: /switch <username>".to_string()));
|
||||||
|
}
|
||||||
|
self.switch_chat(args)?;
|
||||||
|
Ok(Some(format!("Switched to {}", args)))
|
||||||
|
}
|
||||||
|
"/peers" => {
|
||||||
|
let peers = self.transport.list_peers();
|
||||||
|
if peers.is_empty() {
|
||||||
|
Ok(Some(
|
||||||
|
"No peers found. Start another chat-cli instance.".to_string(),
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
self.add_system_message(&format!("── Peers ({}) ──", peers.len()));
|
||||||
|
for peer in &peers {
|
||||||
|
self.add_system_message(&format!(" • {}", peer));
|
||||||
|
}
|
||||||
|
Ok(Some(format!("{} peer(s)", peers.len())))
|
||||||
|
}
|
||||||
|
}
|
||||||
"/status" => {
|
"/status" => {
|
||||||
let status = match &self.current_chat_id {
|
let chats = self.state.sessions.len();
|
||||||
Some(id) => format!(
|
let active = self.state.active_chat.as_deref().unwrap_or("none");
|
||||||
"Chat ID: {}\nRemote: {}\nAddress: {}",
|
let status = format!(
|
||||||
&id[..8.min(id.len())],
|
"User: {}\nAddress: {}\nChats: {}\nActive: {}",
|
||||||
self.remote_user.as_deref().unwrap_or("none"),
|
self.user_name,
|
||||||
self.manager.local_address()
|
self.manager.local_address(),
|
||||||
),
|
chats,
|
||||||
None => format!(
|
active
|
||||||
"No active chat\nAddress: {}",
|
);
|
||||||
self.manager.local_address()
|
|
||||||
),
|
|
||||||
};
|
|
||||||
Ok(Some(status))
|
Ok(Some(status))
|
||||||
}
|
}
|
||||||
"/clear" => {
|
"/clear" => {
|
||||||
self.messages.clear();
|
if let Some(active) = &self.state.active_chat {
|
||||||
|
if let Some(session) = self.state.sessions.get_mut(active) {
|
||||||
|
session.messages.clear();
|
||||||
|
self.save_state()?;
|
||||||
|
}
|
||||||
|
}
|
||||||
Ok(Some("Messages cleared".to_string()))
|
Ok(Some("Messages cleared".to_string()))
|
||||||
}
|
}
|
||||||
"/quit" => {
|
"/quit" => Ok(None),
|
||||||
Ok(None) // Signal to quit
|
_ => Ok(Some(format!(
|
||||||
}
|
"Unknown command: {}. Type /help for commands.",
|
||||||
_ => Ok(Some(format!("Unknown command: {}", command))),
|
command
|
||||||
|
))),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
//! Chat CLI - A terminal chat application using logos-chat.
|
//! Chat CLI - A terminal chat application using logos-chat.
|
||||||
//!
|
//!
|
||||||
//! This application demonstrates how to use the logos-chat library
|
//! This application demonstrates how to use the logos-chat library
|
||||||
//! with a file-based transport for local simulation.
|
//! with file-based transport for local communication.
|
||||||
//!
|
//!
|
||||||
//! # Usage
|
//! # Usage
|
||||||
//!
|
//!
|
||||||
@ -32,6 +32,16 @@ use std::path::PathBuf;
|
|||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
|
|
||||||
|
/// Get the data directory (in project folder).
|
||||||
|
fn get_data_dir() -> PathBuf {
|
||||||
|
// Use the directory where the binary is or current working directory
|
||||||
|
let manifest_dir = env!("CARGO_MANIFEST_DIR");
|
||||||
|
PathBuf::from(manifest_dir)
|
||||||
|
.parent()
|
||||||
|
.unwrap_or(&PathBuf::from("."))
|
||||||
|
.join("chat-cli-data")
|
||||||
|
}
|
||||||
|
|
||||||
fn main() -> Result<()> {
|
fn main() -> Result<()> {
|
||||||
// Parse arguments
|
// Parse arguments
|
||||||
let args: Vec<String> = std::env::args().collect();
|
let args: Vec<String> = std::env::args().collect();
|
||||||
@ -45,26 +55,15 @@ fn main() -> Result<()> {
|
|||||||
|
|
||||||
let user_name = &args[1];
|
let user_name = &args[1];
|
||||||
|
|
||||||
// Setup directories
|
// Setup data directory in project folder
|
||||||
let base_dir = dirs::data_local_dir()
|
let data_dir = get_data_dir();
|
||||||
.unwrap_or_else(|| PathBuf::from("."))
|
|
||||||
.join("chat-cli");
|
|
||||||
let data_dir = base_dir.join("data");
|
|
||||||
let transport_dir = base_dir.join("transport");
|
|
||||||
|
|
||||||
std::fs::create_dir_all(&data_dir).context("Failed to create data directory")?;
|
std::fs::create_dir_all(&data_dir).context("Failed to create data directory")?;
|
||||||
std::fs::create_dir_all(&transport_dir).context("Failed to create transport directory")?;
|
|
||||||
|
|
||||||
println!("Starting chat as '{}'...", user_name);
|
println!("Starting chat as '{}'...", user_name);
|
||||||
println!("Data dir: {:?}", data_dir);
|
println!("Data dir: {:?}", data_dir);
|
||||||
println!("Transport dir: {:?}", transport_dir);
|
|
||||||
|
|
||||||
// Create app
|
// Create app
|
||||||
let mut app = app::ChatApp::new(user_name, &data_dir, &transport_dir)
|
let mut app = app::ChatApp::new(user_name, &data_dir).context("Failed to create chat app")?;
|
||||||
.context("Failed to create chat app")?;
|
|
||||||
|
|
||||||
// Process any existing messages
|
|
||||||
app.process_existing()?;
|
|
||||||
|
|
||||||
// Initialize terminal UI
|
// Initialize terminal UI
|
||||||
let mut terminal = ui::init().context("Failed to initialize terminal")?;
|
let mut terminal = ui::init().context("Failed to initialize terminal")?;
|
||||||
|
|||||||
@ -1,126 +1,134 @@
|
|||||||
//! File-based transport for local chat simulation.
|
//! File-based transport for local chat communication.
|
||||||
//!
|
//!
|
||||||
//! Each user has an inbox directory where other users drop messages.
|
//! Messages are passed between users via files in a shared directory.
|
||||||
//! Messages are JSON files with envelope data.
|
|
||||||
|
|
||||||
use std::fs::{self, File};
|
use std::collections::HashSet;
|
||||||
use std::io::{Read, Write};
|
use std::fs;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::PathBuf;
|
||||||
use std::sync::mpsc::{self, Receiver, Sender};
|
|
||||||
use std::thread;
|
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use notify::{Config, RecommendedWatcher, RecursiveMode, Watcher, Event, EventKind};
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
/// A message envelope for file-based transport.
|
/// A message envelope for transport.
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct FileEnvelope {
|
pub struct MessageEnvelope {
|
||||||
pub from: String,
|
pub from: String,
|
||||||
pub data: Vec<u8>,
|
pub data: Vec<u8>,
|
||||||
pub timestamp: u64,
|
pub timestamp: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// File-based transport for simulating message passing.
|
/// File-based transport for local communication.
|
||||||
pub struct FileTransport {
|
pub struct FileTransport {
|
||||||
/// Our user name (used for inbox directory).
|
/// Our user name.
|
||||||
user_name: String,
|
user_name: String,
|
||||||
/// Base directory for all inboxes.
|
/// Base directory for transport files.
|
||||||
base_dir: PathBuf,
|
base_dir: PathBuf,
|
||||||
/// Channel for receiving incoming messages.
|
/// Our inbox directory.
|
||||||
incoming_rx: Receiver<FileEnvelope>,
|
inbox_dir: PathBuf,
|
||||||
/// Watcher handle (kept alive).
|
/// Set of processed message files (to avoid reprocessing).
|
||||||
_watcher: RecommendedWatcher,
|
processed: HashSet<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FileTransport {
|
impl FileTransport {
|
||||||
/// Create a new file transport.
|
/// Create a new file transport.
|
||||||
///
|
pub fn new(user_name: &str, data_dir: &PathBuf) -> Result<Self> {
|
||||||
/// `user_name` is used to create an inbox directory.
|
let base_dir = data_dir.join("transport");
|
||||||
/// `base_dir` is the shared directory where all user inboxes live.
|
|
||||||
pub fn new(user_name: &str, base_dir: &Path) -> Result<Self> {
|
|
||||||
let inbox_dir = base_dir.join(user_name);
|
let inbox_dir = base_dir.join(user_name);
|
||||||
|
|
||||||
|
// Create our inbox directory
|
||||||
fs::create_dir_all(&inbox_dir)
|
fs::create_dir_all(&inbox_dir)
|
||||||
.with_context(|| format!("Failed to create inbox dir: {:?}", inbox_dir))?;
|
.context("Failed to create inbox directory")?;
|
||||||
|
|
||||||
let (tx, rx) = mpsc::channel();
|
|
||||||
let watcher = Self::start_watcher(&inbox_dir, tx)?;
|
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
user_name: user_name.to_string(),
|
user_name: user_name.to_string(),
|
||||||
base_dir: base_dir.to_path_buf(),
|
base_dir,
|
||||||
incoming_rx: rx,
|
inbox_dir,
|
||||||
_watcher: watcher,
|
processed: HashSet::new(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Start watching the inbox directory for new messages.
|
/// Send a message to a specific user.
|
||||||
fn start_watcher(inbox_dir: &Path, tx: Sender<FileEnvelope>) -> Result<RecommendedWatcher> {
|
|
||||||
let mut watcher = RecommendedWatcher::new(
|
|
||||||
move |res: Result<Event, notify::Error>| {
|
|
||||||
if let Ok(event) = res {
|
|
||||||
if matches!(event.kind, EventKind::Create(_)) {
|
|
||||||
for path in event.paths {
|
|
||||||
if path.extension().map(|e| e == "json").unwrap_or(false) {
|
|
||||||
// Small delay to ensure file is fully written
|
|
||||||
thread::sleep(Duration::from_millis(50));
|
|
||||||
if let Ok(envelope) = Self::read_message(&path) {
|
|
||||||
let _ = tx.send(envelope);
|
|
||||||
// Delete the message after reading
|
|
||||||
let _ = fs::remove_file(&path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Config::default().with_poll_interval(Duration::from_millis(100)),
|
|
||||||
)?;
|
|
||||||
|
|
||||||
watcher.watch(inbox_dir, RecursiveMode::NonRecursive)?;
|
|
||||||
Ok(watcher)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read a message from a file.
|
|
||||||
fn read_message(path: &Path) -> Result<FileEnvelope> {
|
|
||||||
let mut file = File::open(path)?;
|
|
||||||
let mut contents = String::new();
|
|
||||||
file.read_to_string(&mut contents)?;
|
|
||||||
let envelope: FileEnvelope = serde_json::from_str(&contents)?;
|
|
||||||
Ok(envelope)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Send a message to another user's inbox.
|
|
||||||
pub fn send(&self, to_user: &str, data: Vec<u8>) -> Result<()> {
|
pub fn send(&self, to_user: &str, data: Vec<u8>) -> Result<()> {
|
||||||
let to_inbox = self.base_dir.join(to_user);
|
let target_dir = self.base_dir.join(to_user);
|
||||||
fs::create_dir_all(&to_inbox)?;
|
|
||||||
|
// Create target inbox if it doesn't exist
|
||||||
|
fs::create_dir_all(&target_dir)
|
||||||
|
.context("Failed to create target inbox")?;
|
||||||
|
|
||||||
let timestamp = std::time::SystemTime::now()
|
let envelope = MessageEnvelope {
|
||||||
.duration_since(std::time::UNIX_EPOCH)
|
|
||||||
.unwrap()
|
|
||||||
.as_millis() as u64;
|
|
||||||
|
|
||||||
let envelope = FileEnvelope {
|
|
||||||
from: self.user_name.clone(),
|
from: self.user_name.clone(),
|
||||||
data,
|
data,
|
||||||
timestamp,
|
timestamp: now(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let filename = format!("{}_{}.json", self.user_name, timestamp);
|
// Write message to a unique file
|
||||||
let path = to_inbox.join(filename);
|
let filename = format!("{}_{}.json", self.user_name, now());
|
||||||
|
let filepath = target_dir.join(&filename);
|
||||||
|
|
||||||
let json = serde_json::to_string_pretty(&envelope)?;
|
let json = serde_json::to_string_pretty(&envelope)?;
|
||||||
let mut file = File::create(&path)?;
|
fs::write(&filepath, json)
|
||||||
file.write_all(json.as_bytes())?;
|
.context("Failed to write message file")?;
|
||||||
file.sync_all()?;
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Try to receive an incoming message (non-blocking).
|
/// Try to receive an incoming message (non-blocking).
|
||||||
pub fn try_recv(&self) -> Option<FileEnvelope> {
|
pub fn try_recv(&mut self) -> Option<MessageEnvelope> {
|
||||||
self.incoming_rx.try_recv().ok()
|
// List files in our inbox
|
||||||
|
let entries = match fs::read_dir(&self.inbox_dir) {
|
||||||
|
Ok(e) => e,
|
||||||
|
Err(_) => return None,
|
||||||
|
};
|
||||||
|
|
||||||
|
for entry in entries.flatten() {
|
||||||
|
let path = entry.path();
|
||||||
|
|
||||||
|
// Skip non-json files
|
||||||
|
if path.extension().map(|e| e != "json").unwrap_or(true) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let filename = path.file_name()
|
||||||
|
.and_then(|n| n.to_str())
|
||||||
|
.unwrap_or("")
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
// Skip already processed files
|
||||||
|
if self.processed.contains(&filename) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to read and parse the message
|
||||||
|
if let Ok(contents) = fs::read_to_string(&path) {
|
||||||
|
if let Ok(envelope) = serde_json::from_str::<MessageEnvelope>(&contents) {
|
||||||
|
// Mark as processed and delete
|
||||||
|
self.processed.insert(filename);
|
||||||
|
let _ = fs::remove_file(&path);
|
||||||
|
return Some(envelope);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// List available peers (users with inbox directories).
|
||||||
|
pub fn list_peers(&self) -> Vec<String> {
|
||||||
|
let mut peers = Vec::new();
|
||||||
|
|
||||||
|
if let Ok(entries) = fs::read_dir(&self.base_dir) {
|
||||||
|
for entry in entries.flatten() {
|
||||||
|
if entry.path().is_dir() {
|
||||||
|
if let Some(name) = entry.file_name().to_str() {
|
||||||
|
if name != self.user_name {
|
||||||
|
peers.push(name.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
peers
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get our user name.
|
/// Get our user name.
|
||||||
@ -128,26 +136,11 @@ impl FileTransport {
|
|||||||
pub fn user_name(&self) -> &str {
|
pub fn user_name(&self) -> &str {
|
||||||
&self.user_name
|
&self.user_name
|
||||||
}
|
}
|
||||||
|
}
|
||||||
/// Process any existing messages in inbox on startup.
|
|
||||||
pub fn process_existing_messages(&self) -> Vec<FileEnvelope> {
|
fn now() -> u64 {
|
||||||
let inbox_dir = self.base_dir.join(&self.user_name);
|
std::time::SystemTime::now()
|
||||||
let mut messages = Vec::new();
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.unwrap()
|
||||||
if let Ok(entries) = fs::read_dir(&inbox_dir) {
|
.as_millis() as u64
|
||||||
for entry in entries.flatten() {
|
|
||||||
let path = entry.path();
|
|
||||||
if path.extension().map(|e| e == "json").unwrap_or(false) {
|
|
||||||
if let Ok(envelope) = Self::read_message(&path) {
|
|
||||||
messages.push(envelope);
|
|
||||||
let _ = fs::remove_file(&path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort by timestamp
|
|
||||||
messages.sort_by_key(|m| m.timestamp);
|
|
||||||
messages
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -54,9 +54,9 @@ pub fn draw(frame: &mut Frame, app: &ChatApp) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn draw_header(frame: &mut Frame, app: &ChatApp, area: Rect) {
|
fn draw_header(frame: &mut Frame, app: &ChatApp, area: Rect) {
|
||||||
let title = match &app.remote_user {
|
let title = match app.current_session() {
|
||||||
Some(remote) => format!(" 💬 Chat: {} ↔ {} ", app.user_name, remote),
|
Some(session) => format!(" 💬 Chat: {} ↔ {} ", app.user_name, session.remote_user),
|
||||||
None => format!(" 💬 {} (no active chat) ", app.user_name),
|
None => format!(" 💬 {} (no active chat - use /connect or /chats) ", app.user_name),
|
||||||
};
|
};
|
||||||
|
|
||||||
let header = Paragraph::new(title)
|
let header = Paragraph::new(title)
|
||||||
@ -67,35 +67,37 @@ fn draw_header(frame: &mut Frame, app: &ChatApp, area: Rect) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn draw_messages(frame: &mut Frame, app: &ChatApp, area: Rect) {
|
fn draw_messages(frame: &mut Frame, app: &ChatApp, area: Rect) {
|
||||||
|
let remote_name = app
|
||||||
|
.current_session()
|
||||||
|
.map(|s| s.remote_user.as_str())
|
||||||
|
.unwrap_or("Them");
|
||||||
|
|
||||||
let messages: Vec<ListItem> = app
|
let messages: Vec<ListItem> = app
|
||||||
.messages
|
.messages()
|
||||||
.iter()
|
.iter()
|
||||||
.map(|msg| {
|
.map(|msg| {
|
||||||
let (prefix, style) = if msg.from_self {
|
let (prefix, style) = if msg.from_self {
|
||||||
("You: ", Style::default().fg(Color::Green))
|
("You", Style::default().fg(Color::Green))
|
||||||
} else {
|
} else {
|
||||||
let name = app.remote_user.as_deref().unwrap_or("Them");
|
(remote_name, Style::default().fg(Color::Yellow))
|
||||||
(name, Style::default().fg(Color::Yellow))
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let content = if msg.from_self {
|
let content = Line::from(vec![
|
||||||
Line::from(vec![
|
Span::styled(format!("{}: ", prefix), style.add_modifier(Modifier::BOLD)),
|
||||||
Span::styled(prefix, style.add_modifier(Modifier::BOLD)),
|
Span::raw(&msg.content),
|
||||||
Span::raw(&msg.content),
|
]);
|
||||||
])
|
|
||||||
} else {
|
|
||||||
Line::from(vec![
|
|
||||||
Span::styled(format!("{}: ", prefix), style.add_modifier(Modifier::BOLD)),
|
|
||||||
Span::raw(&msg.content),
|
|
||||||
])
|
|
||||||
};
|
|
||||||
|
|
||||||
ListItem::new(content)
|
ListItem::new(content)
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
let title = match app.current_session() {
|
||||||
|
Some(session) => format!(" Messages with {} ", session.remote_user),
|
||||||
|
None => " Messages ".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
let messages_widget = List::new(messages)
|
let messages_widget = List::new(messages)
|
||||||
.block(Block::default().title(" Messages ").borders(Borders::ALL));
|
.block(Block::default().title(title).borders(Borders::ALL));
|
||||||
|
|
||||||
frame.render_widget(messages_widget, area);
|
frame.render_widget(messages_widget, area);
|
||||||
}
|
}
|
||||||
@ -134,6 +136,10 @@ pub fn handle_events(app: &mut ChatApp) -> io::Result<bool> {
|
|||||||
|
|
||||||
match key.code {
|
match key.code {
|
||||||
KeyCode::Esc => return Ok(false),
|
KeyCode::Esc => return Ok(false),
|
||||||
|
// Handle Ctrl+C
|
||||||
|
KeyCode::Char('c') if key.modifiers.contains(event::KeyModifiers::CONTROL) => {
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
KeyCode::Enter => {
|
KeyCode::Enter => {
|
||||||
if !app.input.is_empty() {
|
if !app.input.is_empty() {
|
||||||
let input = std::mem::take(&mut app.input);
|
let input = std::mem::take(&mut app.input);
|
||||||
@ -151,7 +157,7 @@ pub fn handle_events(app: &mut ChatApp) -> io::Result<bool> {
|
|||||||
app.status = format!("Error: {}", e);
|
app.status = format!("Error: {}", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if app.current_chat_id.is_some() {
|
} else if app.current_session().is_some() {
|
||||||
if let Err(e) = app.send_message(&input) {
|
if let Err(e) = app.send_message(&input) {
|
||||||
app.status = format!("Send error: {}", e);
|
app.status = format!("Send error: {}", e);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -39,7 +39,7 @@ impl TryFrom<&[u8]> for Introduction {
|
|||||||
let str_value = String::from_utf8_lossy(value);
|
let str_value = String::from_utf8_lossy(value);
|
||||||
let parts: Vec<&str> = str_value.splitn(3, ':').collect();
|
let parts: Vec<&str> = str_value.splitn(3, ':').collect();
|
||||||
|
|
||||||
if parts[0] != "Bundle" {
|
if parts.len() < 3 || parts[0] != "Bundle" {
|
||||||
return Err(ChatError::BadBundleValue(
|
return Err(ChatError::BadBundleValue(
|
||||||
"not recognized as an introduction bundle".into(),
|
"not recognized as an introduction bundle".into(),
|
||||||
));
|
));
|
||||||
@ -51,7 +51,7 @@ impl TryFrom<&[u8]> for Introduction {
|
|||||||
.map_err(|_| ChatError::InvalidKeyLength)?;
|
.map_err(|_| ChatError::InvalidKeyLength)?;
|
||||||
let installation_key = PublicKey::from(installation_bytes);
|
let installation_key = PublicKey::from(installation_bytes);
|
||||||
|
|
||||||
let ephemeral_bytes: [u8; 32] = hex::decode(parts[1])
|
let ephemeral_bytes: [u8; 32] = hex::decode(parts[2])
|
||||||
.map_err(|_| ChatError::BadParsing("ephemeral_key"))?
|
.map_err(|_| ChatError::BadParsing("ephemeral_key"))?
|
||||||
.try_into()
|
.try_into()
|
||||||
.map_err(|_| ChatError::InvalidKeyLength)?;
|
.map_err(|_| ChatError::InvalidKeyLength)?;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user