Skip to content

syncthing: Syncthing status & control - #372

Merged
ItsLemmy merged 9 commits into
noctalia-dev:mainfrom
rylos:add-syncthing
Aug 16, 2026
Merged

syncthing: Syncthing status & control#372
ItsLemmy merged 9 commits into
noctalia-dev:mainfrom
rylos:add-syncthing

Conversation

@rylos

@rylos rylos commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

Plugin

  • Id: rylos/syncthing
  • New plugin
  • Update to an existing plugin (version bumped in plugin.toml)

This supersedes #71, which @ItsLemmy closed as stale with "re-open the PR when
cleaned up" — GitHub refuses to reopen it now that the branch has been rebased,
so here it is as a fresh PR. Both review points are addressed, and the bug that
made the poller stall has been found and fixed; details below.

What it does

Syncthing status and control from the bar: overall state at a glance, and a
panel to pause, resume and rescan without opening the web UI.

  • Bar widget — one glyph for the overall state (up to date, syncing,
    paused, unreachable, API key rejected …), with an optional count.
  • Panel — three counters (devices, folders, pending bytes), then two tabs:
    Folders with per-row pause/resume and rescan, and Devices with per-row
    pause/resume and the address and version of each connected device.
  • Launcher (/st) — every folder, with its state, and Enter to rescan.
  • Desktop widget and a shortcut entry for the same state.
  • Reads the API key and address straight out of Syncthing's own config.xml
    (~/.local/state/syncthing/config.xml, falling back to
    ~/.config/syncthing/config.xml), so there is nothing to configure in the
    common case; both can be overridden in settings.

External dependencies

  • syncthing — the REST API on the local instance is what the plugin polls.
  • gio / xdg-open — only to open the Syncthing web UI when the header button
    is clicked. Both are declared in dependencies.

No curl: HTTP goes through the host's own request API.

Testing

In daily use on niri against a real Syncthing instance with 3 folders and 4
devices, which is what the screenshots show.

  • Panel: both tabs, pause and resume, rescan and the web-UI button — exercised
    over the course of using it.
  • Launcher: /st and the rescan rows, re-checked for this PR after the badge
    fix below.
  • The poller: watched ~/.cache/noctalia/noctalia.log across a long session,
    which is how the CPU-budget abort described below was found, and confirmed
    clean afterwards.
  • noctalia plugins lint syncthing and
    python3 .github/workflows/scripts/validate-plugins.py are clean.

Since the version that was closed as stale:

  1. Both review points are done. The curl fallback is gone, and gio and
    xdg-open are declared.
  2. No more numbered callback slots. Every row passes a closure that captures
    its own folder or device (plugin_api 9), so the list has no length limit —
    previously it silently stopped at the sixteenth row.
  3. The stalling poller is fixed. update was blowing the host's per-callback
    CPU budget inside parseConfigXml, which re-read and re-scanned the ~16 KB
    config.xml on every poll with a non-greedy <gui[^>]*>(.-)</gui> pattern.
    The file is now located by index and parsed once, cached until the config
    changes; the aborted-callback watchdog became a generation counter, so a late
    reply can no longer overwrite a newer snapshot.
  4. The launcher no longer abuses badge. A badge is drawn in the icon slot
    and scaled to fill it, so "Up to date" covered the whole row; the state is
    the first half of the subtitle now.
  • Tested on Niri
  • Tested on Hyprland
  • Tested on Sway
  • Tested on another compositor:
  • Noctalia version tested against: 5.0.0 (97917d9ca07e)
  • Plugin API level: 9

Screenshots / Videos

Panel, Folders tab:

Folders tab

Panel, Devices tab (device addresses masked):

Devices tab

Launcher, /st:

Launcher

Bar widget — the circular arrows, next to the Tailnet dots:

Bar widget

Checklist

  • The directory name matches the part of id after the / in plugin.toml exactly.
  • It ships plugin.toml, README.md, thumbnail.webp, and translations/en.json.
  • README.md follows the
    README template, documents
    every entry id and dependency, and includes exact panel IPC commands and launcher prefixes where applicable.
  • I created thumbnail.webp with the thumbnail generator.
  • version follows semver and is bumped in this PR; plugin_api is the oldest API level this plugin requires.
  • Every non-English translation in this PR uses a locale supported by Noctalia core, and I can read, write, and
    understand that language well enough to review and maintain it (no unreviewed machine/LLM translations).
  • I did not edit catalog.toml; CI generates it.
  • This PR touches exactly one plugin directory.

Code review attestation

Plugins run as trusted, unsandboxed Luau in the user's session. Confirm:

  • The code is readable and not obfuscated, minified, or generated.
  • It does not download and execute remote code.
  • Every network call, filesystem write, and spawned process is something the description above accounts for.
  • I have the right to publish this code under the license declared in plugin.toml.

rylos and others added 9 commits August 15, 2026 17:50
Port of the v4 syncthing-status plugin (by Pir0c0pter0) to the v5 Luau
plugin API. Talks to the Syncthing REST API directly via noctalia.http
(no helper scripts). Bar widget, Folders|Devices panel with per-folder
and per-device pause/resume and rescan, sync progress, transfer rates,
event notifications, /st launcher provider, global-pause shortcut, and
a desktop widget. en + it translations.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review follow-up on noctalia-dev#71:

- every REST request now goes through noctalia.http with
  allow_insecure_tls (plugin_api 3 -> 7); the curl branch that
  interpolated the API key, base URL and body into a shell command line
  is gone, and the GUI opener shell-quotes its URL.
- declare gio and xdg-open in dependencies and document all three
  commands in the README Requirements section.
- format the last-check time with strftime, seed bar and desktop widgets
  from the current snapshot instead of waiting for the next publish, and
  give the panel room for four device rows without a border on each.
The root column's height is measured inside its padding, so pinning it
to the manifest height pushed the footer past the panel edge; subtract
the padding instead.

publish() was the only place clearing `busy`, so an HTTP callback that
never fired left the poller stuck with no further updates and a frozen
last-check time. Restart the poll after a grace period.
Four devices needed a scrollbar. The idle summary line repeated what the
header and the device tile already say, so it now shows only for the
states that add information; the column gap is tighter and the content
leaves slack for the panel frame so the footer is not flush against it.
The panel bound each row to one of 16 predeclared callback slots, so both
the folder and the device list were silently cut off past the sixteenth
entry. Pointer props take a function, so each row's buttons now close over
their own folder or device: MAX_FOLDER_SLOTS, the slot table, the *At
helpers and the numbered onPause/onResume/onRescan globals are gone.

plugin_api rises to 9, the level that introduced function callbacks.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…he poll

The poller kept stalling, and the log finally named it: 'update' exceeded
its CPU budget, in parseConfigXml. Every poll re-read Syncthing's config.xml
— tens of kilobytes — and located the <gui> block with a non-greedy pattern
that rescans the tail of the file for each starting position. When the
callback was cut off mid-flight, busy stayed set and the poller never
recovered.

config.xml is now parsed once and cached, by index rather than by pattern.
The cache is dropped when the plugin settings change and while nothing is
answering, so a Syncthing that gets installed or reconfigured later is still
picked up.

The watchdog it needed becomes a generation number instead: callbacks from a
superseded poll are dropped rather than racing the current one, which the
grace-period watchdog could not prevent.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A LauncherResult badge is drawn in the icon slot and scaled up to fill
it, so "Up to date" covered the whole row and hid the state glyph. The
state now reads as the first half of the subtitle, the way the rest of
the launcher rows are built.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@ItsLemmy
ItsLemmy merged commit 4ecdf38 into noctalia-dev:main Aug 16, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants