Features: Added Update functionality
This commit is contained in:
72
dotfiles/.config/quickshell/config/SystemUpdates.qml
Normal file
72
dotfiles/.config/quickshell/config/SystemUpdates.qml
Normal file
@@ -0,0 +1,72 @@
|
||||
pragma Singleton
|
||||
import QtQuick
|
||||
import Quickshell.Io
|
||||
|
||||
// Shared state for SystemUpdate.qml, the bar's package-update indicator.
|
||||
// Bar.qml is instantiated once per screen (see shell.qml's Variants), so
|
||||
// without this being a singleton, plugging in extra monitors would mean
|
||||
// running `checkupdates` that many times in parallel on every poll — which
|
||||
// isn't just wasteful, it's actively unreliable: concurrent invocations
|
||||
// race over checkupdates' own sync directory and can intermittently report
|
||||
// 0 pending updates. One shared Process here means exactly one `checkupdates`
|
||||
// run at a time, and every bar's icon reflects the same result.
|
||||
QtObject {
|
||||
id: root
|
||||
|
||||
// checkupdates syncs a throwaway copy of the pacman db, so it's not
|
||||
// free to run often — matches the old waybar custom/updater module's
|
||||
// ~21.7 minute poll interval (see .config/waybar/config).
|
||||
readonly property int refreshIntervalMs: 1300 * 1000
|
||||
|
||||
property int updateCount: 0
|
||||
property bool checking: false
|
||||
property bool updating: false
|
||||
|
||||
function refresh() {
|
||||
if (_checkProc.running) return
|
||||
root.checking = true
|
||||
_checkProc.running = true
|
||||
}
|
||||
|
||||
// scriptPath: absolute path to update-system.sh (resolved by the caller
|
||||
// via Quickshell.shellPath, which isn't available as a plain import
|
||||
// here without pulling in the whole Quickshell singleton for one call).
|
||||
function runUpdate(scriptPath) {
|
||||
if (_updateProc.running) return
|
||||
root.updating = true
|
||||
_updateProc.command = [scriptPath]
|
||||
_updateProc.running = true
|
||||
}
|
||||
|
||||
// QtObject has no default property, so these are attached via property
|
||||
// assignment rather than as plain child elements (same as Colors.qml's
|
||||
// FileView).
|
||||
readonly property Process _checkProc: Process {
|
||||
command: ["sh", "-c", "checkupdates 2>/dev/null | wc -l"]
|
||||
stdout: StdioCollector {
|
||||
id: checkCollector
|
||||
onStreamFinished: {
|
||||
root.updateCount = parseInt(checkCollector.text.trim(), 10) || 0
|
||||
root.checking = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Runs update-system.sh, which opens its own terminal and blocks until
|
||||
// that terminal is closed — so this only exits once the user's actually
|
||||
// done updating, which is exactly when a re-check matters.
|
||||
readonly property Process _updateProc: Process {
|
||||
onExited: {
|
||||
root.updating = false
|
||||
root.refresh()
|
||||
}
|
||||
}
|
||||
|
||||
readonly property Timer _timer: Timer {
|
||||
interval: root.refreshIntervalMs
|
||||
running: true
|
||||
repeat: true
|
||||
triggeredOnStart: true
|
||||
onTriggered: root.refresh()
|
||||
}
|
||||
}
|
||||
@@ -60,9 +60,13 @@ RowLayout {
|
||||
|
||||
IconImage {
|
||||
anchors.fill: parent
|
||||
// modelData.icon is a QUrl when it comes from
|
||||
// Qt.resolvedUrl() (Bitwarden/Sunshine's local assets), not
|
||||
// a QString — coerce to string before using String methods
|
||||
// on it, or this throws instead of ever matching.
|
||||
source: slot.running
|
||||
? slot.trayItem.icon
|
||||
: (slot.modelData.icon.includes("://")
|
||||
: (String(slot.modelData.icon).includes("://")
|
||||
? slot.modelData.icon
|
||||
: Quickshell.iconPath(slot.modelData.icon))
|
||||
opacity: slot.running ? 1.0 : 0.5
|
||||
|
||||
226
dotfiles/.config/quickshell/modules/bar/SystemUpdate.qml
Normal file
226
dotfiles/.config/quickshell/modules/bar/SystemUpdate.qml
Normal file
@@ -0,0 +1,226 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Hyprland
|
||||
import "../../config"
|
||||
|
||||
// Package update indicator — replaces the old waybar custom/updater module
|
||||
// (see .config/waybar/config and its scripts/update-distro.sh). The actual
|
||||
// polling/state lives in the SystemUpdates singleton (config/), shared
|
||||
// across every monitor's bar; this is just the icon + panel UI. Clicking
|
||||
// opens a panel showing the pending count with a button that runs the
|
||||
// actual update in a terminal (scripts/update-system.sh, ported from the
|
||||
// same waybar script, distro-detection and all).
|
||||
Item {
|
||||
id: root
|
||||
implicitWidth: row.implicitWidth + 2
|
||||
implicitHeight: Appearance.barHeight
|
||||
|
||||
// The bar's own PanelWindow — same barWindow-anchoring convention as
|
||||
// every other popup-having module here (Volume.qml, PowerButton.qml,
|
||||
// etc.), so the panel opens in a fixed spot rather than following this
|
||||
// icon's exact position in its RowLayout.
|
||||
required property var barWindow
|
||||
|
||||
RowLayout {
|
||||
id: row
|
||||
anchors.centerIn: parent
|
||||
spacing: 4
|
||||
|
||||
Text {
|
||||
id: label
|
||||
// Nerd Font glyph (package-variant) normally, matching the old
|
||||
// waybar custom/updater module's icon — not a Material Symbols
|
||||
// ligature name like the rest of this bar, so it needs the Nerd
|
||||
// Font instead of iconFontFamilyRounded. While a check is running
|
||||
// (the periodic poll or a right-click-triggered refresh) this
|
||||
// swaps to the same spinner glyph/animation as
|
||||
// BluetoothIndicator.qml's scan spinner.
|
||||
font.family: SystemUpdates.checking ? Appearance.iconFontFamilyRounded : Appearance.fontFamily
|
||||
font.variableAxes: Appearance.iconFontAxes
|
||||
font.pixelSize: SystemUpdates.checking ? 15 : 22
|
||||
color: SystemUpdates.updateCount > 0 ? Colors.primary : Colors.foreground
|
||||
text: SystemUpdates.checking ? "progress_activity" : ""
|
||||
|
||||
RotationAnimation on rotation {
|
||||
running: SystemUpdates.checking
|
||||
loops: Animation.Infinite
|
||||
from: 0
|
||||
to: 360
|
||||
duration: 900
|
||||
// Otherwise the icon is left tilted at whatever angle the
|
||||
// spin stopped on once checking finishes and it swaps back
|
||||
// to the static glyph.
|
||||
onRunningChanged: if (!running) label.rotation = 0
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
font.family: Appearance.fontFamily
|
||||
font.pixelSize: Appearance.fontSizeSmall
|
||||
color: label.color
|
||||
text: SystemUpdates.updateCount
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
onClicked: (mouse) => {
|
||||
if (mouse.button === Qt.RightButton) {
|
||||
SystemUpdates.refresh()
|
||||
} else {
|
||||
panel.visible = !panel.visible
|
||||
grab.active = panel.visible
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PopupWindow {
|
||||
id: panel
|
||||
visible: false
|
||||
color: "transparent"
|
||||
// Same contentWidth/contentPadding split as the other panels
|
||||
// (Volume.qml, PowerButton.qml, etc.) rather than a single magic
|
||||
// implicitWidth.
|
||||
readonly property int contentWidth: 200
|
||||
readonly property int contentPadding: 16
|
||||
implicitWidth: contentWidth + contentPadding * 2
|
||||
implicitHeight: mainColumn.implicitHeight + contentPadding * 2
|
||||
|
||||
anchor {
|
||||
window: root.barWindow
|
||||
// Unlike anchor.item, anchor.window doesn't default rect to the
|
||||
// window's own bounds — set it explicitly so edges/gravity below
|
||||
// resolve against the full bar. Bottom alone (no Left/Right)
|
||||
// anchors to the bar's bottom-center, which lines up with this
|
||||
// icon since it sits in the bar's centered RowLayout.
|
||||
rect.x: 0
|
||||
rect.y: 0
|
||||
rect.width: root.barWindow.width
|
||||
rect.height: root.barWindow.height
|
||||
edges: Edges.Bottom
|
||||
gravity: Edges.Bottom
|
||||
margins.bottom: -4
|
||||
}
|
||||
|
||||
HyprlandFocusGrab {
|
||||
id: grab
|
||||
windows: [panel]
|
||||
onCleared: panel.visible = false
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: 0
|
||||
color: Colors.surface
|
||||
border.color: Colors.muted
|
||||
border.width: 3
|
||||
|
||||
Column {
|
||||
id: mainColumn
|
||||
anchors.fill: parent
|
||||
anchors.margins: panel.contentPadding
|
||||
spacing: 10
|
||||
|
||||
RowLayout {
|
||||
width: parent.width
|
||||
spacing: 8
|
||||
|
||||
Text {
|
||||
text: label.text
|
||||
font.family: Appearance.fontFamily
|
||||
font.pixelSize: 23
|
||||
color: Colors.foreground
|
||||
}
|
||||
|
||||
Text {
|
||||
Layout.fillWidth: true
|
||||
text: "System Update"
|
||||
font.family: Appearance.fontFamily
|
||||
font.pixelSize: 18
|
||||
font.bold: true
|
||||
color: Colors.foreground
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
width: parent.width
|
||||
wrapMode: Text.WordWrap
|
||||
font.family: Appearance.fontFamily
|
||||
font.pixelSize: Appearance.fontSize
|
||||
color: Colors.foreground
|
||||
text: {
|
||||
if (SystemUpdates.updating) return "Update running in a terminal…"
|
||||
if (SystemUpdates.checking) return "Checking for updates…"
|
||||
if (SystemUpdates.updateCount === 0) return "System is up to date."
|
||||
return SystemUpdates.updateCount + (SystemUpdates.updateCount === 1 ? " update available." : " updates available.")
|
||||
}
|
||||
}
|
||||
|
||||
// Same selectable-row shape as this project's other panel
|
||||
// actions (e.g. PowerButton.qml's menu entries): radius 0,
|
||||
// fixed-width icon column, fillWidth label, offset hover
|
||||
// halo rather than one drawn on the row's own edge.
|
||||
Rectangle {
|
||||
id: updateBtn
|
||||
width: parent.width
|
||||
radius: 0
|
||||
implicitHeight: btnContent.implicitHeight + 12
|
||||
enabled: !SystemUpdates.updating
|
||||
opacity: SystemUpdates.updating ? 0.5 : 1
|
||||
color: btnMouse.containsMouse ? Colors.primary : "transparent"
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.margins: -3
|
||||
radius: 0
|
||||
color: "transparent"
|
||||
border.color: Colors.foreground
|
||||
border.width: btnMouse.containsMouse ? 1 : 0
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: btnContent
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.margins: 6
|
||||
spacing: 8
|
||||
|
||||
Text {
|
||||
Layout.preferredWidth: 20
|
||||
horizontalAlignment: Text.AlignLeft
|
||||
text: "download"
|
||||
font.family: Appearance.iconFontFamilyRounded
|
||||
font.variableAxes: Appearance.iconFontAxes
|
||||
font.pixelSize: 14
|
||||
color: btnMouse.containsMouse ? Colors.primaryText : Colors.foreground
|
||||
}
|
||||
Text {
|
||||
Layout.fillWidth: true
|
||||
text: "Update Now"
|
||||
font.family: Appearance.fontFamily
|
||||
font.pixelSize: Appearance.fontSize
|
||||
color: btnMouse.containsMouse ? Colors.primaryText : Colors.foreground
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: btnMouse
|
||||
anchors.fill: parent
|
||||
enabled: !SystemUpdates.updating
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
SystemUpdates.runUpdate(Quickshell.shellPath("scripts/update-system.sh"))
|
||||
panel.visible = false
|
||||
grab.active = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
62
dotfiles/.config/quickshell/scripts/update-system.sh
Executable file
62
dotfiles/.config/quickshell/scripts/update-system.sh
Executable file
@@ -0,0 +1,62 @@
|
||||
#!/bin/bash
|
||||
# Launched by SystemUpdate.qml (top bar, next to the clock) — opens a
|
||||
# terminal running whatever update command fits this distro, then notifies
|
||||
# once it's done. Ported from the old waybar custom/updater module
|
||||
# (.config/waybar/scripts/update-distro.sh); kept multi-distro even though
|
||||
# this machine is Arch, since these dotfiles aren't.
|
||||
#
|
||||
# The window it opens is floated + centered by a matching Hyprland rule
|
||||
# (system-update-terminal, in .config/hypr/UserConfigs/WindowRules.conf),
|
||||
# same treatment as Bitwarden's window, rather than tiling into whatever
|
||||
# workspace you happened to be on.
|
||||
|
||||
iDIR="$HOME/.config/swaync/images"
|
||||
|
||||
if ! command -v foot &> /dev/null; then
|
||||
notify-send -i "$iDIR/error.png" "Need foot:" "foot terminal not found. Please install foot terminal."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Printed inside the terminal before the actual update command runs, so it
|
||||
# reads as "updating" from the first frame rather than looking like an
|
||||
# empty/stuck window while pacman/apt/etc. take a moment to start printing.
|
||||
#
|
||||
# foot runs this in a fresh `bash -c`, which doesn't inherit shell functions
|
||||
# from this script's own environment (export -f only reaches bash processes
|
||||
# forked from *this* one, not whatever foot execs) — so instead, the
|
||||
# already-colored text is built once here with real escape bytes ($'...'),
|
||||
# then %q-quoted into a self-contained `echo '...'` that reproduces those
|
||||
# same bytes verbatim when the child bash parses it.
|
||||
msg=" Updating System — please wait "
|
||||
line=$(printf '=%.0s' $(seq 1 ${#msg}))
|
||||
banner_text=$'\033[1;36m'"$line"$'\033[0m\n\033[1;36m'"$msg"$'\033[0m\n\033[1;36m'"$line"$'\033[0m\n\n'
|
||||
BANNER="echo $(printf '%q' "$banner_text")"
|
||||
|
||||
if command -v paru &> /dev/null; then
|
||||
CMD="paru -Syu"
|
||||
LABEL="Arch-based system"
|
||||
elif command -v yay &> /dev/null; then
|
||||
CMD="yay -Syu"
|
||||
LABEL="Arch-based system"
|
||||
elif command -v dnf &> /dev/null; then
|
||||
CMD="sudo dnf update --refresh -y"
|
||||
LABEL="Fedora system"
|
||||
elif command -v apt &> /dev/null; then
|
||||
CMD="sudo apt update && sudo apt upgrade -y"
|
||||
LABEL="Debian/Ubuntu system"
|
||||
elif command -v zypper &> /dev/null; then
|
||||
CMD="sudo zypper dup -y"
|
||||
LABEL="openSUSE system"
|
||||
else
|
||||
notify-send -i "$iDIR/error.png" -u critical "Unsupported system" "This script does not support your distribution."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Keeps the terminal open (whether the update succeeded or failed — read
|
||||
# always runs regardless, since it's ; not &&) until you press Enter,
|
||||
# instead of foot closing the instant the update command exits and taking
|
||||
# any output/errors with it.
|
||||
WAIT_PROMPT="echo; read -p 'Update finished — press Enter to close...'"
|
||||
|
||||
foot -T update bash -c "$BANNER; $CMD; $WAIT_PROMPT"
|
||||
notify-send -i "$iDIR/profile-pic.jpg" -u low "$LABEL" 'has been updated.'
|
||||
Reference in New Issue
Block a user