dms_tailscalectl/tailscalectl/lib.js

122 lines
4 KiB
JavaScript
Raw Normal View History

function parsePeers(peerMap) {
if (!peerMap) return []
return Object.keys(peerMap).map(function (key) {
if (!Object.prototype.hasOwnProperty.call(peerMap, key)) return null
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
}
}).filter(function (peer) { return peer !== null })
}
function makeExitNodeCommand(hostname) {
if (hostname === "") {
return ["tailscale", "set", "--exit-node="]
}
return ["tailscale", "set", "--exit-node=" + hostname]
}
function findActiveExitNode(peerMap) {
if (!peerMap) return ""
for (const key in peerMap) {
if (!Object.prototype.hasOwnProperty.call(peerMap, key)) continue
const p = peerMap[key]
if (p.ExitNode) {
return p.HostName || key
}
}
return ""
}
var safeClipboardTools = ["dms", "wl-copy"]
// 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"] }
}
function validateClipboardTool(tool) {
return typeof tool === "string" && safeClipboardTools.includes(tool)
}
function buildCopyCommand(text, tool) {
const entry = clipboardTools[tool]
if (!entry) return null
// Pure argv — the Process will pass the string verbatim. No shell involved.
return [...entry.argv, text]
}
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()
}
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: "↗"
}
}
// Light UI predicates extracted from QML (advances #43 — keeps view thin)
function shouldShowClearExitNode(currentExitNode) {
return currentExitNode !== ""
}
function isActiveExitNode(currentExitNode, hostname) {
return currentExitNode === hostname
}
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: [] }
}
}
function buildToggleCommand(isConnected) {
return isConnected ? ["tailscale", "down"] : ["tailscale", "up"]
}
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, findActiveExitNode, errorMessage, validateClipboardTool, buildCopyCommand, nextClipboardTool, allClipboardTools, buildToggleCommand, parseStatusResult, getStrings, shouldShowClearExitNode, isActiveExitNode }
}