Files
PCA_Phobos/macos/PhobosMenu.swift
Бобырев Андрей 01632034b0 macos: PCA Phobos for Mac
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 03:54:34 +03:00

98 lines
4.0 KiB
Swift

// PCA Phobos — menu bar app for macOS. Drives /usr/local/bin/phobos through
// `sudo -n` (sudoers NOPASSWD for that one root-owned script). No Network Extension.
import AppKit
final class AppDelegate: NSObject, NSApplicationDelegate {
private let item = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
private var status = ""
func applicationDidFinishLaunching(_ n: Notification) {
refresh()
Timer.scheduledTimer(withTimeInterval: 5, repeats: true) { [weak self] _ in self?.refresh() }
}
@discardableResult
private func phobos(_ args: [String]) -> String {
let p = Process()
p.executableURL = URL(fileURLWithPath: "/usr/bin/sudo")
p.arguments = ["-n", "/usr/local/bin/phobos"] + args
let pipe = Pipe()
p.standardOutput = pipe
p.standardError = pipe
try? p.run()
p.waitUntilExit()
return String(data: pipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8) ?? ""
}
private func refresh() {
DispatchQueue.global().async {
let s = self.phobos(["status"])
DispatchQueue.main.async { self.status = s; self.rebuild() }
}
}
private var isOn: Bool { status.contains("VPN: включён") }
private func rebuild() {
item.button?.title = isOn ? (status.contains("TCP") ? "🛡 TCP" : "🛡") : "◌"
item.button?.toolTip = "PCA Phobos"
let m = NSMenu()
for line in status.split(separator: "\n") {
let i = NSMenuItem(title: String(line), action: nil, keyEquivalent: "")
i.isEnabled = false
m.addItem(i)
}
m.addItem(.separator())
m.addItem(action(isOn ? "Отключить" : "Подключить", isOn ? ["off"] : ["on"]))
m.addItem(.separator())
let mode = status.contains("Режим: tcp") ? "tcp" : status.contains("Режим: udp") ? "udp" : "auto"
for (t, v) in [("Канал: Авто", "auto"), ("Канал: UDP", "udp"), ("Канал: TCP", "tcp")] {
let i = action(t, [v]); i.state = mode == v ? .on : .off; m.addItem(i)
}
let ruOn = status.contains("РФ напрямую: да")
let ru = action("Российские сайты — напрямую", ["ru", ruOn ? "off" : "on"]); ru.state = ruOn ? .on : .off
m.addItem(ru)
m.addItem(.separator())
m.addItem(withTitle: "Импорт ссылки phobos:// из буфера", action: #selector(importLink), keyEquivalent: "").target = self
m.addItem(withTitle: "Выход", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "q")
item.menu = m
}
private func action(_ title: String, _ args: [String]) -> NSMenuItem {
let i = NSMenuItem(title: title, action: #selector(run(_:)), keyEquivalent: "")
i.target = self
i.representedObject = args
return i
}
@objc private func run(_ sender: NSMenuItem) {
guard let args = sender.representedObject as? [String] else { return }
item.button?.title = "…"
DispatchQueue.global().async {
let out = self.phobos(args)
DispatchQueue.main.async {
if out.contains("password is required") { self.alert("Нет права sudo без пароля для /usr/local/bin/phobos") }
self.refresh()
}
}
}
@objc private func importLink() {
guard let s = NSPasteboard.general.string(forType: .string), s.hasPrefix("phobos://") else {
alert("Скопируйте ссылку phobos:// из панели (клиент → 📱) и повторите"); return
}
alert(phobos(["import", s]))
refresh()
}
private func alert(_ text: String) {
let a = NSAlert(); a.messageText = "PCA Phobos"; a.informativeText = text; a.runModal()
}
}
let app = NSApplication.shared
let delegate = AppDelegate()
app.delegate = delegate
app.setActivationPolicy(.accessory)
app.run()