add readme
|
@ -2,6 +2,12 @@
|
||||||
name = "jocalsend"
|
name = "jocalsend"
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
authors = ["Joe Ardent <code@ardent.nebcorp.com>"]
|
||||||
|
keywords = ["p2p", "localsend", "tui", "linux"]
|
||||||
|
description = "A terminal implementation of the LocalSend protocol"
|
||||||
|
readme = "README.md"
|
||||||
|
license-file = "LICENSE.md"
|
||||||
|
repository = "https://git.kittencollective.com/nebkor/joecalsend"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
axum = { version = "0.8", features = ["macros"] }
|
axum = { version = "0.8", features = ["macros"] }
|
||||||
|
|
88
README.md
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
# JocalSend, a TUI [LocalSend](https://github.com/localsend/localsend) implementation
|
||||||
|
|
||||||
|
LocalSend is, in its words, "a free, open-source app that allows you to securely share files and
|
||||||
|
messages with nearby devices over your local network without needing an internet connection." It
|
||||||
|
comes in the form a Flutter/Dart cross-platform GUI application that runs on both mobile and desktop
|
||||||
|
devices. Using it on mobile is very nice, but the desktop experience is a bit lacking in zazz.
|
||||||
|
|
||||||
|
JocalSend is an implementation of the [LocalSend protocol](https://github.com/localsend/protocol)
|
||||||
|
that uses [Ratatui](https://github.com/ratatui/ratatui) to provide an interactive terminal-based
|
||||||
|
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.
|
||||||
|
|
||||||
|
## Capabilities and screenshots
|
||||||
|
|
||||||
|
As with the official app, JocalSend can be used to send and receive files and text from other
|
||||||
|
LocalSend instances on your local subnetwork. Most of the modes have the following keybindings
|
||||||
|
available:
|
||||||
|
|
||||||
|
- `M` -> go back to the main screen
|
||||||
|
- `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
|
||||||
|
- `ESC` -> go back to the previous screen
|
||||||
|
- `Q` -> exit the application
|
||||||
|
|
||||||
|
Additionally, 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)
|
||||||
|
|
||||||
|
In addition to the interactive commands, it will also accept commandline arguments to pre-select a
|
||||||
|
file or pre-populate text to send:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ jocalsend -h
|
||||||
|
A terminal implementation of the LocalSend protocol
|
||||||
|
|
||||||
|
Usage: jocalsend [OPTIONS]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
-f, --file <FILE> File to pre-select for sending
|
||||||
|
-t, --text <TEXT> Text string to send
|
||||||
|
-h, --help Print help
|
||||||
|
-V, --version Print version
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sending Files
|
||||||
|
|
||||||
|
JocalSend has a file-picking widget for selecting files to send:
|
||||||
|
|
||||||
|
![./media/sending_file.png]
|
||||||
|
|
||||||
|
but there's no preview available on the receiving side:
|
||||||
|
|
||||||
|
![./media/receiving_file_from_jocalsend.png]
|
||||||
|
|
||||||
|
### Sending text
|
||||||
|
|
||||||
|
JocalSend supports entering text directly:
|
||||||
|
|
||||||
|
![./media/sending_text.png]
|
||||||
|
|
||||||
|
and on the receiving side in the official app, you see
|
||||||
|
|
||||||
|
![./media/receiving_text_on_phone.png]
|
||||||
|
|
||||||
|
### Receiving files
|
||||||
|
|
||||||
|
The main screen shows incoming transfer requests:
|
||||||
|
|
||||||
|
![./media/main_screen_receiving.png]
|
||||||
|
|
||||||
|
hit `r` to enter the "receiving" screen to approve or deny:
|
||||||
|
|
||||||
|
![./media/receiving_file_receiving_screen.png]
|
||||||
|
|
||||||
|
### Receiving text
|
||||||
|
|
||||||
|
If the incoming transfer request is plain text, JocalSend will show a preview in both the main
|
||||||
|
screen and in the receiving screen:
|
||||||
|
|
||||||
|
![./media/receiving_text_main_screen.png]
|
||||||
|
|
||||||
|
![./media/receiving_text_receive_screen.png]
|
BIN
media/main_screen_receiving.png
Normal file
After Width: | Height: | Size: 95 KiB |
BIN
media/receiving_file_from_jocalsend.png
Normal file
After Width: | Height: | Size: 68 KiB |
BIN
media/receiving_file_receiving_screen.png
Normal file
After Width: | Height: | Size: 69 KiB |
BIN
media/receiving_text_main_screen.png
Normal file
After Width: | Height: | Size: 106 KiB |
BIN
media/receiving_text_on_phone.png
Normal file
After Width: | Height: | Size: 65 KiB |
BIN
media/receiving_text_receive_screen.png
Normal file
After Width: | Height: | Size: 82 KiB |
BIN
media/sending_file.png
Normal file
After Width: | Height: | Size: 94 KiB |
BIN
media/sending_text.png
Normal file
After Width: | Height: | Size: 84 KiB |
|
@ -111,27 +111,20 @@ impl JocalService {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn send_file(&self, peer: &str, file_path: PathBuf) -> Result<()> {
|
pub async fn send_file(&self, peer: &str, file_path: PathBuf) -> Result<()> {
|
||||||
// Generate file metadata
|
|
||||||
let file_metadata = FileMetadata::from_path(&file_path)?;
|
let file_metadata = FileMetadata::from_path(&file_path)?;
|
||||||
|
|
||||||
// Prepare files map
|
|
||||||
let mut files = BTreeMap::new();
|
let mut files = BTreeMap::new();
|
||||||
files.insert(file_metadata.id.clone(), file_metadata.clone());
|
files.insert(file_metadata.id.clone(), file_metadata.clone());
|
||||||
|
|
||||||
// Prepare upload
|
|
||||||
let prepare_response = self.prepare_upload(peer, files).await?;
|
let prepare_response = self.prepare_upload(peer, files).await?;
|
||||||
|
|
||||||
// Get file token
|
|
||||||
let token = prepare_response
|
let token = prepare_response
|
||||||
.files
|
.files
|
||||||
.get(&file_metadata.id)
|
.get(&file_metadata.id)
|
||||||
.ok_or(LocalSendError::InvalidToken)?;
|
.ok_or(LocalSendError::InvalidToken)?;
|
||||||
|
|
||||||
// Read file contents
|
|
||||||
let file_contents = tokio::fs::read(&file_path).await?;
|
let file_contents = tokio::fs::read(&file_path).await?;
|
||||||
let bytes = Bytes::from(file_contents);
|
let bytes = Bytes::from(file_contents);
|
||||||
|
|
||||||
// Upload file
|
|
||||||
self.send_bytes(
|
self.send_bytes(
|
||||||
&prepare_response.session_id,
|
&prepare_response.session_id,
|
||||||
&file_metadata.id,
|
&file_metadata.id,
|
||||||
|
@ -144,17 +137,12 @@ impl JocalService {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn send_text(&self, peer: &str, text: &str) -> Result<()> {
|
pub async fn send_text(&self, peer: &str, text: &str) -> Result<()> {
|
||||||
// Generate file metadata
|
|
||||||
let file_metadata = FileMetadata::from_text(text)?;
|
let file_metadata = FileMetadata::from_text(text)?;
|
||||||
|
|
||||||
// Prepare files map
|
|
||||||
let mut files = BTreeMap::new();
|
let mut files = BTreeMap::new();
|
||||||
files.insert(file_metadata.id.clone(), file_metadata.clone());
|
files.insert(file_metadata.id.clone(), file_metadata.clone());
|
||||||
|
|
||||||
// Prepare upload
|
|
||||||
let prepare_response = self.prepare_upload(peer, files).await?;
|
let prepare_response = self.prepare_upload(peer, files).await?;
|
||||||
|
|
||||||
// Get file token
|
|
||||||
let token = prepare_response
|
let token = prepare_response
|
||||||
.files
|
.files
|
||||||
.get(&file_metadata.id)
|
.get(&file_metadata.id)
|
||||||
|
@ -162,7 +150,6 @@ impl JocalService {
|
||||||
|
|
||||||
let bytes = Bytes::from(text.to_owned());
|
let bytes = Bytes::from(text.to_owned());
|
||||||
|
|
||||||
// Upload file
|
|
||||||
self.send_bytes(
|
self.send_bytes(
|
||||||
&prepare_response.session_id,
|
&prepare_response.session_id,
|
||||||
&file_metadata.id,
|
&file_metadata.id,
|
||||||
|
|