Compare commits
2 Commits
38df0301f2
...
d395a155af
| Author | SHA1 | Date | |
|---|---|---|---|
| d395a155af | |||
| 243c90a809 |
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
242
dotfiles/.config/quickshell/config/WeatherData.qml
Normal file
242
dotfiles/.config/quickshell/config/WeatherData.qml
Normal file
@@ -0,0 +1,242 @@
|
|||||||
|
pragma Singleton
|
||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
|
|
||||||
|
// Live weather for the bar's Weather.qml icon. A singleton for the same
|
||||||
|
// reason as SystemUpdates.qml: Bar.qml is instantiated once per monitor
|
||||||
|
// (shell.qml's Variants), so without this being shared, every extra screen
|
||||||
|
// would mean another set of network requests running in parallel.
|
||||||
|
//
|
||||||
|
// Location is auto-detected from the public IP (via ipwho.is, no API key)
|
||||||
|
// and cached to disk (~/.cache/quickshell/weather-location.json) so it's
|
||||||
|
// only ever looked up once, not on every quickshell restart — IP-geolocation
|
||||||
|
// free tiers rate-limit fast, and a desktop's location barely ever changes
|
||||||
|
// anyway. Only the weather itself re-polls on a timer. Conditions come from
|
||||||
|
// Open-Meteo (api.open-meteo.com) — also free and keyless, and it does its
|
||||||
|
// own °C/km-h conversion server-side via query params.
|
||||||
|
//
|
||||||
|
// Loosely modeled on omarchy's weather panel
|
||||||
|
// (github.com/omacom/omarchy, quattro branch,
|
||||||
|
// shell/plugins/panels/weather/Model.js) for the WMO weather-code
|
||||||
|
// grouping, but the icons are this bar's own Material Symbols glyphs
|
||||||
|
// rather than pulling in a separate icon font just for this.
|
||||||
|
QtObject {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
// Location barely ever changes for a desktop machine, so it's fetched
|
||||||
|
// once (and cached); the weather itself is worth re-checking
|
||||||
|
// periodically.
|
||||||
|
readonly property int refreshIntervalMs: 15 * 60 * 1000
|
||||||
|
|
||||||
|
property bool locating: true
|
||||||
|
property bool loading: false
|
||||||
|
property bool hasLocation: false
|
||||||
|
property string locationName: ""
|
||||||
|
|
||||||
|
property real latitude: 0
|
||||||
|
property real longitude: 0
|
||||||
|
|
||||||
|
function saveLocation() {
|
||||||
|
_locationCache.setText(JSON.stringify({
|
||||||
|
latitude: root.latitude,
|
||||||
|
longitude: root.longitude,
|
||||||
|
name: root.locationName
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Called from Weather.qml's search box once you pick a result — always
|
||||||
|
// wins over whatever IP-based guess was cached before, and persists so
|
||||||
|
// it sticks across restarts.
|
||||||
|
function selectLocation(name, lat, lon) {
|
||||||
|
root.latitude = lat
|
||||||
|
root.longitude = lon
|
||||||
|
root.locationName = name
|
||||||
|
root.hasLocation = true
|
||||||
|
root.locating = false
|
||||||
|
root.saveLocation()
|
||||||
|
root.refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
property bool searching: false
|
||||||
|
property var searchResults: []
|
||||||
|
|
||||||
|
function search(query) {
|
||||||
|
const q = String(query || "").trim()
|
||||||
|
if (q.length < 2) {
|
||||||
|
root.searchResults = []
|
||||||
|
return
|
||||||
|
}
|
||||||
|
root.searching = true
|
||||||
|
_searchProc.command = ["curl", "-s", "--max-time", "6",
|
||||||
|
"https://geocoding-api.open-meteo.com/v1/search?count=5&language=en&format=json&name=" + encodeURIComponent(q)]
|
||||||
|
_searchProc.running = true
|
||||||
|
}
|
||||||
|
|
||||||
|
property real tempC: 0
|
||||||
|
property real feelsLikeC: 0
|
||||||
|
property int humidity: 0
|
||||||
|
property real windKmh: 0
|
||||||
|
property int weatherCode: 0
|
||||||
|
property bool isDay: true
|
||||||
|
|
||||||
|
// [{date, code, maxC, minC}] — tomorrow onward; today is already
|
||||||
|
// covered by the current-conditions fields above.
|
||||||
|
property var forecast: []
|
||||||
|
|
||||||
|
function locate() {
|
||||||
|
if (_locateProc.running) return
|
||||||
|
root.locating = true
|
||||||
|
_locateProc.running = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function refresh() {
|
||||||
|
if (!root.hasLocation || _weatherProc.running) return
|
||||||
|
root.loading = true
|
||||||
|
_weatherProc.command = ["curl", "-s", "--max-time", "8",
|
||||||
|
"https://api.open-meteo.com/v1/forecast?latitude=" + root.latitude
|
||||||
|
+ "&longitude=" + root.longitude
|
||||||
|
+ "¤t=temperature_2m,apparent_temperature,relative_humidity_2m,weather_code,is_day,wind_speed_10m"
|
||||||
|
+ "&daily=weather_code,temperature_2m_max,temperature_2m_min"
|
||||||
|
+ "&temperature_unit=celsius&wind_speed_unit=kmh&timezone=auto&forecast_days=4"]
|
||||||
|
_weatherProc.running = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Material Symbols Rounded glyph for a WMO weather code (see
|
||||||
|
// https://open-meteo.com/en/docs#weathervariables), grouped the same
|
||||||
|
// way omarchy's Model.js groups them into its own icon set.
|
||||||
|
function iconFor(code, day) {
|
||||||
|
if (code === 0) return day ? "sunny" : "clear_night"
|
||||||
|
if (code === 1 || code === 2) return day ? "partly_cloudy_day" : "partly_cloudy_night"
|
||||||
|
if (code === 3) return "cloud"
|
||||||
|
if (code === 45 || code === 48) return "foggy"
|
||||||
|
if ([51, 53, 55, 56, 57, 61, 63, 65, 66, 67, 80, 81, 82].indexOf(code) !== -1) return "rainy"
|
||||||
|
if ([71, 73, 75, 77, 85, 86].indexOf(code) !== -1) return "weather_snowy"
|
||||||
|
if (code === 95 || code === 96 || code === 99) return "thunderstorm"
|
||||||
|
return "cloud"
|
||||||
|
}
|
||||||
|
|
||||||
|
// QtObject has no default property, so these are attached via property
|
||||||
|
// assignment rather than as plain child elements (same as Colors.qml's
|
||||||
|
// FileView and SystemUpdates.qml's Process/Timer).
|
||||||
|
|
||||||
|
// Read once at startup; onLoaded/onLoadFailed (below) decide whether a
|
||||||
|
// network lookup is still needed.
|
||||||
|
readonly property FileView _locationCache: FileView {
|
||||||
|
path: Quickshell.env("HOME") + "/.cache/quickshell/weather-location.json"
|
||||||
|
onLoaded: {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(text())
|
||||||
|
const lat = parseFloat(data.latitude)
|
||||||
|
const lon = parseFloat(data.longitude)
|
||||||
|
if (!isNaN(lat) && !isNaN(lon)) {
|
||||||
|
root.latitude = lat
|
||||||
|
root.longitude = lon
|
||||||
|
root.locationName = data.name || ""
|
||||||
|
root.hasLocation = true
|
||||||
|
root.locating = false
|
||||||
|
root.refresh()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// fall through to a fresh lookup below
|
||||||
|
}
|
||||||
|
root.locate()
|
||||||
|
}
|
||||||
|
onLoadFailed: root.locate()
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly property Process _locateProc: Process {
|
||||||
|
command: ["curl", "-s", "--max-time", "6", "https://ipwho.is/"]
|
||||||
|
stdout: StdioCollector {
|
||||||
|
id: locateCollector
|
||||||
|
onStreamFinished: {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(locateCollector.text)
|
||||||
|
const lat = parseFloat(data.latitude)
|
||||||
|
const lon = parseFloat(data.longitude)
|
||||||
|
root.hasLocation = data.success !== false && !isNaN(lat) && !isNaN(lon)
|
||||||
|
if (root.hasLocation) {
|
||||||
|
root.latitude = lat
|
||||||
|
root.longitude = lon
|
||||||
|
root.locationName = data.city || data.region || data.country || ""
|
||||||
|
root.saveLocation()
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("Weather: locate parse failed:", e)
|
||||||
|
root.hasLocation = false
|
||||||
|
}
|
||||||
|
root.locating = false
|
||||||
|
if (root.hasLocation) root.refresh()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly property Process _weatherProc: Process {
|
||||||
|
stdout: StdioCollector {
|
||||||
|
id: weatherCollector
|
||||||
|
onStreamFinished: {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(weatherCollector.text)
|
||||||
|
const current = data.current || {}
|
||||||
|
root.tempC = current.temperature_2m
|
||||||
|
root.feelsLikeC = current.apparent_temperature
|
||||||
|
root.humidity = current.relative_humidity_2m
|
||||||
|
root.windKmh = current.wind_speed_10m
|
||||||
|
root.weatherCode = current.weather_code
|
||||||
|
root.isDay = current.is_day === 1
|
||||||
|
|
||||||
|
const daily = data.daily || {}
|
||||||
|
const times = daily.time || []
|
||||||
|
const days = []
|
||||||
|
// Start at 1 — index 0 is today, already shown as the
|
||||||
|
// current conditions above; this is "what's coming".
|
||||||
|
for (let i = 1; i < times.length && days.length < 3; i++) {
|
||||||
|
days.push({
|
||||||
|
date: times[i],
|
||||||
|
code: (daily.weather_code || [])[i],
|
||||||
|
maxC: (daily.temperature_2m_max || [])[i],
|
||||||
|
minC: (daily.temperature_2m_min || [])[i]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
root.forecast = days
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("Weather: failed to parse Open-Meteo response:", e)
|
||||||
|
}
|
||||||
|
root.loading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly property Process _searchProc: Process {
|
||||||
|
stdout: StdioCollector {
|
||||||
|
id: searchCollector
|
||||||
|
onStreamFinished: {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(searchCollector.text)
|
||||||
|
const results = data.results || []
|
||||||
|
root.searchResults = results.filter(function(r) {
|
||||||
|
return r && r.name && r.latitude !== undefined && r.longitude !== undefined
|
||||||
|
}).map(function(r) {
|
||||||
|
return {
|
||||||
|
name: r.name,
|
||||||
|
description: [r.admin1, r.country].filter(function(p) { return !!p }).join(", "),
|
||||||
|
latitude: r.latitude,
|
||||||
|
longitude: r.longitude
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
root.searchResults = []
|
||||||
|
}
|
||||||
|
root.searching = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly property Timer _timer: Timer {
|
||||||
|
interval: root.refreshIntervalMs
|
||||||
|
running: true
|
||||||
|
repeat: true
|
||||||
|
onTriggered: root.refresh()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,3 +2,5 @@ module config
|
|||||||
singleton Colors 1.0 Colors.qml
|
singleton Colors 1.0 Colors.qml
|
||||||
singleton Appearance 1.0 Appearance.qml
|
singleton Appearance 1.0 Appearance.qml
|
||||||
singleton VpnExclusivity 1.0 VpnExclusivity.qml
|
singleton VpnExclusivity 1.0 VpnExclusivity.qml
|
||||||
|
singleton SystemUpdates 1.0 SystemUpdates.qml
|
||||||
|
singleton WeatherData 1.0 WeatherData.qml
|
||||||
|
|||||||
@@ -35,8 +35,12 @@ PanelWindow {
|
|||||||
ActiveWindow {}
|
ActiveWindow {}
|
||||||
}
|
}
|
||||||
|
|
||||||
Clock {
|
RowLayout {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
|
spacing: Appearance.spacing
|
||||||
|
Weather { barWindow: bar }
|
||||||
|
Clock {}
|
||||||
|
SystemUpdate { barWindow: bar }
|
||||||
}
|
}
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
|
|||||||
@@ -60,9 +60,13 @@ RowLayout {
|
|||||||
|
|
||||||
IconImage {
|
IconImage {
|
||||||
anchors.fill: parent
|
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
|
source: slot.running
|
||||||
? slot.trayItem.icon
|
? slot.trayItem.icon
|
||||||
: (slot.modelData.icon.includes("://")
|
: (String(slot.modelData.icon).includes("://")
|
||||||
? slot.modelData.icon
|
? slot.modelData.icon
|
||||||
: Quickshell.iconPath(slot.modelData.icon))
|
: Quickshell.iconPath(slot.modelData.icon))
|
||||||
opacity: slot.running ? 1.0 : 0.5
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
413
dotfiles/.config/quickshell/modules/bar/Weather.qml
Normal file
413
dotfiles/.config/quickshell/modules/bar/Weather.qml
Normal file
@@ -0,0 +1,413 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Hyprland
|
||||||
|
import "../../config"
|
||||||
|
|
||||||
|
// Weather indicator — modeled on omarchy's weather bar pill
|
||||||
|
// (github.com/omacom/omarchy, quattro branch,
|
||||||
|
// shell/plugins/panels/weather) but rebuilt in this bar's own style: the
|
||||||
|
// actual location/polling logic lives in the WeatherData singleton (config/),
|
||||||
|
// shared across every monitor's bar; this is just the icon + panel UI,
|
||||||
|
// same split as SystemUpdate.qml.
|
||||||
|
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, SystemUpdate.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
|
||||||
|
|
||||||
|
readonly property bool busy: WeatherData.locating || WeatherData.loading
|
||||||
|
|
||||||
|
// Local to this popup instance — unlike WeatherData's own state, there's
|
||||||
|
// no need to share an in-progress search across monitors' bars.
|
||||||
|
property bool searchOpen: false
|
||||||
|
property string searchQuery: ""
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
id: row
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: 4
|
||||||
|
|
||||||
|
Text {
|
||||||
|
id: label
|
||||||
|
font.family: Appearance.iconFontFamilyRounded
|
||||||
|
font.variableAxes: Appearance.iconFontAxes
|
||||||
|
font.pixelSize: 18
|
||||||
|
color: Colors.foreground
|
||||||
|
// Same spinner treatment as SystemUpdate.qml/
|
||||||
|
// BluetoothIndicator.qml's scan spinner while the location or
|
||||||
|
// conditions are still loading.
|
||||||
|
text: root.busy ? "progress_activity" : (WeatherData.hasLocation ? WeatherData.iconFor(WeatherData.weatherCode, WeatherData.isDay) : "cloud_off")
|
||||||
|
|
||||||
|
RotationAnimation on rotation {
|
||||||
|
running: root.busy
|
||||||
|
loops: Animation.Infinite
|
||||||
|
from: 0
|
||||||
|
to: 360
|
||||||
|
duration: 900
|
||||||
|
onRunningChanged: if (!running) label.rotation = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
visible: WeatherData.hasLocation && !WeatherData.locating
|
||||||
|
font.family: Appearance.fontFamily
|
||||||
|
font.pixelSize: Appearance.fontSizeSmall
|
||||||
|
color: Colors.foreground
|
||||||
|
text: Math.round(WeatherData.tempC) + "°"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||||
|
onClicked: (mouse) => {
|
||||||
|
if (mouse.button === Qt.RightButton) {
|
||||||
|
WeatherData.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, SystemUpdate.qml, etc.) rather than a single magic
|
||||||
|
// implicitWidth.
|
||||||
|
readonly property int contentWidth: 220
|
||||||
|
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.iconFontFamilyRounded
|
||||||
|
font.variableAxes: Appearance.iconFontAxes
|
||||||
|
font.pixelSize: 28
|
||||||
|
color: Colors.foreground
|
||||||
|
}
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: 0
|
||||||
|
|
||||||
|
Text {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
elide: Text.ElideRight
|
||||||
|
text: {
|
||||||
|
if (WeatherData.locating) return "Locating…"
|
||||||
|
if (!WeatherData.hasLocation) return "Location unavailable"
|
||||||
|
return WeatherData.locationName || "Weather"
|
||||||
|
}
|
||||||
|
font.family: Appearance.fontFamily
|
||||||
|
font.pixelSize: 18
|
||||||
|
font.bold: true
|
||||||
|
color: Colors.foreground
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
visible: WeatherData.hasLocation && !WeatherData.locating
|
||||||
|
text: WeatherData.loading ? "Updating…" : "Feels like " + Math.round(WeatherData.feelsLikeC) + "°"
|
||||||
|
font.family: Appearance.fontFamily
|
||||||
|
font.pixelSize: Appearance.fontSizeSmall
|
||||||
|
color: Colors.muted
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
visible: WeatherData.hasLocation && !WeatherData.locating
|
||||||
|
text: Math.round(WeatherData.tempC) + "°"
|
||||||
|
font.family: Appearance.fontFamily
|
||||||
|
font.pixelSize: 28
|
||||||
|
font.bold: true
|
||||||
|
color: Colors.foreground
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggles the search box below — same tap target size
|
||||||
|
// and hover feedback as the picker-target rows in
|
||||||
|
// VpnIndicator.qml.
|
||||||
|
Text {
|
||||||
|
text: root.searchOpen ? "close" : "edit_location"
|
||||||
|
font.family: Appearance.iconFontFamilyRounded
|
||||||
|
font.variableAxes: Appearance.iconFontAxes
|
||||||
|
font.pixelSize: 16
|
||||||
|
color: editLocationMouse.containsMouse ? Colors.primary : Colors.muted
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: editLocationMouse
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: -4
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
root.searchOpen = !root.searchOpen
|
||||||
|
root.searchQuery = ""
|
||||||
|
WeatherData.searchResults = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// City search — always available regardless of how the
|
||||||
|
// current location was resolved (IP guess or a previous
|
||||||
|
// search), so a wrong auto-detected city is one click away
|
||||||
|
// from being fixed. Same search-box + result-list shape as
|
||||||
|
// VpnIndicator.qml's Mullvad location picker.
|
||||||
|
Column {
|
||||||
|
visible: root.searchOpen
|
||||||
|
width: parent.width
|
||||||
|
spacing: 6
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width
|
||||||
|
implicitHeight: 24
|
||||||
|
radius: 0
|
||||||
|
color: Colors.surface
|
||||||
|
border.color: Colors.muted
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 6
|
||||||
|
visible: citySearchInput.text.length === 0
|
||||||
|
text: "Search city…"
|
||||||
|
font.family: Appearance.fontFamily
|
||||||
|
font.pixelSize: Appearance.fontSizeSmall
|
||||||
|
color: Colors.muted
|
||||||
|
}
|
||||||
|
|
||||||
|
TextInput {
|
||||||
|
id: citySearchInput
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 6
|
||||||
|
color: Colors.foreground
|
||||||
|
font.family: Appearance.fontFamily
|
||||||
|
font.pixelSize: Appearance.fontSizeSmall
|
||||||
|
clip: true
|
||||||
|
focus: root.searchOpen
|
||||||
|
text: root.searchQuery
|
||||||
|
onTextChanged: {
|
||||||
|
root.searchQuery = text
|
||||||
|
searchDebounce.restart()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: searchDebounce
|
||||||
|
interval: 400
|
||||||
|
onTriggered: WeatherData.search(root.searchQuery)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
visible: WeatherData.searching
|
||||||
|
text: "Searching…"
|
||||||
|
font.family: Appearance.fontFamily
|
||||||
|
font.pixelSize: Appearance.fontSizeSmall
|
||||||
|
color: Colors.muted
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
visible: !WeatherData.searching && citySearchInput.text.length >= 2 && WeatherData.searchResults.length === 0
|
||||||
|
text: "No matches"
|
||||||
|
font.family: Appearance.fontFamily
|
||||||
|
font.pixelSize: Appearance.fontSizeSmall
|
||||||
|
color: Colors.muted
|
||||||
|
}
|
||||||
|
|
||||||
|
Flickable {
|
||||||
|
width: parent.width
|
||||||
|
height: Math.min(resultsColumn.implicitHeight, 150)
|
||||||
|
contentWidth: width
|
||||||
|
contentHeight: resultsColumn.implicitHeight
|
||||||
|
clip: true
|
||||||
|
boundsBehavior: Flickable.StopAtBounds
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: resultsColumn
|
||||||
|
width: parent.width
|
||||||
|
spacing: 1
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: WeatherData.searchResults
|
||||||
|
|
||||||
|
delegate: Rectangle {
|
||||||
|
required property var modelData
|
||||||
|
width: parent.width
|
||||||
|
implicitHeight: resultText.implicitHeight + 8
|
||||||
|
radius: 0
|
||||||
|
color: resultMouse.containsMouse ? Colors.primary : "transparent"
|
||||||
|
|
||||||
|
Text {
|
||||||
|
id: resultText
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
anchors.margins: 4
|
||||||
|
elide: Text.ElideRight
|
||||||
|
text: modelData.description ? (modelData.name + " — " + modelData.description) : modelData.name
|
||||||
|
font.family: Appearance.fontFamily
|
||||||
|
font.pixelSize: Appearance.fontSizeSmall
|
||||||
|
color: resultMouse.containsMouse ? Colors.primaryText : Colors.foreground
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: resultMouse
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
WeatherData.selectLocation(modelData.name, modelData.latitude, modelData.longitude)
|
||||||
|
root.searchOpen = false
|
||||||
|
root.searchQuery = ""
|
||||||
|
WeatherData.searchResults = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Humidity / wind — same two-stat layout, just laid out as
|
||||||
|
// a row of icon+value pairs rather than full sentences.
|
||||||
|
RowLayout {
|
||||||
|
visible: WeatherData.hasLocation && !WeatherData.locating
|
||||||
|
width: parent.width
|
||||||
|
spacing: 16
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
spacing: 4
|
||||||
|
Text {
|
||||||
|
text: "water_drop"
|
||||||
|
font.family: Appearance.iconFontFamilyRounded
|
||||||
|
font.variableAxes: Appearance.iconFontAxes
|
||||||
|
font.pixelSize: 14
|
||||||
|
color: Colors.muted
|
||||||
|
}
|
||||||
|
Text {
|
||||||
|
text: WeatherData.humidity + "%"
|
||||||
|
font.family: Appearance.fontFamily
|
||||||
|
font.pixelSize: Appearance.fontSizeSmall
|
||||||
|
color: Colors.foreground
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
spacing: 4
|
||||||
|
Text {
|
||||||
|
text: "air"
|
||||||
|
font.family: Appearance.iconFontFamilyRounded
|
||||||
|
font.variableAxes: Appearance.iconFontAxes
|
||||||
|
font.pixelSize: 14
|
||||||
|
color: Colors.muted
|
||||||
|
}
|
||||||
|
Text {
|
||||||
|
text: Math.round(WeatherData.windKmh) + " km/h"
|
||||||
|
font.family: Appearance.fontFamily
|
||||||
|
font.pixelSize: Appearance.fontSizeSmall
|
||||||
|
color: Colors.foreground
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
visible: WeatherData.forecast.length > 0
|
||||||
|
width: parent.width
|
||||||
|
height: 1
|
||||||
|
color: Colors.muted
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
visible: WeatherData.forecast.length > 0
|
||||||
|
width: parent.width
|
||||||
|
spacing: 4
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: WeatherData.forecast
|
||||||
|
|
||||||
|
delegate: ColumnLayout {
|
||||||
|
required property var modelData
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: 2
|
||||||
|
|
||||||
|
Text {
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
text: Qt.formatDateTime(new Date(modelData.date + "T12:00:00"), "ddd")
|
||||||
|
font.family: Appearance.fontFamily
|
||||||
|
font.pixelSize: Appearance.fontSizeSmall
|
||||||
|
color: Colors.muted
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
text: WeatherData.iconFor(modelData.code, true)
|
||||||
|
font.family: Appearance.iconFontFamilyRounded
|
||||||
|
font.variableAxes: Appearance.iconFontAxes
|
||||||
|
font.pixelSize: 18
|
||||||
|
color: Colors.foreground
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
text: Math.round(modelData.maxC) + "°/" + Math.round(modelData.minC) + "°"
|
||||||
|
font.family: Appearance.fontFamily
|
||||||
|
font.pixelSize: Appearance.fontSizeSmall
|
||||||
|
color: Colors.foreground
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
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