some qs fixes
This commit is contained in:
1 parent
32d5683fad
commit
6addb504e6
26 files changed
+510
-82
No files matched your search
+7
-3
@@ -4,9 +4,10 @@
|
|||||||
-- lua/, and each require() is its own scope: an error in one file does not stop
|
-- lua/, and each require() is its own scope: an error in one file does not stop
|
||||||
-- the others from loading.
|
-- the others from loading.
|
||||||
--
|
--
|
||||||
-- Order matters only in two places: palette must exist before anything paints
|
-- Order matters only in three places: palette must exist before anything
|
||||||
-- with it, and monitors must exist before the lid handler binds to them. Both
|
-- paints with it, monitors must exist before the lid handler binds to them,
|
||||||
-- are handled by the requires below, top to bottom.
|
-- and game mode reads the look and the animations as configured. All three are
|
||||||
|
-- handled by the requires below, top to bottom.
|
||||||
|
|
||||||
-- what the desktop looks like
|
-- what the desktop looks like
|
||||||
require("lua/palette")
|
require("lua/palette")
|
||||||
@@ -27,6 +28,9 @@ require("lua/autostart")
|
|||||||
require("lua/rules/windows")
|
require("lua/rules/windows")
|
||||||
require("lua/rules/layers")
|
require("lua/rules/layers")
|
||||||
|
|
||||||
|
-- after look and animations: it reads their values to put back
|
||||||
|
require("lua/gamemode")
|
||||||
|
|
||||||
-- how it is driven
|
-- how it is driven
|
||||||
require("lua/binds/apps")
|
require("lua/binds/apps")
|
||||||
require("lua/binds/windows")
|
require("lua/binds/windows")
|
||||||
|
|||||||
@@ -4,8 +4,9 @@
|
|||||||
-- Volume and brightness repeat when held.
|
-- Volume and brightness repeat when held.
|
||||||
local held = { locked = true, repeating = true }
|
local held = { locked = true, repeating = true }
|
||||||
|
|
||||||
hl.bind("XF86AudioRaiseVolume", hl.dsp.exec_cmd("fluxo vol up 5"), held)
|
-- wpctl rather than `fluxo vol`, which goes up to 150%; `-l 1` stops at 100%.
|
||||||
hl.bind("XF86AudioLowerVolume", hl.dsp.exec_cmd("fluxo vol down 5"), held)
|
hl.bind("XF86AudioRaiseVolume", hl.dsp.exec_cmd("wpctl set-volume -l 1 @DEFAULT_AUDIO_SINK@ 5%+"), held)
|
||||||
|
hl.bind("XF86AudioLowerVolume", hl.dsp.exec_cmd("wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%-"), held)
|
||||||
hl.bind("XF86AudioMute", hl.dsp.exec_cmd("fluxo vol mute"), held)
|
hl.bind("XF86AudioMute", hl.dsp.exec_cmd("fluxo vol mute"), held)
|
||||||
hl.bind("XF86AudioMicMute", hl.dsp.exec_cmd("fluxo mic mute"), held)
|
hl.bind("XF86AudioMicMute", hl.dsp.exec_cmd("fluxo mic mute"), held)
|
||||||
hl.bind("XF86MonBrightnessUp", hl.dsp.exec_cmd("brightnessctl s +10%"), held)
|
hl.bind("XF86MonBrightnessUp", hl.dsp.exec_cmd("brightnessctl s +10%"), held)
|
||||||
|
|||||||
@@ -0,0 +1,124 @@
|
|||||||
|
-- Game mode: while a fullscreen game is on screen, neither the compositor nor
|
||||||
|
-- the shell spends anything on the desktop it covers.
|
||||||
|
--
|
||||||
|
-- on <=> some monitor shows a game, fullscreen, on its active workspace
|
||||||
|
--
|
||||||
|
-- Like lua/lid.lua, the answer is re-derived from live state on every trigger
|
||||||
|
-- and nothing is remembered, so a reload mid-game simply finds the game again.
|
||||||
|
--
|
||||||
|
-- A game is a window that says so through the content-type protocol
|
||||||
|
-- (gamescope does), or one whose class gives it away: Proton names every Steam
|
||||||
|
-- game steam_app_<id>. Add to GAME_CLASSES for anything else.
|
||||||
|
--
|
||||||
|
-- The shell is told through its IPC and pauses its own polling and animation
|
||||||
|
-- (quickshell/services/GameMode.qml).
|
||||||
|
|
||||||
|
-- Lua patterns, matched against the window class.
|
||||||
|
local GAME_CLASSES = {
|
||||||
|
"^steam_app_%d+$",
|
||||||
|
"^gamescope$",
|
||||||
|
}
|
||||||
|
|
||||||
|
-- What game mode sets. What it puts back is read from the config as loaded, so
|
||||||
|
-- lua/look.lua and lua/animations.lua stay the only place those are decided. A
|
||||||
|
-- reload resets every option before the config runs, so this never reads game
|
||||||
|
-- mode's own values back.
|
||||||
|
local GAME = {
|
||||||
|
-- Nothing under a fullscreen window is worth animating, and leaving the
|
||||||
|
-- game should be instant.
|
||||||
|
["animations.enabled"] = false,
|
||||||
|
["decoration.shadow.enabled"] = false,
|
||||||
|
-- Hand the game's frames straight to the display instead of compositing
|
||||||
|
-- them every refresh. Only while playing: with it on all the time, some
|
||||||
|
-- ordinary fullscreen apps flicker or show the wrong colours.
|
||||||
|
["render.direct_scanout"] = 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
local NORMAL = {}
|
||||||
|
for option in pairs(GAME) do
|
||||||
|
NORMAL[option] = hl.get_config(option)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ------------------------------------------------------------- reading state
|
||||||
|
|
||||||
|
local function is_game(win)
|
||||||
|
if win.content_type == "game" then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
local class = win.class or ""
|
||||||
|
for _, pattern in ipairs(GAME_CLASSES) do
|
||||||
|
if class:match(pattern) then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
-- `ignore` is a window being closed: during window.close it can still be
|
||||||
|
-- listed, and counting it would keep game mode on after the game has gone.
|
||||||
|
-- Fullscreen 2 is real fullscreen; a maximised game (1) still shows the hull.
|
||||||
|
local function playing(ignore)
|
||||||
|
for _, win in ipairs(hl.get_windows()) do
|
||||||
|
if win ~= ignore
|
||||||
|
and win.mapped
|
||||||
|
and win.fullscreen >= 2
|
||||||
|
and win.monitor
|
||||||
|
and win.monitor.active_workspace == win.workspace
|
||||||
|
and is_game(win)
|
||||||
|
then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------- reconciling
|
||||||
|
|
||||||
|
-- A fresh config is the normal one, so only a game found at load needs
|
||||||
|
-- applying. The shell heard the last "false" before the reload.
|
||||||
|
local active = false
|
||||||
|
|
||||||
|
local function tell_shell()
|
||||||
|
hl.exec_cmd("qs ipc call shell gameMode " .. tostring(active))
|
||||||
|
end
|
||||||
|
|
||||||
|
local function reconcile(ignore)
|
||||||
|
local want = playing(ignore)
|
||||||
|
if want == active then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
active = want
|
||||||
|
hl.config(active and GAME or NORMAL)
|
||||||
|
tell_shell()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ------------------------------------------------------------------ triggers
|
||||||
|
|
||||||
|
for _, event in ipairs({
|
||||||
|
"window.open",
|
||||||
|
"window.fullscreen",
|
||||||
|
"window.move_to_workspace",
|
||||||
|
-- Proton sets the class after the window opens.
|
||||||
|
"window.class",
|
||||||
|
"workspace.active",
|
||||||
|
"monitor.removed",
|
||||||
|
}) do
|
||||||
|
hl.on(event, function()
|
||||||
|
reconcile()
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
hl.on("window.close", function(win)
|
||||||
|
reconcile(win)
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- A shell started mid-game (Super+Shift+R, or a crash) begins with game mode
|
||||||
|
-- off. Its bar mapping is the sign it is up and listening.
|
||||||
|
hl.on("layer.opened", function(layer)
|
||||||
|
if active and layer.namespace == "takemi-bar" then
|
||||||
|
tell_shell()
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
reconcile()
|
||||||
@@ -30,6 +30,11 @@ local GAMESCOPE = "^(gamescope)$"
|
|||||||
-- })
|
-- })
|
||||||
hl.window_rule({ match = { class = "^$", title = "^$" }, no_blur = true })
|
hl.window_rule({ match = { class = "^$", title = "^$" }, no_blur = true })
|
||||||
|
|
||||||
|
-- Hold idle off while any window is fullscreen. Players do not all ask for
|
||||||
|
-- that themselves (Brave goes through D-Bus, which nothing here listens to),
|
||||||
|
-- and the shell's dim and lock (quickshell/services/Idle.qml) respect inhibitors.
|
||||||
|
hl.window_rule({ match = { class = ".*" }, idle_inhibit = "fullscreen" })
|
||||||
|
|
||||||
-- ==========================================
|
-- ==========================================
|
||||||
-- FLOATING & CENTERED DIALOGS
|
-- FLOATING & CENTERED DIALOGS
|
||||||
-- ==========================================
|
-- ==========================================
|
||||||
|
|||||||
@@ -119,6 +119,20 @@ not disturb and stay awake, plus night light when `hyprsunset` is installed
|
|||||||
(it runs at 4200 K for as long as the switch is on). On is the light slab
|
(it runs at 4200 K for as long as the switch is on). On is the light slab
|
||||||
with ink type, the same as a selection everywhere else; off is a hairline.
|
with ink type, the same as a selection everywhere else; off is a hairline.
|
||||||
|
|
||||||
|
## Game mode
|
||||||
|
|
||||||
|
While a fullscreen game is on screen, Hyprland switches off its animations and
|
||||||
|
shadows and turns on direct scanout (`~/.config/hypr/lua/gamemode.lua`). It
|
||||||
|
also tells the shell through `qs ipc call shell gameMode true`. The game covers
|
||||||
|
the hull, but covered surfaces still render, so the shell stops what it does by
|
||||||
|
itself: the fluxo stream, the backlight poll, the media tick and equaliser, and
|
||||||
|
the rail and workspace pulses. Toasts wait in the log, except critical ones.
|
||||||
|
Both sides come back when the game leaves fullscreen, closes or is switched
|
||||||
|
away from.
|
||||||
|
|
||||||
|
A game is a window with the `game` content type, or one whose class matches
|
||||||
|
`GAME_CLASSES` in `gamemode.lua` (`steam_app_<id>` and `gamescope` so far).
|
||||||
|
|
||||||
## Modules
|
## Modules
|
||||||
|
|
||||||
| Module | Where | Left click | Right click | Middle | Scroll | Hover |
|
| Module | Where | Left click | Right click | Middle | Scroll | Hover |
|
||||||
|
|||||||
@@ -170,7 +170,7 @@ BarPill {
|
|||||||
|
|
||||||
Timer {
|
Timer {
|
||||||
interval: eq.tickMs
|
interval: eq.tickMs
|
||||||
running: Media.playing && eq.visible && (eq.Window.window?.visible ?? true)
|
running: Media.playing && eq.visible && (eq.Window.window?.visible ?? true) && !GameMode.active
|
||||||
repeat: true
|
repeat: true
|
||||||
onTriggered: eq.phase++
|
onTriggered: eq.phase++
|
||||||
// Restart the cycle from its trough, so playback always begins
|
// Restart the cycle from its trough, so playback always begins
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ PanelWindow {
|
|||||||
statusText: Math.round(Battery.percent) + "% · " + Battery.timeLabel
|
statusText: Math.round(Battery.percent) + "% · " + Battery.timeLabel
|
||||||
suppressTooltip: battPopout.detailActive
|
suppressTooltip: battPopout.detailActive
|
||||||
alarm: Battery.critical
|
alarm: Battery.critical
|
||||||
pulsing: Battery.critical
|
pulsing: Battery.critical && !GameMode.active
|
||||||
onClicked: battPopout.togglePinned()
|
onClicked: battPopout.togglePinned()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import Quickshell
|
|||||||
import Quickshell.Hyprland
|
import Quickshell.Hyprland
|
||||||
import "root:/config"
|
import "root:/config"
|
||||||
import "root:/components"
|
import "root:/components"
|
||||||
|
import "root:/services"
|
||||||
|
|
||||||
// Workspace indicator: soft pills that stretch open when focused. Occupied
|
// Workspace indicator: soft pills that stretch open when focused. Occupied
|
||||||
// workspaces fill in, urgent ones snap to the alarm colour.
|
// workspaces fill in, urgent ones snap to the alarm colour.
|
||||||
@@ -104,7 +105,7 @@ Item {
|
|||||||
|
|
||||||
// Urgent is the one pip that has to be seen peripherally.
|
// Urgent is the one pip that has to be seen peripherally.
|
||||||
SequentialAnimation on opacity {
|
SequentialAnimation on opacity {
|
||||||
running: pip.urgent && !pip.focused
|
running: pip.urgent && !pip.focused && !GameMode.active
|
||||||
loops: Animation.Infinite
|
loops: Animation.Infinite
|
||||||
alwaysRunToEnd: true
|
alwaysRunToEnd: true
|
||||||
NumberAnimation { to: 0.4; duration: 520; easing.type: Easing.InOutQuad }
|
NumberAnimation { to: 0.4; duration: 520; easing.type: Easing.InOutQuad }
|
||||||
|
|||||||
@@ -206,7 +206,10 @@ Item {
|
|||||||
|
|
||||||
Row {
|
Row {
|
||||||
x: Theme.marker + Theme.padS
|
x: Theme.marker + Theme.padS
|
||||||
visible: root.showActions && (root.notif?.actions?.length ?? 0) > 0
|
// Gone once the app has closed the notification: nothing is
|
||||||
|
// listening for the action any more.
|
||||||
|
visible: root.showActions && !Notifs.isRetracted(root.notif)
|
||||||
|
&& (root.notif?.actions?.length ?? 0) > 0
|
||||||
spacing: Theme.padS
|
spacing: Theme.padS
|
||||||
topPadding: 4
|
topPadding: 4
|
||||||
|
|
||||||
@@ -265,10 +268,7 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
TapHandler {
|
TapHandler {
|
||||||
onTapped: {
|
onTapped: Notifs.invoke(root.notif, actionChip.modelData)
|
||||||
actionChip.modelData.invoke();
|
|
||||||
root.dismissed();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,7 +65,18 @@ PanelWindow {
|
|||||||
implicitHeight: root.bodyHeight + 1
|
implicitHeight: root.bodyHeight + 1
|
||||||
color: "transparent"
|
color: "transparent"
|
||||||
|
|
||||||
mask: Region { item: body }
|
// A Region follows its item's own geometry, not its parents', so masking
|
||||||
|
// to the body, which rides on the sliding sheet, would freeze the mask
|
||||||
|
// wherever the sheet was when it was built — above the window, letting
|
||||||
|
// every click through to whatever is below and dropping the focus grab.
|
||||||
|
mask: Region { item: hitbox }
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: hitbox
|
||||||
|
x: root.fillet
|
||||||
|
width: root.bodyWidth
|
||||||
|
height: parent.height
|
||||||
|
}
|
||||||
|
|
||||||
HyprlandFocusGrab {
|
HyprlandFocusGrab {
|
||||||
active: root.open
|
active: root.open
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ WlSessionLock {
|
|||||||
id: lock
|
id: lock
|
||||||
|
|
||||||
locked: Actions.lockRequested
|
locked: Actions.lockRequested
|
||||||
|
onSecureChanged: Actions.lockSecure = lock.secure
|
||||||
|
|
||||||
WlSessionLockSurface {
|
WlSessionLockSurface {
|
||||||
id: lockSurface
|
id: lockSurface
|
||||||
|
|||||||
@@ -27,13 +27,26 @@ PanelWindow {
|
|||||||
id: shade
|
id: shade
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
color: Theme.ink
|
color: Theme.ink
|
||||||
opacity: Idle.dimmed ? 0.6 : 0
|
opacity: 0
|
||||||
|
|
||||||
Behavior on opacity {
|
// States, not a Behavior: a Behavior whose duration is bound to
|
||||||
NumberAnimation {
|
// Idle.dimmed can start before that binding updates, so waking up
|
||||||
duration: Idle.dimmed ? 8000 : Theme.durBase
|
// would replay the slow fade in reverse.
|
||||||
easing.type: Idle.dimmed ? Easing.InOutSine : Easing.OutCubic
|
states: State {
|
||||||
}
|
name: "dimmed"
|
||||||
|
when: Idle.dimmed
|
||||||
|
PropertyChanges { shade.opacity: 0.6 }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
transitions: [
|
||||||
|
Transition {
|
||||||
|
to: "dimmed"
|
||||||
|
NumberAnimation { property: "opacity"; duration: 8000; easing.type: Easing.InOutSine }
|
||||||
|
},
|
||||||
|
Transition {
|
||||||
|
from: "dimmed"
|
||||||
|
NumberAnimation { property: "opacity"; duration: Theme.durBase; easing.type: Easing.OutCubic }
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Shapes
|
import QtQuick.Shapes
|
||||||
import Quickshell
|
import Quickshell
|
||||||
|
import Quickshell.Hyprland
|
||||||
import Quickshell.Wayland
|
import Quickshell.Wayland
|
||||||
import Quickshell.Services.Notifications
|
import Quickshell.Services.Notifications
|
||||||
import "root:/config"
|
import "root:/config"
|
||||||
@@ -15,6 +16,10 @@ import "root:/services"
|
|||||||
// with each notification an entry in it divided by its own header hairline.
|
// with each notification an entry in it divided by its own header hairline.
|
||||||
// The drawer grows and shrinks with the entries and slides back behind the
|
// The drawer grows and shrinks with the entries and slides back behind the
|
||||||
// frame when the last one goes.
|
// frame when the last one goes.
|
||||||
|
//
|
||||||
|
// A fullscreen window covers the bar and the rail, so there is no frame to
|
||||||
|
// lower the drawer out of. Then it floats free in the corner as a plain
|
||||||
|
// rounded panel instead.
|
||||||
PanelWindow {
|
PanelWindow {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
@@ -30,17 +35,20 @@ PanelWindow {
|
|||||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
||||||
exclusionMode: ExclusionMode.Ignore
|
exclusionMode: ExclusionMode.Ignore
|
||||||
|
|
||||||
|
readonly property HyprlandMonitor monitor: Hyprland.monitorFor(root.screen)
|
||||||
|
readonly property bool floating: root.monitor?.activeWorkspace?.hasFullscreen ?? false
|
||||||
|
|
||||||
// One pixel over the bar and the rail, so the drawer covers the rim the
|
// One pixel over the bar and the rail, so the drawer covers the rim the
|
||||||
// hull draws along both edges instead of sitting inside it.
|
// hull draws along both edges instead of sitting inside it.
|
||||||
anchors { top: true; right: true }
|
anchors { top: true; right: true }
|
||||||
margins {
|
margins {
|
||||||
top: Theme.barHeight - 1
|
top: root.floating ? Theme.padM : Theme.barHeight - 1
|
||||||
right: Theme.railWidth - 1
|
right: root.floating ? Theme.padM : Theme.railWidth - 1
|
||||||
}
|
}
|
||||||
|
|
||||||
readonly property real fillet: Theme.frameRadius
|
readonly property real fillet: root.floating ? 0 : Theme.frameRadius
|
||||||
readonly property real corner: Theme.frameRadius
|
readonly property real corner: Theme.frameRadius
|
||||||
readonly property real seam: 1
|
readonly property real seam: root.floating ? 0 : 1
|
||||||
readonly property real drawerWidth: 400
|
readonly property real drawerWidth: 400
|
||||||
|
|
||||||
// Height of the entries, eased, so the drawer grows rather than jumps.
|
// Height of the entries, eased, so the drawer grows rather than jumps.
|
||||||
@@ -75,6 +83,10 @@ PanelWindow {
|
|||||||
if (Actions.focusedScreenName() !== root.modelData.name) return;
|
if (Actions.focusedScreenName() !== root.modelData.name) return;
|
||||||
root.live = [notif, ...root.live].slice(0, root.maxLive);
|
root.live = [notif, ...root.live].slice(0, root.maxLive);
|
||||||
}
|
}
|
||||||
|
// Closed by its app or from the log: take it off screen too.
|
||||||
|
function onNotifRetracted(notif) {
|
||||||
|
root.retire(notif);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function retire(notif) {
|
function retire(notif) {
|
||||||
@@ -99,6 +111,7 @@ PanelWindow {
|
|||||||
// onto the rail through the bottom fillet — Popout's "topRight".
|
// onto the rail through the bottom fillet — Popout's "topRight".
|
||||||
Shape {
|
Shape {
|
||||||
id: silhouette
|
id: silhouette
|
||||||
|
visible: !root.floating
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
preferredRendererType: Shape.CurveRenderer
|
preferredRendererType: Shape.CurveRenderer
|
||||||
|
|
||||||
@@ -125,6 +138,15 @@ PanelWindow {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
visible: root.floating
|
||||||
|
anchors.fill: parent
|
||||||
|
radius: root.corner
|
||||||
|
color: Theme.hull
|
||||||
|
border.width: 1
|
||||||
|
border.color: Theme.rim
|
||||||
|
}
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
id: stack
|
id: stack
|
||||||
x: root.fillet
|
x: root.fillet
|
||||||
@@ -132,8 +154,11 @@ PanelWindow {
|
|||||||
width: root.drawerWidth
|
width: root.drawerWidth
|
||||||
bottomPadding: Theme.padS
|
bottomPadding: Theme.padS
|
||||||
|
|
||||||
|
// A ScriptModel, not the bare list: it hands the Repeater only
|
||||||
|
// what changed, so a new toast does not rebuild the others and
|
||||||
|
// restart their fuses.
|
||||||
Repeater {
|
Repeater {
|
||||||
model: root.live
|
model: ScriptModel { values: [...root.live] }
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: toast
|
id: toast
|
||||||
@@ -146,10 +171,9 @@ PanelWindow {
|
|||||||
id: card
|
id: card
|
||||||
width: parent.width
|
width: parent.width
|
||||||
notif: toast.modelData
|
notif: toast.modelData
|
||||||
onDismissed: {
|
// Closing it retracts it, which retires the toast;
|
||||||
Notifs.dismiss(toast.modelData);
|
// nothing may run here after that, the entry is gone.
|
||||||
root.retire(toast.modelData);
|
onDismissed: Notifs.dismiss(toast.modelData)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Countdown: a hairline draining along the entry's
|
// Countdown: a hairline draining along the entry's
|
||||||
|
|||||||
@@ -5,12 +5,12 @@ import "root:/config"
|
|||||||
import "root:/components"
|
import "root:/components"
|
||||||
import "root:/services"
|
import "root:/services"
|
||||||
|
|
||||||
// Link state, throughput, and the Wi-Fi picker.
|
// Link state, throughput, the wired ports, and the Wi-Fi picker.
|
||||||
Popout {
|
Popout {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
contentWidth: 360
|
contentWidth: 360
|
||||||
contentHeight: 300
|
contentHeight: 300 + (wired.visible ? wired.height + Theme.padM : 0)
|
||||||
|
|
||||||
// Scan only while the popout is actually on screen.
|
// Scan only while the popout is actually on screen.
|
||||||
onShownChanged: Net.setScanning(shown)
|
onShownChanged: Net.setScanning(shown)
|
||||||
@@ -76,13 +76,49 @@ Popout {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SectionRule {
|
// One row per Ethernet port; a click brings the link up or down.
|
||||||
id: apsLabel
|
Column {
|
||||||
|
id: wired
|
||||||
anchors.top: controls.bottom
|
anchors.top: controls.bottom
|
||||||
anchors.topMargin: Theme.padM
|
anchors.topMargin: Theme.padM
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
text: Net.scanning ? "Scanning…" : "Networks"
|
visible: Net.wiredDevices.length > 0
|
||||||
|
spacing: 2
|
||||||
|
|
||||||
|
SectionRule {
|
||||||
|
width: parent.width
|
||||||
|
text: "Wired"
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: parent.width
|
||||||
|
height: Theme.padS - 2
|
||||||
|
}
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: Net.wiredDevices
|
||||||
|
|
||||||
|
delegate: ListRow {
|
||||||
|
required property var modelData
|
||||||
|
|
||||||
|
width: wired.width
|
||||||
|
icon: modelData.hasLink ? "" : ""
|
||||||
|
label: "Ethernet"
|
||||||
|
detail: Net.wiredDetail(modelData)
|
||||||
|
current: modelData.connected
|
||||||
|
onClicked: Net.toggleWired(modelData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SectionRule {
|
||||||
|
id: apsLabel
|
||||||
|
anchors.top: wired.visible ? wired.bottom : controls.bottom
|
||||||
|
anchors.topMargin: Theme.padM
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
text: Net.scanning ? "Scanning…" : "Wi-Fi"
|
||||||
}
|
}
|
||||||
|
|
||||||
ListView {
|
ListView {
|
||||||
|
|||||||
@@ -168,8 +168,41 @@ Singleton {
|
|||||||
function logout() { root.run(["uwsm", "stop"]); }
|
function logout() { root.run(["uwsm", "stop"]); }
|
||||||
function reboot() { root.run(["systemctl", "reboot"]); }
|
function reboot() { root.run(["systemctl", "reboot"]); }
|
||||||
function shutdown() { root.run(["systemctl", "poweroff"]); }
|
function shutdown() { root.run(["systemctl", "poweroff"]); }
|
||||||
function suspend() { root.lock(); root.run(["systemctl", "suspend"]); }
|
function suspend() { root.sleep(["systemctl", "suspend"]); }
|
||||||
function hibernate() { root.lock(); root.run(["systemctl", "hibernate"]); }
|
function hibernate() { root.sleep(["systemctl", "hibernate"]); }
|
||||||
|
|
||||||
|
// Set by lock/Lock.qml once the compositor has the lock surfaces up.
|
||||||
|
property bool lockSecure: false
|
||||||
|
// A sleep waiting for the lock. Suspending first would leave the desktop
|
||||||
|
// on screen for a moment on wake, before the lock had drawn.
|
||||||
|
property var pendingSleep: null
|
||||||
|
|
||||||
|
function sleep(argv: var) {
|
||||||
|
root.lock();
|
||||||
|
if (root.lockSecure) {
|
||||||
|
root.run(argv);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
root.pendingSleep = argv;
|
||||||
|
sleepFallback.restart();
|
||||||
|
}
|
||||||
|
|
||||||
|
function _sleepNow() {
|
||||||
|
if (!root.pendingSleep) return;
|
||||||
|
const argv = root.pendingSleep;
|
||||||
|
root.pendingSleep = null;
|
||||||
|
sleepFallback.stop();
|
||||||
|
root.run(argv);
|
||||||
|
}
|
||||||
|
|
||||||
|
onLockSecureChanged: if (root.lockSecure) root._sleepNow()
|
||||||
|
|
||||||
|
// A lock that never comes up must not keep the machine awake on battery.
|
||||||
|
Timer {
|
||||||
|
id: sleepFallback
|
||||||
|
interval: 3000
|
||||||
|
onTriggered: root._sleepNow()
|
||||||
|
}
|
||||||
function reloadShell() { Quickshell.reload(true); }
|
function reloadShell() { Quickshell.reload(true); }
|
||||||
|
|
||||||
// ------------------------------------------------------------- helpers
|
// ------------------------------------------------------------- helpers
|
||||||
@@ -195,6 +228,7 @@ Singleton {
|
|||||||
function toggleDnd(): void { Notifs.toggleDnd(); }
|
function toggleDnd(): void { Notifs.toggleDnd(); }
|
||||||
function clearNotifications(): void { Notifs.clearAll(); }
|
function clearNotifications(): void { Notifs.clearAll(); }
|
||||||
function toggleStayAwake(): void { Idle.toggleStayAwake(); }
|
function toggleStayAwake(): void { Idle.toggleStayAwake(); }
|
||||||
|
function gameMode(on: bool): void { GameMode.active = on; }
|
||||||
function overview(): void { root.toggleOverview(); }
|
function overview(): void { root.toggleOverview(); }
|
||||||
function overviewNext(): void { root.overviewStep(1); }
|
function overviewNext(): void { root.overviewStep(1); }
|
||||||
function overviewPrev(): void { root.overviewStep(-1); }
|
function overviewPrev(): void { root.overviewStep(-1); }
|
||||||
|
|||||||
@@ -107,8 +107,15 @@ Singleton {
|
|||||||
root.lastCost = 0;
|
root.lastCost = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set while a run is being killed on purpose, so its exit is not reported
|
||||||
|
// as a failure.
|
||||||
|
property bool stopping: false
|
||||||
|
|
||||||
function stop(): void {
|
function stop(): void {
|
||||||
if (proc.running) proc.running = false;
|
if (proc.running) {
|
||||||
|
root.stopping = true;
|
||||||
|
proc.running = false;
|
||||||
|
}
|
||||||
root.busy = false;
|
root.busy = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,7 +132,10 @@ Singleton {
|
|||||||
|
|
||||||
root.exitCode = 0;
|
root.exitCode = 0;
|
||||||
root.stderrText = "";
|
root.stderrText = "";
|
||||||
proc.running = false;
|
if (proc.running) {
|
||||||
|
root.stopping = true;
|
||||||
|
proc.running = false;
|
||||||
|
}
|
||||||
// stdin is closed explicitly. Inherited from the shell it never ends,
|
// stdin is closed explicitly. Inherited from the shell it never ends,
|
||||||
// and the CLI waits three seconds for input that is never coming
|
// and the CLI waits three seconds for input that is never coming
|
||||||
// before it starts on the question.
|
// before it starts on the question.
|
||||||
@@ -236,6 +246,10 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onExited: (code, status) => {
|
onExited: (code, status) => {
|
||||||
|
if (root.stopping) {
|
||||||
|
root.stopping = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
root.busy = false;
|
root.busy = false;
|
||||||
root.exitCode = code;
|
root.exitCode = code;
|
||||||
if (code === 0 || root.error !== "") return;
|
if (code === 0 || root.error !== "") return;
|
||||||
|
|||||||
@@ -49,9 +49,10 @@ Singleton {
|
|||||||
onMicPercentChanged: micBumped()
|
onMicPercentChanged: micBumped()
|
||||||
onMicMutedChanged: micBumped()
|
onMicMutedChanged: micBumped()
|
||||||
|
|
||||||
|
// Capped at 100%: past it PipeWire only amplifies in software, and clips.
|
||||||
function setVolume(v: real) {
|
function setVolume(v: real) {
|
||||||
if (!sink?.audio) return;
|
if (!sink?.audio) return;
|
||||||
sink.audio.volume = Math.max(0, Math.min(1.5, v));
|
sink.audio.volume = Math.max(0, Math.min(1, v));
|
||||||
}
|
}
|
||||||
|
|
||||||
function changeVolume(delta: real) {
|
function changeVolume(delta: real) {
|
||||||
@@ -65,7 +66,7 @@ Singleton {
|
|||||||
|
|
||||||
function setMicVolume(v: real) {
|
function setMicVolume(v: real) {
|
||||||
if (!source?.audio) return;
|
if (!source?.audio) return;
|
||||||
source.audio.volume = Math.max(0, Math.min(1.5, v));
|
source.audio.volume = Math.max(0, Math.min(1, v));
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleMicMute() {
|
function toggleMicMute() {
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ Singleton {
|
|||||||
const v = parseInt(text().trim());
|
const v = parseInt(text().trim());
|
||||||
if (!isNaN(v) && v !== root.raw) {
|
if (!isNaN(v) && v !== root.raw) {
|
||||||
root.raw = v;
|
root.raw = v;
|
||||||
|
// Settled, or moved by something else: step from here again.
|
||||||
|
if (!setter.running && root.pending < 0) root.target = -1;
|
||||||
root.bumped();
|
root.bumped();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -45,24 +47,44 @@ Singleton {
|
|||||||
// sysfs writes do not always fire inotify for backlight, so poll gently too.
|
// sysfs writes do not always fire inotify for backlight, so poll gently too.
|
||||||
Timer {
|
Timer {
|
||||||
interval: 1000
|
interval: 1000
|
||||||
running: true
|
running: !GameMode.active
|
||||||
repeat: true
|
repeat: true
|
||||||
onTriggered: currentFile.reload()
|
onTriggered: currentFile.reload()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The level last asked for, until the backlight reports it. Steps build on
|
||||||
|
// it rather than on `percent`, which only catches up on the next read, so
|
||||||
|
// a fast scroll adds up instead of repeating one step.
|
||||||
|
property int target: -1
|
||||||
|
// A level asked for while a write is still running. Restarting the write
|
||||||
|
// would kill it half done, so the newest request waits and the rest drop.
|
||||||
|
property int pending: -1
|
||||||
|
|
||||||
Process {
|
Process {
|
||||||
id: setter
|
id: setter
|
||||||
|
onExited: {
|
||||||
|
currentFile.reload();
|
||||||
|
if (root.pending < 0) return;
|
||||||
|
const next = root.pending;
|
||||||
|
root.pending = -1;
|
||||||
|
root._write(next);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function _write(p: int) {
|
||||||
|
setter.command = ["brightnessctl", "-d", root.device, "-q", "set", p + "%"];
|
||||||
|
setter.running = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function setPercent(p: real) {
|
function setPercent(p: real) {
|
||||||
const clamped = Math.max(1, Math.min(100, Math.round(p)));
|
const clamped = Math.max(1, Math.min(100, Math.round(p)));
|
||||||
setter.running = false;
|
root.target = clamped;
|
||||||
setter.command = ["brightnessctl", "-d", root.device, "-q", "set", clamped + "%"];
|
if (setter.running) root.pending = clamped;
|
||||||
setter.running = true;
|
else root._write(clamped);
|
||||||
}
|
}
|
||||||
|
|
||||||
function change(delta: real) {
|
function change(delta: real) {
|
||||||
setPercent(percent + delta);
|
setPercent((root.target >= 0 ? root.target : percent) + delta);
|
||||||
}
|
}
|
||||||
|
|
||||||
readonly property string icon: {
|
readonly property string icon: {
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
pragma Singleton
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
|
||||||
|
// A fullscreen game is on screen. Hyprland decides (~/.config/hypr/lua/gamemode.lua)
|
||||||
|
// and says so with `qs ipc call shell gameMode true|false`.
|
||||||
|
//
|
||||||
|
// The game covers the hull, but a covered surface still renders: Hyprland keeps
|
||||||
|
// sending it frame callbacks, so anything that moves keeps redrawing at the
|
||||||
|
// panel's refresh rate with nobody watching. While a game runs the shell stops
|
||||||
|
// what it does on its own:
|
||||||
|
//
|
||||||
|
// Sys the fluxo stream, so no telemetry wakes the shell
|
||||||
|
// Brightness the backlight poll
|
||||||
|
// Media the position tick, and the equaliser on the bar
|
||||||
|
// Rail the critical battery's pulse
|
||||||
|
// Workspaces the urgent pip's pulse
|
||||||
|
// Notifs toasts, except critical ones; everything still reaches the log
|
||||||
|
//
|
||||||
|
// Anything you ask for yourself, the OSD included, still shows.
|
||||||
|
Singleton {
|
||||||
|
property bool active: false
|
||||||
|
}
|
||||||
@@ -74,7 +74,7 @@ Singleton {
|
|||||||
// MPRIS position is pull-only; tick it so the seek bar moves.
|
// MPRIS position is pull-only; tick it so the seek bar moves.
|
||||||
Timer {
|
Timer {
|
||||||
interval: 1000
|
interval: 1000
|
||||||
running: root.playing && root.positionSupported
|
running: root.playing && root.positionSupported && !GameMode.active
|
||||||
repeat: true
|
repeat: true
|
||||||
onTriggered: if (root.active) root.active.positionChanged()
|
onTriggered: if (root.active) root.active.positionChanged()
|
||||||
}
|
}
|
||||||
|
|||||||
+27
-15
@@ -16,10 +16,13 @@ Singleton {
|
|||||||
readonly property var devices: Networking.devices?.values ?? []
|
readonly property var devices: Networking.devices?.values ?? []
|
||||||
|
|
||||||
readonly property var wifiDevices: devices.filter(d => d.type === DeviceType.Wifi)
|
readonly property var wifiDevices: devices.filter(d => d.type === DeviceType.Wifi)
|
||||||
readonly property var wiredDevices: devices.filter(d => d.type === DeviceType.Wired)
|
// Docker's veth pairs show up as wired too; NetworkManager leaves them
|
||||||
|
// unmanaged, and so does the popout.
|
||||||
|
readonly property var wiredDevices: devices.filter(d => d.type === DeviceType.Wired && d.nmManaged)
|
||||||
|
|
||||||
readonly property WifiDevice wifi: wifiDevices.length > 0 ? wifiDevices[0] : null
|
readonly property WifiDevice wifi: wifiDevices.length > 0 ? wifiDevices[0] : null
|
||||||
readonly property WiredDevice wired: wiredDevices.length > 0 ? wiredDevices[0] : null
|
// The port in use, or failing that the first one there is.
|
||||||
|
readonly property WiredDevice wired: wiredDevices.find(d => d.connected) ?? wiredDevices[0] ?? null
|
||||||
|
|
||||||
readonly property bool wiredUp: wired?.connected ?? false
|
readonly property bool wiredUp: wired?.connected ?? false
|
||||||
|
|
||||||
@@ -55,14 +58,8 @@ Singleton {
|
|||||||
if (!root.wifiEnabled) return "";
|
if (!root.wifiEnabled) return "";
|
||||||
if (!root.activeWifi) return "";
|
if (!root.activeWifi) return "";
|
||||||
const s = root.activeWifi.signalStrength ?? 0;
|
const s = root.activeWifi.signalStrength ?? 0;
|
||||||
// Strength is only sampled while a scan is running, which the shell
|
// Connected but not yet sampled should read as full, not dead.
|
||||||
// does on demand. Connected-but-unsampled should read as full, not dead.
|
return s <= 0 ? "" : root.signalIcon(s);
|
||||||
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: {
|
readonly property string label: {
|
||||||
@@ -72,14 +69,29 @@ Singleton {
|
|||||||
return "Offline";
|
return "Offline";
|
||||||
}
|
}
|
||||||
|
|
||||||
function signalIcon(strength: int): string {
|
// Strength is a fraction, 0 to 1, not a percentage.
|
||||||
if (strength >= 80) return "";
|
function signalIcon(strength: real): string {
|
||||||
if (strength >= 60) return "";
|
if (strength >= 0.8) return "";
|
||||||
if (strength >= 40) return "";
|
if (strength >= 0.6) return "";
|
||||||
if (strength >= 20) return "";
|
if (strength >= 0.4) return "";
|
||||||
|
if (strength >= 0.2) return "";
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// "1 Gb/s" when up, otherwise why not. NetworkManager reports Mb/s.
|
||||||
|
function wiredDetail(d): string {
|
||||||
|
if (!d.hasLink) return d.name + " · no cable";
|
||||||
|
if (!d.connected) return d.name + " · disconnected";
|
||||||
|
const mbps = d.linkSpeed ?? 0;
|
||||||
|
if (mbps <= 0) return d.name;
|
||||||
|
return d.name + " · " + (mbps >= 1000 ? (mbps / 1000) + " Gb/s" : mbps + " Mb/s");
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleWired(d) {
|
||||||
|
if (d.connected) d.disconnect();
|
||||||
|
else if (d.network) d.network.connect();
|
||||||
|
}
|
||||||
|
|
||||||
function secured(n): bool {
|
function secured(n): bool {
|
||||||
return n && n.security !== undefined
|
return n && n.security !== undefined
|
||||||
&& n.security !== WifiSecurityType.Open
|
&& n.security !== WifiSecurityType.Open
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ Singleton {
|
|||||||
property list<Notification> history: []
|
property list<Notification> history: []
|
||||||
property int maxHistory: 60
|
property int maxHistory: 60
|
||||||
|
|
||||||
readonly property int count: history.length
|
readonly property int count: history.filter(n => n).length
|
||||||
|
|
||||||
// The history by sender, for the log: one entry per app, newest first
|
// The history by sender, for the log: one entry per app, newest first
|
||||||
// inside each, the apps ordered by their newest notification.
|
// inside each, the apps ordered by their newest notification.
|
||||||
@@ -24,14 +24,21 @@ Singleton {
|
|||||||
const byApp = {};
|
const byApp = {};
|
||||||
const order = [];
|
const order = [];
|
||||||
for (const n of root.history) {
|
for (const n of root.history) {
|
||||||
|
if (!n) continue;
|
||||||
const app = n.appName || "System";
|
const app = n.appName || "System";
|
||||||
if (!(app in byApp)) { byApp[app] = []; order.push(app); }
|
if (!(app in byApp)) { byApp[app] = []; order.push(app); }
|
||||||
byApp[app].push(n);
|
byApp[app].push(n);
|
||||||
}
|
}
|
||||||
return order.map(app => ({ app: app, items: byApp[app] }));
|
return order.map(app => ({ app: app, items: byApp[app] }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The server hands out ids in rising order, so everything in the log above
|
||||||
|
// the newest id seen when the log was last opened is unread. Derived from
|
||||||
|
// the history rather than counted, so dismissing an entry anywhere takes
|
||||||
|
// it off the count too.
|
||||||
|
property int readId: 0
|
||||||
|
readonly property int unread: history.filter(n => n && n.id > root.readId).length
|
||||||
readonly property bool hasUnread: unread > 0
|
readonly property bool hasUnread: unread > 0
|
||||||
property int unread: 0
|
|
||||||
|
|
||||||
NotificationServer {
|
NotificationServer {
|
||||||
id: server
|
id: server
|
||||||
@@ -49,42 +56,103 @@ Singleton {
|
|||||||
onNotification: notif => {
|
onNotification: notif => {
|
||||||
// Hold the notification open until we drop it ourselves.
|
// Hold the notification open until we drop it ourselves.
|
||||||
notif.tracked = true;
|
notif.tracked = true;
|
||||||
|
root.adopt(notif);
|
||||||
|
|
||||||
root.history = [notif, ...root.history].slice(0, root.maxHistory);
|
const next = [notif, ...root.history.filter(n => n)];
|
||||||
root.unread++;
|
for (const n of next.slice(root.maxHistory)) root.release(n);
|
||||||
|
root.history = next.slice(0, root.maxHistory);
|
||||||
|
|
||||||
if (!root.dnd) root.popupRequested(notif);
|
// A game holds every toast but a critical one; the log has them all.
|
||||||
|
const held = GameMode.active && notif.urgency !== NotificationUrgency.Critical;
|
||||||
|
if (!root.dnd && !held) root.popupRequested(notif);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The server outlives a reload, and keeps what it was tracking; this
|
||||||
|
// object does not. Take those back into the log, already read.
|
||||||
|
Component.onCompleted: {
|
||||||
|
const kept = server.trackedNotifications.values.slice().sort((a, b) => b.id - a.id);
|
||||||
|
for (const n of kept) root.adopt(n);
|
||||||
|
root.history = kept.slice(0, root.maxHistory);
|
||||||
|
for (const n of kept.slice(root.maxHistory)) root.release(n);
|
||||||
|
root.markRead();
|
||||||
|
}
|
||||||
|
|
||||||
|
// An app may close its own notification at any time, and the server then
|
||||||
|
// destroys the object, leaving a null in the history. The lock keeps it
|
||||||
|
// alive, frozen as it was, until the log lets it go; closing it only takes
|
||||||
|
// it off screen.
|
||||||
|
function adopt(notif: Notification) {
|
||||||
|
root.holds[notif.id] = hold.createObject(root, { object: notif });
|
||||||
|
notif.closed.connect(() => {
|
||||||
|
root.retracted = Object.assign({}, root.retracted, { [notif.id]: true });
|
||||||
|
root.notifRetracted(notif);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: hold
|
||||||
|
RetainableLock { locked: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
// One lock per notification in the history, by id.
|
||||||
|
property var holds: ({})
|
||||||
|
// Ids of the notifications whose app has closed them. They stay in the log
|
||||||
|
// but can no longer be acted on.
|
||||||
|
property var retracted: ({})
|
||||||
|
|
||||||
signal popupRequested(Notification notif)
|
signal popupRequested(Notification notif)
|
||||||
|
signal notifRetracted(Notification notif)
|
||||||
|
|
||||||
|
function isRetracted(notif: Notification): bool {
|
||||||
|
return !notif || root.retracted[notif.id] === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close a notification if its app has not, and let the object go.
|
||||||
|
function release(notif: Notification) {
|
||||||
|
if (!notif) return;
|
||||||
|
const id = notif.id;
|
||||||
|
if (!root.isRetracted(notif)) notif.dismiss();
|
||||||
|
root.holds[id]?.destroy();
|
||||||
|
delete root.holds[id];
|
||||||
|
if (id in root.retracted) {
|
||||||
|
const next = Object.assign({}, root.retracted);
|
||||||
|
delete next[id];
|
||||||
|
root.retracted = next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function dismiss(notif: Notification) {
|
function dismiss(notif: Notification) {
|
||||||
if (!notif) return;
|
if (!notif) return;
|
||||||
const idx = root.history.indexOf(notif);
|
root.history = root.history.filter(n => n && n !== notif);
|
||||||
if (idx !== -1) {
|
root.release(notif);
|
||||||
const next = root.history.slice();
|
|
||||||
next.splice(idx, 1);
|
|
||||||
root.history = next;
|
|
||||||
}
|
|
||||||
notif.dismiss();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function dismissApp(app: string) {
|
function dismissApp(app: string) {
|
||||||
const gone = root.history.filter(n => (n.appName || "System") === app);
|
const from = n => n.appName || "System";
|
||||||
root.history = root.history.filter(n => (n.appName || "System") !== app);
|
const gone = root.history.filter(n => n && from(n) === app);
|
||||||
for (const n of gone) n.dismiss();
|
root.history = root.history.filter(n => n && from(n) !== app);
|
||||||
|
for (const n of gone) root.release(n);
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearAll() {
|
function clearAll() {
|
||||||
const all = root.history.slice();
|
const all = root.history.filter(n => n);
|
||||||
root.history = [];
|
root.history = [];
|
||||||
root.unread = 0;
|
for (const n of all) root.release(n);
|
||||||
for (const n of all) n.dismiss();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function markRead() {
|
function markRead() {
|
||||||
root.unread = 0;
|
root.readId = Math.max(root.readId, ...root.history.filter(n => n).map(n => n.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run one of a notification's actions and take it out of the log. Done
|
||||||
|
// here rather than in the card: invoking closes a non-resident
|
||||||
|
// notification, which takes its toast, and the card with it, off screen
|
||||||
|
// before the card's handler has finished.
|
||||||
|
function invoke(notif: Notification, action: NotificationAction) {
|
||||||
|
if (!notif || root.isRetracted(notif)) return;
|
||||||
|
action.invoke();
|
||||||
|
root.dismiss(notif);
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleDnd() {
|
function toggleDnd() {
|
||||||
|
|||||||
@@ -167,7 +167,17 @@ Singleton {
|
|||||||
id: retry
|
id: retry
|
||||||
interval: 2000
|
interval: 2000
|
||||||
repeat: false
|
repeat: false
|
||||||
onTriggered: source.running = true
|
onTriggered: source.running = !GameMode.active
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nothing reads the meters while a game covers the bar. Drop the stream
|
||||||
|
// and pick it up again afterwards; the first lines back refresh every value.
|
||||||
|
Connections {
|
||||||
|
target: GameMode
|
||||||
|
|
||||||
|
function onActiveChanged() {
|
||||||
|
source.running = !GameMode.active;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------ formatting
|
// ------------------------------------------------------------ formatting
|
||||||
|
|||||||
@@ -19,8 +19,10 @@ Singleton {
|
|||||||
// The wallpaper to use when no particular monitor is being asked about.
|
// The wallpaper to use when no particular monitor is being asked about.
|
||||||
property string path: ""
|
property string path: ""
|
||||||
|
|
||||||
|
// Encoded segment by segment: a `#` or `?` in a file name would otherwise
|
||||||
|
// be read as the start of a fragment or a query.
|
||||||
readonly property url source: root.path !== ""
|
readonly property url source: root.path !== ""
|
||||||
? Qt.resolvedUrl("file://" + root.path)
|
? "file://" + root.path.split("/").map(encodeURIComponent).join("/")
|
||||||
: Theme.fallbackWallpaper
|
: Theme.fallbackWallpaper
|
||||||
|
|
||||||
signal changed()
|
signal changed()
|
||||||
|
|||||||
@@ -1,3 +1,10 @@
|
|||||||
|
// The Samsung on DP-1 drops off the bus when its display powers down, so DPMS
|
||||||
|
// off (services/Idle.qml) removes an output under every window on it. With
|
||||||
|
// Qt's threaded render loop the render thread can still be swapping a surface
|
||||||
|
// the GUI thread just destroyed, which segfaults in Mesa and takes the lock
|
||||||
|
// screen down with it. The basic loop renders on the GUI thread, so it cannot.
|
||||||
|
//@ pragma Env QSG_RENDER_LOOP=basic
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import Quickshell
|
import Quickshell
|
||||||
import "root:/config"
|
import "root:/config"
|
||||||
|
|||||||
Reference in new issue
Block a user