Compare commits

...

3 Commits

Author SHA1 Message Date
Margret Riegert
bdeda666c1 api deserialization, sidebars 2026-03-20 02:43:24 -04:00
Margret Riegert
505cc110aa Rename cargo package 2026-03-20 01:56:02 -04:00
Margret Riegert
269e8ef6ee Add ongoing notes 2026-03-20 01:52:14 -04:00
7 changed files with 338 additions and 31 deletions

163
Cargo.lock generated
View File

@@ -1234,6 +1234,17 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "ermine"
version = "0.1.0"
dependencies = [
"serde",
"serde_json",
"slint",
"slint-build",
"ureq",
]
[[package]]
name = "errno"
version = "0.3.14"
@@ -1645,6 +1656,17 @@ dependencies = [
"unicode-width",
]
[[package]]
name = "getrandom"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
dependencies = [
"cfg-if",
"libc",
"wasi",
]
[[package]]
name = "getrandom"
version = "0.3.4"
@@ -1858,6 +1880,22 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48ce8546b993eaf241d69ded33b1be6d205dd9857ec879d9d18bd05d3676e144"
[[package]]
name = "http"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a"
dependencies = [
"bytes",
"itoa",
]
[[package]]
name = "httparse"
version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
[[package]]
name = "i-slint-backend-linuxkms"
version = "1.15.1"
@@ -3803,6 +3841,20 @@ dependencies = [
"bytemuck",
]
[[package]]
name = "ring"
version = "0.17.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
dependencies = [
"cc",
"cfg-if",
"getrandom 0.2.17",
"libc",
"untrusted",
"windows-sys 0.52.0",
]
[[package]]
name = "rowan"
version = "0.16.1"
@@ -3884,6 +3936,41 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "rustls"
version = "0.23.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4"
dependencies = [
"log",
"once_cell",
"ring",
"rustls-pki-types",
"rustls-webpki",
"subtle",
"zeroize",
]
[[package]]
name = "rustls-pki-types"
version = "1.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd"
dependencies = [
"zeroize",
]
[[package]]
name = "rustls-webpki"
version = "0.103.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53"
dependencies = [
"ring",
"rustls-pki-types",
"untrusted",
]
[[package]]
name = "rustversion"
version = "1.0.22"
@@ -4154,14 +4241,6 @@ dependencies = [
"spin_on",
]
[[package]]
name = "slint-rust-template"
version = "0.1.0"
dependencies = [
"slint",
"slint-build",
]
[[package]]
name = "slotmap"
version = "1.1.1"
@@ -4362,6 +4441,12 @@ dependencies = [
"syn",
]
[[package]]
name = "subtle"
version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "svgtypes"
version = "0.16.1"
@@ -4782,12 +4867,47 @@ version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
[[package]]
name = "untrusted"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]]
name = "unty"
version = "0.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae"
[[package]]
name = "ureq"
version = "3.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fdc97a28575b85cfedf2a7e7d3cc64b3e11bd8ac766666318003abbacc7a21fc"
dependencies = [
"base64",
"flate2",
"log",
"percent-encoding",
"rustls",
"rustls-pki-types",
"ureq-proto",
"utf-8",
"webpki-roots",
]
[[package]]
name = "ureq-proto"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d81f9efa9df032be5934a46a068815a10a042b494b6a58cb0a1a97bb5467ed6f"
dependencies = [
"base64",
"http",
"httparse",
"log",
]
[[package]]
name = "url"
version = "2.5.8"
@@ -4827,6 +4947,12 @@ dependencies = [
"xmlwriter",
]
[[package]]
name = "utf-8"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
[[package]]
name = "utf8_iter"
version = "1.0.4"
@@ -4894,6 +5020,12 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "wasi"
version = "0.11.1+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]]
name = "wasip2"
version = "1.0.2+wasi-0.2.9"
@@ -5160,6 +5292,15 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "webpki-roots"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed"
dependencies = [
"rustls-pki-types",
]
[[package]]
name = "weezl"
version = "0.1.12"
@@ -6215,6 +6356,12 @@ dependencies = [
"synstructure",
]
[[package]]
name = "zeroize"
version = "1.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
[[package]]
name = "zerotrie"
version = "0.2.3"

View File

@@ -1,12 +1,15 @@
[package]
name = "slint-rust-template"
version = "0.1.0"
edition = "2021"
name = "ermine"
version = "0.1.0"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
serde = {version = "1.0.149", features = ["derive"]}
serde_json = "1.0.149"
slint = "1.14.1"
ureq = "3.2.0"
[build-dependencies]
slint-build = "1.14.1"

70
NOTES.md Normal file
View File

@@ -0,0 +1,70 @@
# 2026-02-22
## 2200
Requirements:
- Need a better Revolt client that's cross-platform native
- Ideally macOS / iPadOS / iOS / Windows / Linux / Android / Web
- Need the UI / UX to be top-notch and feel good in the hands, otherwise people won't use it
- On top of this, it should also be _fun_ and _cute_
- Accessibility should be considered, but I do not have experience in this area, will need to do research
- Currently Revolt has a (now deprecated) web app and a native app that doesn't support custom endpoints
- We're forking from Revolt, so don't need to worry about breaking changes on their end
- Sticking with the name ermine cause I like it (stylized `ermine` all lowercase to be cool)
- Decided to try using Slint + Rust for a more bare-bones low-level approach
- Licensing
- Slint requires GPLv3 without a paid license - fine with this
- Revolt is also AGPLv3
- Code I write will be MIT as I don't want to hinder people re-using anything for any reason
- Have already spent a number of hours 100% vibe coding a prototype
- Used Flutter + Dart (was considered for this iteration)
- It worked okay, had a UX close to what I want as a starting point, so we know what we want is possible, we just need to build it
- There are just too many bugs and there's no way to trust the code written underneath
- Feel awful and ashamed of vibe coding, giving away my agency and creativity to an LLM and to big tech, rotting away my ability to create
- This is a project to push myself outside of my comfort zone towards something I've not done before, to grow beyond the limitations I've placed upon myself
- Having seen what other people've worked on, over months, and the kind of dedication they have towards software quality, I know this is something that will take time to craft instead of something I can bang out in a few hyperfixated days
Resources:
- Slint
- https://docs.slint.dev/latest/docs/slint/
- Rust
- https://docs.rs/http/latest/http/
- https://docs.rs/sqlite/latest/sqlite/
- https://docs.rs/json/latest/json/
- https://docs.rs/websocket/latest/websocket/
- https://doc.rust-lang.org/rust-by-example/testing/unit_testing.html
- Revolt
- https://developers.stoat.chat/developers/endpoints/
- https://developers.stoat.chat/api-reference
- https://stoat.chat/api
- https://revolt.handmadecities.com/api
- https://github.com/livekit/livekit
- HMC
- https://git.handmadecities.com/HMC/handmade-revolt
Notes:
- Slint reminds me a lot of when I tried messing with SwiftUI
- Trying to make things look pretty early on is a fools errand
- Need to remember to make things work, then make them look nice
- Have not used Rust before so this should be interesting
- HMC Revolt seems to be on version 0.8.2 while Stoat is now on 0.11.5
- Have an OpenAPI spec for the latest stoat, but no historical ones
- Need to figure out what differences exist, if any, and how to support different server versions cleanly
- Seems like there's an `/api` endpoint with information about the server (if it's a revolt server)
- App version
- autumn endpoint (file server)
- january endpoint (media server)
- voso ?
- livekit ?
- livekit seems to be what's used for voice / video support
- Currently not supported by HMC Revolt instance
- main websocket
- Trying to use progenitor + openAPI spec to generate rust code, not going that well, and Gwen said it may be better to write these by hand
- progenitor didn't work due to it operating under the assumption that endpoints only return one type, which this spec violates (https://github.com/oxidecomputer/progenitor/issues/950)
- Read through documentation mostly, taking it slow and making sure I understand how things are working, being patient with myself
# 2026-03-20
## 0220
Figured the API endpoint (`/api`) would be a good place to start. Now have successful deserialization.
Sidebars are important, so I added two, one for channel list and another for server list. I'm still trying to figure out the best way to integrate different instances seamlessly.

32
src/api.rs Normal file
View File

@@ -0,0 +1,32 @@
#[derive(serde::Deserialize, Debug)]
pub struct ServerInfo {
// version number
revolt: String,
// main websocket
ws: String,
// main app URL
app: String,
vapid: String,
features: ServerFeatures,
}
#[derive(serde::Deserialize, Debug)]
struct ServerFeatures {
email: bool,
invite_only: bool,
// file server
autumn: Option<ServerFeatureEndpoint>,
// media server
january: Option<ServerFeatureEndpoint>,
//
voso: Option<ServerFeatureEndpoint>,
// voip server
livekit: Option<ServerFeatureEndpoint>,
}
#[derive(serde::Deserialize, Debug)]
struct ServerFeatureEndpoint {
enabled: bool,
url: Option<String>,
ws: Option<String>,
}

View File

@@ -2,10 +2,20 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
use std::error::Error;
mod api;
slint::include_modules!();
fn main() -> Result<(), Box<dyn Error>> {
let response = ureq::get("https://revolt.handmadecities.com/api")
.call()?
.body_mut()
.read_to_string()?;
let server_info: api::ServerInfo = serde_json::from_str(&response).unwrap();
println!("{:#?}", server_info);
let ui = AppWindow::new()?;
// ui.on_request_increase_value({

View File

@@ -1,32 +1,50 @@
import { Button, VerticalBox, TextEdit } from "std-widgets.slint";
import { ChatView } from "chat-view.slint";
import { ChannelSidebar, ServerSidebar } from "sidebar.slint";
export component AppWindow inherits Window {
title: "ermine";
VerticalLayout {
property <bool> sidebar-open: true;
ChatView {
messages: [
{ text: "hello", sender: "Bob", time: "10:00" },
{ text: "hey!", sender: "Joe", time: "10:01" },
{ text: "i like cats c:", sender: "Bob", time: "10:03" },
{ text: "me too c:", sender: "Joe", time: "10:08" },
{
text: "meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow ",
sender: "Joe",
time: "10:08"
},
{
text: "meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow ",
sender: "Bob",
time: "10:12"
},
];
HorizontalLayout {
ServerSidebar {
open: root.width > 600px;
server_list: [{ icon: "😳" }, { icon: "😍" }];
background: yellow;
}
TextEdit {
height: 64px;
font-size: 16px;
ChannelSidebar {
open: root.width > 600px ? sidebar-open : false;
channel_list: [{ title: "Foo" }, { title: "Bar" }];
background: blue;
}
VerticalLayout {
ChatView {
messages: [
{ text: "hello", sender: "Bob", time: "10:00" },
{ text: "hey!", sender: "Joe", time: "10:01" },
{ text: "i like cats c:", sender: "Bob", time: "10:03" },
{ text: "me too c:", sender: "Joe", time: "10:08" },
{
text: "meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow ",
sender: "Joe",
time: "10:08"
},
{
text: "meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow ",
sender: "Bob",
time: "10:12"
},
];
}
TextEdit {
height: 64px;
font-size: 16px;
}
}
}
}

27
ui/sidebar.slint Normal file
View File

@@ -0,0 +1,27 @@
export component ChannelSidebar inherits Rectangle {
in property <bool> open;
in property <[{title: string}]> channel_list;
width: open ? 200px : 0px;
clip: true;
VerticalLayout {
width: 200px;
for channel in channel_list: Text {
text: channel.title;
}
}
}
export component ServerSidebar inherits Rectangle {
in property <bool> open;
in property <[{icon: string}]> server_list;
width: open ? 48px : 0px;
VerticalLayout {
width: 48px;
for server in server_list: Text {
text: server.icon;
}
}
}