added quickshell
This commit is contained in:
@@ -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(); }
|
||||
}
|
||||
}
|
||||
@@ -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 ? "" : ""
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 "";
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user