From 843e3d01f0b7412d961669cfd8b0f8d7f635c5f9 Mon Sep 17 00:00:00 2001 From: vybe Date: Sun, 24 May 2026 20:58:31 +0000 Subject: [PATCH] feat(proc): direct drop-in port from 4x raw Process to Proc singleton (exact poll-act-poll behavior preserved) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Removed import Quickshell.Io + all 4 Process { toggleProcess, copyProcess, exitNodeProcess, statusCheck } - All one-shot commands now use Proc.runCommand(id, argv, (stdout, exitCode) => {...}) - _pendingAction moved to clean root property (was hacked onto statusCheck object) - Introduced _runStatusCheck() helper + updated wrappers (toggleTailscale, refreshStatus, setExitNode, copyToClipboard, _runNextCopy) - Exact same sequencing and defensive logic: * toggle: set pending → status poll for truth → decide up/down → act → post-act status verify * copy retry state machine unchanged (just Proc instead of .command/.running) * exit-node and generic refresh paths identical - Added explicit comment documenting why the poll-act-poll exists (defensive, not forbidden state per AGENTS.md) - stderr detail dropped from action errors (per chosen A strategy; generic messages only) - All new JS follows repo ; + {} style - I18n wrappers already in place from prior slice Written by AI agent working for @jtmorris. Model: grok-build-0.1. --- tailscalectl/TailscaleWidget.qml | 147 +++++++++++++------------------ 1 file changed, 60 insertions(+), 87 deletions(-) diff --git a/tailscalectl/TailscaleWidget.qml b/tailscalectl/TailscaleWidget.qml index a47423a..0c2ea12 100644 --- a/tailscalectl/TailscaleWidget.qml +++ b/tailscalectl/TailscaleWidget.qml @@ -4,7 +4,6 @@ import qs.Common import qs.Services import qs.Widgets import qs.Modules.Plugins -import Quickshell.Io import "./lib.js" as TailscaleLib PluginComponent { @@ -21,121 +20,95 @@ PluginComponent { popoutWidth: 360 popoutHeight: 400 + // Transient coordination for the defensive poll-act-poll toggle (exact behavior preserved). + // We poll for on-the-ground truth (so we choose the correct "up"/"down" and don't lie to the user), + // act, then poll again for verification. This is *not* long-term cached state (see AGENTS.md). + // The _pendingAction is short-lived per user action only. + property string _pendingAction: "" + Component.onCompleted: { // Initial status fetch on load. Subsequent fetches are on-demand (popout open, explicit refresh, or post-action verification). - statusCheck.running = true + root._runStatusCheck(); } - Process { - id: toggleProcess - - stderr: StdioCollector {} - - onExited: (code, status) => { - if (code !== 0) { - var action = root.isConnected ? "disconnect" : "connect"; - var detail = toggleProcess.stderr.text.trim(); - ToastService.showError("tailscalectl", I18n.tr(TailscaleLib.formatError(action, detail))); - } - root.refreshStatus(); - } - } - - Process { - id: copyProcess - - stderr: StdioCollector {} - - onExited: (code, status) => { + function _runStatusCheck() { + Proc.runCommand("tailscale-status", TailscaleLib.getStatusCommand(), (stdout, code) => { if (code === 0) { - ToastService.showInfo(I18n.tr(TailscaleLib.getStrings().copied).arg(root._copyText)) + const state = TailscaleLib.parseStatusResult(stdout); + root.isConnected = state.isConnected; + root.tailscaleIP = state.tailscaleIP; + root.currentExitNode = state.currentExitNode; + root.peers = state.peers; } else { - root._copyIndex += 1 - root._runNextCopy() - } - } - } - - Process { - id: exitNodeProcess - - // Command is set dynamically by setExitNode() - - stderr: StdioCollector {} - - onExited: (code, status) => { - if (code !== 0) { - var detail = exitNodeProcess.stderr.text.trim(); - ToastService.showError("tailscalectl", I18n.tr(TailscaleLib.formatError("set", detail))); - } - root.refreshStatus(); - } - } - - Process { - id: statusCheck - - command: ["tailscale", "status", "--json"] - - stdout: StdioCollector {} - - onExited: (code, status) => { - if (code === 0) { - const state = TailscaleLib.parseStatusResult(statusCheck.stdout.text) - root.isConnected = state.isConnected - root.tailscaleIP = state.tailscaleIP - root.currentExitNode = state.currentExitNode - root.peers = state.peers - } else { - root.isConnected = false - root.tailscaleIP = "" - root.currentExitNode = "" - root.peers = [] - ToastService.showError("tailscalectl", I18n.tr(TailscaleLib.formatError("status"))) + root.isConnected = false; + root.tailscaleIP = ""; + root.currentExitNode = ""; + root.peers = []; + ToastService.showError("tailscalectl", I18n.tr(TailscaleLib.formatError("status"))); } - const cmd = TailscaleLib.commandForPendingAction(statusCheck._pendingAction, root.isConnected); + const cmd = TailscaleLib.commandForPendingAction(root._pendingAction, root.isConnected); if (cmd) { - toggleProcess.command = cmd; - toggleProcess.running = true; + // Exact same decision point as before: act based on fresh poll truth, then verify. + Proc.runCommand("tailscale-toggle", cmd, (out, c) => { + if (c !== 0) { + const action = root.isConnected ? "disconnect" : "connect"; + // Note: Proc callback provides no stderr (per chosen strategy); generic error only. + ToastService.showError("tailscalectl", I18n.tr(TailscaleLib.formatError(action))); + } + root._pendingAction = ""; + root._runStatusCheck(); // post-action verification poll (exact behavior) + }); + } else { + root._pendingAction = ""; } - statusCheck._pendingAction = null; - } + }); } function toggleTailscale() { - statusCheck._pendingAction = "toggle"; - statusCheck.running = true; + root._pendingAction = "toggle"; + root._runStatusCheck(); } function refreshStatus() { - statusCheck.running = true; + root._runStatusCheck(); } function setExitNode(hostname) { - const cmd = TailscaleLib.makeExitNodeCommand(hostname) + const cmd = TailscaleLib.makeExitNodeCommand(hostname); if (!cmd) { - ToastService.showError("tailscalectl", I18n.tr(TailscaleLib.getStrings().invalidExitNodeHostname)) - return + ToastService.showError("tailscalectl", I18n.tr(TailscaleLib.getStrings().invalidExitNodeHostname)); + return; } - exitNodeProcess.command = cmd - exitNodeProcess.running = true + Proc.runCommand("tailscale-exit", cmd, (stdout, code) => { + if (code !== 0) { + // Note: no stderr detail available from Proc (accepted per plan). + ToastService.showError("tailscalectl", I18n.tr(TailscaleLib.formatError("set"))); + } + root._runStatusCheck(); + }); } function copyToClipboard(text) { - root._copyText = text - root._copyIndex = 0 - root._runNextCopy() + root._copyText = text; + root._copyIndex = 0; + root._runNextCopy(); } function _runNextCopy() { - const cmds = TailscaleLib.getClipboardCommands(root._copyText) + const cmds = TailscaleLib.getClipboardCommands(root._copyText); if (root._copyIndex >= cmds.length) { - ToastService.showError("tailscalectl", I18n.tr(TailscaleLib.formatError("clipboard"))) - return + ToastService.showError("tailscalectl", I18n.tr(TailscaleLib.formatError("clipboard"))); + return; } - copyProcess.command = cmds[root._copyIndex] - copyProcess.running = true + Proc.runCommand("tailscale-copy-" + root._copyIndex, cmds[root._copyIndex], (stdout, code) => { + if (code === 0) { + ToastService.showInfo(I18n.tr(TailscaleLib.getStrings().copied).arg(root._copyText)); + } else { + root._copyIndex += 1; + root._runNextCopy(); + } + }); } popoutContent: Component {