second jocalsend post
This commit is contained in:
parent
ac2415bc2f
commit
8648dc0441
9 changed files with 431 additions and 3 deletions
|
@ -1,6 +1,7 @@
|
||||||
+++
|
+++
|
||||||
title = "Speculative Work, by NebCorp HIAS"
|
title = "Speculative Research & Development, by NebCorp HIAS"
|
||||||
sort_by = "date"
|
sort_by = "date"
|
||||||
|
path = "rnd"
|
||||||
toc = true
|
toc = true
|
||||||
[extra]
|
[extra]
|
||||||
toc = true
|
toc = true
|
||||||
|
|
BIN
content/rnd/jocalsend_impl/almost_1_dot_0.png
Normal file
BIN
content/rnd/jocalsend_impl/almost_1_dot_0.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 66 KiB |
BIN
content/rnd/jocalsend_impl/first_working_version_on_phone.png
Normal file
BIN
content/rnd/jocalsend_impl/first_working_version_on_phone.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 58 KiB |
427
content/rnd/jocalsend_impl/index.md
Normal file
427
content/rnd/jocalsend_impl/index.md
Normal file
|
@ -0,0 +1,427 @@
|
||||||
|
+++
|
||||||
|
title = "Jocalsend: behind the scenes"
|
||||||
|
slug = "jocalsend-development"
|
||||||
|
date = "2025-09-02"
|
||||||
|
[taxonomies]
|
||||||
|
tags = ["software", "rnd", "proclamation", "rust", "jocalsend"]
|
||||||
|
+++
|
||||||
|
|
||||||
|
Recently, I released a piece of software called [`jocalsend`](/sundries/jocalsend/), to great
|
||||||
|
acclaim. In that post announcing it, I wrote,
|
||||||
|
|
||||||
|
> I plan on writing a follow-up about the design and implementation of jocalsend, but that's going
|
||||||
|
> to be a much longer and even-more-niche-interest post, so I'll just skip that for now.
|
||||||
|
|
||||||
|
It seems that now the time has come to fulfill that promise. If the nittty-gritty of the
|
||||||
|
implementation and design of terminal-UI async Rust programs is your jam, then today is your lucky
|
||||||
|
day.
|
||||||
|
|
||||||
|
## Humble beginnings
|
||||||
|
|
||||||
|
Once I learned about the existence of LocalSend and had decided to write a TUI version, I looked
|
||||||
|
around for any existing Rust crates that might have already made some in-roads on that problem.
|
||||||
|
Right off the bat, I found [this one, called `localsend`](https://github.com/wylited/localsend),
|
||||||
|
which sounded promising. I cloned the repo and took a look.
|
||||||
|
|
||||||
|
There wasn't any example code using it, but it wasn't too tough to [whip up a simple
|
||||||
|
toy](https://git.kittencollective.com/nebkor/joecalsend/src/commit/c422bbcd00070140f77466ff576dbc1c37dce7e7/src/main.rs)
|
||||||
|
to try it out:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use joecalsend::{models::device::DeviceInfo, Client};
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
let device = DeviceInfo::default();
|
||||||
|
dbg!(device);
|
||||||
|
|
||||||
|
let client = Client::with_config(
|
||||||
|
DeviceInfo::default(),
|
||||||
|
53317,
|
||||||
|
"/home/ardent/joecalsend".into(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let (h1, h2, h3) = client.start().await.unwrap();
|
||||||
|
tokio::join!(h1, h2, h3);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Running it produced the following, including a log line from me sending a screenshot of LocalSend on
|
||||||
|
my phone to "joecalsend" on my desktop:
|
||||||
|
|
||||||
|
```
|
||||||
|
[src/main.rs:6:5] device = DeviceInfo {
|
||||||
|
alias: "RustSend",
|
||||||
|
version: "2.1",
|
||||||
|
device_model: None,
|
||||||
|
device_type: Some(
|
||||||
|
Headless,
|
||||||
|
),
|
||||||
|
fingerprint: "01K43EBSN50004H9BRHK2AE45X",
|
||||||
|
port: 53317,
|
||||||
|
protocol: "http",
|
||||||
|
download: false,
|
||||||
|
announce: Some(
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
Socket local addr: 0.0.0.0:53317
|
||||||
|
Listening on multicast addr: 224.0.0.167:53317
|
||||||
|
HTTP server listening on 0.0.0.0:53317
|
||||||
|
Error during HTTP announcement: Request error: error sending request for url (http://192.168.1.110:53317/api/localsend/v2/register)
|
||||||
|
Error during HTTP announcement: Request error: error sending request for url (http://192.168.1.110:53317/api/localsend/v2/register)
|
||||||
|
Error during HTTP announcement: Request error: error sending request for url (http://192.168.1.110:53317/api/localsend/v2/register)
|
||||||
|
Error during HTTP announcement: Request error: error sending request for url (http://192.168.1.110:53317/api/localsend/v2/register)
|
||||||
|
Received upload request from alias: phone
|
||||||
|
```
|
||||||
|
|
||||||
|
On my phone, it showed up like this:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
When it came time to approve the send request, a GUI dialog popped up on my desktop:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
This was a hugely validating step for me, since it showed that I could interact with the reference
|
||||||
|
LocalSend app *at all*. It gave me the confidence I needed to press forward with more, more
|
||||||
|
complicated, work. It's hard to overstate the benefit of this kind of early affirmation, which
|
||||||
|
proves out the fundamental assumptions, like, "interoperating with the official implementation is
|
||||||
|
possible." I highly recommend getting something working as end to end as possible, as quickly as possible.
|
||||||
|
|
||||||
|
Since I wanted to make a terminal version, I knew I'd need to get rid of that GUI piece of the
|
||||||
|
backend, which is a nice segue into...
|
||||||
|
|
||||||
|
### *Great artists steal* -- an original phrase, copyright: me
|
||||||
|
|
||||||
|
The astute may have noticed that my toy program was not doing `use localsend::{...}`, but rather,
|
||||||
|
`use joecalsend::{...}`[^joe-v-jo]. This was because as convenient as just using `localsend` as a
|
||||||
|
3rd-party dependency would be, I knew I'd need to change it so much that simply copying its source
|
||||||
|
code into my project would be much easier and faster than trying to get my changes accepted in a
|
||||||
|
stranger's project[^upstreaming]. Plus I'm a fan in general of having as much control as possible
|
||||||
|
over your critical dependencies, and nothing is more controlled than simply inlining them into your
|
||||||
|
own stuff. Luckily, localsend was released under the terms of the [Apache
|
||||||
|
license](https://git.kittencollective.com/nebkor/joecalsend/src/commit/c422bbcd00070140f77466ff576dbc1c37dce7e7/3P-LICENSE.txt),
|
||||||
|
which permits this kind of chicanery.
|
||||||
|
|
||||||
|
In addition to needing to ditch the GUI dialog, I wanted to add support for HTTPS, as well as stuff
|
||||||
|
like persistent configuration files. The initial backend code from this first commit was 825 lines;
|
||||||
|
at the time of writing, the latest version of jocalsend's backend[^frontend] is 1252 lines, about
|
||||||
|
1.5x as long[^loc].
|
||||||
|
|
||||||
|
As it was, though, this initial version couldn't do much besides announce itself as a peer on the
|
||||||
|
local subnetwork, and receive files from other peers, saving them to a hard-coded location in my
|
||||||
|
homedir; the commit message from this first commit was simply: "can receive text and files". It was
|
||||||
|
time to start thinking about an actual interactive user interface.
|
||||||
|
|
||||||
|
## Cooking up a terminal UI with Ratatui
|
||||||
|
|
||||||
|
I knew that I wanted to use [ratatui](https://ratatui.rs/) for my frontend. In its words, it's
|
||||||
|
|
||||||
|
> ... a Rust library for cooking up delicious TUIs (terminal user interfaces). It is a
|
||||||
|
> lightweight library that provides a set of widgets and utilities to build simple or complex rust
|
||||||
|
> TUIs.
|
||||||
|
|
||||||
|
I'd had some previous exposure to [tui-rs](https://github.com/fdehau/tui-rs/), of which ratatui was
|
||||||
|
a blessed fork and continuation, but hadn't done too too much with it. Still, it was the framework
|
||||||
|
powering a few TUI apps I was already familiar with and a fan of, like [Atuin](https://atuin.sh/)
|
||||||
|
and [Yazi](https://yazi-rs.github.io/), so it wasn't really a hard decision.
|
||||||
|
|
||||||
|
However, I didn't have *much* exposure to it, nor do I typically do much frontend development; I joke
|
||||||
|
that most of the software I write uses a network and logs as the UI. Another tiny thing is that most
|
||||||
|
of the Ratatui examples were not async, and jocalsend definitely was. I decided to get my feet wet
|
||||||
|
by opening a [pull request](https://github.com/ratatui/crates-tui/pull/142) to do some code updates
|
||||||
|
for a Ratatui [example application](https://github.com/ratatui/crates-tui) that used the
|
||||||
|
[Tokio](https://tokio.rs/) async runtime, which jocalsend also used.
|
||||||
|
|
||||||
|
Once I had that merged, I finally got to work on the actual TUI portion of jocalsend, [which started
|
||||||
|
out quite
|
||||||
|
humbly](https://git.kittencollective.com/nebkor/joecalsend/src/commit/f7295233440567009a1b53ff358c6e1d566b49c1/src/main.rs#L48-L147);
|
||||||
|
it had been two days since I started the project. It didn't actually do anything except display a
|
||||||
|
frame with "Counter App Tutorial" (an artifact of stealing from a [Ratatui
|
||||||
|
tutorial](https://ratatui.rs/tutorials/counter-app/)) and accept `q` as a way to quit the program,
|
||||||
|
but it was a start.
|
||||||
|
|
||||||
|
After about a week[^week] of banging on the frontend, I was [able to
|
||||||
|
remove](https://git.kittencollective.com/nebkor/joecalsend/commit/5f2e2f3eb2ca359601ac2b23d94c73a4f327527f)
|
||||||
|
the dependency on the `native-dialog` GUI notification library, and rely solely on terminal
|
||||||
|
interaction. The look of it was very close to the final version, though functionality was still
|
||||||
|
limited:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
By this time, the lines of frontend code was just over half as many as backend; 497 vs. 943,
|
||||||
|
respectively. The greatest challenge I'd faced and overcome with the frontend was getting the
|
||||||
|
"Listeners" widget to display text featuring both left AND right justification, *on the same line*
|
||||||
|
(the solution was to use a table).
|
||||||
|
|
||||||
|
After another five or so days, it was looking *very* close to its final form. I was able to send and
|
||||||
|
receive both files and text, with preview for text shown:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Around that time, I was chatting about it with some colleagues, and made a list of things I wanted
|
||||||
|
before 1.0:
|
||||||
|
|
||||||
|
> - https (self-signed certs)
|
||||||
|
> - persistent identity (currently it generates a new self-id every time its run, but it once there's https, the id will be the sha256 of the private key)
|
||||||
|
> - related to the above, configuration files and persistent ssl keys for identity
|
||||||
|
> - CLI flags to just invoke it with text or file already input or selected
|
||||||
|
> - filtering of files in the file browser, currently you need to navigate with arrows and enter key
|
||||||
|
> - readme/documentation
|
||||||
|
> - change name from joecalsend to jocalsend
|
||||||
|
|
||||||
|
and a few days after that, everything but file filtering was done, and I felt comfortable enough to
|
||||||
|
release the first version on [crates.io](https://crates.io/crates/jocalsend/1.0.0). For the 1.0
|
||||||
|
release, the amounts of backend and frontend code were nearly the same, at 1180 vs 919 lines
|
||||||
|
respectively.
|
||||||
|
|
||||||
|
## Back to backend
|
||||||
|
|
||||||
|
By now, the frontend was at least *looking* complete, so I turned most of my attention back to some
|
||||||
|
outstanding issues on the backend (though I did take a couple hours to [add fuzzy file
|
||||||
|
selection](https://git.kittencollective.com/nebkor/joecalsend/commit/8150bfacf24622cbbc501d2674554891ec601122)
|
||||||
|
to the frontend).
|
||||||
|
|
||||||
|
### I smell trouble...
|
||||||
|
|
||||||
|
There was still something bothering me, though, and it turns out it was a pretty serious bug. Here's
|
||||||
|
a high-level diagram of the main application event loop:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Shortly before the 1.0 release, here's what it looked like in code, roughly:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
loop {
|
||||||
|
terminal.draw(|frame| app.draw(frame))?;
|
||||||
|
|
||||||
|
app.handle_events().await?;
|
||||||
|
|
||||||
|
if app.screen() == CurrentScreen::Stopping {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that all the `handle_*()` methods are async; `handle_keyboard()` could potentially invoke a
|
||||||
|
number of actions, which are not shown for simplicity.
|
||||||
|
|
||||||
|
Following the chain from `handle_events()` to `prepare_upload()`, we have a sequence of async
|
||||||
|
calls until we finally get to the following in the [backend
|
||||||
|
code](https://git.kittencollective.com/nebkor/joecalsend/src/commit/d84a046ec9e745561d63a70298423b1498096e05/src/transfer.rs#L65-L79):
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let response = self
|
||||||
|
.client
|
||||||
|
.post(format!(
|
||||||
|
"{}://{}/api/localsend/v2/prepare-upload",
|
||||||
|
device.protocol, addr
|
||||||
|
))
|
||||||
|
.json(&PrepareUploadRequest {
|
||||||
|
info: self.config.device.clone(),
|
||||||
|
files: files.clone(),
|
||||||
|
})
|
||||||
|
.timeout(Duration::from_secs(30)).send().await?;
|
||||||
|
```
|
||||||
|
|
||||||
|
What's happening there is that the backend is making an HTTP request to the peer in order to get
|
||||||
|
permission to upload files to that remote. This manifests on the remote (receiving) side as an
|
||||||
|
interactive dialog where the user approves or denies the request.
|
||||||
|
|
||||||
|
Meanwhile, the future from that request is just sitting there waiting for the remote peer to reply,
|
||||||
|
which means that `handle_events()` is *also* just sitting there waiting for `prepare_upload()`. This
|
||||||
|
blocked the main frontend application runloop, which was bad user experience.
|
||||||
|
|
||||||
|
For the 1.0 release, I changed the runloop to look like this:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let mut alarm = tokio::time::interval(Duration::from_millis(200));
|
||||||
|
loop {
|
||||||
|
terminal.draw(|frame| app.draw(frame))?;
|
||||||
|
|
||||||
|
tokio::select! {
|
||||||
|
res = app.handle_events() => {
|
||||||
|
res?;
|
||||||
|
}
|
||||||
|
_ = alarm.tick() => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if app.screen() == CurrentScreen::Stopping {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The big difference there is that I've introduced a timeout ("alarm") that fires every 200
|
||||||
|
milliseconds, and put it into a
|
||||||
|
[`tokio::select`](https://docs.rs/tokio/latest/tokio/macro.select.html) branch along with the call
|
||||||
|
to `handle_events()`. `tokio::select` is a way to simultaneously wait on multiple futures; it
|
||||||
|
polls each future in a random order until one of them completes, and then cancels the rest.
|
||||||
|
|
||||||
|
This meant that it would only wait 200 milliseconds for the `prepare_upload()` future to complete,
|
||||||
|
and hence the `handle_events()` future to complete. Since it required a human person on the remote
|
||||||
|
side to see and respond to that request in that time, it meant that actually sending files was
|
||||||
|
completely broken.
|
||||||
|
|
||||||
|
At the time, I didn't quite realize this, because during my testing, I was using the text-sending
|
||||||
|
functionality rather than trying to send actual files. One quirk of the official LocalSend app is
|
||||||
|
that when it receives plain text, it does not offer you the ability to download it, or approve the
|
||||||
|
download; you can copy it to your clipboard or close the request, and that's it. So when the
|
||||||
|
`prepare_upload()` future was canceled, nothing seemed out of the ordinary on the remote side.
|
||||||
|
|
||||||
|
Despite my blissful ignorance, I knew that this design was not correct, and a few days later I'd
|
||||||
|
[updated the `prepare_upload()`
|
||||||
|
code](https://git.kittencollective.com/nebkor/joecalsend/src/commit/7eece474a326091c8d1ac354908aa2212a9ba024/src/transfer.rs#L103-L171)
|
||||||
|
to spawn a separate [Tokio task](https://docs.rs/tokio/latest/tokio/task/fn.spawn.html) that would
|
||||||
|
run on its own and communicate back to the frontend via a
|
||||||
|
[channel](https://docs.rs/tokio/latest/tokio/sync/mpsc/fn.channel.html). The main app runloop was
|
||||||
|
changed back to the version without `tokio::select!`, and all was well.
|
||||||
|
|
||||||
|
### Multi-woes
|
||||||
|
|
||||||
|
There were a couple other annoyances I'd noticed but hadn't dug into, but the time had come to
|
||||||
|
vanquish them. Most notably, it would sometimes get confused about the IP of remote peers after
|
||||||
|
receiving a remote peer's multicast datagram *from its own IP*, and think that its own IP was the
|
||||||
|
peer's. This would cause it to be unable to send text or files, though it could still receive. The
|
||||||
|
official LocalSend app never had this issue, so I decided to see what it did differently.
|
||||||
|
|
||||||
|
I noticed that it was setting the TTL on its multicast packets, used for discovery, to 1:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
while jocalsend [set the TTL to
|
||||||
|
8](https://git.kittencollective.com/nebkor/joecalsend/src/commit/7eece474a326091c8d1ac354908aa2212a9ba024/src/lib.rs#L118).
|
||||||
|
While looking at that, I also noticed that multicast loopback [was
|
||||||
|
enabled](https://git.kittencollective.com/nebkor/joecalsend/src/commit/7eece474a326091c8d1ac354908aa2212a9ba024/src/lib.rs#L117).
|
||||||
|
And for that matter, it was [using
|
||||||
|
`0.0.0.0`](https://git.kittencollective.com/nebkor/joecalsend/src/commit/7eece474a326091c8d1ac354908aa2212a9ba024/src/lib.rs#L119)
|
||||||
|
as the interface joining the multicast group, which was probably more promiscuous than I wanted.
|
||||||
|
|
||||||
|
Ultimately, the multicast setup went from,
|
||||||
|
|
||||||
|
```rust
|
||||||
|
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))?;
|
||||||
|
```
|
||||||
|
|
||||||
|
to,
|
||||||
|
|
||||||
|
```rust
|
||||||
|
socket.set_multicast_loop_v4(false)?;
|
||||||
|
socket.set_multicast_ttl_v4(1)?; // local subnet only
|
||||||
|
socket.join_multicast_v4(MULTICAST_IP, local_ip_addr)?;
|
||||||
|
```
|
||||||
|
|
||||||
|
and that solved the issue where jocalsend would receive remote datagrams from its own IP.
|
||||||
|
|
||||||
|
### Rust so fancy
|
||||||
|
|
||||||
|
I'd made a [few more releases](https://crates.io/crates/jocalsend/versions) with the fixes outlined
|
||||||
|
above, but then I got some reports of people running into build issues when trying to install with a
|
||||||
|
reasonably up-to-date rustc:
|
||||||
|
|
||||||
|
```
|
||||||
|
error[E0658]: let expressions in this position are unstable
|
||||||
|
--> /home/ygingras/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/jocalsend-1.6.18033/src/app/widgets.rs:415:12
|
||||||
|
|
|
||||||
|
415 | if let Some(md) = request.files.values().next()
|
||||||
|
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
|
||||||
|
= note: see issue #53667 <https://github.com/rust-lang/rust/issues/53667> for more information
|
||||||
|
```
|
||||||
|
|
||||||
|
Visiting the [issue from the error message](https://github.com/rust-lang/rust/issues/53667), it
|
||||||
|
shows that the feature was stabilized in Rust version 1.88, one version behind the latest stable
|
||||||
|
version. The solution was to add a [minimum supported Rust
|
||||||
|
version](https://doc.rust-lang.org/cargo/reference/rust-version.html) to jocalsend's `Cargo.toml`
|
||||||
|
file, like,
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[package]
|
||||||
|
name = "jocalsend"
|
||||||
|
edition = "2024"
|
||||||
|
rust-version = "1.89"
|
||||||
|
```
|
||||||
|
|
||||||
|
Once that was done, I [released a new version](https://crates.io/crates/jocalsend/1.6.180339) with
|
||||||
|
it, and confirmed with the users that it was working correctly.
|
||||||
|
|
||||||
|
## Summary, by the numbers
|
||||||
|
|
||||||
|
As of writing this, on September 2, 2025, we have the following stats:
|
||||||
|
|
||||||
|
- 2472 total lines of code
|
||||||
|
- 1252 in the backend
|
||||||
|
- 1220 in the frontend
|
||||||
|
- 1575 downloads from `crates.io`
|
||||||
|
- 122 commits on the `main` branch
|
||||||
|
- 7 published versions
|
||||||
|
- 5 pull requests merged in other projects' repos:
|
||||||
|
- update [`ratatui/crates-tui`](https://github.com/ratatui/crates-tui/pull/143) to edition2024
|
||||||
|
- update code patterns in [`ratatui/crates-tui`](https://github.com/ratatui/crates-tui/pull/142)
|
||||||
|
- add methods to increment or decrement log levels in [`rust-lang/log`](https://github.com/rust-lang/log/pull/692) (merged while I was writing this post!)
|
||||||
|
- upstream my backend changes to [`wylited/localsend`](https://github.com/wylited/localsend/pull/1)
|
||||||
|
- add method to reset the state of the fuzzy matcher in [`andylokandy/simsearch-rs`](https://github.com/andylokandy/simsearch-rs/pull/17)
|
||||||
|
- 1 developer
|
||||||
|
|
||||||
|
## What's next?
|
||||||
|
|
||||||
|
I believe in software being "done", and this software is pretty close. There are a couple small QoL
|
||||||
|
features I want to add, like putting the CWD in the file picking widget, and I want to update some
|
||||||
|
of the deps to use newly-released functionality (like removing [this bit of `unsafe`
|
||||||
|
code](https://git.kittencollective.com/nebkor/joecalsend/src/commit/4d78d67abeeecd3b986b3b016ffac61761184778/src/app/mod.rs#L152)
|
||||||
|
to change the log level in favor of using the methods I added in the PR linked above).
|
||||||
|
|
||||||
|
I'm also aware that I'm not validating the remote SSL certs, which are self-signed; I'm not even
|
||||||
|
noting their fingerprints in some application storage so I can check that they don't change
|
||||||
|
unexpectedly. This means that someone could, with enough preparation or luck, do a
|
||||||
|
person-in-the-middle attack and defeat the point of using SSL in the first place. I'm not sure what
|
||||||
|
the best thing to do here is, though I do want to do something.
|
||||||
|
|
||||||
|
But beyond those things, I don't think I'll be doing much more development on it. I'll continue to
|
||||||
|
use and enjoy it, though!
|
||||||
|
|
||||||
|
### Ooooh, shiny
|
||||||
|
|
||||||
|
That's not to say that I'm done with this type of software. While I was writing jocalsend, I learned
|
||||||
|
about [Iroh](https://www.iroh.computer/), which is a Rust library that "lets you establish direct
|
||||||
|
peer-to-peer connections whenever possible, falling back to relay servers if necessary. This gives
|
||||||
|
you fast, reliable connections that are authenticated and encrypted end-to-end using
|
||||||
|
[QUIC](https://en.wikipedia.org/wiki/QUIC)."
|
||||||
|
|
||||||
|
It would be neat to build a version of jocalsend with Iroh; the frontend code from jocalsend should
|
||||||
|
work almost without modification. I even have a name ready: "jirohsend"[^joe-again].
|
||||||
|
|
||||||
|
Of course, I'd need to build a mobile app for jirohsend, but I have a little bit of experience
|
||||||
|
building [Flutter apps with Rust backends](https://git.kittencollective.com/nebkor/cuttle), and I
|
||||||
|
don't think it would need a more complicated UI than the existing LocalSend Flutter app.
|
||||||
|
|
||||||
|
So if something like jirohsend sounds interesting to you, you know where to look for updates!
|
||||||
|
|
||||||
|
----
|
||||||
|
|
||||||
|
[^joe-v-jo]: I initially called the project "joecalsend" but later decided to make it *slightly*
|
||||||
|
less ego-centric. I didn't bother changing the repo name, though.
|
||||||
|
|
||||||
|
[^upstreaming]: I didn't want to just take without giving, so I [opened a
|
||||||
|
PR](https://github.com/wylited/localsend/pull/1) to upstream most of my changes back to
|
||||||
|
localsend. It was a pretty massive change so I wasn't really expecting it to be accepted, but to
|
||||||
|
my surprise, after several weeks of silence, it was merged! This project spurred a few external
|
||||||
|
PRs based on things I wanted or needed in it.
|
||||||
|
|
||||||
|
[^frontend]: The frontend code is 1220 lines of Rust, and definitely trickier! Application
|
||||||
|
development is harder than systems dev.
|
||||||
|
|
||||||
|
[^loc]: "lines of code" is generally a terrible metric for software, but what can you do; at least
|
||||||
|
this is reasonably apples-to-apples.
|
||||||
|
|
||||||
|
[^week]: For most of the development history, I had multiple commits per day, but there was a
|
||||||
|
roughly two-week period that fell in the middle of the time when I was really working on the
|
||||||
|
frontend where I made no progress on it, because I was working on a job application for [Oxide
|
||||||
|
Computer](https://oxide.computer/), which has been a long-time dream-job for me, and that took
|
||||||
|
most of my energy.
|
||||||
|
|
||||||
|
[^joe-again]: sorry, I can't help myself
|
BIN
content/rnd/jocalsend_impl/jocalsend_application_runloop.png
Normal file
BIN
content/rnd/jocalsend_impl/jocalsend_application_runloop.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 188 KiB |
BIN
content/rnd/jocalsend_impl/localsend_multicast_packet.png
Normal file
BIN
content/rnd/jocalsend_impl/localsend_multicast_packet.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 286 KiB |
BIN
content/rnd/jocalsend_impl/no_more_gui_dialog.png
Normal file
BIN
content/rnd/jocalsend_impl/no_more_gui_dialog.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 60 KiB |
BIN
content/rnd/jocalsend_impl/rustsend_native_dialog.png
Normal file
BIN
content/rnd/jocalsend_impl/rustsend_native_dialog.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 7 KiB |
|
@ -2,7 +2,7 @@
|
||||||
title = "Jocalsend: actually useful software if you're a weirdo like me"
|
title = "Jocalsend: actually useful software if you're a weirdo like me"
|
||||||
slug = "jocalsend"
|
slug = "jocalsend"
|
||||||
date = "2025-08-22"
|
date = "2025-08-22"
|
||||||
updated = "2025-08-23"
|
updated = "2025-09-02"
|
||||||
[taxonomies]
|
[taxonomies]
|
||||||
tags = ["software", "sundry", "proclamation", "localfirst", "useful", "p2p"]
|
tags = ["software", "sundry", "proclamation", "localfirst", "useful", "p2p"]
|
||||||
+++
|
+++
|
||||||
|
@ -99,7 +99,7 @@ and it'll do what you expect.
|
||||||
|
|
||||||
## How do I use it?
|
## How do I use it?
|
||||||
|
|
||||||
When you first run it, you'll be on the main screen. Clockwise from the bottom, the lower left contains information about the
|
When you first run it, you'll be on the main screen. Counter-clockwise from the bottom, the lower left contains information about the
|
||||||
network it's on, the lower right has a list of any LocalSend peers it discovered, the upper right
|
network it's on, the lower right has a list of any LocalSend peers it discovered, the upper right
|
||||||
has log output from jocalsend, and finally the upper left will show any incoming transfer requests,
|
has log output from jocalsend, and finally the upper left will show any incoming transfer requests,
|
||||||
with possible text preview:
|
with possible text preview:
|
||||||
|
|
Loading…
Reference in a new issue