{
  "format" : 1,
  "manifest" : {
    "capabilities" : [
      {
        "run" : {
          "commands" : [
            "top",
            "vm_stat",
            "df",
            "kill"
          ]
        }
      }
    ],
    "homepage" : "https://github.com/navbytes/stackbar-plugins/tree/main/Monitoring/SystemMonitor",
    "id" : "io.navbytes.stackbar.plugins.system-monitor",
    "minimumStackbar" : "0.11.0",
    "name" : "System Monitor",
    "summary" : "CPU, memory and disk in the menu bar, with the processes using the most.",
    "surfaces" : [
      "menuBar",
      "panel",
      "widget",
      "settings"
    ],
    "triggers" : [
      {
        "launch" : {

        }
      },
      {
        "every" : {
          "seconds" : 10
        }
      },
      {
        "wake" : {

        }
      }
    ],
    "version" : "1.0.0"
  },
  "publisher" : {
    "name" : "navbytes",
    "publicKey" : "4dpLtXEpFXdi+6cviY/KdYuUulJPbEmCxxIK0EEN2/w="
  },
  "signature" : "U0Bi6Mq4v0m4ZMVfHaGP3yPIv1cbebGmdh51dQQ5iSVgiFUxKQjrVn8orSv1I4lXCaH4ZehRD2ZpVqE+Y09EAw==",
  "source" : "import StackbarKit\nimport Foundation\n\n/// CPU, memory and disk in the menu bar, with the processes using the most.\n///\n/// Every 10 seconds it runs `top` once for two one-second samples (the\n/// second is the accurate one), `vm_stat` for memory the way Activity\n/// Monitor counts it (app memory, wired and compressed, not the file cache)\n/// and `df` for the disk, through Stackbar. Nothing runs in a shell.\n@main\nfinal class SystemMonitor: StackbarPlugin {\n    static let manifest = Manifest(\n        id: \"io.navbytes.stackbar.plugins.system-monitor\",\n        name: \"System Monitor\",\n        version: \"1.0.0\",\n        summary: \"CPU, memory and disk in the menu bar, with the processes using the most.\",\n        surfaces: [.menuBar, .panel, .widget, .settings],\n        triggers: [.launch, .every(.seconds(10)), .wake],\n        capabilities: [.run(commands: [\"top\", \"vm_stat\", \"df\", \"kill\"])],\n        minimumStackbar: \"0.11.0\",\n        homepage: \"https://github.com/navbytes/stackbar-plugins/tree/main/Monitoring/SystemMonitor\"\n    )\n\n    struct Sample: Equatable {\n        var cpu: Double = 0\n        var memoryUsed: Double = 0\n        var memoryTotal: Double = 0\n        var load: [Double] = []\n        var processes: [ProcessRow] = []\n        var memoryFraction: Double { memoryTotal > 0 ? memoryUsed / memoryTotal : 0 }\n    }\n\n    struct ProcessRow: Equatable, Identifiable {\n        var pid: Int\n        var command: String\n        var cpu: Double\n        var memory: String\n        var id: Int { pid }\n    }\n\n    struct Disk: Equatable {\n        var used: Double\n        var total: Double\n        var fraction: Double { total > 0 ? used / total : 0 }\n    }\n\n    var sample: Sample?\n    var disk: Disk?\n    var cpuHistory: [Double] = []\n    var problem: String?\n\n    @Stored(\"menuBarShows\") var menuBarShows = \"CPU and memory\"\n    @Stored(\"sortBy\") var sortBy = \"CPU\"\n\n    func refresh() async throws {\n        await measure()\n    }\n\n    func measure() async {\n        do {\n            let order = sortBy == \"Memory\" ? \"mem\" : \"cpu\"\n            let result = try await Stackbar.run(\"top\", [\"-l\", \"2\", \"-s\", \"1\", \"-n\", \"8\", \"-o\", order, \"-stats\", \"pid,command,cpu,mem\"], timeout: .seconds(15))\n            guard result.succeeded, var parsed = Self.parseTop(result.text) else {\n                problem = result.succeeded ? \"Couldn't read top's output\" : \"top failed (\\(result.status)): \\(result.errorText.prefix(160))\"\n                return\n            }\n            if let vm = try? await Stackbar.run(\"vm_stat\", timeout: .seconds(10)), vm.succeeded, let used = Self.parseVMStat(vm.text) {\n                parsed.memoryUsed = min(used, parsed.memoryTotal)\n            }\n            sample = parsed\n            cpuHistory = Array((cpuHistory + [parsed.cpu]).suffix(30))\n            problem = nil\n        } catch {\n            problem = \"\\(error)\"\n        }\n        disk = await Self.readDisk()\n    }\n\n    static func readDisk() async -> Disk? {\n        for path in [\"/System/Volumes/Data\", \"/\"] {\n            guard let result = try? await Stackbar.run(\"df\", [\"-k\", path], timeout: .seconds(10)), result.succeeded else { continue }\n            if let disk = parseDF(result.text) { return disk }\n        }\n        return nil\n    }\n\n    /// Memory Used as Activity Monitor shows it: app memory (anonymous\n    /// pages that can't be purged), wired and compressed.\n    static func parseVMStat(_ output: String) -> Double? {\n        var pageSize = 4096.0\n        var pages: [String: Double] = [:]\n        for line in output.split(separator: \"\\n\") {\n            if let range = line.range(of: \"page size of \") {\n                pageSize = Double(line[range.upperBound...].prefix { $0.isNumber }) ?? pageSize\n                continue\n            }\n            let parts = line.split(separator: \":\", maxSplits: 1)\n            guard parts.count == 2 else { continue }\n            pages[parts[0].trimmingCharacters(in: .whitespaces)] = Double(parts[1].trimmingCharacters(in: CharacterSet(charactersIn: \" .\")))\n        }\n        guard let anonymous = pages[\"Anonymous pages\"], let wired = pages[\"Pages wired down\"] else { return nil }\n        let app = max(0, anonymous - (pages[\"Pages purgeable\"] ?? 0))\n        return (app + wired + (pages[\"Pages occupied by compressor\"] ?? 0)) * pageSize\n    }\n\n    /// `df -k`: the second line's blocks, used and available, in KiB.\n    static func parseDF(_ output: String) -> Disk? {\n        let lines = output.split(separator: \"\\n\")\n        guard lines.count >= 2 else { return nil }\n        let fields = lines[1].split(separator: \" \", omittingEmptySubsequences: true)\n        guard fields.count >= 4, let used = Double(fields[2]), let available = Double(fields[3]) else { return nil }\n        // used + available, not the block count, which includes space\n        // reserved for other APFS volumes.\n        return Disk(used: used * 1024, total: (used + available) * 1024)\n    }\n\n    /// Reads the last of top's samples.\n    static func parseTop(_ output: String) -> Sample? {\n        let samples = output.components(separatedBy: \"Processes:\")\n        guard samples.count >= 2, let last = samples.last else { return nil }\n        var sample = Sample()\n        var foundCPU = false\n        var inTable = false\n        for raw in last.split(separator: \"\\n\") {\n            let line = raw.trimmingCharacters(in: .whitespaces)\n            if line.hasPrefix(\"CPU usage:\") {\n                // CPU usage: 7.69% user, 10.25% sys, 82.5% idle\n                let numbers = percentages(in: line)\n                if numbers.count >= 3 {\n                    sample.cpu = min(100, max(0, 100 - numbers[2]))\n                    foundCPU = true\n                }\n            } else if line.hasPrefix(\"Load Avg:\") {\n                sample.load = line.dropFirst(\"Load Avg:\".count).split(separator: \",\").compactMap { Double($0.trimmingCharacters(in: .whitespaces)) }\n            } else if line.hasPrefix(\"PhysMem:\") {\n                // PhysMem: 15G used (2101M wired, 3240M compressor), 140M unused.\n                let words = line.split(separator: \" \")\n                for (index, word) in words.enumerated() where index > 0 {\n                    let next = words.indices.contains(index + 1) ? words[index + 1] : \"\"\n                    if next.hasPrefix(\"used\") { sample.memoryUsed = bytes(String(word)) ?? 0 }\n                    if next.hasPrefix(\"unused\") { sample.memoryTotal = sample.memoryUsed + (bytes(String(word)) ?? 0) }\n                }\n            } else if line.hasPrefix(\"PID\") {\n                inTable = true\n            } else if inTable {\n                let fields = line.split(separator: \" \", omittingEmptySubsequences: true)\n                guard fields.count >= 4, let pid = Int(fields[0]), let cpu = Double(fields[fields.count - 2]) else { continue }\n                let command = fields[1..<(fields.count - 2)].joined(separator: \" \")\n                sample.processes.append(ProcessRow(pid: pid, command: command, cpu: cpu, memory: String(fields[fields.count - 1])))\n            }\n        }\n        return foundCPU ? sample : nil\n    }\n\n    static func percentages(in line: String) -> [Double] {\n        line.split(whereSeparator: { $0 == \" \" || $0 == \",\" }).compactMap { word in\n            word.hasSuffix(\"%\") ? Double(word.dropLast()) : nil\n        }\n    }\n\n    /// `15G`, `2101M`, `512K`, `100B` (top's units are binary).\n    static func bytes(_ text: String) -> Double? {\n        let trimmed = text.trimmingCharacters(in: CharacterSet(charactersIn: \"+-.,()\"))\n        guard let unit = trimmed.last, let value = Double(trimmed.dropLast()) else { return Double(trimmed) }\n        switch unit {\n        case \"T\": return value * pow(1024, 4)\n        case \"G\": return value * pow(1024, 3)\n        case \"M\": return value * pow(1024, 2)\n        case \"K\": return value * 1024\n        case \"B\": return value\n        default: return Double(trimmed)\n        }\n    }\n\n    static func size(_ bytes: Double) -> String {\n        let gigabytes = bytes / pow(1024, 3)\n        return gigabytes >= 100 ? String(format: \"%.0f GB\", gigabytes) : String(format: \"%.1f GB\", gigabytes)\n    }\n\n    static func role(_ fraction: Double) -> Role {\n        fraction >= 0.9 ? .critical : fraction >= 0.75 ? .warning : .accent\n    }\n\n    // MARK: Surfaces\n\n    var menuBar: ViewContent {\n        if let sample {\n            switch menuBarShows {\n            case \"CPU\": Label(\"\\(Int(sample.cpu.rounded()))%\", symbol: \"cpu\").role(Self.role(sample.cpu / 100))\n            case \"Memory\": Label(\"\\(Int((sample.memoryFraction * 100).rounded()))%\", symbol: \"memorychip\").role(Self.role(sample.memoryFraction))\n            default: Label(\"\\(Int(sample.cpu.rounded()))% · \\(Int((sample.memoryFraction * 100).rounded()))%\", symbol: \"gauge.with.dots.needle.33percent\").role(Self.role(max(sample.cpu / 100, sample.memoryFraction)))\n            }\n        } else {\n            Label(\"\", symbol: \"gauge.with.dots.needle.33percent\").role(.secondary)\n        }\n    }\n\n    var panel: ViewContent {\n        if let problem, sample == nil {\n            ContentUnavailableView(\"Can't read the system\", symbol: \"exclamationmark.triangle\", message: problem)\n        } else if let sample {\n            Section(\"CPU\") {\n                Chart(\"CPU\", values: cpuHistory, kind: .area, role: Self.role(sample.cpu / 100), unit: \"%\")\n                LabeledContent(\"Now\", value: \"\\(Int(sample.cpu.rounded()))%\")\n                if sample.load.count == 3 {\n                    LabeledContent(\"Load\", value: sample.load.map { String(format: \"%.2f\", $0) }.joined(separator: \"  \"))\n                }\n            }\n            Section(\"Memory and disk\") {\n                ProgressBar(\"Memory \\(Self.size(sample.memoryUsed)) of \\(Self.size(sample.memoryTotal))\", value: sample.memoryFraction, role: Self.role(sample.memoryFraction))\n                if let disk {\n                    ProgressBar(\"Disk \\(Self.size(disk.used)) of \\(Self.size(disk.total))\", value: disk.fraction, role: Self.role(disk.fraction))\n                }\n            }\n            Section(\"Top processes by \\(sortBy == \"Memory\" ? \"memory\" : \"CPU\")\") {\n                ForEach(sample.processes.prefix(8)) { process in\n                    Row(process.command, subtitle: \"pid \\(process.pid) · \\(process.memory)\", symbol: \"app\", key: \"\\(process.pid)\")\n                        .badge(String(format: \"%.1f%%\", process.cpu), role: process.cpu > 80 ? .warning : .secondary)\n                        .action(\"Copy PID\") { Stackbar.copy(\"\\(process.pid)\") }\n                        .action(\"Quit Process\", role: .critical) { try await self.quit(process) }\n                }\n            }\n        } else {\n            ContentUnavailableView(\"Measuring…\", symbol: \"gauge.with.dots.needle.33percent\")\n        }\n    }\n\n    var widget: ViewContent {\n        if let sample {\n            ProgressBar(\"CPU \\(Int(sample.cpu.rounded()))%\", value: sample.cpu / 100, role: Self.role(sample.cpu / 100))\n            ProgressBar(\"Memory \\(Int((sample.memoryFraction * 100).rounded()))%\", value: sample.memoryFraction, role: Self.role(sample.memoryFraction))\n            if let disk { ProgressBar(\"Disk \\(Int((disk.fraction * 100).rounded()))%\", value: disk.fraction, role: Self.role(disk.fraction)) }\n        } else {\n            Text(\"Measuring…\").style(.caption)\n        }\n    }\n\n    func quit(_ process: ProcessRow) async throws {\n        let result = try await Stackbar.run(\"kill\", [\"-TERM\", \"\\(process.pid)\"], timeout: .seconds(10))\n        guard result.succeeded else {\n            throw HostError(\"Couldn't quit \\(process.command): \" + (result.errorText.contains(\"not permitted\") ? \"it belongs to another user\" : result.errorText))\n        }\n        await measure()\n    }\n\n    var settings: ViewContent {\n        Section(\"Menu bar\") {\n            Picker(\"Show\", options: [\"CPU and memory\", \"CPU\", \"Memory\"], selection: menuBarShows) { self.menuBarShows = $0 }\n        }\n        Section(\"Processes\") {\n            Picker(\"Sort by\", options: [\"CPU\", \"Memory\"], selection: sortBy) { choice in\n                self.sortBy = choice\n                await self.measure()\n            }\n        }\n    }\n\n    var tools: [AgentTool] {\n        AgentTool(\"usage\", description: \"CPU, memory and disk use now, and the busiest processes\") { _ in\n            guard let sample = self.sample else { return self.problem ?? \"not measured yet\" }\n            var lines = [\n                \"cpu: \\(Int(sample.cpu.rounded()))%\",\n                \"memory: \\(Self.size(sample.memoryUsed)) of \\(Self.size(sample.memoryTotal))\",\n            ]\n            if let disk = self.disk { lines.append(\"disk: \\(Self.size(disk.used)) of \\(Self.size(disk.total))\") }\n            lines += sample.processes.map { \"\\($0.pid) \\($0.command) \\($0.cpu)% cpu \\($0.memory)\" }\n            return lines.joined(separator: \"\\n\")\n        }\n    }\n}\n",
  "sourceDigest" : "010e6f401a68e3151b3cc1cd80820dfe06998cc581d6677288fa3fe584815f0b"
}