Repository navigation
Expand file tree
/
Copy pathModel.js
More file actions
185 lines (171 loc) · 6.55 KB
/
Copy pathModel.js
File metadata and controls
185 lines (171 loc) · 6.55 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
// Proton VPN CLI output parsing helpers.
.pragma library
// Last transition that has already produced a desktop notification. The bar
// mounts one Service per monitor, and every instance polls the same CLI;
// deduping here keeps a connect/disconnect from notifying once per monitor.
var _lastNotifiedTransition = { connected: false, at: 0 }
var _notifyDedupeWindowMs = 45000
function shouldNotifyTransition(connected, now) {
if (_lastNotifiedTransition.connected === connected && now - _lastNotifiedTransition.at < _notifyDedupeWindowMs) return false
_lastNotifiedTransition.connected = connected
_lastNotifiedTransition.at = now
return true
}
// Parses the official `protonvpn status` output:
//
// Status: Connected
// Server: US-FREE#38 in New York, US
// Load: 42%
// Protocol: wireguard
function parseCliStatus(text) {
var result = {
connected: false,
serverName: "",
serverCity: "",
serverCountry: "",
load: "",
protocol: ""
}
var lines = String(text || "").split("\n")
for (var i = 0; i < lines.length; i++) {
var line = lines[i].replace(/^\s+|\s+$/g, "")
var status = line.match(/^status\s*:\s*(.+)$/i)
if (status) {
result.connected = /^connected$/i.test(status[1].trim())
continue
}
var server = line.match(/^server\s*:\s*(.+?)\s+in\s+(.+)$/i)
if (server) {
result.serverName = stripMarkup(server[1].trim())
var location = server[2].trim().split(/\s*,\s*/)
result.serverCountry = location.length > 1 ? stripMarkup(location[location.length - 1]) : ""
result.serverCity = location.length > 1 ? stripMarkup(location.slice(0, -1).join(", ")) : stripMarkup(location[0])
continue
}
var load = line.match(/^load\s*:\s*(.+)$/i)
if (load) {
result.load = stripMarkup(load[1].trim())
continue
}
var protocol = line.match(/^protocol\s*:\s*(.+)$/i)
if (protocol) result.protocol = stripMarkup(protocol[1].trim())
}
return result
}
// `nmcli -g IP4.ADDRESS dev show <device>` may print several addresses.
function parseDeviceIp(text) {
var s = String(text || "").trim()
var ipv4 = s.match(/(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})/)
return ipv4 ? ipv4[1] : ""
}
// Parses `nmcli -t -f DEVICE,TYPE,STATE dev status` and returns the active
// WireGuard device used by the CLI. The app commonly names it proton0, but
// the device name is intentionally discovered instead of hard-coded.
function parseWireguardDevice(text) {
var lines = String(text || "").split("\n")
for (var i = 0; i < lines.length; i++) {
var parts = lines[i].trim().split(":")
if (parts.length >= 3 && parts[1] === "wireguard" && /connected/.test(parts[2])) return parts[0]
}
return ""
}
// Removes angle-bracket markup from strings that originated from the Proton
// VPN CLI. With Text.PlainText the text is rendered literally, so we only need
// to strip any `<...>` fragments that would otherwise be interpreted as rich
// text under Qt's default Text.AutoText.
function stripMarkup(text) {
return String(text || "").replace(/<[^>]*>/g, "")
}
function serverLabel(serverName, serverCity, serverCountry, fallback) {
var parts = []
if (serverName) parts.push(stripMarkup(serverName))
if (serverCity) parts.push(stripMarkup(serverCity))
if (serverCountry && serverCountry !== serverCity) parts.push(stripMarkup(serverCountry))
if (parts.length === 0 && fallback) parts.push(stripMarkup(fallback))
return parts.join(" \u00b7 ")
}
function parseCountriesOutput(text) {
var names = {}
var lines = String(text || "").split("\n")
for (var i = 0; i < lines.length; i++) {
var match = lines[i].match(/^\s*(.+?)\s{2,}([A-Z]{2})\s*$/)
if (match && match[2] !== "CO") names[match[2]] = stripMarkup(match[1].trim())
}
return names
}
// Projects Proton's cached logical server list into a compact country list.
// Tier 0 is Proton's Free tier; offline logical servers are omitted.
function freeCountryRows(raw, countryNames) {
var result = {}
try {
var logicals = JSON.parse(String(raw || "")).LogicalServers || []
for (var i = 0; i < logicals.length; i++) {
var server = logicals[i]
if (!server || Number(server.Tier) !== 0 || Number(server.Status) !== 1) continue
var code = String(server.ExitCountry || "").toUpperCase()
if (!code) continue
if (!result[code]) {
result[code] = {
code: code,
name: countryNames && countryNames[code] ? stripMarkup(countryNames[code]) : code,
count: 0,
bestLoad: Number(server.Load)
}
}
result[code].count += 1
var load = Number(server.Load)
if (isFinite(load) && load < result[code].bestLoad) result[code].bestLoad = load
}
} catch (e) {
return []
}
var rows = []
for (var code in result) {
var row = result[code]
row.hint = row.count + " server" + (row.count === 1 ? "" : "s") + " \u00b7 " + row.bestLoad + "% best load"
rows.push(row)
}
rows.sort(function(a, b) {
if (a.bestLoad !== b.bestLoad) return a.bestLoad - b.bestLoad
return a.name.localeCompare(b.name)
})
return rows
}
function elideStatus(text) {
var value = stripMarkup(String(text || "").replace(/\s+/g, " ").trim())
return value.length > 140 ? value.substring(0, 137) + "\u2026" : value
}
// Formats a millisecond duration as a compact "1h 23m" / "45s" string.
function formatUptime(ms) {
var total = Math.max(0, Math.floor(Number(ms) / 1000))
var hours = Math.floor(total / 3600)
var minutes = Math.floor((total % 3600) / 60)
var seconds = total % 60
if (hours > 0) return hours + "h " + minutes + "m"
if (minutes > 0) return minutes + "m " + seconds + "s"
return seconds + "s"
}
// Extracts rx/tx byte counters for a specific interface from `cat /proc/net/dev`.
function parseNetDev(text, device) {
if (!device) return null
var lines = String(text || "").split("\n")
for (var i = 0; i < lines.length; i++) {
var colon = lines[i].indexOf(":")
if (colon < 1) continue
var name = lines[i].substring(0, colon).replace(/\s/g, "")
if (name !== device) continue
var fields = lines[i].substring(colon + 1).trim().split(/\s+/)
if (fields.length < 9) return null
return { rx: Number(fields[0]) || 0, tx: Number(fields[8]) || 0 }
}
return null
}
// Formats bytes per second as a compact "1.2 MiB/s" / "340 KiB/s" string.
function formatRate(bytes, seconds) {
var bps = seconds > 0 ? Math.max(0, bytes / seconds) : 0
var units = ["B", "KiB", "MiB", "GiB"]
var u = 0
var v = bps
while (v >= 1024 && u < units.length - 1) { v /= 1024; u++ }
return (v >= 100 ? v.toFixed(0) : v.toFixed(1)) + " " + units[u] + "/s"
}