add readme

This commit is contained in:
Joe Ardent 2025-08-10 13:35:54 -07:00
parent ffab0f261a
commit 3913d8f827
11 changed files with 94 additions and 13 deletions

View file

@ -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
View 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]

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

BIN
media/sending_file.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

BIN
media/sending_text.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

View file

@ -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,