Repository navigation
Expand file tree
/
Copy pathModel.js
More file actions
233 lines (197 loc) · 7.71 KB
/
Copy pathModel.js
File metadata and controls
233 lines (197 loc) · 7.71 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
// Pure helpers: no QML objects, no side effects, safe from a property binding.
var KIB_PER_MIB = 1024
var MIB_PER_GIB = 1024
// The two supported clients: the discord package publishes discord/Discord, vesktop vesktop.
var APP_IDS = ["discord", "vesktop"]
function isAppId(value) {
return APP_IDS.indexOf(String(value || "").toLowerCase()) !== -1
}
// ---------------------------------------------------------------- desktop
// Quickshell 0.3 exposes StartupWMClass as startupClass and no entry id to match.
// Desktop list order is arbitrary, so pick the client last seen running, then APP_IDS order.
function findEntry(applications, preferredId) {
var list = applications || []
var wanted = [isAppId(preferredId) ? String(preferredId).toLowerCase() : ""].concat(APP_IDS)
for (var w = 0; w < wanted.length; w++) {
if (wanted[w] === "") continue
for (var i = 0; i < list.length; i++) {
var entry = list[i]
if (entry && String(entry.startupClass || "").toLowerCase() === wanted[w]) return entry
}
}
return null
}
// The app id of the client actually on screen, which is what disambiguates a later cold launch.
function runningClientId(toplevels) {
var matched = matchWindows(toplevels)
return matched.length > 0 ? String(toplevelClass(matched[0])).toLowerCase() : ""
}
// ---------------------------------------------------------------- windows
function toplevelClass(toplevel) {
if (!toplevel) return ""
// The Wayland handle carries the app id; the IPC snapshot is the fallback.
var wayland = toplevel.wayland
if (wayland && wayland.appId) return String(wayland.appId)
var ipc = toplevel.lastIpcObject
return ipc ? String(ipc["class"] || ipc["initialClass"] || "") : ""
}
// hyprctl reports class and initialClass as the client's own app id here.
function matchWindows(toplevels) {
var list = toplevels || []
var out = []
for (var i = 0; i < list.length; i++) {
if (isAppId(toplevelClass(list[i]))) out.push(list[i])
}
return out
}
// Hyprland raises urgency from xdg-activation, which Discord uses for a mention.
function anyUrgent(windows) {
var list = windows || []
for (var i = 0; i < list.length; i++) {
if (list[i] && list[i].urgent === true) return true
}
return false
}
function workspaceLabel(toplevel) {
var workspace = toplevel ? toplevel.workspace : null
if (!workspace) return ""
var name = String(workspace.name || "")
if (name !== "") return name
return workspace.id === undefined ? "" : String(workspace.id)
}
// ---------------------------------------------------------------- pipewire
// PwNode.properties is only valid once a PwObjectTracker has bound the node.
function nodeProps(node) {
return node && node.ready && node.properties ? node.properties : {}
}
function streamNodes(nodes) {
var list = nodes || []
var out = []
for (var i = 0; i < list.length; i++) {
if (list[i] && list[i].isStream) out.push(list[i])
}
return out
}
// Streams say "WEBRTC VoiceEngine", so the process binary alone names the app.
function isOwnedByDiscord(node) {
return isAppId(nodeProps(node)["application.process.binary"])
}
// vesktop names every stream vesktop, so a call is only its audio capture stream, never video or playback.
// Discord publishes five nodes in a call and only one is the mic, so the name alone is not enough.
function isVoiceStream(node) {
if (!isOwnedByDiscord(node)) return false
var name = String(nodeProps(node)["application.name"] || "").toLowerCase()
if (name !== "webrtc voiceengine" && name !== "vesktop") return false
return !!node.audio && !isPlaybackStream(node)
}
function hasVoiceStream(nodes) {
var list = nodes || []
for (var i = 0; i < list.length; i++) {
if (isVoiceStream(list[i])) return true
}
return false
}
// A playback stream publishes with isSink true, the same test the audio panel uses.
function isPlaybackStream(node) {
if (!node || !node.isStream) return false
return node.isSink === true
}
function findDiscordStream(nodes, playback) {
var list = nodes || []
for (var i = 0; i < list.length; i++) {
var node = list[i]
if (!node || !node.audio) continue
if (isPlaybackStream(node) !== playback) continue
if (isOwnedByDiscord(node)) return node
}
return null
}
// ---------------------------------------------------------------- process
// discord: "239958 272772 /home/gm/.config/discord/app-1.0.154/Discord --type=renderer"
// vesktop: "248461 384348 /usr/lib/vesktop/vesktop"
// The main process is the one with no --type=; signalling a child files a crash report.
function parseProcesses(raw) {
var lines = String(raw || "").split("\n")
var count = 0
var rssKib = 0
var mainPid = 0
for (var i = 0; i < lines.length; i++) {
var line = lines[i].trim()
if (line === "") continue
var fields = line.split(/\s+/)
var pid = parseInt(fields[0], 10)
var rss = parseInt(fields[1], 10)
if (!isFinite(pid) || !isFinite(rss)) continue
count += 1
rssKib += rss
if (mainPid === 0 && fields.slice(2).join(" ").indexOf("--type=") === -1) mainPid = pid
}
return { count: count, memoryMib: rssKib / KIB_PER_MIB, mainPid: mainPid }
}
// -------------------------------------------------------------------- rpc
// lines look like {"ok":true,"channel":"General","guild":"GM's Server","mute":false,"speaking":["gm"]}
function parseRpcLine(raw) {
try {
var state = JSON.parse(String(raw))
return state && typeof state === "object" ? state : null
} catch (error) {
return null
}
}
// "General" names half the voice channels in existence, so say whose it is.
function callPlace(channel, guild) {
if (!channel) return ""
return guild ? channel + " · " + guild : String(channel)
}
// ---------------------------------------------------------------- signal
// Omarchy has no green or yellow, so quality is glyph strength first (NOTES.md).
var PING_GOOD_MS = 100
var PING_FAIR_MS = 250
function pingQuality(ping, connected) {
if (!connected) return "none"
if (!(ping > 0)) return "unknown"
if (ping <= PING_GOOD_MS) return "good"
if (ping <= PING_FAIR_MS) return "fair"
return "poor"
}
function pingGlyph(quality) {
if (quality === "good") return ""
if (quality === "fair") return ""
if (quality === "poor") return ""
return ""
}
// ---------------------------------------------------------------- format
function formatMemory(mib) {
if (!(mib > 0)) return "--"
if (mib >= MIB_PER_GIB) return (mib / MIB_PER_GIB).toFixed(1) + " GiB"
return Math.round(mib) + " MiB"
}
// titles read "#general | GM's Server - Discord"; every one carries the suffix
function windowTitle(toplevel) {
var title = toplevel && toplevel.title ? String(toplevel.title) : ""
var suffix = " - Discord"
if (title.length > suffix.length && title.slice(-suffix.length) === suffix) {
title = title.slice(0, -suffix.length)
}
return title === "" ? "Discord" : title
}
function formatUsage(mib, count) {
if (!(count > 0)) return "--"
var processes = count === 1 ? "1 process" : count + " processes"
return formatMemory(mib) + " · " + processes
}
// The line under the title in the panel hero, and the widget's tooltip.
function statusPhrase(service) {
if (!service.installed) return "Not installed"
if (!service.running) return "Not running"
if (service.attention) return "Wants your attention"
if (service.inVoice) {
// The bridge knows which call; PipeWire only knows that there is one.
var place = callPlace(service.callChannel, service.callGuild)
if (place === "") return service.micLive ? "In a call" : "In a call · mic closed"
return service.micLive ? place : place + " · muted"
}
if (!service.hasWindow) return "Running in the background"
if (service.workspace !== "") return "Open on workspace " + service.workspace
return "Running"
}