2026-05-18 18:27:41 +00:00
|
|
|
|
function parsePeers(peerMap) {
|
|
|
|
|
|
if (!peerMap) return []
|
|
|
|
|
|
return Object.keys(peerMap).map(function (key) {
|
2026-05-21 10:11:03 +00:00
|
|
|
|
if (!Object.prototype.hasOwnProperty.call(peerMap, key)) return null
|
2026-05-18 18:27:41 +00:00
|
|
|
|
var p = peerMap[key]
|
|
|
|
|
|
return {
|
|
|
|
|
|
hostname: p.HostName || key,
|
|
|
|
|
|
ip: (p.TailscaleIPs && p.TailscaleIPs.length) ? p.TailscaleIPs[0] : "",
|
|
|
|
|
|
online: p.Online || false,
|
|
|
|
|
|
exitNode: p.ExitNodeOption || false
|
|
|
|
|
|
}
|
2026-05-21 10:11:03 +00:00
|
|
|
|
}).filter(function (peer) { return peer !== null })
|
2026-05-18 18:27:41 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function makeExitNodeCommand(hostname) {
|
2026-05-21 10:11:03 +00:00
|
|
|
|
if (hostname === "") {
|
|
|
|
|
|
return ["tailscale", "set", "--exit-node="]
|
|
|
|
|
|
}
|
2026-05-18 18:27:41 +00:00
|
|
|
|
return ["tailscale", "set", "--exit-node=" + hostname]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-18 22:10:37 +00:00
|
|
|
|
function findActiveExitNode(peerMap) {
|
|
|
|
|
|
if (!peerMap) return ""
|
|
|
|
|
|
for (const key in peerMap) {
|
2026-05-21 10:11:03 +00:00
|
|
|
|
if (!Object.prototype.hasOwnProperty.call(peerMap, key)) continue
|
2026-05-18 22:10:37 +00:00
|
|
|
|
const p = peerMap[key]
|
|
|
|
|
|
if (p.ExitNode) {
|
|
|
|
|
|
return p.HostName || key
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return ""
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-22 06:33:08 +00:00
|
|
|
|
var safeClipboardTools = ["dms", "wl-copy"]
|
2026-05-20 10:08:59 +00:00
|
|
|
|
|
2026-05-22 06:33:08 +00:00
|
|
|
|
// Direct argv form — no sh -c, no escaping, no future injection surface (#49)
|
|
|
|
|
|
// clipmanctl intentionally dropped (niche optional history manager, not needed on DMS + Wayland)
|
|
|
|
|
|
var clipboardTools = {
|
|
|
|
|
|
"dms": { argv: ["dms", "cl", "copy"] },
|
|
|
|
|
|
"wl-copy": { argv: ["wl-copy"] }
|
2026-05-20 10:08:59 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-21 06:37:14 +00:00
|
|
|
|
function validateClipboardTool(tool) {
|
|
|
|
|
|
return typeof tool === "string" && safeClipboardTools.includes(tool)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function buildCopyCommand(text, tool) {
|
2026-05-22 06:33:08 +00:00
|
|
|
|
const entry = clipboardTools[tool]
|
|
|
|
|
|
if (!entry) return null
|
|
|
|
|
|
// Pure argv — the Process will pass the string verbatim. No shell involved.
|
|
|
|
|
|
return [...entry.argv, text]
|
2026-05-21 06:37:14 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function nextClipboardTool(currentTool) {
|
|
|
|
|
|
var idx = safeClipboardTools.indexOf(currentTool)
|
|
|
|
|
|
if (idx < 0) return safeClipboardTools[0]
|
|
|
|
|
|
return safeClipboardTools[(idx + 1) % safeClipboardTools.length]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function allClipboardTools() {
|
|
|
|
|
|
return safeClipboardTools.slice()
|
2026-05-20 10:11:57 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-22 06:33:08 +00:00
|
|
|
|
function getStrings() {
|
|
|
|
|
|
return {
|
|
|
|
|
|
header: "Tailscale",
|
|
|
|
|
|
connected: "Connected",
|
|
|
|
|
|
disconnected: "Disconnected",
|
|
|
|
|
|
exitNodePrefix: "Exit node: ",
|
|
|
|
|
|
none: "None",
|
|
|
|
|
|
copied: function (text) { return "Copied " + text + " to clipboard" },
|
|
|
|
|
|
noClipboardTool: "No clipboard tool found",
|
|
|
|
|
|
invalidClipboardCommand: "Invalid clipboard command",
|
|
|
|
|
|
clearExitNode: "×",
|
|
|
|
|
|
setExitNode: "↗"
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-20 19:29:02 +00:00
|
|
|
|
function parseStatusResult(jsonText) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const data = JSON.parse(jsonText)
|
|
|
|
|
|
return {
|
|
|
|
|
|
isConnected: data.BackendState === "Running",
|
|
|
|
|
|
tailscaleIP: (data.Self && data.Self.TailscaleIPs && data.Self.TailscaleIPs[0]) || "",
|
|
|
|
|
|
currentExitNode: findActiveExitNode(data.Peer || {}),
|
|
|
|
|
|
peers: parsePeers(data.Peer || {})
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
return { isConnected: false, tailscaleIP: "", currentExitNode: "", peers: [] }
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-20 19:21:17 +00:00
|
|
|
|
function buildToggleCommand(isConnected) {
|
|
|
|
|
|
return isConnected ? ["tailscale", "down"] : ["tailscale", "up"]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-18 20:53:32 +00:00
|
|
|
|
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"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-18 18:39:39 +00:00
|
|
|
|
// CommonJS export for Node.js tests (ignored by QML)
|
|
|
|
|
|
if (typeof module !== "undefined" && module.exports) {
|
2026-05-22 06:33:08 +00:00
|
|
|
|
module.exports = { parsePeers, makeExitNodeCommand, findActiveExitNode, errorMessage, validateClipboardTool, buildCopyCommand, nextClipboardTool, allClipboardTools, buildToggleCommand, parseStatusResult, getStrings }
|
2026-05-18 18:39:39 +00:00
|
|
|
|
}
|