A native, minimalist TODO & reminder application for Fedora / GNOME, written in Rust.
solo/
├── Cargo.toml # Cargo workspace
├── crates/
│ ├── solo-core/ # shared model, JSON storage, DBus constants
│ │ └── src/{lib,model,storage}.rs
│ ├── solo-daemon/ # background service → binary: solo-daemon
│ │ └── src/{main,dbus,scheduler}.rs
│ └── solo-ui/ # dashboard → binary: solo
│ └── src/{main,proxy}.rs
├── data/
│ ├── org.solo.Solo.desktop # application launcher (app grid)
│ ├── solo-daemon.desktop # XDG autostart fallback
│ └── solo-daemon.service # systemd user service (preferred)
├── data/icons/org.solo.Solo.svg # scalable app icon (SVG)
├── gnome-extension/[email protected]/ # GJS panel extension
│ ├── extension.js
│ └── metadata.json
├── scripts/install.sh # build + install everything into ~/.local
└── README.md
You need the Rust toolchain plus the GTK4 / libadwaita / D-Bus development headers
(the solo dashboard links against the system GTK4 and libadwaita libraries).
On Fedora:
sudo dnf install rust cargo gtk4-devel libadwaita-devel dbus-devel pkgconf-pkg-configIf your distro's
rustcis older than what the crates require, install the toolchain via rustup instead:curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
You also need a GNOME session (the daemon notifications and the panel extension target GNOME Shell 45–48, which is what current Fedora Workstation ships).
All install methods are per-user and require no root — everything lands under
~/.localand~/.config.
From the repository root:
./scripts/install.shThis will:
cargo build --release(producestarget/release/soloandtarget/release/solo-daemon).- Install both binaries to
~/.local/bin/. - Install the app launcher to
~/.local/share/applications/. - Install and start the daemon as a systemd user service
(
~/.config/systemd/user/solo-daemon.service). Ifsystemctlis unavailable it falls back to an XDG autostart.desktopfile and launches the daemon for the current session. - Copy the GNOME Shell extension to
~/.local/share/gnome-shell/extensions/[email protected]/.
The script prints the remaining manual steps when it finishes.
PATH note: make sure
~/.local/binis on yourPATH. On Fedora it usually is. Ifsolois "command not found", add this to~/.bashrcand re-login:export PATH="$HOME/.local/bin:$PATH"
If you'd rather do each step yourself:
# 1. Build release binaries
cargo build --release
# 2. Install the binaries
mkdir -p ~/.local/bin
install -m 0755 target/release/solo ~/.local/bin/solo
install -m 0755 target/release/solo-daemon ~/.local/bin/solo-daemon
# 3. Install the app launcher (so "Solo" shows in the app grid)
mkdir -p ~/.local/share/applications
install -m 0644 data/org.solo.Solo.desktop ~/.local/share/applications/
# 4a. Install the daemon as a systemd user service (preferred)
mkdir -p ~/.config/systemd/user
install -m 0644 data/solo-daemon.service ~/.config/systemd/user/
systemctl --user daemon-reload
systemctl --user enable --now solo-daemon.service
# ── OR ──
# 4b. Use XDG autostart instead of systemd
mkdir -p ~/.config/autostart
install -m 0644 data/solo-daemon.desktop ~/.config/autostart/
solo-daemon & # start it now for this session
# 5. Install the GNOME Shell extension
mkdir -p ~/.local/share/gnome-shell/extensions/[email protected]
cp -r gnome-extension/[email protected]/. \
~/.local/share/gnome-shell/extensions/[email protected]/After the extension files are in place:
# Reload the extension list (Wayland: log out and back in; Xorg: Alt+F2 then type "r")
gnome-extensions enable [email protected]Check it loaded:
gnome-extensions info [email protected]On Wayland (Fedora's default) you must log out and back in before GNOME Shell sees a newly added extension — there is no in-session shell reload on Wayland.
-
Daemon is running:
systemctl --user status solo-daemon.service # if installed via systemd # or, regardless of install method: gdbus introspect --session --dest org.solo.Daemon --object-path /org/solo/Daemon
The introspection should list the
org.solo.Daemon1interface. -
End-to-end test — create a task that fires in one minute and lasts two:
gdbus call --session --dest org.solo.Daemon --object-path /org/solo/Daemon \ --method org.solo.Daemon1.AddTask "Test reminder" \ "$(( $(date +%s) + 60 ))" 120
- In ~60 s you should get a desktop notification "⏰ Test reminder".
- The task title + a ticking countdown should appear in the top panel for 2 minutes.
- Open the dashboard (
solo) and you'll see the task listed.
Launch Solo from the app grid, or run solo in a terminal.
- Add a task: fill in Title, Starts in (minutes from now) and Duration (minutes), then press Add task.
- Edit: press the ✎ button on a row — its values load into the form; press Save.
- Delete: press the 🗑 button on a row.
- Close the window: the dashboard process exits, but the daemon keeps running, keeps firing reminders, and keeps updating the panel countdown.
Start time is entered as minutes from now to keep the UI minimal. See Development for how to switch to an absolute date/time picker.
-
On login: the systemd user service (
WantedBy=default.target,After=graphical-session.target) startssolo-daemonautomatically. With the XDG fallback, the GNOME session launches it from~/.config/autostart. -
On crash: the systemd unit has
Restart=on-failure(2 s back-off). -
On reboot: the daemon reloads
tasks.jsonand reinjects all tasks. Tasks already notified keep theirnotifiedflag, so reminders are never re-fired for past events. -
Manual control:
systemctl --user start solo-daemon.service systemctl --user stop solo-daemon.service systemctl --user restart solo-daemon.service journalctl --user -u solo-daemon.service -f # live logs
Service org.solo.Daemon, object /org/solo/Daemon, interface org.solo.Daemon1:
| Member | Kind | Signature | Notes |
|---|---|---|---|
AddTask(title, start, duration) |
method | (s x x) → s |
returns new task id |
UpdateTask(id, title, start, duration) |
method | (s s x x) → b |
true if found |
DeleteTask(id) |
method | s → b |
true if removed |
ListTasks() |
method | → s |
JSON array of tasks |
ActiveTask() |
method | → s |
JSON {title,start,end} or {} |
ActiveTaskChanged |
signal | (s x x) |
title, start, end (epoch s); empty title ⇒ none active |
TasksChanged |
signal | () |
emitted after any mutation |
x = 64-bit signed integer (epoch seconds, UTC), s = string, b = boolean.
- Tasks are stored as JSON at
~/.local/share/solo/tasks.json(XDG data dir). - Writes are atomic (temp file + rename) so a crash mid-write can't corrupt the list.
- The daemon is the only writer; the UI mutates exclusively through D-Bus.
Example record:
[
{
"id": "0b6c…",
"title": "Stand-up",
"start_time": 1760000000,
"duration_secs": 900,
"notified": false
}
]Pull the latest code and re-run the installer:
git pull
./scripts/install.sh
systemctl --user restart solo-daemon.service # if you used systemdIf you changed the extension, log out/in (Wayland) so GNOME Shell reloads it.
# Stop & disable the daemon
systemctl --user disable --now solo-daemon.service 2>/dev/null || true
rm -f ~/.config/systemd/user/solo-daemon.service
systemctl --user daemon-reload
# Remove autostart fallback (if used)
rm -f ~/.config/autostart/solo-daemon.desktop
# Disable & remove the extension
gnome-extensions disable [email protected] 2>/dev/null || true
rm -rf ~/.local/share/gnome-shell/extensions/[email protected]
# Remove binaries and launcher
rm -f ~/.local/bin/solo ~/.local/bin/solo-daemon
rm -f ~/.local/share/applications/org.solo.Solo.desktop
# Remove data (optional — this deletes your tasks)
rm -rf ~/.local/share/solo| Symptom | Fix |
|---|---|
solo: command not found |
Add ~/.local/bin to PATH and re-login. |
| Dashboard shows "daemon unavailable" | Daemon isn't running: systemctl --user start solo-daemon.service, or check journalctl --user -u solo-daemon.service. |
| No desktop notifications | Ensure a notification daemon is active (GNOME provides one). Test with notify-send hello. |
| Panel countdown never appears | Confirm the extension is enabled (gnome-extensions info [email protected]) and that you logged out/in on Wayland. Check journalctl --user -f for Solo: log lines. |
| Build fails on missing headers | Install gtk4-devel libadwaita-devel dbus-devel (see Prerequisites). |
cargo too old |
Use rustup (see Prerequisites) and cargo update. |
| App grid shows a generic/blank icon | Run gtk-update-icon-cache -f ~/.local/share/icons/hicolor and re-login. |
These are both produced by the daemon's scheduler, so check it directly:
# 1. Make sure you're running the freshly built binary, not an old one.
systemctl --user restart solo-daemon.service
# 2. Watch the daemon log, then add a task that fires in ~5 seconds.
journalctl --user -u solo-daemon.service -f &
gdbus call --session --dest org.solo.Daemon --object-path /org/solo/Daemon \
--method org.solo.Daemon1.AddTask "Smoke test" "$(( $(date +%s) + 5 ))" 60Within ~5 s the log should print notifying — Smoke test and active task -> Smoke test ….
-
You see those log lines but no desktop popup: confirm a notification server is running (
notify-send hishould pop). On GNOME it always is. -
You see no log lines at all: the daemon isn't processing — confirm it's the new binary (
systemctl --user restart) and that only one instance owns the bus name. -
Panel stays empty. First confirm the daemon reports an active task while one is running:
gdbus call --session --dest org.solo.Daemon --object-path /org/solo/Daemon \ --method org.solo.Daemon1.ActiveTask
- Returns
('{"title":"…","start":…,"end":…}',)⇒ daemon is fine; the issue is the extension. Re-copyextension.js, ensure it's enabled (gnome-extensions info [email protected]), and log out/in on Wayland so the Shell reloads it. Watchjournalctl --user -fforSolo:lines. - Returns
('{}',)while a task should be active ⇒ daemon side: check the task's start time hasn't already passed its duration.
The panel polls
ActiveTask()once per second, so no D-Bus signal needs to be delivered to the Shell for it to work. - Returns
Historical bug (fixed): the notification call is blocking and used to be invoked on the async runtime, which could panic and kill the scheduler loop — taking notifications and the panel signal down together. It now runs on a dedicated blocking thread.
Run the pieces directly without installing:
cargo run -p solo-daemon # terminal 1: background service (logs to stderr)
cargo run -p solo-ui # terminal 2: dashboard windowType-check / lint everything:
cargo check
cargo clippy --all-targetsNotable extension points:
- Absolute date/time picker: the two
adw::SpinRows incrates/solo-ui/src/main.rsencode start time as minutes from now. Replace them with agtk::Calendar+ time entry and convert to epoch seconds before callingAddTask. - SQLite instead of JSON:
crates/solo-core/src/storage.rsis the only module that touches the filesystem. Swap its body forrusqliteand nothing else needs to change. - Dependency versions assume the current ecosystem (relm4 0.9 / gtk4 0.9 /
libadwaita 0.7 / zbus 5). Run
cargo updateif your distro pins different minors.
| Requirement | Where it lives |
|---|---|
| Rust + GTK4/relm4/libadwaita, follows system dark mode | crates/solo-ui (adw::StyleManager) |
Local persistence in ~/.local/share/solo/ |
crates/solo-core/src/storage.rs |
| Daemon auto-starts, survives reboot, reinjects tasks | data/solo-daemon.service, crates/solo-daemon/src/main.rs |
| Closing the dashboard keeps the daemon alive | Separate processes; UI never owns the daemon |
| Native notifications via DBus | crates/solo-daemon/src/scheduler.rs (notify-rust) |
| Daemon exposes active-task state on DBus | crates/solo-daemon/src/dbus.rs (ActiveTaskChanged) |
| GNOME top-panel live countdown | gnome-extension/[email protected]/extension.js |
MIT.