add readme
|
@ -2,6 +2,12 @@
|
|||
name = "jocalsend"
|
||||
version = "1.0.0"
|
||||
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]
|
||||
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<()> {
|
||||
// Generate file metadata
|
||||
let file_metadata = FileMetadata::from_path(&file_path)?;
|
||||
|
||||
// Prepare files map
|
||||
let mut files = BTreeMap::new();
|
||||
files.insert(file_metadata.id.clone(), file_metadata.clone());
|
||||
|
||||
// Prepare upload
|
||||
let prepare_response = self.prepare_upload(peer, files).await?;
|
||||
|
||||
// Get file token
|
||||
let token = prepare_response
|
||||
.files
|
||||
.get(&file_metadata.id)
|
||||
.ok_or(LocalSendError::InvalidToken)?;
|
||||
|
||||
// Read file contents
|
||||
let file_contents = tokio::fs::read(&file_path).await?;
|
||||
let bytes = Bytes::from(file_contents);
|
||||
|
||||
// Upload file
|
||||
self.send_bytes(
|
||||
&prepare_response.session_id,
|
||||
&file_metadata.id,
|
||||
|
@ -144,17 +137,12 @@ impl JocalService {
|
|||
}
|
||||
|
||||
pub async fn send_text(&self, peer: &str, text: &str) -> Result<()> {
|
||||
// Generate file metadata
|
||||
let file_metadata = FileMetadata::from_text(text)?;
|
||||
|
||||
// Prepare files map
|
||||
let mut files = BTreeMap::new();
|
||||
files.insert(file_metadata.id.clone(), file_metadata.clone());
|
||||
|
||||
// Prepare upload
|
||||
let prepare_response = self.prepare_upload(peer, files).await?;
|
||||
|
||||
// Get file token
|
||||
let token = prepare_response
|
||||
.files
|
||||
.get(&file_metadata.id)
|
||||
|
@ -162,7 +150,6 @@ impl JocalService {
|
|||
|
||||
let bytes = Bytes::from(text.to_owned());
|
||||
|
||||
// Upload file
|
||||
self.send_bytes(
|
||||
&prepare_response.session_id,
|
||||
&file_metadata.id,
|
||||
|
|