Compare commits
2 commits
2b1e0f7cb8
...
04b6388b2c
Author | SHA1 | Date | |
---|---|---|---|
|
04b6388b2c | ||
|
77d44b8868 |
7 changed files with 84 additions and 72 deletions
22
Cargo.lock
generated
22
Cargo.lock
generated
|
@ -414,7 +414,7 @@ dependencies = [
|
||||||
"anstream",
|
"anstream",
|
||||||
"anstyle",
|
"anstyle",
|
||||||
"clap_lex",
|
"clap_lex",
|
||||||
"strsim 0.11.1",
|
"strsim",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -545,7 +545,7 @@ dependencies = [
|
||||||
"ident_case",
|
"ident_case",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"strsim 0.11.1",
|
"strsim",
|
||||||
"syn 2.0.106",
|
"syn 2.0.106",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -1326,7 +1326,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "jocalsend"
|
name = "jocalsend"
|
||||||
version = "1.6.1803"
|
version = "1.6.18033"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"axum",
|
"axum",
|
||||||
"axum-server",
|
"axum-server",
|
||||||
|
@ -2281,11 +2281,11 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "simsearch"
|
name = "simsearch"
|
||||||
version = "0.2.5"
|
version = "0.3.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9c869b25830e4824ef7279015cfc298a0674aca6a54eeff2efce8d12bf3701fe"
|
checksum = "629d21c4ebf25655995cda9eb93e85539fa68b0438acb85e9e5d10f6fe2404bc"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"strsim 0.10.0",
|
"strsim",
|
||||||
"triple_accel",
|
"triple_accel",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -2323,12 +2323,6 @@ version = "1.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "strsim"
|
|
||||||
version = "0.10.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "strsim"
|
name = "strsim"
|
||||||
version = "0.11.1"
|
version = "0.11.1"
|
||||||
|
@ -2699,9 +2693,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "triple_accel"
|
name = "triple_accel"
|
||||||
version = "0.3.4"
|
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 = "622b09ce2fe2df4618636fb92176d205662f59803f39e70d1c333393082de96c"
|
checksum = "22048bc95dfb2ffd05b1ff9a756290a009224b60b2f0e7525faeee7603851e63"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "try-lock"
|
name = "try-lock"
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
[package]
|
[package]
|
||||||
name = "jocalsend"
|
name = "jocalsend"
|
||||||
# 1.61803398874989484
|
# 1.61803398874989484
|
||||||
#-------^
|
#--------^
|
||||||
version = "1.6.1803"
|
version = "1.6.18033"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
authors = ["Joe Ardent <code@ardent.nebcorp.com>"]
|
authors = ["Joe Ardent <code@ardent.nebcorp.com>"]
|
||||||
keywords = ["p2p", "localsend", "tui", "linux"]
|
keywords = ["p2p", "localsend", "tui", "linux"]
|
||||||
description = "A terminal implementation of the LocalSend protocol"
|
description = "A TUI for LocalSend"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
license-file = "LICENSE.md"
|
license-file = "LICENSE.md"
|
||||||
repository = "https://git.kittencollective.com/nebkor/joecalsend"
|
repository = "https://git.kittencollective.com/nebkor/joecalsend"
|
||||||
|
@ -34,7 +34,7 @@ rustix = { version = "1", default-features = false, features = ["system"] }
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
sha256 = "1.6"
|
sha256 = "1.6"
|
||||||
simsearch = "0.2"
|
simsearch = "0.3"
|
||||||
thiserror = "2"
|
thiserror = "2"
|
||||||
tokio = { version = "1", default-features = false, features = ["time", "macros", "rt-multi-thread"] }
|
tokio = { version = "1", default-features = false, features = ["time", "macros", "rt-multi-thread"] }
|
||||||
tokio-rustls = { version = "0.26", default-features = false, features = ["tls12", "logging"] }
|
tokio-rustls = { version = "0.26", default-features = false, features = ["tls12", "logging"] }
|
||||||
|
|
18
README.md
18
README.md
|
@ -10,7 +10,8 @@ that uses [Ratatui](https://github.com/ratatui/ratatui) to provide an interactiv
|
||||||
application, and is compatible with the official app.
|
application, and is compatible with the official app.
|
||||||
|
|
||||||
Install with `cargo install jocalsend` (requires [Rust](https://rustup.rs/)); tested on Linux, it
|
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.
|
||||||
|
|
||||||
## Capabilities and screenshots
|
## Capabilities and screenshots
|
||||||
|
|
||||||
|
@ -22,18 +23,21 @@ available:
|
||||||
- `S` -> go to the sending screen, defaulting to sending files
|
- `S` -> go to the sending screen, defaulting to sending files
|
||||||
- `R` -> go to the receiving screen to approve or deny incoming transfers
|
- `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
|
- `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
|
- `ESC` -> go back to the previous screen
|
||||||
- `Q` -> exit the application
|
- `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
|
- `TAB` -> switch between content selection and peer selection
|
||||||
- `P` -> switch to peer selection
|
- `T` -> enter text directly to send, `ESC` to cancel
|
||||||
- `T` -> switch to entering text to send
|
- `/` -> fuzzy filename search, use `ESC` to stop inputting text
|
||||||
- `F` -> switch to selecting files to send (not available when entering text, use `ESC` to exit text entry)
|
|
||||||
|
|
||||||
In addition to the interactive commands, it will also accept commandline arguments to pre-select a
|
When in the receiving screen, use `A` to approve the incoming transfer request, or `D` to deny it.
|
||||||
file or pre-populate text to send:
|
|
||||||
|
Finally, it will also accept commandline arguments to pre-select a file or pre-populate text to
|
||||||
|
send:
|
||||||
|
|
||||||
```
|
```
|
||||||
$ jocalsend -h
|
$ jocalsend -h
|
||||||
|
|
2
VERSION
2
VERSION
|
@ -1 +1 @@
|
||||||
1.61803
|
1.618033
|
||||||
|
|
|
@ -15,20 +15,18 @@ pub(crate) struct FileFinder {
|
||||||
pub input: Input,
|
pub input: Input,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn searcher() -> SimSearch<usize> {
|
|
||||||
SimSearch::new_with(
|
|
||||||
SearchOptions::new()
|
|
||||||
.stop_words(vec![std::path::MAIN_SEPARATOR_STR.to_string()])
|
|
||||||
.stop_whitespace(false)
|
|
||||||
.threshold(0.0),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FileFinder {
|
impl FileFinder {
|
||||||
pub fn new() -> Result<Self> {
|
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 {
|
Ok(Self {
|
||||||
explorer: FileExplorer::new()?,
|
explorer: FileExplorer::new()?,
|
||||||
fuzzy: searcher(),
|
fuzzy,
|
||||||
working_dir: None,
|
working_dir: None,
|
||||||
input: Default::default(),
|
input: Default::default(),
|
||||||
})
|
})
|
||||||
|
@ -54,14 +52,10 @@ impl FileFinder {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn reset_fuzzy(&mut self) {
|
pub fn reset_fuzzy(&mut self) {
|
||||||
self.clear_fuzzy();
|
self.fuzzy.clear();
|
||||||
self.input.reset();
|
self.input.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn clear_fuzzy(&mut self) {
|
|
||||||
self.fuzzy = searcher();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn index(&mut self) {
|
pub fn index(&mut self) {
|
||||||
if let Some(owd) = self.working_dir.as_ref()
|
if let Some(owd) = self.working_dir.as_ref()
|
||||||
&& owd == self.cwd()
|
&& owd == self.cwd()
|
||||||
|
@ -69,7 +63,7 @@ impl FileFinder {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
self.working_dir = Some(self.cwd().to_path_buf());
|
self.working_dir = Some(self.cwd().to_path_buf());
|
||||||
self.clear_fuzzy();
|
self.reset_fuzzy();
|
||||||
|
|
||||||
for (i, f) in self.explorer.files().iter().enumerate() {
|
for (i, f) in self.explorer.files().iter().enumerate() {
|
||||||
self.fuzzy.insert(i, f.name());
|
self.fuzzy.insert(i, f.name());
|
||||||
|
|
|
@ -253,8 +253,6 @@ fn outer_frame(screen: &CurrentScreen, menu: &Line, area: Rect, buf: &mut Buffer
|
||||||
}
|
}
|
||||||
|
|
||||||
fn help_screen(area: Rect, buf: &mut Buffer) {
|
fn help_screen(area: Rect, buf: &mut Buffer) {
|
||||||
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 spacer = "".to_line().centered();
|
let spacer = "".to_line().centered();
|
||||||
let main_bindings = vec![
|
let main_bindings = vec![
|
||||||
Row::new(vec!["".to_line(), spacer.clone(), "".to_line()]),
|
Row::new(vec!["".to_line(), spacer.clone(), "".to_line()]),
|
||||||
|
@ -266,7 +264,7 @@ fn help_screen(area: Rect, buf: &mut Buffer) {
|
||||||
]),
|
]),
|
||||||
// Receiving
|
// Receiving
|
||||||
Row::new(vec![
|
Row::new(vec![
|
||||||
"Manage incoming data requests (receive data)"
|
"Manage incoming transfer requests"
|
||||||
.bold()
|
.bold()
|
||||||
.into_right_aligned_line(),
|
.into_right_aligned_line(),
|
||||||
spacer.clone(),
|
spacer.clone(),
|
||||||
|
@ -280,23 +278,19 @@ fn help_screen(area: Rect, buf: &mut Buffer) {
|
||||||
spacer.clone(),
|
spacer.clone(),
|
||||||
"L".bold().into_left_aligned_line(),
|
"L".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: main menu
|
// misc: main menu
|
||||||
Row::new(vec![
|
Row::new(vec![
|
||||||
"Go to the main screen".bold().into_right_aligned_line(),
|
"Go to the main screen".bold().into_right_aligned_line(),
|
||||||
spacer.clone(),
|
spacer.clone(),
|
||||||
"M".bold().into_left_aligned_line(),
|
"M".bold().into_left_aligned_line(),
|
||||||
]),
|
]),
|
||||||
// misc: quit
|
// misc: clear peers
|
||||||
Row::new(vec![
|
Row::new(vec![
|
||||||
"Quit the application".bold().into_right_aligned_line(),
|
"Clear peers and rediscover"
|
||||||
|
.bold()
|
||||||
|
.into_right_aligned_line(),
|
||||||
spacer.clone(),
|
spacer.clone(),
|
||||||
"Q".bold().into_left_aligned_line(),
|
"C".bold().into_left_aligned_line(),
|
||||||
]),
|
]),
|
||||||
// misc: help
|
// misc: help
|
||||||
Row::new(vec![
|
Row::new(vec![
|
||||||
|
@ -304,10 +298,27 @@ fn help_screen(area: Rect, buf: &mut Buffer) {
|
||||||
spacer.clone(),
|
spacer.clone(),
|
||||||
"H or ?".bold().into_left_aligned_line(),
|
"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::Min(1)]);
|
let layout = Layout::vertical(vec![
|
||||||
let [intro_area, bindings_area] = layout.areas(area);
|
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![
|
let widths = vec![
|
||||||
Constraint::Percentage(50),
|
Constraint::Percentage(50),
|
||||||
|
@ -322,8 +333,15 @@ fn help_screen(area: Rect, buf: &mut Buffer) {
|
||||||
|
|
||||||
Clear.render(area, buf);
|
Clear.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 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 });
|
||||||
|
|
||||||
intro.render(intro_area, buf);
|
intro.render(intro_area, buf);
|
||||||
main_bindings.render(bindings_area, buf);
|
main_bindings.render(bindings_area, buf);
|
||||||
|
outro.render(outro_area, buf);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn logger(area: Rect, buf: &mut Buffer) {
|
fn logger(area: Rect, buf: &mut Buffer) {
|
||||||
|
|
|
@ -131,7 +131,6 @@ impl JocalService {
|
||||||
let token = match prepare_response.files.get(&metadata.id) {
|
let token = match prepare_response.files.get(&metadata.id) {
|
||||||
Some(t) => t,
|
Some(t) => t,
|
||||||
None => {
|
None => {
|
||||||
log::warn!("");
|
|
||||||
send_tx(
|
send_tx(
|
||||||
JocalEvent::SendFailed {
|
JocalEvent::SendFailed {
|
||||||
error: "missing token in prepare response from remote".into(),
|
error: "missing token in prepare response from remote".into(),
|
||||||
|
@ -144,6 +143,15 @@ impl JocalService {
|
||||||
|
|
||||||
let content_id = &metadata.id;
|
let content_id = &metadata.id;
|
||||||
let session_id = prepare_response.session_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;
|
let resp = do_send_bytes(sessions, client, &session_id, content_id, token, bytes).await;
|
||||||
|
|
||||||
match resp {
|
match resp {
|
||||||
|
@ -273,11 +281,7 @@ pub async fn handle_receive_upload(
|
||||||
let file_metadata = match session.files.get(file_id) {
|
let file_metadata = match session.files.get(file_id) {
|
||||||
Some(metadata) => metadata,
|
Some(metadata) => metadata,
|
||||||
None => {
|
None => {
|
||||||
return (
|
return (StatusCode::BAD_REQUEST, "File not found".to_string()).into_response();
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
|
||||||
"File not found".to_string(),
|
|
||||||
)
|
|
||||||
.into_response();
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -285,11 +289,8 @@ pub async fn handle_receive_upload(
|
||||||
|
|
||||||
// Create directory if it doesn't exist
|
// Create directory if it doesn't exist
|
||||||
if let Err(e) = tokio::fs::create_dir_all(download_dir).await {
|
if let Err(e) = tokio::fs::create_dir_all(download_dir).await {
|
||||||
return (
|
log::error!("could not create download directory '{download_dir:?}', got {e}");
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
return (StatusCode::INTERNAL_SERVER_ERROR, "could not save content").into_response();
|
||||||
format!("Failed to create directory: {e}"),
|
|
||||||
)
|
|
||||||
.into_response();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create file path
|
// Create file path
|
||||||
|
@ -297,13 +298,14 @@ pub async fn handle_receive_upload(
|
||||||
|
|
||||||
// Write file
|
// Write file
|
||||||
if let Err(e) = tokio::fs::write(&file_path, body).await {
|
if let Err(e) = tokio::fs::write(&file_path, body).await {
|
||||||
return (
|
log::warn!("could not save content to {file_path:?}, got {e}");
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
return (StatusCode::INTERNAL_SERVER_ERROR, "could not save content").into_response();
|
||||||
format!("Failed to write file: {e}"),
|
|
||||||
)
|
|
||||||
.into_response();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log::info!(
|
||||||
|
"saved content from {} to {file_path:?}",
|
||||||
|
&session.sender.alias
|
||||||
|
);
|
||||||
if let Ok(id) = Julid::from_str(session_id) {
|
if let Ok(id) = Julid::from_str(session_id) {
|
||||||
service.send_event(JocalEvent::ReceivedInbound(id));
|
service.send_event(JocalEvent::ReceivedInbound(id));
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in a new issue