added quickshell

This commit is contained in:
2026-08-10 23:57:26 +02:00
parent cbaf55cfc5
commit 47ff5233bd
69 changed files with 6945 additions and 80 deletions
+69
View File
@@ -0,0 +1,69 @@
pragma Singleton
import QtQuick
import Quickshell
import Quickshell.Io
import "root:/services"
// Shell-wide UI state and the handful of external commands the shell fires.
// Also exposes an IPC surface so Hyprland keybinds can drive the same actions
// (`qs ipc call shell lock`, `qs ipc call shell power`, ...).
Singleton {
id: root
// ------------------------------------------------------------- UI state
property bool powerMenuOpen: false
property bool lockRequested: false
property bool sidebarOpen: false
signal osdRequested(string kind)
function togglePowerMenu() { root.powerMenuOpen = !root.powerMenuOpen; }
function closePowerMenu() { root.powerMenuOpen = false; }
function lock() { root.powerMenuOpen = false; root.lockRequested = true; }
// ------------------------------------------------------------- commands
// Each slot gets its own Process so a slow one cannot cancel another.
Process { id: slotA }
Process { id: slotB }
Process { id: slotC }
property int _slot: 0
function run(argv: var) {
const slots = [slotA, slotB, slotC];
const p = slots[root._slot % slots.length];
root._slot++;
p.running = false;
p.command = argv;
p.running = true;
}
function launch(app: string) {
root.run(["uwsm", "app", "--", app]);
}
// --------------------------------------------------------------- power
function logout() { root.run(["uwsm", "stop"]); }
function reboot() { root.run(["systemctl", "reboot"]); }
function shutdown() { root.run(["systemctl", "poweroff"]); }
function suspend() { root.lock(); root.run(["systemctl", "suspend"]); }
function hibernate() { root.lock(); root.run(["systemctl", "hibernate"]); }
function reloadShell() { Quickshell.reload(true); }
// ------------------------------------------------------------- helpers
function openAudioSettings() { root.launch("pavucontrol"); }
function openSystemMonitor() { root.run(["alacritty", "-e", "btop"]); }
function openNetworkSettings() { root.run(["nm-connection-editor"]); }
function copyText(s: string) { root.run(["wl-copy", "--", s]); }
IpcHandler {
target: "shell"
function lock(): void { root.lock(); }
function power(): void { root.togglePowerMenu(); }
function reloadConfig(): void { root.reloadShell(); }
function toggleDnd(): void { Notifs.toggleDnd(); }
function clearNotifications(): void { Notifs.clearAll(); }
}
}
+99
View File
@@ -0,0 +1,99 @@
pragma Singleton
import QtQuick
import Quickshell
import Quickshell.Services.Pipewire
// Live audio state straight from PipeWire, so the bar and OSD react the instant
// a volume key is pressed rather than on the next poll.
Singleton {
id: root
readonly property PwNode sink: Pipewire.defaultAudioSink
readonly property PwNode source: Pipewire.defaultAudioSource
// Binding node.audio.* only works while the node is tracked.
PwObjectTracker {
objects: [root.sink, root.source].filter(n => n !== null)
}
readonly property bool ready: sink !== null && sink.audio !== null
readonly property real volume: sink?.audio?.volume ?? 0
readonly property bool muted: sink?.audio?.muted ?? true
readonly property int volumePercent: Math.round(volume * 100)
readonly property string sinkName: sink?.nickname || sink?.description || sink?.name || "No output"
readonly property real micVolume: source?.audio?.volume ?? 0
readonly property bool micMuted: source?.audio?.muted ?? true
readonly property int micPercent: Math.round(micVolume * 100)
readonly property string sourceName: source?.nickname || source?.description || source?.name || "No input"
// Every sink on the box, for the output picker in the audio popout.
readonly property var sinks: Pipewire.nodes.values.filter(n =>
n.isSink && !n.isStream && n.audio !== null)
// Application streams, for the per-app mixer.
readonly property var streams: Pipewire.nodes.values.filter(n =>
n.isStream && n.audio !== null && n.isSink === false)
PwObjectTracker {
objects: root.streams
}
signal volumeBumped()
signal micBumped()
onVolumePercentChanged: volumeBumped()
onMutedChanged: volumeBumped()
onMicPercentChanged: micBumped()
onMicMutedChanged: micBumped()
function setVolume(v: real) {
if (!sink?.audio) return;
sink.audio.volume = Math.max(0, Math.min(1.5, v));
}
function changeVolume(delta: real) {
setVolume(volume + delta);
}
function toggleMute() {
if (!sink?.audio) return;
sink.audio.muted = !sink.audio.muted;
}
function setMicVolume(v: real) {
if (!source?.audio) return;
source.audio.volume = Math.max(0, Math.min(1.5, v));
}
function toggleMicMute() {
if (!source?.audio) return;
source.audio.muted = !source.audio.muted;
}
function setSink(node: PwNode) {
Pipewire.preferredDefaultAudioSink = node;
}
function streamName(node: PwNode): string {
if (!node) return "";
return node.properties?.["application.name"]
|| node.properties?.["media.name"]
|| node.nickname
|| node.description
|| node.name;
}
// Nerd Font glyph for the current output level.
readonly property string icon: {
if (muted) return "󰝟";
if (volumePercent === 0) return "󰕿";
if (volumePercent < 34) return "󰕿";
if (volumePercent < 67) return "󰖀";
return "󰕾";
}
readonly property string micIcon: micMuted ? "󰍭" : "󰍬"
}
+101
View File
@@ -0,0 +1,101 @@
pragma Singleton
import QtQuick
import Quickshell
import Quickshell.Services.UPower
import "root:/config"
import "root:/services"
// Battery and power-profile state. UPower gives live percentage and time
// estimates; Sys.powerDetail adds the TLP/AC line fluxo reports.
Singleton {
id: root
readonly property UPowerDevice device: UPower.displayDevice
readonly property bool available: device !== null && device.isLaptopBattery && device.isPresent
readonly property bool onBattery: UPower.onBattery
readonly property real percent: device?.percentage !== undefined ? device.percentage * 100 : 0
readonly property int state: device?.state ?? UPowerDeviceState.Unknown
readonly property bool charging: state === UPowerDeviceState.Charging
|| state === UPowerDeviceState.PendingCharge
readonly property bool full: state === UPowerDeviceState.FullyCharged
readonly property real changeRate: device?.changeRate ?? 0 // watts
readonly property real health: device?.healthSupported ? (device?.healthPercentage ?? 0) : 0
readonly property bool low: available && !charging && percent <= 20
readonly property bool critical: available && !charging && percent <= 10
readonly property real secondsLeft: charging
? (device?.timeToFull ?? 0)
: (device?.timeToEmpty ?? 0)
readonly property string timeLabel: {
if (root.full) return "Full";
if (!(root.secondsLeft > 0)) return charging ? "Charging" : "Estimating…";
const mins = Math.round(root.secondsLeft / 60);
const h = Math.floor(mins / 60);
const m = mins % 60;
const dur = h > 0 ? h + "h " + m + "m" : m + "m";
return root.charging ? dur + " to full" : dur + " left";
}
readonly property string detail: Sys.powerDetail
// Ten-step battery glyph ramp, plus the charging variants.
readonly property string icon: {
if (!root.available) return "󰚥";
const p = Math.max(0, Math.min(100, root.percent));
const step = Math.min(10, Math.floor(p / 10));
if (root.charging) {
const charge = ["󰢜", "󰂆", "󰂇", "󰂈", "󰢝", "󰂉", "󰢞", "󰂊", "󰂋", "󰂅", "󰂅"];
return charge[step];
}
const drain = ["󰁺", "󰁻", "󰁼", "󰁽", "󰁾", "󰁿", "󰂀", "󰂁", "󰂂", "󰁹", "󰁹"];
return drain[step];
}
readonly property color tint: {
if (root.charging || root.full) return Theme.ok;
if (root.critical) return Theme.accent;
if (root.low) return Theme.warn;
return Theme.primary;
}
// ------------------------------------------------------- power profiles
readonly property bool hasPerformance: PowerProfiles.hasPerformanceProfile
readonly property int profile: PowerProfiles.profile
readonly property string profileLabel: {
switch (root.profile) {
case PowerProfile.PowerSaver: return "Power Saver";
case PowerProfile.Performance: return "Performance";
default: return "Balanced";
}
}
readonly property string profileIcon: {
switch (root.profile) {
case PowerProfile.PowerSaver: return "󰌪";
case PowerProfile.Performance: return "󰓅";
default: return "󰾅";
}
}
function setProfile(p: int) {
PowerProfiles.profile = p;
}
function cycleProfile() {
if (root.profile === PowerProfile.PowerSaver) {
root.setProfile(PowerProfile.Balanced);
} else if (root.profile === PowerProfile.Balanced
&& PowerProfiles.hasPerformanceProfile) {
root.setProfile(PowerProfile.Performance);
} else {
root.setProfile(PowerProfile.PowerSaver);
}
}
}
+75
View File
@@ -0,0 +1,75 @@
pragma Singleton
import QtQuick
import Quickshell
import Quickshell.Io
// Backlight control. Reads the sysfs value directly so the OSD is exact, and
// writes through brightnessctl, which already owns the udev permissions here.
Singleton {
id: root
readonly property string device: "amdgpu_bl1"
property int raw: 0
property int maxRaw: 65535
readonly property real percent: maxRaw > 0 ? raw / maxRaw * 100 : 0
readonly property bool available: maxRaw > 0
signal bumped()
FileView {
id: maxFile
path: "/sys/class/backlight/" + root.device + "/max_brightness"
watchChanges: false
onLoaded: {
const v = parseInt(text().trim());
if (v > 0) root.maxRaw = v;
}
}
FileView {
id: currentFile
path: "/sys/class/backlight/" + root.device + "/brightness"
watchChanges: true
onFileChanged: reload()
onLoaded: {
const v = parseInt(text().trim());
if (!isNaN(v) && v !== root.raw) {
root.raw = v;
root.bumped();
}
}
}
// sysfs writes do not always fire inotify for backlight, so poll gently too.
Timer {
interval: 1000
running: true
repeat: true
onTriggered: currentFile.reload()
}
Process {
id: setter
}
function setPercent(p: real) {
const clamped = Math.max(1, Math.min(100, Math.round(p)));
setter.running = false;
setter.command = ["brightnessctl", "-d", root.device, "-q", "set", clamped + "%"];
setter.running = true;
}
function change(delta: real) {
setPercent(percent + delta);
}
readonly property string icon: {
const p = root.percent;
if (p >= 80) return "󰃠";
if (p >= 55) return "󰃝";
if (p >= 30) return "󰃟";
return "󰃞";
}
}
+80
View File
@@ -0,0 +1,80 @@
pragma Singleton
import QtQuick
import Quickshell
import Quickshell.Bluetooth
// Bluetooth via BlueZ. Connection and pairing are driven directly from the
// popout; `fluxo bt` stays available as a CLI for the existing keybind.
Singleton {
id: root
readonly property BluetoothAdapter adapter: Bluetooth.defaultAdapter
readonly property bool available: adapter !== null
readonly property bool enabled: adapter?.enabled ?? false
readonly property bool discovering: adapter?.discovering ?? false
readonly property var devices: adapter
? adapter.devices.values.slice().sort((a, b) => {
if (a.connected !== b.connected) return a.connected ? -1 : 1;
if (a.paired !== b.paired) return a.paired ? -1 : 1;
return (a.deviceName || a.name || "").localeCompare(b.deviceName || b.name || "");
})
: []
readonly property var connected: devices.filter(d => d.connected)
readonly property BluetoothDevice primary: connected.length > 0 ? connected[0] : null
readonly property string label: {
if (!available) return "No adapter";
if (!enabled) return "Off";
if (connected.length === 0) return "Disconnected";
if (connected.length === 1) return root.deviceLabel(primary);
return connected.length + " devices";
}
readonly property string icon: {
if (!available || !enabled) return "󰂲";
if (connected.length > 0) return "󰂱";
return "󰂯";
}
function deviceLabel(d: BluetoothDevice): string {
if (!d) return "";
return d.deviceName || d.name || d.address;
}
// Nerd Font glyph matching BlueZ's icon hint.
function deviceIcon(d: BluetoothDevice): string {
const i = d?.icon || "";
if (i.includes("headset") || i.includes("headphone")) return "󰋋";
if (i.includes("audio")) return "󰓃";
if (i.includes("phone")) return "󰄜";
if (i.includes("mouse")) return "󰍽";
if (i.includes("keyboard")) return "󰌌";
if (i.includes("computer")) return "󰟀";
if (i.includes("watch")) return "󰖉";
if (i.includes("input-gaming")) return "󰊗";
if (i.includes("printer")) return "󰐪";
return "󰂯";
}
function toggle() {
if (adapter) adapter.enabled = !adapter.enabled;
}
function toggleScan() {
if (adapter) adapter.discovering = !adapter.discovering;
}
function toggleDevice(d: BluetoothDevice) {
if (!d) return;
if (d.connected) {
d.disconnect();
} else if (d.paired || d.bonded) {
d.connect();
} else {
d.pair();
}
}
}
+99
View File
@@ -0,0 +1,99 @@
pragma Singleton
import QtQuick
import Quickshell
import Quickshell.Services.Mpris
// MPRIS. Prefers whichever player is actually playing, falling back to the last
// one that was, so the bar does not flip between Spotify and a stray browser tab.
Singleton {
id: root
property string preferredId: ""
readonly property var players: Mpris.players?.values ?? []
readonly property bool hasPlayer: active !== null
readonly property MprisPlayer active: {
if (players.length === 0) return null;
// An explicit pick wins, while it still exists.
if (root.preferredId !== "") {
for (const p of players) if (p.dbusName === root.preferredId) return p;
}
for (const p of players) if (p.isPlaying) return p;
return players[0];
}
readonly property string title: active?.trackTitle || ""
readonly property string artist: active?.trackArtist || ""
readonly property string album: active?.trackAlbum || ""
readonly property string artUrl: active?.trackArtUrl || ""
readonly property string identity: active?.identity || ""
readonly property bool playing: active?.isPlaying ?? false
readonly property bool canGoNext: active?.canGoNext ?? false
readonly property bool canGoPrevious: active?.canGoPrevious ?? false
readonly property bool canSeek: (active?.canSeek ?? false) && (active?.lengthSupported ?? false)
readonly property real length: active?.lengthSupported ? (active?.length ?? 0) : 0
readonly property real position: active?.positionSupported ? (active?.position ?? 0) : 0
readonly property real progress: length > 0 ? Math.min(1, position / length) : 0
readonly property string label: {
if (!hasPlayer) return "Nothing playing";
if (title === "") return identity || "Unknown track";
if (artist === "") return title;
return artist + " — " + title;
}
signal trackChanged()
onTitleChanged: trackChanged()
// MPRIS position is pull-only; tick it so the seek bar moves.
Timer {
interval: 500
running: root.playing && root.canSeek
repeat: true
onTriggered: if (root.active) root.active.positionChanged()
}
function playPause() { if (active?.canTogglePlaying) active.togglePlaying(); }
function next() { if (canGoNext) active.next(); }
function previous() { if (canGoPrevious) active.previous(); }
function seekTo(fraction: real) {
if (!canSeek || length <= 0) return;
active.position = Math.max(0, Math.min(length, fraction * length));
}
function raise() { if (active?.canRaise) active.raise(); }
function select(player: MprisPlayer) {
root.preferredId = player ? player.dbusName : "";
}
// Nerd Font glyph for the source app, so the bar reads at a glance.
readonly property string sourceIcon: {
const id = (identity || "").toLowerCase();
if (id.includes("spotify")) return "";
if (id.includes("firefox")) return "";
if (id.includes("chrom") || id.includes("brave")) return "";
if (id.includes("mpv")) return "";
if (id.includes("vlc")) return "󰕼";
if (id.includes("jellyfin")) return "󰋩";
if (id.includes("youtube")) return "";
return "󰝚";
}
function timeString(micros: real): string {
if (!(micros > 0)) return "0:00";
const total = Math.floor(micros / 1000000);
const m = Math.floor(total / 60);
const s = total % 60;
if (m >= 60) {
const h = Math.floor(m / 60);
return h + ":" + String(m % 60).padStart(2, "0") + ":" + String(s).padStart(2, "0");
}
return m + ":" + String(s).padStart(2, "0");
}
}
+96
View File
@@ -0,0 +1,96 @@
pragma Singleton
import QtQuick
import Quickshell
import Quickshell.Networking
// NetworkManager state for the network popout. Throughput and the active IP
// come from Sys (fluxo); this covers device topology and the Wi-Fi picker.
Singleton {
id: root
readonly property bool wifiEnabled: Networking.wifiEnabled
readonly property bool wifiHardwareEnabled: Networking.wifiHardwareEnabled
readonly property int connectivity: Networking.connectivity
readonly property var devices: Networking.devices?.values ?? []
readonly property var wifiDevices: devices.filter(d => d.type === DeviceType.Wifi)
readonly property var wiredDevices: devices.filter(d => d.type === DeviceType.Wired)
readonly property WifiDevice wifi: wifiDevices.length > 0 ? wifiDevices[0] : null
readonly property WiredDevice wired: wiredDevices.length > 0 ? wiredDevices[0] : null
readonly property bool wiredUp: wired?.connected ?? false
readonly property Network activeWifi: {
if (!wifi) return null;
const nets = wifi.networks?.values ?? [];
for (const n of nets) if (n.connected) return n;
return null;
}
// Strongest-first, one entry per SSID, current network pinned to the top.
readonly property var wifiNetworks: {
if (!wifi) return [];
const seen = {};
const out = [];
const nets = (wifi.networks?.values ?? []).slice().sort((a, b) =>
(b.signalStrength ?? 0) - (a.signalStrength ?? 0));
for (const n of nets) {
if (!n.name || seen[n.name]) continue;
seen[n.name] = true;
out.push(n);
}
return out.sort((a, b) => {
if (a.connected !== b.connected) return a.connected ? -1 : 1;
return (b.signalStrength ?? 0) - (a.signalStrength ?? 0);
});
}
readonly property bool scanning: wifi?.scannerEnabled ?? false
readonly property string icon: {
if (root.wiredUp) return "󰈀";
if (!root.wifiEnabled) return "󰤭";
if (!root.activeWifi) return "󰤯";
const s = root.activeWifi.signalStrength ?? 0;
// Strength is only sampled while a scan is running, which the shell
// does on demand. Connected-but-unsampled should read as full, not dead.
if (s <= 0) return "󰤨";
if (s >= 80) return "󰤨";
if (s >= 60) return "󰤥";
if (s >= 40) return "󰤢";
if (s >= 20) return "󰤟";
return "󰤯";
}
readonly property string label: {
if (root.wiredUp) return "Ethernet";
if (!root.wifiEnabled) return "Wi-Fi off";
if (root.activeWifi) return root.activeWifi.name;
return "Offline";
}
function signalIcon(strength: int): string {
if (strength >= 80) return "󰤨";
if (strength >= 60) return "󰤥";
if (strength >= 40) return "󰤢";
if (strength >= 20) return "󰤟";
return "󰤯";
}
function secured(n): bool {
return n && n.security !== undefined
&& n.security !== WifiSecurityType.Open
&& n.security !== WifiSecurityType.Unknown;
}
function toggleWifi() {
Networking.wifiEnabled = !Networking.wifiEnabled;
}
function setScanning(on: bool) {
if (wifi) wifi.scannerEnabled = on;
}
}
+111
View File
@@ -0,0 +1,111 @@
pragma Singleton
import QtQuick
import Quickshell
import Quickshell.Services.Notifications
import "root:/config"
// Notification server. Replaces dunst: the shell owns the org.freedesktop
// .Notifications name and renders popups and the history list itself.
Singleton {
id: root
property bool dnd: false
// Newest first. Popups read the head of this list.
property list<Notification> history: []
property int maxHistory: 60
readonly property int count: history.length
readonly property bool hasUnread: unread > 0
property int unread: 0
NotificationServer {
id: server
keepOnReload: true
bodySupported: true
bodyMarkupSupported: true
bodyImagesSupported: true
actionsSupported: true
actionIconsSupported: true
imageSupported: true
inlineReplySupported: true
persistenceSupported: true
onNotification: notif => {
// Hold the notification open until we drop it ourselves.
notif.tracked = true;
root.history = [notif, ...root.history].slice(0, root.maxHistory);
root.unread++;
if (!root.dnd) root.popupRequested(notif);
}
}
signal popupRequested(Notification notif)
function dismiss(notif: Notification) {
if (!notif) return;
const idx = root.history.indexOf(notif);
if (idx !== -1) {
const next = root.history.slice();
next.splice(idx, 1);
root.history = next;
}
notif.dismiss();
}
function clearAll() {
const all = root.history.slice();
root.history = [];
root.unread = 0;
for (const n of all) n.dismiss();
}
function markRead() {
root.unread = 0;
}
function toggleDnd() {
root.dnd = !root.dnd;
}
readonly property string icon: {
if (root.dnd) return "󰂛";
if (root.hasUnread) return "󱅫";
return "󰂚";
}
// Urgency-driven accents for the popup chrome.
function urgencyColor(u: int): color {
switch (u) {
case NotificationUrgency.Critical: return Theme.accent;
case NotificationUrgency.Low: return Theme.muted;
default: return Theme.primary;
}
}
function appIconFor(notif: Notification): string {
if (!notif) return "󰂚";
const app = (notif.appName || "").toLowerCase();
if (app.includes("spotify")) return "";
if (app.includes("discord")) return "󰙯";
if (app.includes("slack")) return "󰒱";
if (app.includes("thunderbird") || app.includes("mail")) return "󰇮";
if (app.includes("firefox")) return "";
if (app.includes("volume") || app.includes("audio")) return "󰕾";
if (app.includes("screenshot") || app.includes("grim")) return "󰄄";
return "󰂚";
}
// How long a popup stays up, in ms.
function timeoutFor(notif: Notification): int {
if (!notif) return 5000;
if (notif.urgency === NotificationUrgency.Critical) return 0; // sticky
if (notif.expireTimeout > 0) return notif.expireTimeout;
if (notif.urgency === NotificationUrgency.Low) return 3500;
return 6000;
}
}
+223
View File
@@ -0,0 +1,223 @@
pragma Singleton
import QtQuick
import Quickshell
import Quickshell.Io
// Hardware telemetry, sourced from the user's `fluxo` daemon. fluxo's hardware
// modules are configured to emit raw pipe-delimited values (see
// ~/.config/fluxo/config.toml); everything here is parsing and formatting.
Singleton {
id: root
// Poll interval. The bar reads these continuously, so keep it modest.
property int interval: 2000
// ------------------------------------------------------------------ cpu
property real cpuUsage: 0
property real cpuTemp: 0
property string cpuModel: ""
// ------------------------------------------------------------------ mem
property real memUsed: 0 // GB
property real memTotal: 0 // GB
readonly property real memPercent: memTotal > 0 ? memUsed / memTotal * 100 : 0
// ------------------------------------------------------------------ gpu
property bool gpuAvailable: false
property real gpuUsage: 0
property real gpuVramUsed: 0
property real gpuVramTotal: 0
property real gpuTemp: 0
property string gpuModel: ""
// ----------------------------------------------------------------- disk
property real diskUsed: 0 // GB
property real diskTotal: 0 // GB
property real diskPercent: 0
property string diskFree: ""
// ------------------------------------------------------------------ sys
property string uptime: ""
property real load1: 0
property real load5: 0
property real load15: 0
property int procs: 0
// ------------------------------------------------------------------ net
property string netInterface: ""
property string netIp: ""
property real netRx: 0 // MB/s
property real netTx: 0 // MB/s
readonly property bool netUp: netInterface !== "" && netInterface !== "none"
readonly property bool netWireless: netInterface.startsWith("wl")
// -------------------------------------------------------------- battery
// UPower drives the battery UI; fluxo supplies the TLP/AC detail line.
property string powerDetail: ""
// fluxo wraps values in zero-width spaces for Waybar's benefit.
function _clean(s: string): string {
return (s || "").replace(//g, "").trim();
}
function _fields(json: string): var {
try {
const o = JSON.parse(json);
return { parts: root._clean(o.text).split("|"), obj: o };
} catch (e) {
return null;
}
}
function _num(v): real {
const n = parseFloat(v);
return isNaN(n) ? 0 : n;
}
Timer {
interval: root.interval
running: true
repeat: true
triggeredOnStart: true
onTriggered: {
cpuReader.running = true;
memReader.running = true;
gpuReader.running = true;
netReader.running = true;
sysReader.running = true;
}
}
// Disk and battery move slowly; poll them a tenth as often.
Timer {
interval: root.interval * 10
running: true
repeat: true
triggeredOnStart: true
onTriggered: {
diskReader.running = true;
powerReader.running = true;
}
}
Process {
id: cpuReader
command: ["fluxo", "cpu"]
stdout: StdioCollector {
onStreamFinished: {
const f = root._fields(text);
if (!f) return;
root.cpuUsage = root._num(f.parts[0]);
root.cpuTemp = root._num(f.parts[1]);
root.cpuModel = f.obj.tooltip || "";
}
}
}
Process {
id: memReader
command: ["fluxo", "mem"]
stdout: StdioCollector {
onStreamFinished: {
const f = root._fields(text);
if (!f) return;
root.memUsed = root._num(f.parts[0]);
root.memTotal = root._num(f.parts[1]);
}
}
}
Process {
id: gpuReader
command: ["fluxo", "gpu"]
stdout: StdioCollector {
onStreamFinished: {
const f = root._fields(text);
if (!f || f.parts.length < 4) {
root.gpuAvailable = false;
return;
}
root.gpuAvailable = true;
root.gpuUsage = root._num(f.parts[0]);
root.gpuVramUsed = root._num(f.parts[1]);
root.gpuVramTotal = root._num(f.parts[2]);
root.gpuTemp = root._num(f.parts[3]);
const m = /Model:\s*(.+)/.exec(f.obj.tooltip || "");
if (m) root.gpuModel = m[1];
}
}
}
Process {
id: diskReader
command: ["fluxo", "disk", "/"]
stdout: StdioCollector {
onStreamFinished: {
const f = root._fields(text);
if (!f) return;
root.diskUsed = root._num(f.parts[1]);
root.diskTotal = root._num(f.parts[2]);
root.diskPercent = f.obj.percentage || 0;
const m = /Free:\s*(\S+)/.exec(f.obj.tooltip || "");
root.diskFree = m ? m[1] : "";
}
}
}
Process {
id: netReader
command: ["fluxo", "net"]
stdout: StdioCollector {
onStreamFinished: {
const f = root._fields(text);
if (!f) return;
root.netInterface = f.parts[0] || "";
root.netIp = f.parts[1] || "";
root.netRx = root._num(f.parts[2]);
root.netTx = root._num(f.parts[3]);
}
}
}
Process {
id: sysReader
command: ["fluxo", "sys"]
stdout: StdioCollector {
onStreamFinished: {
const f = root._fields(text);
if (!f) return;
root.uptime = f.parts[0] || "";
root.load1 = root._num(f.parts[1]);
root.load5 = root._num(f.parts[2]);
root.load15 = root._num(f.parts[3]);
const m = /Processes:\s*(\d+)/.exec(f.obj.tooltip || "");
if (m) root.procs = parseInt(m[1]);
}
}
}
Process {
id: powerReader
command: ["fluxo", "power"]
stdout: StdioCollector {
onStreamFinished: {
try {
root.powerDetail = JSON.parse(text).tooltip || "";
} catch (e) {}
}
}
}
// ------------------------------------------------------------ formatting
function rate(mbps: real): string {
if (mbps >= 1) return mbps.toFixed(1) + " MB/s";
const kb = mbps * 1024;
if (kb >= 1) return Math.round(kb) + " KB/s";
return "idle";
}
function gb(v: real): string {
return v >= 100 ? Math.round(v) + "G" : v.toFixed(1) + "G";
}
}