Compare commits

...

19 commits

Author SHA1 Message Date
Joe Ardent
00092dc97b deadlock less 2025-09-04 11:48:38 -07:00
Joe Ardent
6167522aaa ready for new release, 1.61803398 2025-09-03 15:37:06 -07:00
Joe Ardent
8572f1431e remove unsafe code, add CWD to file picking and fuzzy selecting 2025-09-03 15:32:32 -07:00
Joe Ardent
4d78d67abe release 1.6180339, with MSRV 1.89 2025-08-22 12:28:06 -07:00
Joe Ardent
3eb6169a7b add MSRV 2025-08-22 12:26:17 -07:00
Joe Ardent
04b6388b2c release 1.618033 2025-08-21 12:11:30 -07:00
Joe Ardent
77d44b8868 update simsearch, fill out help screen 2025-08-21 12:09:33 -07:00
Joe Ardent
2b1e0f7cb8 new version without yanked slab dep 2025-08-19 13:53:09 -07:00
Joe Ardent
bd075c50cf new version with all the other improvements 2025-08-19 13:16:38 -07:00
Joe Ardent
bdc5382aa3 move file finder into its own submodule 2025-08-18 17:38:58 -07:00
Joe Ardent
e0738db7d2 show best matches asap in fuzzy file finder 2025-08-18 17:14:28 -07:00
Joe Ardent
46435b3796 don't info log the config 2025-08-17 21:00:17 -07:00
Joe Ardent
3595f0bbc6 little bit of code shuffle 2025-08-16 15:55:24 -07:00
Joe Ardent
6feb6f8ab8 better multicast 2025-08-16 13:46:17 -07:00
Joe Ardent
bb6241ac97 start of help screen 2025-08-15 21:48:50 -07:00
Joe Ardent
bcc485f2c0 release 1.618 2025-08-15 15:59:40 -07:00
Joe Ardent
5e40f29294 don't loop multicast back 2025-08-15 15:54:03 -07:00
Joe Ardent
9b1734f8c0 move event handling code into submodule of app 2025-08-15 14:57:27 -07:00
Joe Ardent
5f09268c45 prepare for new release 2025-08-14 18:15:34 -07:00
16 changed files with 814 additions and 611 deletions

269
Cargo.lock generated
View file

@ -105,13 +105,13 @@ checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457"
[[package]]
name = "async-trait"
version = "0.1.88"
version = "0.1.89"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5"
checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.104",
"syn 2.0.106",
]
[[package]]
@ -137,9 +137,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]]
name = "aws-lc-rs"
version = "1.13.2"
version = "1.13.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08b5d4e069cbc868041a64bd68dc8cb39a0d79585cd6c5a24caa8c2d622121be"
checksum = "5c953fe1ba023e6b7730c0d4b031d06f267f23a46167dcbd40316644b10a17ba"
dependencies = [
"aws-lc-sys",
"zeroize",
@ -221,7 +221,7 @@ checksum = "604fde5e028fea851ce1d8570bbdc034bec850d157f7569d10f347d06808c05c"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.104",
"syn 2.0.106",
]
[[package]]
@ -286,15 +286,15 @@ dependencies = [
"regex",
"rustc-hash",
"shlex",
"syn 2.0.104",
"syn 2.0.106",
"which",
]
[[package]]
name = "bitflags"
version = "2.9.1"
version = "2.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394"
[[package]]
name = "block-buffer"
@ -313,9 +313,9 @@ checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
[[package]]
name = "bytemuck"
version = "1.23.1"
version = "1.23.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c76a5792e44e4abe34d3abf15636779261d45a7450612059293d1d2cfc63422"
checksum = "3995eaeebcdf32f91f980d360f78732ddc061097ab4e39991ae7a6ace9194677"
[[package]]
name = "byteorder"
@ -346,10 +346,11 @@ dependencies = [
[[package]]
name = "cc"
version = "1.2.31"
version = "1.2.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3a42d84bb6b69d3a8b3eaacf0d88f179e1929695e1ad012b6cf64d9caaa5fd2"
checksum = "590f9024a68a8c40351881787f1934dc11afd69090f5edb6831464694d836ea3"
dependencies = [
"find-msvc-tools",
"jobserver",
"libc",
"shlex",
@ -366,9 +367,9 @@ dependencies = [
[[package]]
name = "cfg-if"
version = "1.0.1"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268"
checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9"
[[package]]
name = "chrono"
@ -397,9 +398,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.5.43"
version = "4.5.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50fd97c9dc2399518aa331917ac6f274280ec5eb34e555dd291899745c48ec6f"
checksum = "7eac00902d9d136acd712710d71823fb8ac8004ca445a89e73a41d45aa712931"
dependencies = [
"clap_builder",
"clap_derive",
@ -407,26 +408,26 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.5.43"
version = "4.5.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c35b5830294e1fa0462034af85cc95225a4cb07092c088c55bda3147cfcd8f65"
checksum = "2ad9bbf750e73b5884fb8a211a9424a1906c1e156724260fdae972f31d70e1d6"
dependencies = [
"anstream",
"anstyle",
"clap_lex",
"strsim 0.11.1",
"strsim",
]
[[package]]
name = "clap_derive"
version = "4.5.41"
version = "4.5.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef4f52386a59ca4c860f7393bcf8abd8dfd91ecccc0f774635ff68e92eeef491"
checksum = "bbfd7eae0b0f1a6e63d4b13c9c478de77c2eb546fba158ad50b4203dc24b9f9c"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn 2.0.104",
"syn 2.0.106",
]
[[package]]
@ -545,8 +546,8 @@ dependencies = [
"ident_case",
"proc-macro2",
"quote",
"strsim 0.11.1",
"syn 2.0.104",
"strsim",
"syn 2.0.106",
]
[[package]]
@ -557,14 +558,14 @@ checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead"
dependencies = [
"darling_core",
"quote",
"syn 2.0.104",
"syn 2.0.106",
]
[[package]]
name = "deranged"
version = "0.4.0"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e"
checksum = "d630bccd429a5bb5a64b5e94f693bfc48c9f8566418fda4c494cc94f911f87cc"
dependencies = [
"powerfmt",
]
@ -608,7 +609,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.104",
"syn 2.0.106",
]
[[package]]
@ -626,7 +627,7 @@ dependencies = [
"enum-ordinalize",
"proc-macro2",
"quote",
"syn 2.0.104",
"syn 2.0.106",
]
[[package]]
@ -661,7 +662,7 @@ checksum = "0d28318a75d4aead5c4db25382e8ef717932d0346600cacae6357eb5941bc5ff"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.104",
"syn 2.0.106",
]
[[package]]
@ -712,6 +713,12 @@ dependencies = [
"version_check",
]
[[package]]
name = "find-msvc-tools"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e178e4fba8a2726903f6ba98a6d221e76f9c12c650d5dc0e6afdc50677b49650"
[[package]]
name = "fnv"
version = "1.0.7"
@ -741,9 +748,9 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
[[package]]
name = "form_urlencoded"
version = "1.2.1"
version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456"
checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
dependencies = [
"percent-encoding",
]
@ -820,7 +827,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.104",
"syn 2.0.106",
]
[[package]]
@ -892,7 +899,7 @@ dependencies = [
"cfg-if",
"libc",
"r-efi",
"wasi 0.14.2+wasi-0.2.4",
"wasi 0.14.3+wasi-0.2.4",
]
[[package]]
@ -903,9 +910,9 @@ checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
[[package]]
name = "glob"
version = "0.3.2"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2"
checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
[[package]]
name = "h2"
@ -928,9 +935,9 @@ dependencies = [
[[package]]
name = "hashbrown"
version = "0.15.4"
version = "0.15.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5"
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
dependencies = [
"allocator-api2",
"equivalent",
@ -1006,13 +1013,14 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
[[package]]
name = "hyper"
version = "1.6.0"
version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80"
checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e"
dependencies = [
"atomic-waker",
"bytes",
"futures-channel",
"futures-util",
"futures-core",
"h2",
"http",
"http-body",
@ -1020,6 +1028,7 @@ dependencies = [
"httpdate",
"itoa",
"pin-project-lite",
"pin-utils",
"smallvec",
"tokio",
"want",
@ -1201,9 +1210,9 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
[[package]]
name = "idna"
version = "1.0.3"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e"
checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"
dependencies = [
"idna_adapter",
"smallvec",
@ -1222,9 +1231,9 @@ dependencies = [
[[package]]
name = "indexmap"
version = "2.10.0"
version = "2.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661"
checksum = "f2481980430f9f78649238835720ddccc57e52df14ffce1c6f37391d61b563e9"
dependencies = [
"equivalent",
"hashbrown",
@ -1252,14 +1261,14 @@ dependencies = [
"indoc",
"proc-macro2",
"quote",
"syn 2.0.104",
"syn 2.0.106",
]
[[package]]
name = "io-uring"
version = "0.7.9"
version = "0.7.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d93587f37623a1a17d94ef2bc9ada592f5465fe7732084ab7beefabe5c77c0c4"
checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b"
dependencies = [
"bitflags",
"cfg-if",
@ -1314,9 +1323,9 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
[[package]]
name = "jobserver"
version = "0.1.33"
version = "0.1.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a"
checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"
dependencies = [
"getrandom 0.3.3",
"libc",
@ -1324,7 +1333,7 @@ dependencies = [
[[package]]
name = "jocalsend"
version = "1.0.0"
version = "1.6.1803398"
dependencies = [
"axum",
"axum-server",
@ -1393,9 +1402,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
[[package]]
name = "libc"
version = "0.2.174"
version = "0.2.175"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776"
checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543"
[[package]]
name = "libloading"
@ -1459,9 +1468,9 @@ dependencies = [
[[package]]
name = "log"
version = "0.4.27"
version = "0.4.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432"
[[package]]
name = "lru"
@ -1571,9 +1580,9 @@ dependencies = [
[[package]]
name = "network-interface"
version = "2.0.2"
version = "2.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "862f41f1276e7148fb597fc55ed8666423bebe045199a1298c3515a73ec5cdd9"
checksum = "07709a6d4eba90ab10ec170a0530b3aafc81cb8a2d380e4423ae41fc55fe5745"
dependencies = [
"cc",
"libc",
@ -1651,7 +1660,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.104",
"syn 2.0.106",
]
[[package]]
@ -1727,7 +1736,7 @@ dependencies = [
"proc-macro2",
"proc-macro2-diagnostics",
"quote",
"syn 2.0.104",
"syn 2.0.106",
]
[[package]]
@ -1742,9 +1751,9 @@ dependencies = [
[[package]]
name = "percent-encoding"
version = "2.3.1"
version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
[[package]]
name = "pin-project-lite"
@ -1766,9 +1775,9 @@ checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
[[package]]
name = "potential_utf"
version = "0.1.2"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585"
checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a"
dependencies = [
"zerovec",
]
@ -1790,19 +1799,19 @@ dependencies = [
[[package]]
name = "prettyplease"
version = "0.2.35"
version = "0.2.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "061c1221631e079b26479d25bbf2275bfe5917ae8419cd7e34f13bfc2aa7539a"
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
dependencies = [
"proc-macro2",
"syn 2.0.104",
"syn 2.0.106",
]
[[package]]
name = "proc-macro2"
version = "1.0.95"
version = "1.0.101"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de"
dependencies = [
"unicode-ident",
]
@ -1815,7 +1824,7 @@ checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.104",
"syn 2.0.106",
"version_check",
"yansi",
]
@ -1931,9 +1940,9 @@ dependencies = [
[[package]]
name = "regex"
version = "1.11.1"
version = "1.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912"
dependencies = [
"aho-corasick",
"memchr",
@ -1943,9 +1952,9 @@ dependencies = [
[[package]]
name = "regex-automata"
version = "0.4.9"
version = "0.4.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
checksum = "6b9458fa0bfeeac22b5ca447c63aaf45f28439a709ccd244698632f9aa6394d6"
dependencies = [
"aho-corasick",
"memchr",
@ -1954,15 +1963,15 @@ dependencies = [
[[package]]
name = "regex-syntax"
version = "0.8.5"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001"
[[package]]
name = "reqwest"
version = "0.12.22"
version = "0.12.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cbc931937e6ca3a06e3b6c0aa7841849b160a90351d6ab467a8b9b9959767531"
checksum = "d429f34c8092b2d42c7c93cec323bb4adeb7c67698f70839adec842ec10c7ceb"
dependencies = [
"base64",
"bytes",
@ -2097,9 +2106,9 @@ dependencies = [
[[package]]
name = "rustversion"
version = "1.0.21"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d"
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
[[package]]
name = "ryu"
@ -2162,14 +2171,14 @@ checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.104",
"syn 2.0.106",
]
[[package]]
name = "serde_json"
version = "1.0.142"
version = "1.0.143"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "030fedb782600dcbd6f02d479bf0d817ac3bb40d644745b769d6a96bc3afc5a7"
checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a"
dependencies = [
"itoa",
"memchr",
@ -2279,19 +2288,19 @@ dependencies = [
[[package]]
name = "simsearch"
version = "0.2.5"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c869b25830e4824ef7279015cfc298a0674aca6a54eeff2efce8d12bf3701fe"
checksum = "629d21c4ebf25655995cda9eb93e85539fa68b0438acb85e9e5d10f6fe2404bc"
dependencies = [
"strsim 0.10.0",
"strsim",
"triple_accel",
]
[[package]]
name = "slab"
version = "0.4.10"
version = "0.4.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d"
checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589"
[[package]]
name = "smallvec"
@ -2321,12 +2330,6 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]]
name = "strsim"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]]
name = "strsim"
version = "0.11.1"
@ -2352,7 +2355,7 @@ dependencies = [
"proc-macro2",
"quote",
"rustversion",
"syn 2.0.104",
"syn 2.0.106",
]
[[package]]
@ -2374,9 +2377,9 @@ dependencies = [
[[package]]
name = "syn"
version = "2.0.104"
version = "2.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40"
checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6"
dependencies = [
"proc-macro2",
"quote",
@ -2400,7 +2403,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.104",
"syn 2.0.106",
]
[[package]]
@ -2426,42 +2429,42 @@ dependencies = [
[[package]]
name = "tempfile"
version = "3.20.0"
version = "3.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1"
checksum = "15b61f8f20e3a6f7e0649d825294eaf317edce30f82cf6026e7e4cb9222a7d1e"
dependencies = [
"fastrand",
"getrandom 0.3.3",
"once_cell",
"rustix 1.0.8",
"windows-sys 0.59.0",
"windows-sys 0.60.2",
]
[[package]]
name = "thiserror"
version = "2.0.12"
version = "2.0.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "2.0.12"
version = "2.0.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.104",
"syn 2.0.106",
]
[[package]]
name = "time"
version = "0.3.41"
version = "0.3.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40"
checksum = "83bde6f1ec10e72d583d91623c939f623002284ef622b87de38cfd546cbf2031"
dependencies = [
"deranged",
"num-conv",
@ -2472,9 +2475,9 @@ dependencies = [
[[package]]
name = "time-core"
version = "0.1.4"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c"
checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b"
[[package]]
name = "tinystr"
@ -2512,7 +2515,7 @@ checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.104",
"syn 2.0.106",
]
[[package]]
@ -2697,9 +2700,9 @@ dependencies = [
[[package]]
name = "triple_accel"
version = "0.3.4"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "622b09ce2fe2df4618636fb92176d205662f59803f39e70d1c333393082de96c"
checksum = "22048bc95dfb2ffd05b1ff9a756290a009224b60b2f0e7525faeee7603851e63"
[[package]]
name = "try-lock"
@ -2797,13 +2800,14 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]]
name = "url"
version = "2.5.4"
version = "2.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60"
checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b"
dependencies = [
"form_urlencoded",
"idna",
"percent-encoding",
"serde",
]
[[package]]
@ -2847,11 +2851,11 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]]
name = "wasi"
version = "0.14.2+wasi-0.2.4"
version = "0.14.3+wasi-0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3"
checksum = "6a51ae83037bdd272a9e28ce236db8c07016dd0d50c27038b3f407533c030c95"
dependencies = [
"wit-bindgen-rt",
"wit-bindgen",
]
[[package]]
@ -2876,7 +2880,7 @@ dependencies = [
"log",
"proc-macro2",
"quote",
"syn 2.0.104",
"syn 2.0.106",
"wasm-bindgen-shared",
]
@ -2911,7 +2915,7 @@ checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.104",
"syn 2.0.106",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
@ -2990,7 +2994,7 @@ checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.104",
"syn 2.0.106",
]
[[package]]
@ -3001,7 +3005,7 @@ checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.104",
"syn 2.0.106",
]
[[package]]
@ -3197,21 +3201,18 @@ checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486"
[[package]]
name = "winnow"
version = "0.7.12"
version = "0.7.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95"
checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf"
dependencies = [
"memchr",
]
[[package]]
name = "wit-bindgen-rt"
version = "0.39.0"
name = "wit-bindgen"
version = "0.45.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
dependencies = [
"bitflags",
]
checksum = "052283831dbae3d879dc7f51f3d92703a316ca49f91540417d38591826127814"
[[package]]
name = "writeable"
@ -3254,7 +3255,7 @@ checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.104",
"syn 2.0.106",
"synstructure",
]
@ -3275,7 +3276,7 @@ checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.104",
"syn 2.0.106",
]
[[package]]
@ -3295,7 +3296,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.104",
"syn 2.0.106",
"synstructure",
]
@ -3335,5 +3336,5 @@ checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.104",
"syn 2.0.106",
]

View file

@ -1,10 +1,13 @@
[package]
name = "jocalsend"
version = "1.0.0"
# 1.61803398874989484
#----------^
version = "1.6.1803398"
edition = "2024"
rust-version = "1.89"
authors = ["Joe Ardent <code@ardent.nebcorp.com>"]
keywords = ["p2p", "localsend", "tui", "linux"]
description = "A terminal implementation of the LocalSend protocol"
description = "A TUI for LocalSend"
readme = "README.md"
license-file = "LICENSE.md"
repository = "https://git.kittencollective.com/nebkor/joecalsend"
@ -32,7 +35,7 @@ rustix = { version = "1", default-features = false, features = ["system"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
sha256 = "1.6"
simsearch = "0.2"
simsearch = "0.3"
thiserror = "2"
tokio = { version = "1", default-features = false, features = ["time", "macros", "rt-multi-thread"] }
tokio-rustls = { version = "0.26", default-features = false, features = ["tls12", "logging"] }

View file

@ -10,7 +10,12 @@ that uses [Ratatui](https://github.com/ratatui/ratatui) to provide an interactiv
application, and is compatible with the official app.
Install with `cargo install jocalsend` (requires [Rust](https://rustup.rs/)); tested on Linux, it
will probably work on Macs but if you're on a Mac, you probably have AirDrop.
will probably work on Macs but if you're on a Mac, you probably have AirDrop. It's also available in
nixpkgs, and so if you're a NixOS user, `nix-shell -p jocalsend` will do what you expect.
## BLOG POSTS!
- [Announcement post](https://proclamations.nebcorp-hias.com/sundries/jocalsend/)
- [Design and development](https://proclamations.nebcorp-hias.com/rnd/jocalsend-development/)
## Capabilities and screenshots
@ -22,18 +27,21 @@ available:
- `S` -> go to the sending screen, defaulting to sending files
- `R` -> go to the receiving screen to approve or deny incoming transfers
- `L` -> go to the logging screen where you can adjust the log level
- `C` -> clear the list of local peers and re-discover them
- `H` or `?` -> go to help screen
- `ESC` -> go back to the previous screen
- `Q` -> exit the application
Additionally, when in the sending screen, the following are available
When in the sending screen, the following are available
- `TAB` -> switch between content selection and peer selection
- `P` -> switch to peer selection
- `T` -> switch to entering text to send
- `F` -> switch to selecting files to send (not available when entering text, use `ESC` to exit text entry)
- `T` -> enter text directly to send, `ESC` to cancel
- `/` -> fuzzy filename search, use `ESC` to stop inputting text
In addition to the interactive commands, it will also accept commandline arguments to pre-select a
file or pre-populate text to send:
When in the receiving screen, use `A` to approve the incoming transfer request, or `D` to deny it.
Finally, it will also accept commandline arguments to pre-select a file or pre-populate text to
send:
```
$ jocalsend -h

1
VERSION Normal file
View file

@ -0,0 +1 @@
1.61803398

31
VERSIONING.md Normal file
View file

@ -0,0 +1,31 @@
# Golden Versioning
This software is versioned under a scheme I call "goldver", as an homage to the
vastly inferior [semver](https://semver.org).
## What does "goldver" mean?
When projects are versioned with goldver, the first version is "1". Note that it
is not "1.0", or, "1.0-prealpha-release-preview", or anything nonsensical like
that. As new versions are released, decimals from *phi*, the [Golden
Ratio](https://en.wikipedia.org/wiki/Golden_ratio), are appended after an
initial decimal point. So the second released version will be "1.6", the third
would be "1.61", etc., and on until perfection is asymptotically approached as
the number of released versions goes to infinity.
## Wait, didn't Donald Knuth do this?
No! He uses [pi for TeX and e for MetaFont](https://texfaq.org/FAQ-TeXfuture),
obviously COMPLETELY different.
## Ok.
Cool.
## What version is JocalSend now?
Canonically, see the `VERSION` file. Heretically, once there have been
at enough releases, the version string in the `Cargo.toml` file will
always be of the form "1.6.x", where *x* is at least one digit long, starting
with "1". Each subsequent release will append the next digit of *phi* to
*x*.

72
src/app/file_finder.rs Normal file
View file

@ -0,0 +1,72 @@
use std::path::{Path, PathBuf};
use crossterm::event::Event;
use jocalsend::error::Result;
use ratatui::widgets::WidgetRef;
use ratatui_explorer::FileExplorer;
use simsearch::{SearchOptions, SimSearch};
use tui_input::Input;
#[derive(Clone)]
pub(crate) struct FileFinder {
pub explorer: FileExplorer,
pub fuzzy: SimSearch<usize>,
pub working_dir: Option<PathBuf>,
pub input: Input,
}
impl FileFinder {
pub fn new() -> Result<Self> {
let fuzzy = SimSearch::new_with(
SearchOptions::new()
.stop_words(vec![std::path::MAIN_SEPARATOR_STR.to_string()])
.stop_whitespace(false)
.threshold(0.0),
);
Ok(Self {
explorer: FileExplorer::new()?,
fuzzy,
working_dir: None,
input: Default::default(),
})
}
pub fn handle(&mut self, event: &Event) -> Result<()> {
self.index();
Ok(self.explorer.handle(event)?)
}
pub fn cwd(&self) -> &Path {
self.explorer.cwd()
}
pub fn set_cwd(&mut self, cwd: &Path) -> Result<()> {
self.explorer.set_cwd(cwd)?;
self.index();
Ok(())
}
pub fn widget(&self) -> impl WidgetRef {
self.explorer.widget()
}
pub fn reset_fuzzy(&mut self) {
self.fuzzy.clear();
self.input.reset();
}
pub fn index(&mut self) {
if let Some(owd) = self.working_dir.as_ref()
&& owd == self.cwd()
{
return;
}
self.working_dir = Some(self.cwd().to_path_buf());
self.reset_fuzzy();
for (i, f) in self.explorer.files().iter().enumerate() {
self.fuzzy.insert(i, f.name());
}
}
}

341
src/app/handle.rs Normal file
View file

@ -0,0 +1,341 @@
use crossterm::event::{Event, KeyCode, KeyEvent};
use jocalsend::ReceiveDialog;
use log::{debug, error, warn};
use tui_input::backend::crossterm::EventHandler;
use crate::app::{App, CurrentScreen, FileMode, SendingScreen};
impl App {
pub(super) async fn handle_key_event(
&mut self,
key_event: KeyEvent,
event: crossterm::event::Event,
) {
let code = key_event.code;
let mode = self.screen();
match mode {
CurrentScreen::Main
| CurrentScreen::Help
| CurrentScreen::Logging
| CurrentScreen::Receiving => match code {
KeyCode::Char('q') => self.exit().await,
KeyCode::Char('s') => self.send(),
KeyCode::Char('r') => self.recv(),
KeyCode::Char('l') => self.logs(),
KeyCode::Char('m') => self.main(),
KeyCode::Char('h') | KeyCode::Char('?') => self.help(),
KeyCode::Char('c') => self.service.clear_peers().await,
KeyCode::Esc => self.pop(),
_ => match mode {
CurrentScreen::Main | CurrentScreen::Help => {}
CurrentScreen::Logging => match code {
KeyCode::Left => change_log_level(LogDelta::Down),
KeyCode::Right => change_log_level(LogDelta::Up),
_ => {}
},
CurrentScreen::Receiving => match code {
KeyCode::Up => self.receiving_state.select_previous(),
KeyCode::Down => self.receiving_state.select_next(),
KeyCode::Char('a') => self.accept(),
KeyCode::Char('d') => self.deny(),
_ => {}
},
CurrentScreen::Stopping | CurrentScreen::Sending(_) => unreachable!(),
},
},
CurrentScreen::Sending(_) => self.sending_screen(key_event, event).await,
CurrentScreen::Stopping => {}
}
}
async fn sending_screen(&mut self, key_event: KeyEvent, event: Event) {
let mode = self.screen();
let CurrentScreen::Sending(mode) = mode else {
return;
};
let code = key_event.code;
match mode {
SendingScreen::Peers => match code {
KeyCode::Char('q') => self.exit().await,
KeyCode::Char('s') => self.send(),
KeyCode::Char('r') => self.recv(),
KeyCode::Char('l') => self.logs(),
KeyCode::Char('m') => self.main(),
KeyCode::Char('h') | KeyCode::Char('?') => self.help(),
KeyCode::Char('c') => self.service.clear_peers().await,
KeyCode::Char('t') => self.sending_text(),
KeyCode::Tab => self.sending_files(),
KeyCode::Enter => self.send_content().await,
KeyCode::Esc => self.pop(),
_ => {}
},
SendingScreen::Files(FileMode::Picking) => match code {
KeyCode::Char('q') => self.exit().await,
KeyCode::Char('s') => self.send(),
KeyCode::Char('r') => self.recv(),
KeyCode::Char('l') => self.logs(),
KeyCode::Char('m') => self.main(),
KeyCode::Char('h') | KeyCode::Char('?') => self.help(),
KeyCode::Char('c') => self.service.clear_peers().await,
KeyCode::Char('t') => self.sending_text(),
KeyCode::Tab => self.sending_peers(),
KeyCode::Enter => self.chdir_or_send_file().await,
KeyCode::Esc => self.pop(),
KeyCode::Char('/') => self.sending_fuzzy(),
_ => self.file_finder.handle(&event).unwrap_or_default(),
},
SendingScreen::Files(FileMode::Fuzzy) => match code {
KeyCode::Tab => self.sending_peers(),
KeyCode::Enter => self.chdir_or_send_file().await,
KeyCode::Esc => {
self.file_finder.reset_fuzzy();
self.sending_files();
}
KeyCode::Up | KeyCode::Down => {
if let Err(e) = self.file_finder.handle(&event) {
log::error!("error selecting file: {e:?}");
}
}
_ => {
self.file_finder.index();
if let Some(changed) = self.file_finder.input.handle_event(&event)
&& changed.value
{
let id = self
.file_finder
.fuzzy
.search(self.file_finder.input.value())
.first()
.copied()
.unwrap_or(0);
self.file_finder.explorer.set_selected_idx(id);
}
}
},
SendingScreen::Text => match code {
KeyCode::Tab => self.sending_peers(),
KeyCode::Enter => self.send_text().await,
KeyCode::Esc => {
self.text = None;
self.input.reset();
self.sending_files();
}
_ => {
if let Some(changed) = self.input.handle_event(&event)
&& changed.value
{
if self.input.value().is_empty() {
self.text = None;
} else {
self.text = Some(self.input.to_string());
}
}
}
},
}
}
pub async fn exit(&mut self) {
self.screen.push(CurrentScreen::Stopping);
self.service.stop().await;
}
pub fn send(&mut self) {
let last = self.screen.last();
match last {
Some(CurrentScreen::Sending(_)) => {}
_ => self
.screen
.push(CurrentScreen::Sending(SendingScreen::Files(
FileMode::Picking,
))),
}
}
pub fn recv(&mut self) {
let last = self.screen.last();
match last {
Some(CurrentScreen::Receiving) => {}
_ => self.screen.push(CurrentScreen::Receiving),
}
}
pub fn logs(&mut self) {
let last = self.screen.last();
match last {
Some(CurrentScreen::Logging) => {}
_ => self.screen.push(CurrentScreen::Logging),
}
}
pub fn pop(&mut self) {
self.screen.pop();
if self.screen.last().is_none() {
self.screen.push(CurrentScreen::Main);
}
}
pub fn main(&mut self) {
let last = self.screen.last();
match last {
Some(CurrentScreen::Main) => {}
_ => self.screen.push(CurrentScreen::Main),
}
}
pub fn help(&mut self) {
let last = self.screen.last();
match last {
Some(CurrentScreen::Help) => {}
_ => self.screen.push(CurrentScreen::Help),
}
}
fn sending_peers(&mut self) {
if let CurrentScreen::Sending(mode) = self.screen_mut() {
*mode = SendingScreen::Peers;
}
}
fn sending_text(&mut self) {
if let CurrentScreen::Sending(mode) = self.screen_mut() {
*mode = SendingScreen::Text;
}
}
fn sending_fuzzy(&mut self) {
if let CurrentScreen::Sending(mode) = self.screen_mut() {
*mode = SendingScreen::Files(FileMode::Fuzzy);
}
}
fn sending_files(&mut self) {
let doing_files = self.text.is_none();
let doing_picking = self.file_finder.input.value().is_empty();
let screen = self.screen_mut();
if let CurrentScreen::Sending(mode) = screen {
if doing_files {
if doing_picking {
*mode = SendingScreen::Files(FileMode::Picking);
} else {
*mode = SendingScreen::Files(FileMode::Fuzzy);
}
} else {
*mode = SendingScreen::Text;
}
}
}
// accept a content receive request
fn accept(&mut self) {
let Some(idx) = self.receiving_state.selected() else {
return;
};
// keys are sorted, so we can use the table selection index
let keys: Vec<_> = self.receive_requests.keys().collect();
let Some(key) = keys.get(idx) else {
warn!("could not get id from selection index {idx}");
return;
};
let Some(req) = self.receive_requests.get(key) else {
return;
};
if let Err(e) = req.tx.send(ReceiveDialog::Approve) {
error!("got error sending upload confirmation: {e:?}");
};
}
// reject an content receive request
fn deny(&mut self) {
let Some(idx) = self.receiving_state.selected() else {
return;
};
// keys are sorted, so we can use the table selection index
let keys: Vec<_> = self.receive_requests.keys().cloned().collect();
let Some(key) = keys.get(idx) else {
warn!("could not get id from selection index {idx}");
return;
};
let Some(req) = self.receive_requests.get(key).cloned() else {
return;
};
if let Err(e) = req.tx.send(ReceiveDialog::Deny) {
error!("got error sending upload confirmation: {e:?}");
};
self.receive_requests.remove(key);
}
async fn chdir_or_send_file(&mut self) {
let file = self.file_finder.explorer.current().path().clone();
if file.is_dir()
&& let Err(e) = self.file_finder.set_cwd(&file)
{
error!("could not list directory {file:?}: {e}");
return;
} else if file.is_dir() {
self.file_finder.input.reset();
return;
}
let Some(peer_idx) = self.peer_state.selected() else {
warn!("no peer selected to send to");
return;
};
let Some(peer) = self.peers.get(peer_idx) else {
warn!("invalid peer index {peer_idx}");
return;
};
if file.is_file() {
debug!("sending {file:?}");
if let Err(e) = self.service.send_file(&peer.fingerprint, file).await {
error!("got error sending content: {e:?}");
}
}
}
// send content to selected peer, or change directories in the file explorer
async fn send_text(&mut self) {
debug!("sending text");
let Some(peer_idx) = self.peer_state.selected() else {
debug!("no peer selected to send to");
return;
};
let Some(peer) = self.peers.get(peer_idx) else {
warn!("invalid peer index {peer_idx}");
return;
};
let Some(text) = &self.text else {
debug!("no text to send");
return;
};
if let Err(e) = self.service.send_text(&peer.fingerprint, text).await {
error!("got error sending \"{text}\" to {}: {e:?}", peer.alias);
}
}
async fn send_content(&mut self) {
if self.text.is_some() {
self.send_text().await;
} else {
self.chdir_or_send_file().await;
}
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
enum LogDelta {
Up,
Down,
}
fn change_log_level(delta: LogDelta) {
let level = match delta {
LogDelta::Up => log::max_level().increment_severity(),
LogDelta::Down => log::max_level().decrement_severity(),
};
log::set_max_level(level);
}

View file

@ -1,25 +1,24 @@
use std::{
collections::BTreeMap,
net::SocketAddr,
path::{Path, PathBuf},
};
use std::{collections::BTreeMap, net::SocketAddr};
use crossterm::event::{Event, EventStream, KeyCode, KeyEvent, KeyEventKind};
use crossterm::event::{Event, EventStream, KeyEventKind};
use futures::{FutureExt, StreamExt};
use jocalsend::{JocalEvent, JocalService, ReceiveDialog, ReceiveRequest, error::Result};
use jocalsend::{JocalEvent, JocalService, ReceiveRequest, error::Result};
use julid::Julid;
use log::{LevelFilter, debug, error, warn};
use ratatui::{
Frame,
widgets::{ListState, TableState, WidgetRef},
widgets::{ListState, TableState},
};
use ratatui_explorer::FileExplorer;
use simsearch::{SearchOptions, SimSearch};
use tokio::sync::mpsc::UnboundedReceiver;
use tui_input::{Input, backend::crossterm::EventHandler};
use tui_input::Input;
pub mod widgets;
mod file_finder;
use file_finder::FileFinder;
mod handle;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Peer {
pub alias: String,
@ -27,25 +26,9 @@ pub struct Peer {
pub addr: SocketAddr,
}
#[derive(Clone)]
struct FileFinder {
explorer: FileExplorer,
fuzzy: SimSearch<usize>,
working_dir: Option<PathBuf>,
input: Input,
}
fn searcher() -> SimSearch<usize> {
SimSearch::new_with(
SearchOptions::new()
.stop_words(vec![std::path::MAIN_SEPARATOR_STR.to_string()])
.stop_whitespace(false),
)
}
pub struct App {
pub service: JocalService,
pub events: EventStream,
pub terminal_events: EventStream,
pub peers: Vec<Peer>,
pub peer_state: ListState,
pub receive_requests: BTreeMap<Julid, ReceiveRequest>,
@ -53,7 +36,7 @@ pub struct App {
receiving_state: TableState,
// for getting messages back from the web server or web client about things we've done; the
// other end is held by the service
event_listener: UnboundedReceiver<JocalEvent>,
jocal_event_rx: UnboundedReceiver<JocalEvent>,
file_finder: FileFinder,
text: Option<String>,
input: Input,
@ -66,6 +49,7 @@ pub enum CurrentScreen {
Receiving,
Stopping,
Logging,
Help,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@ -85,11 +69,11 @@ impl App {
pub fn new(service: JocalService, event_listener: UnboundedReceiver<JocalEvent>) -> Self {
App {
service,
event_listener,
jocal_event_rx: event_listener,
screen: vec![CurrentScreen::Main],
file_finder: FileFinder::new().expect("could not create file explorer"),
text: None,
events: Default::default(),
terminal_events: Default::default(),
peers: Default::default(),
peer_state: Default::default(),
receive_requests: Default::default(),
@ -100,20 +84,8 @@ impl App {
pub async fn handle_events(&mut self) -> Result<()> {
tokio::select! {
event = self.events.next().fuse() => {
if let Some(Ok(evt)) = event {
match evt {
Event::Key(key)
if key.kind == KeyEventKind::Press
=> self.handle_key_event(key, evt).await,
Event::Mouse(_) => {}
Event::Resize(_, _) => {}
_ => {}
}
}
}
transfer_event = self.event_listener.recv().fuse() => {
if let Some(event) = transfer_event {
jocal_event = self.jocal_event_rx.recv().fuse() => {
if let Some(event) = jocal_event {
log::trace!("got JocalEvent {event:?}");
match event {
JocalEvent::ReceiveRequest { id, request } => {
@ -130,6 +102,19 @@ impl App {
}
}
}
terminal_event = self.terminal_events.next().fuse() => {
if let Some(Ok(evt)) = terminal_event {
match evt {
Event::Key(key)
if key.kind == KeyEventKind::Press
=> self.handle_key_event(key, evt).await,
Event::Mouse(_) => {}
Event::Resize(_, _) => {}
_ => {}
}
}
}
}
Ok(())
@ -154,335 +139,7 @@ impl App {
self.screen.last_mut().unwrap()
}
async fn handle_key_event(&mut self, key_event: KeyEvent, event: crossterm::event::Event) {
let code = key_event.code;
let mode = self.screen.last_mut().unwrap();
match mode {
CurrentScreen::Main
| CurrentScreen::Logging
| CurrentScreen::Receiving
| CurrentScreen::Sending(SendingScreen::Files(FileMode::Picking))
| CurrentScreen::Sending(SendingScreen::Peers) => match code {
KeyCode::Char('q') => self.exit().await,
KeyCode::Char('s') => self.send(),
KeyCode::Char('r') => self.recv(),
KeyCode::Char('l') => self.logs(),
KeyCode::Char('m') => self.main(),
KeyCode::Esc => self.pop(),
_ => match mode {
CurrentScreen::Main => {
if let KeyCode::Char('d') = code {
self.service.refresh_peers().await
}
}
CurrentScreen::Logging => match code {
KeyCode::Left => change_log_level(-1),
KeyCode::Right => change_log_level(1),
_ => {}
},
CurrentScreen::Receiving => match code {
KeyCode::Up => self.receiving_state.select_previous(),
KeyCode::Down => self.receiving_state.select_next(),
KeyCode::Char('a') => self.accept(),
KeyCode::Char('d') => self.deny(),
_ => {}
},
CurrentScreen::Sending(sending_screen) => match sending_screen {
// we can only be in picking mode
SendingScreen::Files(fmode) => match code {
KeyCode::Char('t') => *sending_screen = SendingScreen::Text,
KeyCode::Tab => *sending_screen = SendingScreen::Peers,
KeyCode::Enter => self.chdir_or_send_file().await,
KeyCode::Char('/') => {
*fmode = FileMode::Fuzzy;
}
_ => self.file_finder.handle(&event).unwrap_or_default(),
},
SendingScreen::Peers => match code {
KeyCode::Tab => {
*sending_screen = SendingScreen::Files(FileMode::Picking)
}
KeyCode::Char('t') => *sending_screen = SendingScreen::Text,
KeyCode::Enter => self.send_content().await,
KeyCode::Up => self.peer_state.select_previous(),
KeyCode::Down => self.peer_state.select_next(),
_ => {}
},
SendingScreen::Text => unreachable!(),
},
CurrentScreen::Stopping => unreachable!(),
},
},
// we only need to deal with sending text now or doing fuzzy matching
CurrentScreen::Sending(sending_screen) => match sending_screen {
SendingScreen::Text => match code {
KeyCode::Tab => *sending_screen = SendingScreen::Peers,
KeyCode::Enter => self.send_text().await,
KeyCode::Esc => {
self.text = None;
self.input.reset();
*sending_screen = SendingScreen::Files(FileMode::Picking);
}
_ => {
if let Some(changed) = self.input.handle_event(&event)
&& changed.value
{
if self.input.value().is_empty() {
self.text = None;
} else {
self.text = Some(self.input.to_string());
}
}
}
},
SendingScreen::Files(fmode) => {
if *fmode == FileMode::Fuzzy {
match code {
KeyCode::Tab => *sending_screen = SendingScreen::Peers,
KeyCode::Enter => self.chdir_or_send_file().await,
KeyCode::Esc => {
self.file_finder.reset_fuzzy();
*fmode = FileMode::Picking;
}
KeyCode::Up | KeyCode::Down => {
if let Err(e) = self.file_finder.handle(&event) {
log::error!("error selecting file: {e:?}");
}
}
_ => {
self.file_finder.index();
if let Some(changed) = self.file_finder.input.handle_event(&event)
&& changed.value
{
let id = self
.file_finder
.fuzzy
.search(self.file_finder.input.value())
.first()
.copied()
.unwrap_or(0);
self.file_finder.explorer.set_selected_idx(id);
}
}
}
}
}
SendingScreen::Peers => unreachable!(),
},
CurrentScreen::Stopping => {}
}
}
pub fn draw(&mut self, frame: &mut Frame) {
frame.render_widget(self, frame.area());
}
pub async fn exit(&mut self) {
self.screen.push(CurrentScreen::Stopping);
self.service.stop().await;
}
pub fn send(&mut self) {
let last = self.screen.last();
match last {
Some(CurrentScreen::Sending(_)) => {}
_ => self
.screen
.push(CurrentScreen::Sending(SendingScreen::Files(
FileMode::Picking,
))),
}
}
pub fn recv(&mut self) {
let last = self.screen.last();
match last {
Some(CurrentScreen::Receiving) => {}
_ => self.screen.push(CurrentScreen::Receiving),
}
}
pub fn logs(&mut self) {
let last = self.screen.last();
match last {
Some(CurrentScreen::Logging) => {}
_ => self.screen.push(CurrentScreen::Logging),
}
}
pub fn pop(&mut self) {
self.screen.pop();
if self.screen.last().is_none() {
self.screen.push(CurrentScreen::Main);
}
}
pub fn main(&mut self) {
let last = self.screen.last();
match last {
Some(CurrentScreen::Main) => {}
_ => self.screen.push(CurrentScreen::Main),
}
}
// accept a content receive request
fn accept(&mut self) {
let Some(idx) = self.receiving_state.selected() else {
return;
};
// keys are sorted, so we can use the table selection index
let keys: Vec<_> = self.receive_requests.keys().collect();
let Some(key) = keys.get(idx) else {
warn!("could not get id from selection index {idx}");
return;
};
let Some(req) = self.receive_requests.get(key) else {
return;
};
if let Err(e) = req.tx.send(ReceiveDialog::Approve) {
error!("got error sending upload confirmation: {e:?}");
};
}
// reject an content receive request
fn deny(&mut self) {
let Some(idx) = self.receiving_state.selected() else {
return;
};
// keys are sorted, so we can use the table selection index
let keys: Vec<_> = self.receive_requests.keys().cloned().collect();
let Some(key) = keys.get(idx) else {
warn!("could not get id from selection index {idx}");
return;
};
let Some(req) = self.receive_requests.get(key).cloned() else {
return;
};
if let Err(e) = req.tx.send(ReceiveDialog::Deny) {
error!("got error sending upload confirmation: {e:?}");
};
self.receive_requests.remove(key);
}
async fn chdir_or_send_file(&mut self) {
let file = self.file_finder.explorer.current().path().clone();
if file.is_dir()
&& let Err(e) = self.file_finder.set_cwd(&file)
{
error!("could not list directory {file:?}: {e}");
return;
} else if file.is_dir() {
return;
}
let Some(peer_idx) = self.peer_state.selected() else {
warn!("no peer selected to send to");
return;
};
let Some(peer) = self.peers.get(peer_idx) else {
warn!("invalid peer index {peer_idx}");
return;
};
if file.is_file() {
debug!("sending {file:?}");
if let Err(e) = self.service.send_file(&peer.fingerprint, file).await {
error!("got error sending content: {e:?}");
}
}
}
// send content to selected peer, or change directories in the file explorer
async fn send_text(&mut self) {
debug!("sending text");
let Some(peer_idx) = self.peer_state.selected() else {
debug!("no peer selected to send to");
return;
};
let Some(peer) = self.peers.get(peer_idx) else {
warn!("invalid peer index {peer_idx}");
return;
};
let Some(text) = &self.text else {
debug!("no text to send");
return;
};
if let Err(e) = self.service.send_text(&peer.fingerprint, text).await {
error!("got error sending \"{text}\" to {}: {e:?}", peer.alias);
}
}
async fn send_content(&mut self) {
if self.text.is_some() {
self.send_text().await;
} else {
self.chdir_or_send_file().await;
}
}
}
impl FileFinder {
pub fn new() -> Result<Self> {
Ok(Self {
explorer: FileExplorer::new()?,
fuzzy: searcher(),
working_dir: None,
input: Default::default(),
})
}
pub fn handle(&mut self, event: &Event) -> Result<()> {
self.index();
Ok(self.explorer.handle(event)?)
}
pub fn cwd(&self) -> &Path {
self.explorer.cwd()
}
pub fn set_cwd(&mut self, cwd: &Path) -> Result<()> {
self.explorer.set_cwd(cwd)?;
self.index();
Ok(())
}
pub fn widget(&self) -> impl WidgetRef {
self.explorer.widget()
}
pub fn reset_fuzzy(&mut self) {
self.clear_fuzzy();
self.input.reset();
}
fn clear_fuzzy(&mut self) {
self.fuzzy = searcher();
}
fn index(&mut self) {
if let Some(owd) = self.working_dir.as_ref()
&& owd == self.cwd()
{
return;
}
self.working_dir = Some(self.cwd().to_path_buf());
self.clear_fuzzy();
for (i, f) in self.explorer.files().iter().enumerate() {
self.fuzzy.insert(i, f.name());
}
}
}
fn change_log_level(delta: isize) {
let level = log::max_level() as isize;
let max = log::LevelFilter::max() as isize;
let level = (level + delta).clamp(0, max) as usize;
// levelfilter is repr(usize) so this is safe
let level = unsafe { std::mem::transmute::<usize, LevelFilter>(level) };
log::set_max_level(level);
}

View file

@ -10,7 +10,7 @@ use ratatui::{
text::{Line, Text, ToLine},
widgets::{
Block, Borders, Clear, List, ListItem, ListState, Padding, Paragraph, Row, Table,
TableState, Widget,
TableState, Widget, Wrap,
},
};
use tui_logger::{TuiLoggerLevelOutput, TuiLoggerWidget, TuiWidgetState};
@ -27,6 +27,8 @@ static MAIN_MENU: LazyLock<Line> = LazyLock::new(|| {
"<L>".blue().bold(),
" Previous Screen ".into(),
"<ESC>".blue().bold(),
" Help ".into(),
"<H|?>".blue().bold(),
" Quit ".into(),
"<Q>".blue().bold(),
])
@ -128,10 +130,11 @@ impl Widget for &mut App {
let [header_left, header_right] = header_layout.areas(top);
let header_margin = Margin::new(1, 2);
let top_heavy = Layout::vertical([Constraint::Percentage(66), Constraint::Percentage(34)]);
let [heavy_top, _skinny_bottom] = top_heavy.areas(area);
let subscreen_margin = Margin::new(1, 2);
// it's safe to call `unwrap()` here because we ensure there's always at least
// one element in `self.screen`; see the `self.pop()` method
let current_screen = self.screen();
match current_screen {
CurrentScreen::Main => {
@ -156,6 +159,10 @@ impl Widget for &mut App {
buf,
);
}
CurrentScreen::Help => {
outer_frame(&current_screen, &MAIN_MENU, area, buf);
help_screen(area.inner(subscreen_margin), buf);
}
CurrentScreen::Logging | CurrentScreen::Stopping => {
outer_frame(&current_screen, &LOGGING_MENU, area, buf);
logger(area.inner(subscreen_margin), buf);
@ -185,18 +192,31 @@ impl Widget for &mut App {
}
let file_area = header_left.inner(header_margin);
let cwd = self
.file_finder
.cwd()
.as_os_str()
.to_string_lossy()
.into_owned();
match sending_screen {
SendingScreen::Files(FileMode::Picking) => {
SendingScreen::Files(FileMode::Picking)
| SendingScreen::Peers
| SendingScreen::Text => {
let layout = Layout::vertical([Constraint::Length(3), Constraint::Min(5)]);
let [cwd_area, file_area] = layout.areas(file_area);
let cwd: Line = cwd.into();
Paragraph::new(cwd)
.centered()
.block(Block::bordered())
.render(cwd_area, buf);
self.file_finder.widget().render(file_area, buf);
}
SendingScreen::Files(FileMode::Fuzzy) => {
let layout = Layout::vertical([Constraint::Max(6), Constraint::Min(5)]);
let [input, files] = layout.areas(file_area);
text_popup(self.file_finder.input.value(), "fuzzy search", input, buf);
self.file_finder.widget().render(files, buf);
let layout = Layout::vertical([Constraint::Max(3), Constraint::Min(5)]);
let [input_area, files_area] = layout.areas(file_area);
text_popup(self.file_finder.input.value(), &cwd, input_area, buf);
self.file_finder.widget().render(files_area, buf);
}
_ => {}
}
logger(header_right.inner(header_margin), buf);
@ -209,7 +229,8 @@ impl Widget for &mut App {
);
if sending_screen == SendingScreen::Text {
let rect = centered_rect(area, Constraint::Percentage(80), Constraint::Max(10));
let rect =
centered_rect(heavy_top, Constraint::Percentage(80), Constraint::Max(6));
let text = if let Some(text) = self.text.as_ref() {
text
} else {
@ -243,18 +264,96 @@ fn outer_frame(screen: &CurrentScreen, menu: &Line, area: Rect, buf: &mut Buffer
.render(area, buf);
}
fn text_popup(text: &str, title: &str, area: Rect, buf: &mut Buffer) {
let title = Line::from(title.bold());
let block = Block::bordered().title(title.centered());
fn help_screen(area: Rect, buf: &mut Buffer) {
let spacer = "".to_line().centered();
let main_bindings = vec![
Row::new(vec!["".to_line(), spacer.clone(), "".to_line()]),
Row::new(vec![
// Sending
"Send data".bold().into_right_aligned_line(),
spacer.clone(),
"S".bold().into_left_aligned_line(),
]),
// Receiving
Row::new(vec![
"Manage incoming transfer requests"
.bold()
.into_right_aligned_line(),
spacer.clone(),
"R".bold().into_left_aligned_line(),
]),
// logging
Row::new(vec![
"View logs and change log level"
.bold()
.into_right_aligned_line(),
spacer.clone(),
"L".bold().into_left_aligned_line(),
]),
// misc: main menu
Row::new(vec![
"Go to the main screen".bold().into_right_aligned_line(),
spacer.clone(),
"M".bold().into_left_aligned_line(),
]),
// misc: clear peers
Row::new(vec![
"Clear peers and rediscover"
.bold()
.into_right_aligned_line(),
spacer.clone(),
"C".bold().into_left_aligned_line(),
]),
// misc: help
Row::new(vec![
"This help screen".bold().into_right_aligned_line(),
spacer.clone(),
"H or ?".bold().into_left_aligned_line(),
]),
// misc: pop
Row::new(vec![
"Go to previous screen".bold().into_right_aligned_line(),
spacer.clone(),
"ESC".bold().into_left_aligned_line(),
]),
// misc: quit
Row::new(vec![
"Quit the application".bold().into_right_aligned_line(),
spacer.clone(),
"Q".bold().into_left_aligned_line(),
]),
];
let layout = Layout::vertical(vec![
Constraint::Max(3),
Constraint::Max(12),
Constraint::Max(3),
])
.flex(Flex::SpaceAround);
let [intro_area, bindings_area, outro_area] = layout.areas(area);
let widths = vec![
Constraint::Percentage(50),
Constraint::Max(6),
Constraint::Percentage(50),
];
let main_bindings = Table::new(main_bindings, widths).header(Row::new(vec![
"Action".bold().into_right_aligned_line(),
spacer,
"Key input".bold().into_left_aligned_line(),
]));
Clear.render(area, buf);
block.render(area, buf);
let intro = "JocalSend is a mode-based application that responds to key-presses. Most modes support the following key bindings:".to_line().centered();
let intro = Paragraph::new(intro).wrap(Wrap { trim: true });
let (_, len) = unicode_segmentation::UnicodeSegmentation::graphemes(text, true).size_hint();
let len = len.unwrap_or(text.len()) as u16 + 2;
let area = centered_rect(area, Constraint::Length(len), Constraint::Length(1));
let outro = "Additional key bindings are available when in the sending or receiving screens, and are displayed at the bottom of the screen there.".to_line().centered();
let outro = Paragraph::new(outro).wrap(Wrap { trim: true });
Paragraph::new(text).centered().yellow().render(area, buf);
intro.render(intro_area, buf);
main_bindings.render(bindings_area, buf);
outro.render(outro_area, buf);
}
fn logger(area: Rect, buf: &mut Buffer) {
@ -410,3 +509,17 @@ fn centered_rect(area: Rect, horizontal: Constraint, vertical: Constraint) -> Re
let [area] = Layout::vertical([vertical]).flex(Flex::Center).areas(area);
area
}
fn text_popup(text: &str, title: &str, area: Rect, buf: &mut Buffer) {
let title = Line::from(title.bold());
let block = Block::bordered().title(title.centered());
Clear.render(area, buf);
block.render(area, buf);
let (_, len) = unicode_segmentation::UnicodeSegmentation::graphemes(text, true).size_hint();
let len = len.unwrap_or(text.len()) as u16 + 2;
let area = centered_rect(area, Constraint::Length(len), Constraint::Length(1));
Paragraph::new(text).centered().yellow().render(area, buf);
}

View file

@ -88,7 +88,7 @@ impl Config {
.map_err(Box::new)? // boxed because the error size from figment is large
};
log::info!("using config: {config:?}");
log::debug!("using config: {config:?}");
Ok(config)
}

View file

@ -138,33 +138,3 @@ async fn announce_multicast(
socket.send_to(msg.as_bytes(), addr).await?;
Ok(())
}
/*
async fn announce_unicast(
device: &Device,
ip: Option<SocketAddr>,
client: reqwest::Client,
) -> crate::error::Result<()> {
// for enumerating subnet peers when multicast fails (https://github.com/localsend/protocol?tab=readme-ov-file#32-http-legacy-mode)
let std::net::IpAddr::V4(ip) = local_ip_address::local_ip()? else {
unreachable!()
};
let mut _network_ip = ip;
let nifs = NetworkInterface::show()?;
for addr in nifs.into_iter().flat_map(|i| i.addr) {
if let Addr::V4(V4IfAddr {
ip: ifip,
netmask: Some(netmask),
..
}) = addr
&& ip == ifip
{
_network_ip = ip & netmask;
break;
}
}
todo!()
}
*/

View file

@ -32,6 +32,8 @@ impl JocalService {
log::info!("starting http server");
// need to make a custom tls acceptor, see
// https://github.com/programatik29/axum-server/blob/master/examples/rustls_session.rs
axum_server::bind_rustls(addr, ssl_config)
.handle(handle)
.serve(app.into_make_service_with_connect_info::<SocketAddr>())

View file

@ -30,9 +30,7 @@ use transfer::Session;
pub const DEFAULT_PORT: u16 = 53317;
pub const MULTICAST_IP: Ipv4Addr = Ipv4Addr::new(224, 0, 0, 167);
pub const LISTENING_SOCKET_ADDR: SocketAddrV4 =
SocketAddrV4::new(Ipv4Addr::from_bits(0), DEFAULT_PORT);
pub const DEFAULT_INTERVAL: Duration = Duration::from_millis(200);
pub const DEFAULT_INTERVAL: Duration = Duration::from_millis(100);
pub type Peers = Arc<Mutex<BTreeMap<String, (SocketAddr, Device)>>>;
pub type Sessions = Arc<Mutex<BTreeMap<String, Session>>>; // Session ID to Session
@ -78,7 +76,7 @@ pub struct ReceiveRequest {
impl PartialEq for ReceiveRequest {
fn eq(&self, other: &Self) -> bool {
self.alias == other.alias
self.alias == other.alias && self.files == other.files
}
}
@ -113,10 +111,11 @@ impl JocalService {
config: Config,
) -> crate::error::Result<(Self, UnboundedReceiver<JocalEvent>)> {
let (tx, rx) = mpsc::unbounded_channel();
let socket = UdpSocket::bind(LISTENING_SOCKET_ADDR).await?;
socket.set_multicast_loop_v4(true)?;
socket.set_multicast_ttl_v4(8)?; // 8 hops out from localnet
socket.join_multicast_v4(MULTICAST_IP, Ipv4Addr::from_bits(0))?;
let addr = SocketAddrV4::new(config.local_ip_addr, DEFAULT_PORT);
let socket = UdpSocket::bind(addr).await?;
socket.set_multicast_loop_v4(false)?;
socket.set_multicast_ttl_v4(1)?; // local subnet only
socket.join_multicast_v4(MULTICAST_IP, config.local_ip_addr)?;
let client = reqwest::ClientBuilder::new()
// localsend certs are self-signed
@ -162,15 +161,16 @@ impl JocalService {
let mut tick = tokio::time::interval(DEFAULT_INTERVAL);
loop {
let rstate = service.running_state.lock().await;
if *rstate == RunningState::Stopping {
break;
}
tick.tick().await;
service
.transfer_event_tx
.send(JocalEvent::Tick)
.unwrap_or_else(|e| log::warn!("could not send tick event: {e:?}"));
let rstate = service.running_state.lock().await;
if *rstate == RunningState::Stopping {
break;
}
}
JocalTasks::Tick
});
@ -178,22 +178,26 @@ impl JocalService {
let service = self.clone();
handles.spawn(async move {
loop {
if let Err(e) = service.announce(None).await {
error!("Announcement error: {e}");
}
tokio::time::sleep(Duration::from_secs(2)).await;
let rstate = service.running_state.lock().await;
if *rstate == RunningState::Stopping {
break;
}
if let Err(e) = service.announce(None).await {
error!("Announcement error: {e}");
}
tokio::time::sleep(Duration::from_secs(2)).await;
}
JocalTasks::Udp
});
}
pub async fn stop(&self) {
{
let mut rstate = self.running_state.lock().await;
*rstate = RunningState::Stopping;
}
log::info!("shutting down http server");
self.http_handle
.get()
@ -201,7 +205,7 @@ impl JocalService {
.graceful_shutdown(Some(Duration::from_secs(5)));
}
pub async fn refresh_peers(&self) {
pub async fn clear_peers(&self) {
let mut peers = self.peers.lock().await;
peers.clear();
}

View file

@ -6,7 +6,7 @@ use log::{error, info};
use ratatui::DefaultTerminal;
use ratatui_explorer::FileExplorer;
use tokio::task::JoinSet;
use tui_logger::{LevelFilter, init_logger, set_env_filter_from_env};
use tui_logger::{LevelFilter, init_logger};
mod app;
use app::{App, CurrentScreen, Peer};
@ -18,13 +18,11 @@ fn main() -> Result<()> {
// just in case we need to display the help
let _ = Cli::parse();
if std::env::var("RUST_LOG").is_err() {
unsafe {
std::env::set_var("RUST_LOG", "jocalsend");
}
}
init_logger(LevelFilter::Info).map_err(|e| std::io::Error::other(format!("{e}")))?;
set_env_filter_from_env(None);
tui_logger::set_env_filter_from_string(
&std::env::var("RUST_LOG").unwrap_or("jocalsend".to_string()),
);
let config = Config::new()?;

View file

@ -6,7 +6,7 @@ use serde::{Deserialize, Serialize};
use crate::error::LocalSendError;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct FileMetadata {
pub id: String,
@ -21,7 +21,7 @@ pub struct FileMetadata {
pub metadata: Option<FileMetadataExt>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct FileMetadataExt {
#[serde(skip_serializing_if = "Option::is_none")]
pub modified: Option<String>,

View file

@ -131,7 +131,6 @@ impl JocalService {
let token = match prepare_response.files.get(&metadata.id) {
Some(t) => t,
None => {
log::warn!("");
send_tx(
JocalEvent::SendFailed {
error: "missing token in prepare response from remote".into(),
@ -144,6 +143,15 @@ impl JocalService {
let content_id = &metadata.id;
let session_id = prepare_response.session_id;
log::info!(
"sending {content_id} to {}",
peers
.lock()
.await
.get(&peer)
.map(|(_, peer)| peer.alias.as_str())
.unwrap_or("unknown peer")
);
let resp = do_send_bytes(sessions, client, &session_id, content_id, token, bytes).await;
match resp {
@ -273,11 +281,7 @@ pub async fn handle_receive_upload(
let file_metadata = match session.files.get(file_id) {
Some(metadata) => metadata,
None => {
return (
StatusCode::INTERNAL_SERVER_ERROR,
"File not found".to_string(),
)
.into_response();
return (StatusCode::BAD_REQUEST, "File not found".to_string()).into_response();
}
};
@ -285,11 +289,8 @@ pub async fn handle_receive_upload(
// Create directory if it doesn't exist
if let Err(e) = tokio::fs::create_dir_all(download_dir).await {
return (
StatusCode::INTERNAL_SERVER_ERROR,
format!("Failed to create directory: {e}"),
)
.into_response();
log::error!("could not create download directory '{download_dir:?}', got {e}");
return (StatusCode::INTERNAL_SERVER_ERROR, "could not save content").into_response();
}
// Create file path
@ -297,13 +298,14 @@ pub async fn handle_receive_upload(
// Write file
if let Err(e) = tokio::fs::write(&file_path, body).await {
return (
StatusCode::INTERNAL_SERVER_ERROR,
format!("Failed to write file: {e}"),
)
.into_response();
log::warn!("could not save content to {file_path:?}, got {e}");
return (StatusCode::INTERNAL_SERVER_ERROR, "could not save content").into_response();
}
log::info!(
"saved content from {} to {file_path:?}",
&session.sender.alias
);
if let Ok(id) = Julid::from_str(session_id) {
service.send_event(JocalEvent::ReceivedInbound(id));
};