Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@ Notable changes to Syncthing for Omarchy are documented here.

## Unreleased

## 0.1.7 - 2026-08-31

- reconcile configured and systemd user-service startup states without changing
the independent runtime start and stop controls (thanks @renews for reporting)
- add a configurable interval for detecting external systemd state changes
- add per-folder and all-folder rescans, a clearer host ID copy control, and
remembered local host identity while Syncthing is stopped

## 0.1.6 - 2026-08-22

- Make live and indexed file activity accurate for additions, deletions,
Expand Down
42 changes: 0 additions & 42 deletions DEVELOPMENT.md

This file was deleted.

13 changes: 10 additions & 3 deletions Panel.qml
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,9 @@ Panel {
readonly property string visibleNotice: syncthing
? syncthing.folderMutationNotice || syncthing.settingsNotice : ""
readonly property string visibleWarning: syncthing
? syncthing.recoveryWarning : ""
? syncthing.recoveryWarning || syncthing.serviceStateWarning : ""
readonly property bool serviceStateDialogOpen: syncthing
&& syncthing.serviceStateDrift
readonly property string visibleSyncActivity: syncthing
? syncthing.syncActivity : ""
readonly property string visibleSyncDots: syncthing
Expand All @@ -128,14 +130,18 @@ Panel {
}

function copyLocalDeviceId() {
copyToClipboard(syncthing ? syncthing.localDeviceId : "",
"Device ID copied")
copyToClipboard(syncthing ? syncthing.displayDeviceId : "",
"Host ID copied")
}

function copyFolderId(folderId) {
copyToClipboard(folderId, "Folder ID copied")
}

function chooseServiceStateAction(index) {
if (syncthing) syncthing.chooseServiceStateAction(index)
}

function configureService() {
if (!syncthing) return
syncthing.setRefreshInterval(setting("refreshIntervalSec", 60))
Expand Down Expand Up @@ -533,6 +539,7 @@ Panel {
root.addSubmissionPending = false
}
}

}

Process {
Expand Down
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ any Linux quickshell build.
- select the switch/toggle in the top right to start or stop the user service
- select a folder card to open its directory
- select **+** to configure an existing local directory
- select **Refresh** to request an immediate health update
- select **RESCAN** on a folder or **Rescan all directories** for every folder
- select **Open Web UI** for device setup and advanced folder options
- select the gear or press `s` for appearance settings and clean removal

Expand All @@ -34,7 +34,7 @@ As shown in the footer at the bottom of the main plugin menu

| Key | Action |
| ----- | ------------------------- |
| `r` | refresh status |
| `r` | rescan all folders |
| `w` | open the Web UI |
| `p` | start or stop the service |
| `s` | open plugin settings |
Expand Down Expand Up @@ -184,8 +184,10 @@ Planned work stays at the top. Shipped entries come from

| Release | State | Date | What changed |
| ------- | ------- | ---------- | ------------------------------------------------------- |
| Next | planned | TBD | improve and expand Syncthing Web UI customization |
| 0.1.8 | planned | TBD | improve and expand Syncthing Web UI customization |
| | | | prepare broader Quickshell compatibility |
| 0.1.7 | shipped | 2026-08-31 | fix persistent service-state reconciliation |
| | | | UI/UX: clear semantics on buttons, harmonize font size |
| 0.1.6 | shipped | 2026-08-22 | make live and indexed file activity accurate |
| | | | refine folder lifecycle controls and pending offers |
| | | | add versioned icon and live Web UI theme settings |
Expand Down
81 changes: 81 additions & 0 deletions Service.qml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import QtQuick
import Quickshell
import Quickshell.Io
import "core"
import "models/ServiceStateModel.js" as ServiceStateModel

QtObject {
id: root
Expand All @@ -22,6 +23,14 @@ QtObject {
property var pendingFolders: ({})
property var folderStatuses: ({})
property string localDeviceId: ""
readonly property string lastKnownLocalDeviceId:
identityState.localDeviceId
readonly property string lastKnownLocalDeviceName:
identityState.localDeviceName
readonly property string displayDeviceId: localDeviceId
|| lastKnownLocalDeviceId
readonly property string displayDeviceName: currentLocalDeviceName()
|| lastKnownLocalDeviceName
readonly property var syncingFiles: activityTracker.syncingFiles

readonly property bool folderMutationBusy: folderController.mutationBusy
Expand Down Expand Up @@ -55,6 +64,27 @@ QtObject {
readonly property bool settingsBusy: settings.busy
readonly property string settingsError: settings.error
readonly property string settingsNotice: settings.notice
readonly property bool settingsReady: settings.settingsReady
readonly property string configuredServiceState: settings.serviceState
readonly property int probeIntervalSeconds: settings.probeIntervalSeconds
readonly property string serviceActiveState: installation.serviceActiveState
readonly property string serviceUnitFileState: installation.unitFileState
readonly property var serviceStateDecision: ServiceStateModel.decision(
configuredServiceState, serviceUnitFileState, serviceActiveState)
readonly property bool serviceStateDrift: settingsReady && serviceAvailable
&& serviceStateDecision.status === "drift"
readonly property bool serviceStateActionRunning:
settings.serviceStateActionRunning || installation.unitFileActionRunning
readonly property string serviceStateMessage: serviceStateDrift
? serviceStateDecision.message : ""
readonly property string serviceStatePrimaryLabel: serviceStateDrift
? serviceStateDecision.first.label : ""
readonly property string serviceStateSecondaryLabel: serviceStateDrift
? serviceStateDecision.second.label : ""
readonly property string serviceStateWarning: settingsReady && serviceAvailable
&& serviceStateDecision.status === "unsupported"
&& serviceUnitFileState !== "not-found"
? serviceStateDecision.reason : ""

property string _apiKey: ""
property bool _useTls: false
Expand All @@ -70,6 +100,12 @@ QtObject {
property var _requests: []
property string _keyOutput: ""

property PersistentProperties identityState: PersistentProperties {
reloadableId: "syncshell-local-device-identity"
property string localDeviceId: ""
property string localDeviceName: ""
}

readonly property bool online: phase === "ready"
readonly property bool serviceActive: installation.serviceActive
readonly property bool serviceActionRunning: installation.serviceActionRunning
Expand All @@ -83,6 +119,9 @@ QtObject {
readonly property int syncingFolderCount: countSyncingFolders()
readonly property string summaryText: summary()

onLocalDeviceIdChanged: rememberLocalIdentity()
onDevicesChanged: rememberLocalIdentity()

property ActivityTracker activityTracker: ActivityTracker {
enabled: root._apiKey !== "" && root.canUseRuntime
&& (!root.serviceAvailable || root.serviceActive)
Expand All @@ -105,6 +144,7 @@ QtObject {
property InstallationController installation: InstallationController {
helperPath: root.helperPath
folderMutationBusy: root.folderMutationBusy
probeIntervalSeconds: root.probeIntervalSeconds
onRuntimeUnavailable: function(nextPhase) { root.stopApi(nextPhase) }
onRuntimeAvailable: {
if (!root._apiKey && !apiKeyProcess.running) root.refreshApi()
Expand Down Expand Up @@ -163,6 +203,28 @@ QtObject {
return count
}

function currentLocalDeviceName() {
if (!localDeviceId) return ""
for (var i = 0; i < devices.length; i++) {
var device = devices[i] || ({})
if (String(device.deviceID || "") === localDeviceId && device.name) {
return String(device.name)
}
}
return ""
}

function rememberLocalIdentity() {
var id = String(localDeviceId || "")
if (!id) return
if (id !== identityState.localDeviceId) {
identityState.localDeviceId = id
identityState.localDeviceName = ""
}
var name = currentLocalDeviceName()
if (name) identityState.localDeviceName = name
}

function countFolderProblems() {
var count = 0
var ids = Object.keys(folderStatuses)
Expand Down Expand Up @@ -220,6 +282,17 @@ QtObject {
settings.clearNotice()
}

function chooseServiceStateAction(index) {
if (!serviceStateDrift || serviceStateActionRunning) return false
var action = index === 0
? serviceStateDecision.first : serviceStateDecision.second
if (action.side === "config") return settings.setServiceState(action.value)
if (action.side === "system") {
return installation.setUnitFileState(action.value)
}
return false
}

function requestSelfRemoval(deletePluginSettings) {
settings.requestSelfRemoval(deletePluginSettings)
}
Expand Down Expand Up @@ -336,6 +409,14 @@ QtObject {
return folderController.setLinked(folderId, linked)
}

function rescanFolder(folderId) {
return folderController.rescan(folderId)
}

function rescanAllFolders() {
return folderController.rescanAll()
}

function forgetFolder(folderId) {
return folderController.forget(folderId)
}
Expand Down
8 changes: 7 additions & 1 deletion config/settings.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
version = 1

[style]
# "branded" vs. "themed": classic Syncthing icons vs.Omarchy-colored icons
# "branded" vs. "themed": classic Syncthing icons vs. Omarchy-colored icons
icon_style = "branded"
# "default" vs. "omarchy": Syncthing styling vs. following the active Omarchy theme
web_ui_theme = "omarchy"

[service]
# Persistent Syncthing user-service state, independent of the runtime toggle
service_state = "enabled"
# Seconds between probes for external systemd state changes
probe_interval_seconds = 15
33 changes: 33 additions & 0 deletions core/FolderController.qml
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,39 @@ QtObject {
}, linked ? "Could not link the folder" : "Could not unlink the folder")
}

function rescan(folderId) {
var folder = configuredFolder(folderId)
if (!folder) {
mutationError = "The selected folder is no longer configured"
return false
}
if (folder.paused) {
mutationError = "Link the folder before rescanning it"
return false
}
if (!begin("rescan", folder.id)) return false
requestMutation("scanFolder", {
method: "POST", query: { folder: folder.id }
}, function() {
root.finish("Rescan requested for " + String(folder.label || folder.id))
}, "Could not rescan the folder")
return true
}

function rescanAll() {
if (folders.length === 0) {
mutationError = "No directories are configured"
return false
}
if (!begin("rescan-all", "")) return false
requestMutation("scanFolder", {
method: "POST"
}, function() {
root.finish("Rescan complete for all directories")
}, "Could not rescan all directories")
return true
}

function forget(folderId) {
var folder = configuredFolder(folderId)
if (!folder) {
Expand Down
Loading