diff --git a/tailscalectl/TailscaleWidget.qml b/tailscalectl/TailscaleWidget.qml index 36223c2..2ac6bc6 100644 --- a/tailscalectl/TailscaleWidget.qml +++ b/tailscalectl/TailscaleWidget.qml @@ -11,6 +11,7 @@ PluginComponent { id: root property bool isConnected: false + property bool binaryAvailable: true property string tailscaleIP: "" property string currentExitNode: "" property var peers: [] @@ -35,6 +36,10 @@ PluginComponent { id: toggleProcess onExited: (code, status) => { + if (code !== 0) { + var action = root.isConnected ? "disconnect" : "connect" + ToastService.showError("tailscalectl", TailscaleLib.errorMessage(action)) + } statusCheck.running = true } } @@ -48,7 +53,7 @@ PluginComponent { onExited: (code, status) => { if (code !== 0) { - ToastService.showError("tailscalectl", "Failed to set exit node") + ToastService.showError("tailscalectl", TailscaleLib.errorMessage("set")) } statusCheck.running = true } @@ -99,11 +104,21 @@ PluginComponent { root.tailscaleIP = "" root.currentExitNode = "" root.peers = [] + if (code === 127) { + root.binaryAvailable = false + ToastService.showError("tailscalectl", "Tailscale binary not found") + } else { + ToastService.showError("tailscalectl", TailscaleLib.errorMessage("status")) + } } } } function toggleTailscale() { + if (!root.binaryAvailable) { + ToastService.showError("tailscalectl", "Tailscale not available") + return + } if (root.isConnected) { toggleProcess.command = ["tailscale", "down"] } else { @@ -113,6 +128,10 @@ PluginComponent { } function setExitNode(hostname) { + if (!root.binaryAvailable) { + ToastService.showError("tailscalectl", "Tailscale not available") + return + } exitNodeProcess.command = TailscaleLib.makeExitNodeCommand(hostname) exitNodeProcess.running = true } @@ -138,6 +157,14 @@ PluginComponent { width: parent.width height: Theme.spacingM + statusRow.implicitHeight + Theme.spacingM + peerList.height + Theme.spacingM + StyledText { + visible: !root.binaryAvailable + text: "Tailscale not available" + anchors.centerIn: parent + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + } + Row { id: statusRow y: Theme.spacingM diff --git a/tailscalectl/lib.js b/tailscalectl/lib.js index 74a5bc6..b48dd14 100644 --- a/tailscalectl/lib.js +++ b/tailscalectl/lib.js @@ -15,7 +15,19 @@ function makeExitNodeCommand(hostname) { return ["tailscale", "set", "--exit-node=" + hostname] } +function errorMessage(cmd) { + var messages = { + "up": "Failed to connect to Tailscale", + "connect": "Failed to connect to Tailscale", + "down": "Failed to disconnect from Tailscale", + "disconnect": "Failed to disconnect from Tailscale", + "set": "Failed to set exit node", + "status": "Failed to read Tailscale status" + } + return messages[cmd] || "Tailscale command failed" +} + // CommonJS export for Node.js tests (ignored by QML) if (typeof module !== "undefined" && module.exports) { - module.exports = { parsePeers, makeExitNodeCommand } + module.exports = { parsePeers, makeExitNodeCommand, errorMessage } } diff --git a/test/lib.test.js b/test/lib.test.js index ace694c..8c6c435 100644 --- a/test/lib.test.js +++ b/test/lib.test.js @@ -1,7 +1,7 @@ import { test } from "node:test" import assert from "node:assert" import lib from "../tailscalectl/lib.js" -const { parsePeers, makeExitNodeCommand } = lib +const { parsePeers, makeExitNodeCommand, errorMessage } = lib test("parsePeers extracts exitNode from ExitNodeOption", () => { const peerMap = { @@ -34,3 +34,28 @@ test("makeExitNodeCommand with empty string clears exit node", () => { const cmd = makeExitNodeCommand("") assert.deepStrictEqual(cmd, ["tailscale", "set", "--exit-node="]) }) + +test("errorMessage returns user-friendly message for tailscale up failure", () => { + const msg = errorMessage("up", 1) + assert.strictEqual(msg, "Failed to connect to Tailscale") +}) + +test("errorMessage returns user-friendly message for tailscale down failure", () => { + const msg = errorMessage("down", 1) + assert.strictEqual(msg, "Failed to disconnect from Tailscale") +}) + +test("errorMessage returns user-friendly message for tailscale set failure", () => { + const msg = errorMessage("set", 1) + assert.strictEqual(msg, "Failed to set exit node") +}) + +test("errorMessage returns user-friendly message for tailscale status failure", () => { + const msg = errorMessage("status", 1) + assert.strictEqual(msg, "Failed to read Tailscale status") +}) + +test("errorMessage returns generic message for unknown command", () => { + const msg = errorMessage("unknown", 1) + assert.strictEqual(msg, "Tailscale command failed") +})