Let the Dropbox panel pause and resume syncing

Click the Dropbox hero icon to pause or resume the daemon (dropbox-cli
stop/start), with a tooltip on hover. When paused, both the hero and the
bar icon grey out and the phrase reads "Syncing paused". The greying is
optimistic so it reacts the instant you click, then reconciles with the
real daemon state once it settles (reverting if the command failed).

Also fix status.py's running detection, which was stuck reporting true:
this dropbox-cli inverts the `running` exit code and reports "isn't
running" (not "not running") when stopped, so the paused state never
showed. Detection now keys off the status text alone.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
David Heinemeier Hansson
2026-07-20 14:00:50 -07:00
co-authored by Claude Opus 4.8
parent ff1d1c325e
commit a7c0d0f6a1
3 changed files with 113 additions and 14 deletions
+31 -9
View File
@@ -36,8 +36,8 @@ Panel {
readonly property color urgent: bar ? bar.urgent : Color.urgent readonly property color urgent: bar ? bar.urgent : Color.urgent
readonly property color dim: Qt.darker(foreground, 1.55) readonly property color dim: Qt.darker(foreground, 1.55)
readonly property string fontFamily: bar ? bar.fontFamily : Style.font.family readonly property string fontFamily: bar ? bar.fontFamily : Style.font.family
readonly property color iconColor: dropbox.authenticated ? foreground : dim readonly property color iconColor: dropbox.authenticated && dropbox.active ? foreground : dim
readonly property color barIconColor: dropbox.authenticated ? barForeground : Qt.darker(barForeground, 1.55) readonly property color barIconColor: dropbox.authenticated && dropbox.active ? barForeground : Qt.darker(barForeground, 1.55)
function ensureCursor() { function ensureCursor() {
if (!dropbox.authenticated) { if (!dropbox.authenticated) {
@@ -149,7 +149,7 @@ Panel {
anchors.centerIn: parent anchors.centerIn: parent
iconSize: Style.space(12) iconSize: Style.space(12)
color: root.barIconColor color: root.barIconColor
opacity: dropbox.authenticated ? 1.0 : 0.6 opacity: dropbox.active ? 1.0 : 0.6
} }
} }
} }
@@ -206,14 +206,36 @@ Panel {
visible: dropbox.authenticated visible: dropbox.authenticated
width: parent.width width: parent.width
title: "Dropbox" title: "Dropbox"
meta: root.heroPhraseText meta: dropbox.active ? root.heroPhraseText : "Syncing paused"
foreground: root.foreground foreground: root.foreground
fontFamily: root.fontFamily fontFamily: root.fontFamily
iconOpacity: 1.0 iconOpacity: dropbox.active ? 1.0 : 0.5
iconComponent: Component { iconComponent: Component {
DropboxIcon { Item {
iconSize: Style.font.display implicitWidth: heroIcon.implicitWidth
color: root.iconColor implicitHeight: heroIcon.implicitHeight
DropboxIcon {
id: heroIcon
iconSize: Style.font.display
color: root.iconColor
anchors.centerIn: parent
}
MouseArea {
id: heroMouse
anchors.fill: parent
hoverEnabled: true
enabled: dropbox.installed && !dropbox.busy
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
onClicked: dropbox.toggleRunning()
}
PanelToolTip {
visible: heroMouse.containsMouse
text: dropbox.active ? "Pause syncing" : "Resume syncing"
fontFamily: root.fontFamily
}
} }
} }
} }
@@ -297,7 +319,7 @@ Panel {
Timer { Timer {
id: phraseTimer id: phraseTimer
interval: 2800 interval: 2800
running: root.opened && dropbox.authenticated running: root.opened && dropbox.authenticated && dropbox.active
repeat: true repeat: true
onTriggered: phraseSwap.restart() onTriggered: phraseSwap.restart()
} }
+78 -1
View File
@@ -13,6 +13,12 @@ Item {
property bool installed: false property bool installed: false
property bool running: false property bool running: false
property bool authenticated: false property bool authenticated: false
// Optimistic sync state so the UI reacts the instant you click, rather than
// waiting for dropboxd to actually settle. _desired is -1 while we just
// follow the real state, or 0/1 while a pause/resume is still catching up.
property int _desired: -1
readonly property bool active: _desired === -1 ? running : (_desired === 1)
property bool refreshing: false property bool refreshing: false
property string statusText: "Checking…" property string statusText: "Checking…"
property string accountPath: "" property string accountPath: ""
@@ -26,7 +32,7 @@ Item {
property string lastError: "" property string lastError: ""
readonly property int refreshIntervalSec: intSetting("refreshIntervalSec", 60, 10, 3600) readonly property int refreshIntervalSec: intSetting("refreshIntervalSec", 60, 10, 3600)
readonly property bool busy: statusProcess.running || loginProcess.running readonly property bool busy: statusProcess.running || loginProcess.running || controlProcess.running
readonly property string helperPath: (omarchyPath || "") + "/shell/plugins/panels/dropbox/status.py" readonly property string helperPath: (omarchyPath || "") + "/shell/plugins/panels/dropbox/status.py"
property string _statusOutput: "" property string _statusOutput: ""
@@ -34,6 +40,8 @@ Item {
property string _loginOutput: "" property string _loginOutput: ""
property string _loginError: "" property string _loginError: ""
property bool _loginUrlOpened: false property bool _loginUrlOpened: false
property string _controlOutput: ""
property string _controlError: ""
function setting(name, fallback) { function setting(name, fallback) {
var value = settings ? settings[name] : undefined var value = settings ? settings[name] : undefined
@@ -66,6 +74,8 @@ Item {
installed = parsed.installed === true installed = parsed.installed === true
running = parsed.running === true running = parsed.running === true
authenticated = parsed.authenticated === true authenticated = parsed.authenticated === true
// Reality caught up to the pending pause/resume — stop overriding.
if (_desired !== -1 && running === (_desired === 1)) _desired = -1
statusText = String(parsed.statusText || (installed ? "Stopped" : "Not installed")) statusText = String(parsed.statusText || (installed ? "Stopped" : "Not installed"))
accountPath = String(parsed.accountPath || "") accountPath = String(parsed.accountPath || "")
plan = String(parsed.plan || "") plan = String(parsed.plan || "")
@@ -92,6 +102,30 @@ Item {
loginProcess.running = true loginProcess.running = true
} }
function pause() {
runControl(["dropbox-cli", "stop"], 0)
}
function resume() {
runControl(["dropbox-cli", "start"], 1)
}
function toggleRunning() {
if (active) pause()
else resume()
}
function runControl(command, desired) {
// No progress status here — the greyed icon and hero phrase already convey
// the pause/resume; only surface a message if the command fails.
if (!installed || controlProcess.running) return
_desired = desired
_controlOutput = ""
_controlError = ""
controlProcess.command = command
controlProcess.running = true
}
function openFile(file) { function openFile(file) {
if (!file || !file.path) return if (!file || !file.path) return
Quickshell.execDetached(["uwsm-app", "--", "nautilus", "--select", fileUri(String(file.path))]) Quickshell.execDetached(["uwsm-app", "--", "nautilus", "--select", fileUri(String(file.path))])
@@ -146,6 +180,26 @@ Item {
onTriggered: root.actionStatus = "" onTriggered: root.actionStatus = ""
} }
Timer {
// dropboxd takes a few (variable) seconds to settle after stop/start, so
// re-poll a handful of times to reflect the new state without waiting for
// the next periodic refresh.
id: settleTimer
property int ticks: 0
interval: 1500
repeat: true
running: false
onTriggered: {
settleTimer.ticks += 1
root.refresh()
if (settleTimer.ticks >= 4) {
settleTimer.ticks = 0
settleTimer.running = false
root._desired = -1
}
}
}
Process { Process {
id: statusProcess id: statusProcess
running: false running: false
@@ -180,4 +234,27 @@ Item {
delayedRefresh.restart() delayedRefresh.restart()
} }
} }
Process {
id: controlProcess
running: false
command: []
stdout: StdioCollector { id: controlStdout; waitForEnd: true; onStreamFinished: root._controlOutput = text }
stderr: StdioCollector { id: controlStderr; waitForEnd: true; onStreamFinished: root._controlError = text }
onExited: function(exitCode) {
var stdout = String(controlStdout.text || root._controlOutput || "")
var stderr = String(controlStderr.text || root._controlError || "")
if (exitCode !== 0) {
root._desired = -1
root.lastError = root.elideStatus(stderr || stdout || "Dropbox command failed")
root.actionStatus = root.lastError
} else {
root.lastError = ""
root.actionStatus = ""
}
settleTimer.ticks = 0
settleTimer.restart()
delayedRefresh.restart()
}
}
} }
+4 -4
View File
@@ -99,11 +99,11 @@ def main():
running = False running = False
status_text = "Not installed" status_text = "Not installed"
if dropbox_cli: if dropbox_cli:
running_exit, _ = command_output([dropbox_cli, "running"])
status_exit, status_output = command_output([dropbox_cli, "status"]) status_exit, status_output = command_output([dropbox_cli, "status"])
status_text = status_output if status_exit == 0 and status_output else ("Running" if running else "Stopped") status_text = status_output if status_exit == 0 and status_output else "Stopped"
stopped = "not running" in status_text.lower() or status_text.lower() == "stopped" lowered = status_text.lower()
running = running_exit == 0 or (status_exit == 0 and status_text != "" and not stopped) stopped = "not running" in lowered or "isn't running" in lowered or lowered == "stopped"
running = status_exit == 0 and status_output != "" and not stopped
used, files = scan_dropbox(account_path, limit) if authenticated else (0, []) used, files = scan_dropbox(account_path, limit) if authenticated else (0, [])
usage_percent = (used / quota * 100) if quota > 0 else 0 usage_percent = (used / quota * 100) if quota > 0 else 0