Merge branch 'quattro' into add-default-agent
# Conflicts: # migrations/1785633225.sh
This commit is contained in:
@@ -31,7 +31,9 @@ option_bool() {
|
||||
local signature="$2"
|
||||
local option="$3"
|
||||
|
||||
hyprctl_instance "$runtime_dir" "$signature" -j getoption "$option" 2>/dev/null | jq -r '.bool'
|
||||
# A dead instance makes hyprctl print "Couldn't connect ..." on stdout, so
|
||||
# silence jq too and let the failed pipeline skip the instance.
|
||||
hyprctl_instance "$runtime_dir" "$signature" -j getoption "$option" 2>/dev/null | jq -r '.bool' 2>/dev/null
|
||||
}
|
||||
|
||||
instances() {
|
||||
|
||||
+16
-2
@@ -2,11 +2,25 @@
|
||||
|
||||
# omarchy:summary=Generate a Wi-Fi QR matrix for the shell
|
||||
# omarchy:group=network
|
||||
# omarchy:args=<interface>
|
||||
# omarchy:args=[interface]
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
interface=${1:?Usage: omarchy-network-qr <interface>}
|
||||
interface=${1:-}
|
||||
if [[ -z $interface ]]; then
|
||||
# Prefer the default-route device: it is the connection the panel and the
|
||||
# menu's visibility gate describe. Fall back to the first connected Wi-Fi
|
||||
# device. nmcli localizes state names, so pin the locale, and the prefix
|
||||
# match accepts states like "connected (externally)".
|
||||
route_device=$(ip route get 1.1.1.1 2>/dev/null | awk '{ for (i = 1; i <= NF; i++) if ($i == "dev") { print $(i + 1); exit } }')
|
||||
if [[ -n $route_device && -d /sys/class/net/$route_device/wireless ]]; then
|
||||
interface=$route_device
|
||||
else
|
||||
interface=$(LC_ALL=C nmcli -t -f DEVICE,TYPE,STATE device status 2>/dev/null |
|
||||
awk -F: '$2 == "wifi" && $3 ~ /^connected/ { print $1; exit }')
|
||||
fi
|
||||
fi
|
||||
[[ -n $interface ]] || { echo "No active Wi-Fi connection" >&2; exit 1; }
|
||||
uuid=$(nmcli --get-values GENERAL.CON-UUID device show "$interface" | head -n 1)
|
||||
[[ -n $uuid && $uuid != "--" ]] || { echo "No active Wi-Fi connection" >&2; exit 1; }
|
||||
|
||||
|
||||
@@ -42,3 +42,9 @@ for file in "$HOME"/.local/state/omarchy/restart-*-required; do
|
||||
omarchy-restart-"$service"
|
||||
fi
|
||||
done
|
||||
|
||||
# Updates routinely replace the shell's QML, and a stale process can lazy-load
|
||||
# new files into old code. A restart failure (locked session, ssh, TTY) only
|
||||
# prints its reason: the next update or login gets a fresh shell anyway.
|
||||
echo "Restarting shell"
|
||||
omarchy-restart-shell || true
|
||||
|
||||
@@ -8,6 +8,7 @@ workers=0
|
||||
|
||||
[scrollback]
|
||||
lines=10000
|
||||
multiplier=7.0
|
||||
|
||||
[cursor]
|
||||
style=block
|
||||
|
||||
@@ -19,7 +19,7 @@ o.window({
|
||||
}, { tag = "+floating-window" })
|
||||
o.window("dev.tensaku.Tensaku", { float = true })
|
||||
o.window("dev.tensaku.Tensaku", { center = true })
|
||||
o.window("org.gnome.Calculator", { float = true })
|
||||
o.window("omacalc", { float = true })
|
||||
|
||||
-- Fullscreen screensaver.
|
||||
o.window("org.omarchy.screensaver", { fullscreen = true })
|
||||
|
||||
@@ -8,7 +8,8 @@ o.bind("SUPER + ESCAPE", "System menu", "omarchy-menu toggle system")
|
||||
o.bind("XF86PowerOff", "Power menu", "omarchy-menu toggle system", { locked = true })
|
||||
o.bind("SUPER + K", "Show key bindings", "omarchy-menu-keybindings")
|
||||
o.bind("SUPER + ALT + K", "Show Tmux key bindings", "omarchy-menu-tmux-keybindings")
|
||||
o.bind("XF86Calculator", "Calculator", "gnome-calculator")
|
||||
o.bind("XF86Calculator", "Calculator", "omacalc")
|
||||
o.bind("SUPER + SHIFT + EQUAL", "Calculator", "omacalc")
|
||||
|
||||
o.bind_toggle("SUPER + SHIFT + SPACE", "Toggle top bar", "bar")
|
||||
o.bind("SUPER + CTRL + SPACE", "Background switcher", "omarchy-menu toggle background")
|
||||
|
||||
@@ -122,6 +122,14 @@
|
||||
"setup.keybindings": {"icon":"","label":"Keybindings","when":"[[ -f ~/.config/hypr/bindings.lua ]]","action":"omarchy-launch-config-editor \"$HOME/.config/hypr/bindings.lua\""},
|
||||
"setup.input": {"icon":"","label":"Input","when":"[[ -f ~/.config/hypr/input.lua ]]","action":"omarchy-launch-config-editor \"$HOME/.config/hypr/input.lua\""},
|
||||
"setup.direct-boot": {"icon":"","label":"Direct Boot","action":"omarchy-launch-floating-terminal-with-presentation omarchy-setup-direct-boot"},
|
||||
"setup.network": {"icon":"","label":"Network","aliases":["network"]},
|
||||
"setup.network.dns": {"icon":"","label":"DNS","aliases":["dns"]},
|
||||
"setup.network.dns.dhcp": {"icon":"","label":"DHCP","checked":"[[ \"$(omarchy-dns)\" == \"DHCP\" ]]","action":"omarchy-dns DHCP"},
|
||||
"setup.network.dns.cloudflare": {"icon":"","label":"Cloudflare","checked":"[[ \"$(omarchy-dns)\" == \"Cloudflare\" ]]","action":"omarchy-dns Cloudflare"},
|
||||
"setup.network.dns.google": {"icon":"","label":"Google","checked":"[[ \"$(omarchy-dns)\" == \"Google\" ]]","action":"omarchy-dns Google"},
|
||||
"setup.network.dns.custom": {"icon":"","label":"Custom","checked":"[[ \"$(omarchy-dns)\" == \"Custom\" ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-dns Custom'"},
|
||||
"setup.network.qr": {"icon":"","label":"QR Code","aliases":["wifi-qr"],"when":"[[ $(omarchy-network-status) == wifi* ]]","action":"omarchy-shell omarchy.network showQr"},
|
||||
"setup.network.speedtest": {"icon":"","label":"Speed Test","aliases":["speedtest","speed-test"],"action":"omarchy-shell omarchy.network speedTest"},
|
||||
"setup.default": {"icon":"","label":"Defaults","aliases":["default","defaults"]},
|
||||
"setup.default.agent": {"icon":"","label":"Agent"},
|
||||
"setup.default.agent.claude": {"icon":"","label":"Claude","checked":"[[ \"$(omarchy-default-agent)\" == \"claude\" ]]","action":"omarchy-default-agent claude"},
|
||||
|
||||
@@ -127,6 +127,9 @@ New migration format:
|
||||
- Use helper commands such as `omarchy-cmd-present`, `omarchy-cmd-missing`,
|
||||
`omarchy-pkg-add`, `omarchy-pkg-drop`, `omarchy-pkg-present`, and
|
||||
`omarchy-pkg-missing` when appropriate.
|
||||
- Never restart the Omarchy shell. `omarchy update` restarts it unconditionally
|
||||
after migrations run, and the login-time shell already runs current code and
|
||||
hot-reloads `shell.json` edits.
|
||||
|
||||
Example:
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ The design goal is:
|
||||
| `~/.local/state/omarchy/current/` | user | Generated active theme, selected theme name, and current background symlink. |
|
||||
| `~/.local/state/omarchy/migrations/` | user | Per-user migration markers. |
|
||||
| `~/.local/state/omarchy/reboot-required` | user | Optional reboot marker checked by `omarchy-update-restart`. |
|
||||
| `~/.local/state/omarchy/restart-*-required` | user | Optional service/app restart markers checked by `omarchy-update-restart`. |
|
||||
| `~/.local/state/omarchy/restart-*-required` | user | Optional service/app restart markers checked by `omarchy-update-restart`. The shell needs no marker: it is restarted unconditionally after every update. |
|
||||
|
||||
## Migration layout
|
||||
|
||||
@@ -261,7 +261,7 @@ scripts.
|
||||
| `omarchy-update-mise` | Runs `mise up` for mise-managed tools. | **Keep.** Mise-managed tools are intentionally part of the blessed update path. |
|
||||
| `omarchy-update-orphan-pkgs` | Lists orphans and prompts before removal; noninteractive mode never removes. | **Keep for now.** Safe because it is prompt-only. |
|
||||
| `omarchy-update-analyze-logs` | Scans `/tmp/omarchy-update.log` for known failure patterns, currently initramfs generation. | **Keep/expand.** Useful safety net; should grow only for high-signal checks. |
|
||||
| `omarchy-update-restart` | Prompts for reboot after kernel/Hyprland updates and restarts components with `restart-*-required` markers. | **Keep.** Important final step; may eventually include service-restart checks. |
|
||||
| `omarchy-update-restart` | Prompts for reboot after kernel/Hyprland updates, restarts components with `restart-*-required` markers, and always restarts the shell. | **Keep.** Important final step; may eventually include service-restart checks. |
|
||||
| `omarchy-update-firmware` | Manual firmware update command using fwupd. Not part of the normal update pipeline. | **Keep separate.** Firmware is not a routine system update step. |
|
||||
| `omarchy-update-time` | Restarts `systemd-timesyncd`. | **Question.** Not really an update command. Consider renaming/moving under system/time maintenance. |
|
||||
|
||||
|
||||
@@ -42,7 +42,6 @@ fontconfig
|
||||
foot
|
||||
fzf
|
||||
git
|
||||
gnome-calculator
|
||||
gnome-keyring
|
||||
gnome-themes-extra
|
||||
grim
|
||||
@@ -91,6 +90,7 @@ nss-mdns
|
||||
nvim
|
||||
obs-studio
|
||||
obsidian
|
||||
omacalc
|
||||
omacut
|
||||
omawrite
|
||||
omarchy-nvim
|
||||
@@ -109,7 +109,6 @@ qrencode
|
||||
quickshell-git
|
||||
ripgrep
|
||||
ruby
|
||||
rust
|
||||
tensaku
|
||||
sddm
|
||||
slurp
|
||||
|
||||
@@ -5,5 +5,4 @@ if ! omarchy-pkg-present quickshell-git; then
|
||||
# quickshell package in place; packages depending on quickshell stay
|
||||
# satisfied through the provides.
|
||||
sudo pacman -S --noconfirm --ask 4 quickshell-git
|
||||
omarchy-state set restart-shell-required
|
||||
fi
|
||||
|
||||
@@ -37,5 +37,3 @@ if [[ -s $config_file ]]; then
|
||||
.bar.layout.center |= place_indicators_before_clock
|
||||
' "$config_file" >"$tmp" && mv "$tmp" "$config_file" || rm -f "$tmp"
|
||||
fi
|
||||
|
||||
omarchy-restart-shell
|
||||
|
||||
@@ -118,7 +118,3 @@ if [[ -s $config_file ]] && grep -q 'TmuxAlert' "$config_file"; then
|
||||
|
||||
rm -f "$tmp"
|
||||
fi
|
||||
|
||||
# Nothing to restart from a TTY or over ssh, and that is no reason to stop the
|
||||
# rest of the queue: hand it to the post-update restart instead.
|
||||
omarchy-restart-shell >/dev/null 2>&1 || omarchy-state set restart-shell-required
|
||||
|
||||
@@ -45,5 +45,3 @@ if [[ -s $config_file ]] && omarchy-cmd-present jq; then
|
||||
end
|
||||
' "$config_file" >"$tmp" && mv "$tmp" "$config_file" || rm -f "$tmp"
|
||||
fi
|
||||
|
||||
omarchy-restart-shell
|
||||
|
||||
@@ -1,9 +1,20 @@
|
||||
echo "Install default coding agent mise wrappers"
|
||||
echo "Speed up mouse scrolling in foot"
|
||||
|
||||
if [[ ! -f $HOME/.local/state/omarchy/preinstalls-removed ]]; then
|
||||
omarchy-mise-install github:can1357/oh-my-pi omp
|
||||
omarchy-mise-install npm:@xai-official/grok grok
|
||||
omarchy-mise-install crush
|
||||
elif [[ -f $HOME/.local/bin/omp ]] && grep -Fq 'mise use -g "oh-my-pi"' "$HOME/.local/bin/omp"; then
|
||||
rm -f "$HOME/.local/bin/omp"
|
||||
foot_config="$HOME/.config/foot/foot.ini"
|
||||
|
||||
if [[ -f $foot_config ]] && ! grep -q '^multiplier=' "$foot_config"; then
|
||||
if grep -qxF '[scrollback]' "$foot_config"; then
|
||||
tmp=$(mktemp)
|
||||
awk '
|
||||
{ print }
|
||||
!inserted && $0 == "[scrollback]" {
|
||||
print "multiplier=7.0"
|
||||
inserted = 1
|
||||
}
|
||||
' "$foot_config" >"$tmp"
|
||||
cat "$tmp" >"$foot_config"
|
||||
rm -f "$tmp"
|
||||
else
|
||||
printf '\n[scrollback]\nmultiplier=7.0\n' >>"$foot_config"
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
echo "Replace GNOME Calculator with Omacalc"
|
||||
|
||||
omarchy-pkg-add omacalc
|
||||
omarchy-pkg-drop gnome-calculator
|
||||
@@ -0,0 +1,9 @@
|
||||
echo "Install default coding agent mise wrappers"
|
||||
|
||||
if [[ ! -f $HOME/.local/state/omarchy/preinstalls-removed ]]; then
|
||||
omarchy-mise-install github:can1357/oh-my-pi omp
|
||||
omarchy-mise-install npm:@xai-official/grok grok
|
||||
omarchy-mise-install crush
|
||||
elif [[ -f $HOME/.local/bin/omp ]] && grep -Fq 'mise use -g "oh-my-pi"' "$HOME/.local/bin/omp"; then
|
||||
rm -f "$HOME/.local/bin/omp"
|
||||
fi
|
||||
@@ -12,18 +12,29 @@ var WEEKDAY_NAMES = ["sunday", "monday", "tuesday", "wednesday", "thursday", "fr
|
||||
// ---- Bar label formats. Right-clicking the clock walks these in order and
|
||||
// writes the result back to shell.json, so the label the bar shows and
|
||||
// the format the config stores are always the same thing.
|
||||
//
|
||||
// The locale-shaped time presets are each followed by their 12-hour twin, so
|
||||
// the walk from a 24-hour label to the same label in AM/PM is a single right
|
||||
// click rather than a lap of the ring. The ISO preset is deliberately left
|
||||
// without one: ISO 8601 writes time on a 24-hour clock, so an AM/PM variant
|
||||
// would contradict the only thing that format is for.
|
||||
var CLOCK_FORMATS = [
|
||||
"dddd HH:mm",
|
||||
"dddd h:mm AP",
|
||||
"HH:mm",
|
||||
"h:mm AP",
|
||||
"ddd d MMM HH:mm",
|
||||
"ddd d MMM h:mm AP",
|
||||
"d MMMM 'W'ww yyyy",
|
||||
"yyyy-MM-dd HH:mm"
|
||||
]
|
||||
|
||||
// Vertical bars have room for a few stacked lines and nothing else, so the
|
||||
// ring stays short.
|
||||
// ring stays short. AM/PM costs a fourth line, which is why only the plain
|
||||
// time carries it here.
|
||||
var VERTICAL_CLOCK_FORMATS = [
|
||||
"HH\n—\nmm",
|
||||
"h\n—\nmm\nAP",
|
||||
"dd\nMMM\n'W'ww\n''yy",
|
||||
"HH\nmm"
|
||||
]
|
||||
|
||||
@@ -250,12 +250,6 @@ function formatRate(bytesPerSec) {
|
||||
return formatBytes(bytesPerSec) + "/s"
|
||||
}
|
||||
|
||||
function formatSpeedMbps(mbps) {
|
||||
var value = parseFloat(mbps)
|
||||
if (!isFinite(value) || value <= 0) return "--"
|
||||
return value.toFixed(value > 0 && value < 10 ? 1 : 0) + " Mbps"
|
||||
}
|
||||
|
||||
// `hasSamples` false means no probe has come back yet, which is different from
|
||||
// a probe that timed out. The rows stay mounted through that gap and read "--"
|
||||
// so the grid doesn't reflow a second after the panel opens.
|
||||
@@ -362,7 +356,6 @@ if (typeof module !== "undefined") {
|
||||
formatPacketLoss: formatPacketLoss,
|
||||
formatBytes: formatBytes,
|
||||
formatRate: formatRate,
|
||||
formatSpeedMbps: formatSpeedMbps,
|
||||
formatPingLatency: formatPingLatency,
|
||||
wifiRow: wifiRow,
|
||||
sortWifiRows: sortWifiRows,
|
||||
|
||||
@@ -17,9 +17,28 @@ Panel {
|
||||
manageIpc: false
|
||||
|
||||
// Centralized close so callers can't forget to drop the passphrase prompt.
|
||||
readonly property bool overlayVisible: qrVisible || speedTestModalOpen
|
||||
|
||||
// Shadows the base open(): a summon or toggle while a centered card is up
|
||||
// dismisses the card instead of opening the compact panel behind an
|
||||
// exclusive overlay. The base toggle() dispatches here, so the keybind,
|
||||
// the bar icon, and every IPC route all get this behavior.
|
||||
function open() {
|
||||
if (overlayVisible) {
|
||||
hideWifiQr()
|
||||
hideSpeedTest()
|
||||
return
|
||||
}
|
||||
root.controller.show()
|
||||
}
|
||||
|
||||
function close() {
|
||||
root.controller.hide()
|
||||
cancelPasswordPrompt()
|
||||
// The centered cards outlive the compact panel, but the widget's
|
||||
// canonical close must not leave an overlay (or its traffic) behind.
|
||||
hideWifiQr()
|
||||
hideSpeedTest()
|
||||
}
|
||||
|
||||
function cancelPasswordPrompt() {
|
||||
@@ -83,8 +102,9 @@ Panel {
|
||||
property var bandAvailable: []
|
||||
property string pendingBand: ""
|
||||
property bool speedTestRunning: false
|
||||
property bool speedTestHasRun: false
|
||||
property bool speedTestModalOpen: false
|
||||
property bool speedTestExpectedStop: false
|
||||
property bool pendingSpeedRun: false
|
||||
property string speedTestPhase: ""
|
||||
property string speedTestStderr: ""
|
||||
property string speedTestDownloadMbps: ""
|
||||
@@ -111,6 +131,8 @@ Panel {
|
||||
property string qrError: ""
|
||||
property bool qrLoading: false
|
||||
property bool qrExpectedStop: false
|
||||
property bool pendingQrShow: false
|
||||
property bool pendingQrDetect: false
|
||||
property string qrPassword: ""
|
||||
property bool qrPasswordVisible: false
|
||||
property string qrPasswordError: ""
|
||||
@@ -127,9 +149,9 @@ Panel {
|
||||
property bool cursorActive: false
|
||||
|
||||
// Keyboard focus zone for the panel. j/k crosses row boundaries:
|
||||
// header actions ⇄ band ⇄ DNS row ⇄ speed test ⇄ Wi-Fi networks. h/l move
|
||||
// header actions ⇄ band ⇄ DNS row ⇄ Wi-Fi networks. h/l move
|
||||
// within header actions, band pills, or DNS providers.
|
||||
property string focusSection: "dns" // "header" | "band" | "dns" | "speed" | "wifi"
|
||||
property string focusSection: "dns" // "header" | "band" | "dns" | "wifi"
|
||||
property int headerIndex: 0
|
||||
readonly property bool canDisconnect: !!connectedWifiNetwork
|
||||
readonly property bool headerHasDisconnect: false
|
||||
@@ -139,9 +161,11 @@ Panel {
|
||||
// "off" beside a perfectly live Ethernet connection.
|
||||
readonly property bool canToggleWifi: networkManagerAvailable && wifiStationAvailable
|
||||
readonly property int qrHeaderIndex: canShareWifi ? 0 : -1
|
||||
readonly property int toggleHeaderIndex: canToggleWifi ? (canShareWifi ? 1 : 0) : -1
|
||||
readonly property int headerActionCount: (canShareWifi ? 1 : 0) + (canToggleWifi ? 1 : 0)
|
||||
readonly property int speedHeaderIndex: canRunSpeedTest ? (canShareWifi ? 1 : 0) : -1
|
||||
readonly property int toggleHeaderIndex: canToggleWifi ? (canShareWifi ? 1 : 0) + (canRunSpeedTest ? 1 : 0) : -1
|
||||
readonly property int headerActionCount: (canShareWifi ? 1 : 0) + (canRunSpeedTest ? 1 : 0) + (canToggleWifi ? 1 : 0)
|
||||
readonly property bool qrHeaderHasCursor: cursorActive && focusSection === "header" && headerIndex === qrHeaderIndex
|
||||
readonly property bool speedHeaderHasCursor: cursorActive && focusSection === "header" && headerIndex === speedHeaderIndex
|
||||
readonly property bool toggleHeaderHasCursor: cursorActive && focusSection === "header" && headerIndex === toggleHeaderIndex
|
||||
readonly property string toggleHint: Networking.wifiEnabled ? "Turn Wi-Fi off" : "Turn Wi-Fi on"
|
||||
readonly property var dnsProviders: ["DHCP", "Cloudflare", "Google", "Custom"]
|
||||
@@ -167,8 +191,8 @@ Panel {
|
||||
readonly property bool bandPillsVisible: canSelectBand && bandPinned
|
||||
readonly property string bandSectionTitle: Model.bandSectionTitle(bandEffective, bandCurrent)
|
||||
readonly property bool bandBusy: pendingBand !== ""
|
||||
// The speed test section only exists once there's an interface to test, so
|
||||
// the Run button only joins the cursor chain then.
|
||||
// The speed test needs an interface to test, so its hero action only
|
||||
// appears once there is one.
|
||||
readonly property bool canRunSpeedTest: !!info.iface
|
||||
property int bandIndex: 0
|
||||
// The band section has up to two cursor rows: the Automatic switch on the
|
||||
@@ -191,10 +215,6 @@ Panel {
|
||||
}
|
||||
}
|
||||
|
||||
onCanRunSpeedTestChanged: {
|
||||
if (!canRunSpeedTest && focusSection === "speed") focusSection = "dns"
|
||||
}
|
||||
|
||||
// Collapsing the pills out from under the cursor would leave it pointing at
|
||||
// nothing, so send it up to the switch that is still on screen.
|
||||
onBandPillsVisibleChanged: {
|
||||
@@ -226,10 +246,20 @@ Panel {
|
||||
function hide() { root.close() }
|
||||
function toggle() { root.toggle() }
|
||||
function toggleNetwork() { root.toggleNetwork() }
|
||||
// Menu routes: summon the centered cards directly, panel open or not.
|
||||
function showQr() {
|
||||
root.refresh()
|
||||
root.showWifiQr(true)
|
||||
}
|
||||
function speedTest() {
|
||||
root.refresh()
|
||||
root.showSpeedTest()
|
||||
}
|
||||
}
|
||||
|
||||
function activateHeader() {
|
||||
if (headerIndex === qrHeaderIndex) showWifiQr()
|
||||
else if (headerIndex === speedHeaderIndex) showSpeedTest()
|
||||
else if (headerIndex === toggleHeaderIndex) toggleNetwork()
|
||||
}
|
||||
|
||||
@@ -427,14 +457,27 @@ Panel {
|
||||
|
||||
readonly property string icon: Model.connectionIcon(kind, signalStrength)
|
||||
|
||||
function showWifiQr() {
|
||||
if (qrProc.running || !info.iface || info.type !== "wifi") return
|
||||
function showWifiQr(forceDetect) {
|
||||
if (qrProc.running) {
|
||||
// A dismissal's SIGTERM is still in flight; Process.running stays true
|
||||
// until the child exits, so queue the reopen for onExited.
|
||||
if (qrExpectedStop) {
|
||||
pendingQrShow = true
|
||||
pendingQrDetect = !!forceDetect
|
||||
}
|
||||
return
|
||||
}
|
||||
qrSize = 0
|
||||
qrRows = []
|
||||
qrError = ""
|
||||
qrLoading = true
|
||||
qrExpectedStop = false
|
||||
qrProc.command = ["omarchy-network-qr", info.iface]
|
||||
// The panel's own button shares the interface it is showing. The IPC
|
||||
// route forces self-detection instead: details polling stops while the
|
||||
// panel is closed, so its cached interface can be stale.
|
||||
qrProc.command = !forceDetect && info.type === "wifi" && info.iface
|
||||
? ["omarchy-network-qr", info.iface]
|
||||
: ["omarchy-network-qr"]
|
||||
qrProc.running = true
|
||||
|
||||
// Leave the compact network panel behind while the centered share card is open.
|
||||
@@ -443,6 +486,7 @@ Panel {
|
||||
}
|
||||
|
||||
function hideWifiQr() {
|
||||
pendingQrShow = false
|
||||
if (qrProc.running) {
|
||||
qrExpectedStop = true
|
||||
qrProc.running = false
|
||||
@@ -563,10 +607,6 @@ Panel {
|
||||
return Model.formatRate(bytesPerSec)
|
||||
}
|
||||
|
||||
function formatSpeedMbps(mbps) {
|
||||
return Model.formatSpeedMbps(mbps)
|
||||
}
|
||||
|
||||
function formatPingLatency(ms) {
|
||||
return Model.formatPingLatency(ms, hasInternetPing)
|
||||
}
|
||||
@@ -660,10 +700,42 @@ Panel {
|
||||
speedTestError = ""
|
||||
}
|
||||
|
||||
// The speed test lives in a centered modal card like the QR share.
|
||||
// Opening it starts a fresh run; dismissing it stops the traffic, so the
|
||||
// download workers never keep saturating the link behind a closed card.
|
||||
function showSpeedTest() {
|
||||
if (!speedTestModalOpen) {
|
||||
speedTestModalOpen = true
|
||||
controller.hide()
|
||||
cancelPasswordPrompt()
|
||||
}
|
||||
runSpeedTest()
|
||||
}
|
||||
|
||||
function hideSpeedTest() {
|
||||
speedTestModalOpen = false
|
||||
pendingSpeedRun = false
|
||||
speedTestPhaseTimer.stop()
|
||||
// Clear the phase before killing the process: onExited advances to the
|
||||
// upload phase when it still reads "down".
|
||||
speedTestPhase = ""
|
||||
speedTestRunning = false
|
||||
if (speedTestProc.running) {
|
||||
speedTestExpectedStop = true
|
||||
speedTestProc.running = false
|
||||
}
|
||||
}
|
||||
|
||||
function runSpeedTest() {
|
||||
if (speedTestProc.running) return
|
||||
if (speedTestProc.running) {
|
||||
// A dismissal's SIGTERM is still in flight; Process.running stays true
|
||||
// until the child exits, so queue the fresh run for onExited.
|
||||
if (speedTestExpectedStop) pendingSpeedRun = true
|
||||
return
|
||||
}
|
||||
speedTestError = ""
|
||||
speedTestHasRun = true
|
||||
speedTestDownloadMbps = ""
|
||||
speedTestUploadMbps = ""
|
||||
speedTestRunning = true
|
||||
startSpeedTestPhase("down")
|
||||
}
|
||||
@@ -883,6 +955,12 @@ Panel {
|
||||
}
|
||||
onExited: function(exitCode) {
|
||||
root.qrLoading = false
|
||||
if (root.pendingQrShow) {
|
||||
root.pendingQrShow = false
|
||||
root.qrExpectedStop = false
|
||||
Qt.callLater(function() { root.showWifiQr(root.pendingQrDetect) })
|
||||
return
|
||||
}
|
||||
if (root.qrExpectedStop) return
|
||||
if (exitCode !== 0 || root.qrSize === 0) {
|
||||
root.qrSize = 0
|
||||
@@ -949,6 +1027,13 @@ Panel {
|
||||
onExited: function(exitCode) {
|
||||
speedTestPhaseTimer.stop()
|
||||
|
||||
if (root.pendingSpeedRun) {
|
||||
root.pendingSpeedRun = false
|
||||
root.speedTestExpectedStop = false
|
||||
if (root.speedTestModalOpen) Qt.callLater(root.runSpeedTest)
|
||||
return
|
||||
}
|
||||
|
||||
if (!root.speedTestExpectedStop && exitCode !== 0) {
|
||||
root.speedTestError = root.speedTestStderr || "Speed test failed"
|
||||
root.speedTestPhase = ""
|
||||
@@ -1134,25 +1219,15 @@ Panel {
|
||||
root.focusSection = "header"
|
||||
root.headerIndex = 0
|
||||
}
|
||||
} else if (root.canRunSpeedTest) {
|
||||
root.focusSection = "speed"
|
||||
} else if (root.wifiNetworks.length > 0) {
|
||||
root.focusSection = "wifi"
|
||||
if (root.selectedIndex < 0) root.selectedIndex = 0
|
||||
}
|
||||
} else if (root.focusSection === "speed") {
|
||||
if (dy < 0) {
|
||||
root.focusSection = "dns"
|
||||
} else if (root.wifiNetworks.length > 0) {
|
||||
root.focusSection = "wifi"
|
||||
if (root.selectedIndex < 0) root.selectedIndex = 0
|
||||
}
|
||||
} else { // wifi
|
||||
// k from the top row escapes back up into the speed test's Run
|
||||
// button, or DNS when there is no speed test, rather than wrapping
|
||||
// around to the bottom of the list.
|
||||
// k from the top row escapes back up to the DNS row rather than
|
||||
// wrapping around to the bottom of the list.
|
||||
if (dy < 0 && root.selectedIndex <= 0) {
|
||||
root.focusSection = root.canRunSpeedTest ? "speed" : "dns"
|
||||
root.focusSection = "dns"
|
||||
root.wifiActionFocused = false
|
||||
}
|
||||
else root.selectByDelta(dy)
|
||||
@@ -1170,7 +1245,6 @@ Panel {
|
||||
if (root.focusSection === "header") root.activateHeader()
|
||||
else if (root.focusSection === "band") root.activateBand()
|
||||
else if (root.focusSection === "dns") root.activateDns()
|
||||
else if (root.focusSection === "speed") root.runSpeedTest()
|
||||
else root.activateSelected()
|
||||
}
|
||||
}
|
||||
@@ -1229,6 +1303,22 @@ Panel {
|
||||
onClicked: root.showWifiQr()
|
||||
}
|
||||
|
||||
Button {
|
||||
id: speedAction
|
||||
visible: root.canRunSpeedTest
|
||||
iconText: ""
|
||||
tooltipText: "Run a speed test"
|
||||
foreground: root.bar.foreground
|
||||
fontFamily: root.bar.fontFamily
|
||||
iconSize: Style.font.subtitle * 1.5
|
||||
horizontalPadding: Style.space(5)
|
||||
verticalPadding: Style.space(2)
|
||||
hasCursor: root.speedHeaderHasCursor
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
onHovered: function(on) { if (on) root.setHeaderCursor(root.speedHeaderIndex) }
|
||||
onClicked: root.showSpeedTest()
|
||||
}
|
||||
|
||||
ToggleSwitch {
|
||||
id: powerSwitch
|
||||
visible: root.canToggleWifi
|
||||
@@ -1543,84 +1633,6 @@ Panel {
|
||||
}
|
||||
|
||||
|
||||
PanelSeparator {
|
||||
visible: !!root.info.iface
|
||||
foreground: root.bar.foreground
|
||||
}
|
||||
|
||||
Column {
|
||||
visible: !!root.info.iface
|
||||
width: parent.width
|
||||
spacing: Style.space(12)
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Style.space(8)
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
implicitHeight: Math.max(speedTestHeader.implicitHeight, speedRunButton.implicitHeight)
|
||||
|
||||
PanelSectionHeader {
|
||||
id: speedTestHeader
|
||||
text: "SPEED TEST"
|
||||
foreground: root.bar.foreground
|
||||
fontFamily: root.bar.fontFamily
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
// Scaled to the band section's AUTOMATIC row -- caption type and
|
||||
// trimmed padding -- so both header lines carry a control of the
|
||||
// same visual weight instead of this one dominating.
|
||||
Button {
|
||||
id: speedRunButton
|
||||
text: root.speedTestRunning ? "Running..." : "Run"
|
||||
tooltipText: "Run using fast.com"
|
||||
enabled: !root.speedTestRunning
|
||||
foreground: root.bar.foreground
|
||||
fontFamily: root.bar.fontFamily
|
||||
fontSize: Style.font.caption
|
||||
horizontalPadding: Style.space(8)
|
||||
verticalPadding: Style.space(2)
|
||||
bordered: true
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
hasCursor: root.cursorActive && root.focusSection === "speed"
|
||||
onClicked: root.runSpeedTest()
|
||||
|
||||
onHovered: function(isHovered) {
|
||||
if (!isHovered) return
|
||||
root.cursorActive = true
|
||||
root.focusSection = "speed"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
id: speedTestValues
|
||||
visible: root.speedTestHasRun
|
||||
width: parent.width
|
||||
spacing: Style.space(20)
|
||||
|
||||
readonly property real cellWidth: Math.max(0, (width - spacing * 3) / 4)
|
||||
|
||||
InfoLabel { width: speedTestValues.cellWidth; text: "Download" }
|
||||
DetailValue { width: speedTestValues.cellWidth; text: root.formatSpeedMbps(root.speedTestDownloadMbps) }
|
||||
InfoLabel { width: speedTestValues.cellWidth; text: "Upload" }
|
||||
DetailValue { width: speedTestValues.cellWidth; text: root.formatSpeedMbps(root.speedTestUploadMbps) }
|
||||
}
|
||||
|
||||
InfoValue {
|
||||
visible: root.speedTestError !== ""
|
||||
text: root.speedTestError
|
||||
color: root.bar.urgent
|
||||
width: parent.width
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Wi-Fi networks (only if a Wi-Fi station is available).
|
||||
PanelSeparator {
|
||||
visible: root.wifiStationAvailable
|
||||
@@ -1708,6 +1720,24 @@ Panel {
|
||||
onPasswordToggleRequested: root.toggleQrPassword()
|
||||
}
|
||||
|
||||
SpeedTestPanel {
|
||||
anchorItem: button
|
||||
bar: root.bar
|
||||
running: root.speedTestRunning
|
||||
phase: root.speedTestPhase
|
||||
downloadMbps: root.speedTestDownloadMbps
|
||||
uploadMbps: root.speedTestUploadMbps
|
||||
error: root.speedTestError
|
||||
connectionName: {
|
||||
if (root.info.type === "wifi") return root.info.ssid || "Wi-Fi"
|
||||
if (root.info.type === "ethernet") return "Ethernet"
|
||||
return ""
|
||||
}
|
||||
open: root.speedTestModalOpen
|
||||
onCloseRequested: root.hideSpeedTest()
|
||||
onRunAgainRequested: root.runSpeedTest()
|
||||
}
|
||||
|
||||
// One Wi-Fi band pill. `active` (fill) is the band actually in use and
|
||||
// `selected` (bold) is the pinned choice; with Automatic on nothing is
|
||||
// pinned, so only the live band lights up and the two can no longer read as
|
||||
|
||||
@@ -0,0 +1,392 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Shapes
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import qs.Commons
|
||||
import qs.Ui
|
||||
|
||||
// Centered speed test overlay. No card: like the Tucson's floating cluster,
|
||||
// the two dials (download left, upload right) sit directly on a darkened
|
||||
// scrim -- open 270° arcs, faint tick rings, hubless gradient needles, and a
|
||||
// digital readout in the middle. Esc, the scrim, or the corner dismiss close
|
||||
// it; the needles sweep to full scale and back on open, then track the live
|
||||
// readings.
|
||||
PanelWindow {
|
||||
id: root
|
||||
|
||||
required property Item anchorItem
|
||||
required property QtObject bar
|
||||
required property bool running
|
||||
required property string phase // "down" | "up" | ""
|
||||
required property string downloadMbps
|
||||
required property string uploadMbps
|
||||
required property string error
|
||||
required property string connectionName
|
||||
property bool open: false
|
||||
|
||||
signal closeRequested()
|
||||
signal runAgainRequested()
|
||||
|
||||
readonly property real downloadValue: toMbps(downloadMbps)
|
||||
readonly property real uploadValue: toMbps(uploadMbps)
|
||||
readonly property bool failed: error !== ""
|
||||
readonly property bool finished: !running && !failed && (downloadValue > 0 || uploadValue > 0)
|
||||
|
||||
function toMbps(raw) {
|
||||
var value = parseFloat(raw)
|
||||
return isFinite(value) && value > 0 ? value : 0
|
||||
}
|
||||
|
||||
visible: open
|
||||
// The window is instantiated hidden, so re-acquire focus after mapping and
|
||||
// fire the ignition sweep once the surface is actually on screen.
|
||||
onOpenChanged: {
|
||||
if (open) Qt.callLater(function() {
|
||||
if (!root.open) return
|
||||
keyCatcher.forceActiveFocus()
|
||||
downDial.ignite()
|
||||
upDial.ignite()
|
||||
})
|
||||
}
|
||||
screen: anchorItem.QsWindow.window ? anchorItem.QsWindow.window.screen : null
|
||||
anchors { top: true; bottom: true; left: true; right: true }
|
||||
color: "transparent"
|
||||
exclusionMode: ExclusionMode.Ignore
|
||||
WlrLayershell.namespace: "omarchy-network-speedtest"
|
||||
WlrLayershell.layer: WlrLayer.Overlay
|
||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
|
||||
|
||||
// Deep scrim: with no card behind them, the floating dials need the
|
||||
// backdrop to carry the contrast on any wallpaper, like the near-black
|
||||
// panel behind a real cluster.
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: Qt.rgba(0, 0, 0, 0.78)
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: root.closeRequested()
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: keyCatcher
|
||||
anchors.fill: parent
|
||||
focus: true
|
||||
|
||||
Keys.onEscapePressed: root.closeRequested()
|
||||
Keys.onReturnPressed: if (!root.running) root.runAgainRequested()
|
||||
Keys.onEnterPressed: if (!root.running) root.runAgainRequested()
|
||||
|
||||
Item {
|
||||
id: cluster
|
||||
anchors.centerIn: parent
|
||||
width: content.implicitWidth
|
||||
height: content.implicitHeight
|
||||
// Narrow or heavily scaled outputs: shrink the whole cluster rather
|
||||
// than clipping it at the screen edge.
|
||||
scale: Math.min(1,
|
||||
(keyCatcher.width - Style.space(32)) / Math.max(1, width),
|
||||
(keyCatcher.height - Style.space(32)) / Math.max(1, height))
|
||||
|
||||
// Swallow clicks so only the scrim outside the cluster dismisses.
|
||||
MouseArea { anchors.fill: parent; onClicked: {} }
|
||||
|
||||
ColumnLayout {
|
||||
id: content
|
||||
anchors.fill: parent
|
||||
spacing: Style.space(16)
|
||||
|
||||
Text {
|
||||
visible: root.connectionName !== ""
|
||||
text: root.connectionName.toUpperCase()
|
||||
color: Qt.darker(root.bar.foreground, 1.4)
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: Style.font.caption
|
||||
font.bold: true
|
||||
font.letterSpacing: 2
|
||||
Layout.fillWidth: true
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: Style.space(48)
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
|
||||
SpeedDial {
|
||||
id: downDial
|
||||
label: "DOWNLOAD"
|
||||
value: root.downloadValue
|
||||
live: root.running && root.phase === "down"
|
||||
}
|
||||
|
||||
SpeedDial {
|
||||
id: upDial
|
||||
label: "UPLOAD"
|
||||
value: root.uploadValue
|
||||
live: root.running && root.phase === "up"
|
||||
}
|
||||
}
|
||||
|
||||
// Centered on the dial pair. Fades rather than unmounts while a run
|
||||
// is in flight, so the cluster never shifts.
|
||||
Button {
|
||||
text: "Run Again"
|
||||
tooltipText: "Measure again via fast.com"
|
||||
bordered: true
|
||||
enabled: !root.running
|
||||
opacity: root.running ? 0 : 1
|
||||
foreground: root.bar.foreground
|
||||
fontFamily: root.bar.fontFamily
|
||||
fontSize: Style.font.bodySmall
|
||||
horizontalPadding: Style.space(14)
|
||||
verticalPadding: Style.space(4)
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
onClicked: root.runAgainRequested()
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation { duration: 240; easing.type: Easing.OutCubic }
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
visible: root.failed
|
||||
text: root.error
|
||||
color: root.bar.urgent
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: Style.font.bodySmall
|
||||
wrapMode: Text.Wrap
|
||||
Layout.fillWidth: true
|
||||
Layout.maximumWidth: Style.space(440)
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// One floating cluster dial: an open 270° scale with the gap at the
|
||||
// bottom, a faint tick ring, a glowing accent value arc, a hubless needle
|
||||
// that fades toward the pivot, and a digital readout in the middle. All
|
||||
// writes to the needle funnel through `shown` so the ignition sweep and
|
||||
// live readings share one animation.
|
||||
component SpeedDial: Item {
|
||||
id: dial
|
||||
|
||||
required property string label
|
||||
required property real value
|
||||
required property bool live
|
||||
|
||||
readonly property real diameter: Style.space(210)
|
||||
// 0° = 3 o'clock, increasing clockwise (PathAngleArc's convention).
|
||||
readonly property real dialStart: 135
|
||||
readonly property real dialSweep: 270
|
||||
readonly property int tickCount: 46
|
||||
readonly property real arcWidth: Style.space(4)
|
||||
readonly property real arcRadius: diameter / 2 - arcWidth
|
||||
readonly property color trackColor: Qt.rgba(root.bar.foreground.r, root.bar.foreground.g, root.bar.foreground.b, 0.14)
|
||||
readonly property color minorTickColor: Qt.rgba(root.bar.foreground.r, root.bar.foreground.g, root.bar.foreground.b, 0.12)
|
||||
readonly property color majorTickColor: Qt.rgba(root.bar.foreground.r, root.bar.foreground.g, root.bar.foreground.b, 0.3)
|
||||
// The dial that isn't measuring yet sits dimmed until it gets a figure.
|
||||
readonly property bool engaged: live || value > 0
|
||||
|
||||
property real shown: 0
|
||||
// The digital readout stays on the real figure while the ignition sweep
|
||||
// drives the needle -- a cluster sweeps its gauges, not its numerals.
|
||||
readonly property real reading: ignition.running ? value : shown
|
||||
property real fullScale: 100
|
||||
readonly property var scaleStops: [100, 250, 500, 1000, 2500, 5000, 10000]
|
||||
readonly property real fraction: fullScale > 0 ? Math.max(0, Math.min(1, shown / fullScale)) : 0
|
||||
readonly property bool arcVisible: fraction > 0.004
|
||||
|
||||
width: diameter
|
||||
height: diameter
|
||||
opacity: engaged ? 1 : 0.5
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation { duration: 240; easing.type: Easing.OutCubic }
|
||||
}
|
||||
|
||||
// Live readings land once a second; glide between them rather than snap.
|
||||
Behavior on shown {
|
||||
enabled: !ignition.running
|
||||
NumberAnimation { duration: 600; easing.type: Easing.OutCubic }
|
||||
}
|
||||
|
||||
Behavior on fullScale {
|
||||
enabled: !ignition.running
|
||||
NumberAnimation { duration: 400; easing.type: Easing.OutCubic }
|
||||
}
|
||||
|
||||
// A fresh measurement re-ranges from the base scale. Without this, one
|
||||
// unusually fast run would compress every later one for the lifetime of
|
||||
// the shell process.
|
||||
onLiveChanged: {
|
||||
if (live) fullScale = scaleStops[0]
|
||||
}
|
||||
|
||||
onValueChanged: {
|
||||
// Latch the scale upward to the next stop when a reading approaches the
|
||||
// rim. Never shrink mid-run; a fresh run just re-sweeps from zero.
|
||||
for (var i = 0; i < scaleStops.length; i++) {
|
||||
if (value <= scaleStops[i] * 0.92) {
|
||||
if (scaleStops[i] > fullScale) fullScale = scaleStops[i]
|
||||
break
|
||||
}
|
||||
if (i === scaleStops.length - 1) fullScale = scaleStops[i]
|
||||
}
|
||||
if (!ignition.running) shown = value
|
||||
}
|
||||
|
||||
function ignite() {
|
||||
ignition.restart()
|
||||
}
|
||||
|
||||
// Car-cluster power-on: needle sweeps to full scale and falls back before
|
||||
// the live figures take over.
|
||||
SequentialAnimation {
|
||||
id: ignition
|
||||
NumberAnimation { target: dial; property: "shown"; to: dial.fullScale; duration: 550; easing.type: Easing.InOutCubic }
|
||||
NumberAnimation { target: dial; property: "shown"; to: 0; duration: 650; easing.type: Easing.OutCubic }
|
||||
onFinished: dial.shown = dial.value
|
||||
}
|
||||
|
||||
Shape {
|
||||
anchors.fill: parent
|
||||
preferredRendererType: Shape.CurveRenderer
|
||||
|
||||
// Track: the full scale, always visible, dim.
|
||||
ShapePath {
|
||||
strokeWidth: dial.arcWidth
|
||||
strokeColor: dial.trackColor
|
||||
fillColor: "transparent"
|
||||
capStyle: ShapePath.RoundCap
|
||||
|
||||
PathAngleArc {
|
||||
centerX: dial.width / 2
|
||||
centerY: dial.height / 2
|
||||
radiusX: dial.arcRadius
|
||||
radiusY: dial.arcRadius
|
||||
startAngle: dial.dialStart
|
||||
sweepAngle: dial.dialSweep
|
||||
}
|
||||
}
|
||||
|
||||
// Soft under-glow beneath the value arc, standing in for the backlit
|
||||
// ring of a real cluster. Both arcs go transparent at rest, or their
|
||||
// round caps would leave a stray dot at the foot of the scale.
|
||||
ShapePath {
|
||||
strokeWidth: dial.arcWidth * 3
|
||||
strokeColor: dial.arcVisible ? Qt.rgba(Color.accent.r, Color.accent.g, Color.accent.b, 0.18) : "transparent"
|
||||
fillColor: "transparent"
|
||||
capStyle: ShapePath.RoundCap
|
||||
|
||||
PathAngleArc {
|
||||
centerX: dial.width / 2
|
||||
centerY: dial.height / 2
|
||||
radiusX: dial.arcRadius
|
||||
radiusY: dial.arcRadius
|
||||
startAngle: dial.dialStart
|
||||
sweepAngle: dial.dialSweep * dial.fraction
|
||||
}
|
||||
}
|
||||
|
||||
// Value: fills behind the needle.
|
||||
ShapePath {
|
||||
strokeWidth: dial.arcWidth
|
||||
strokeColor: dial.arcVisible ? Color.accent : "transparent"
|
||||
fillColor: "transparent"
|
||||
capStyle: ShapePath.RoundCap
|
||||
|
||||
PathAngleArc {
|
||||
centerX: dial.width / 2
|
||||
centerY: dial.height / 2
|
||||
radiusX: dial.arcRadius
|
||||
radiusY: dial.arcRadius
|
||||
startAngle: dial.dialStart
|
||||
sweepAngle: dial.dialSweep * dial.fraction
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Faint tick ring just inside the arc; every fifth tick is a major.
|
||||
Repeater {
|
||||
model: dial.tickCount
|
||||
|
||||
Item {
|
||||
required property int index
|
||||
readonly property bool major: index % 5 === 0
|
||||
|
||||
anchors.fill: parent
|
||||
rotation: dial.dialStart + (index / (dial.tickCount - 1)) * dial.dialSweep - 270
|
||||
|
||||
Rectangle {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
y: dial.arcWidth * 2 + (parent.major ? 0 : Style.space(2))
|
||||
width: parent.major ? Math.max(2, Style.space(2)) : 1
|
||||
height: parent.major ? Style.space(10) : Style.space(6)
|
||||
radius: width / 2
|
||||
color: parent.major ? dial.majorTickColor : dial.minorTickColor
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Hubless needle: a slender sliver that fades out toward the pivot, so
|
||||
// it reads as floating like the rest of the cluster.
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
rotation: dial.dialStart + dial.fraction * dial.dialSweep - 270
|
||||
|
||||
Rectangle {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
y: dial.arcWidth * 2 + Style.space(10)
|
||||
width: Math.max(2, Style.space(3))
|
||||
height: dial.diameter * 0.32
|
||||
radius: width / 2
|
||||
|
||||
gradient: Gradient {
|
||||
GradientStop { position: 0.0; color: Color.accent }
|
||||
GradientStop { position: 0.55; color: Color.accent }
|
||||
GradientStop { position: 1.0; color: "transparent" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.top: parent.verticalCenter
|
||||
anchors.topMargin: Style.space(14)
|
||||
spacing: 0
|
||||
|
||||
Text {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: dial.reading < 10 ? dial.reading.toFixed(1) : Math.round(dial.reading).toString()
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: Style.font.display
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: "Mbps"
|
||||
color: Qt.darker(root.bar.foreground, 1.4)
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: Style.font.caption
|
||||
}
|
||||
}
|
||||
|
||||
// The 90° gap at the bottom of the scale is where a cluster prints its
|
||||
// unit; here it names the direction.
|
||||
Text {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.bottom: parent.bottom
|
||||
text: dial.label
|
||||
color: Qt.darker(root.bar.foreground, 1.3)
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: Style.font.caption
|
||||
font.bold: true
|
||||
font.letterSpacing: 1.5
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,8 @@ import Quickshell.Wayland
|
||||
import qs.Commons
|
||||
import qs.Ui
|
||||
|
||||
// Centered Wi-Fi share overlay, presented like the speed test: no card,
|
||||
// just the QR code floating on a heavy scrim. Esc or the scrim dismiss it.
|
||||
PanelWindow {
|
||||
id: root
|
||||
|
||||
@@ -43,9 +45,11 @@ PanelWindow {
|
||||
WlrLayershell.layer: WlrLayer.Overlay
|
||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
|
||||
|
||||
// Deep scrim: the floating code needs the backdrop to carry the contrast
|
||||
// on any wallpaper.
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: Qt.rgba(0, 0, 0, 0.45)
|
||||
color: Qt.rgba(0, 0, 0, 0.78)
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
@@ -53,42 +57,49 @@ PanelWindow {
|
||||
}
|
||||
}
|
||||
|
||||
BorderSurface {
|
||||
width: Style.space(320)
|
||||
height: content.implicitHeight + Style.space(48)
|
||||
anchors.centerIn: parent
|
||||
radius: Style.cornerRadius
|
||||
color: Color.menu.background
|
||||
borderSpec: Border.surfaceSpec("menu", "border", Color.popups.border, Math.max(1, Style.space(2)))
|
||||
Item {
|
||||
id: keyCatcher
|
||||
anchors.fill: parent
|
||||
focus: true
|
||||
|
||||
MouseArea { anchors.fill: parent; onClicked: {} }
|
||||
Keys.onEscapePressed: root.closeRequested()
|
||||
|
||||
Item {
|
||||
id: keyCatcher
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.space(24)
|
||||
focus: true
|
||||
anchors.centerIn: parent
|
||||
width: content.implicitWidth
|
||||
height: content.implicitHeight
|
||||
// Narrow or heavily scaled outputs: shrink the whole card rather than
|
||||
// clipping it at the screen edge.
|
||||
scale: Math.min(1,
|
||||
(keyCatcher.width - Style.space(32)) / Math.max(1, width),
|
||||
(keyCatcher.height - Style.space(32)) / Math.max(1, height))
|
||||
|
||||
Keys.onEscapePressed: root.closeRequested()
|
||||
// Swallow clicks so only the scrim outside the content dismisses.
|
||||
MouseArea { anchors.fill: parent; onClicked: {} }
|
||||
|
||||
ColumnLayout {
|
||||
id: content
|
||||
anchors.fill: parent
|
||||
spacing: Style.space(12)
|
||||
spacing: Style.space(16)
|
||||
|
||||
Text {
|
||||
text: "Share " + (root.ssid || "Wi-Fi")
|
||||
color: root.bar.foreground
|
||||
text: (root.ssid || "Wi-Fi").toUpperCase()
|
||||
color: Qt.darker(root.bar.foreground, 1.4)
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: Style.font.title
|
||||
font.pixelSize: Style.font.caption
|
||||
font.bold: true
|
||||
font.letterSpacing: 2
|
||||
elide: Text.ElideRight
|
||||
Layout.fillWidth: true
|
||||
Layout.maximumWidth: Style.space(320)
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
|
||||
// Render every QR module as an integer-sized native rectangle. This
|
||||
// stays crisp and avoids temporary images and file-cache races.
|
||||
// stays crisp and avoids temporary images and file-cache races. Only
|
||||
// the dark modules paint, so the white canvas can keep its rounded
|
||||
// corners; the spec quiet zone baked into the matrix keeps the code
|
||||
// itself clear of them.
|
||||
Rectangle {
|
||||
id: qrCanvas
|
||||
readonly property int moduleSize: root.qrSize > 0
|
||||
@@ -99,6 +110,7 @@ PanelWindow {
|
||||
width: root.qrSize * moduleSize
|
||||
height: width
|
||||
color: "white"
|
||||
radius: Style.cornerRadius
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
|
||||
Grid {
|
||||
@@ -115,7 +127,7 @@ PanelWindow {
|
||||
|
||||
width: qrCanvas.moduleSize
|
||||
height: qrCanvas.moduleSize
|
||||
color: root.qrRows[matrixRow].charAt(matrixColumn) === "1" ? "#111111" : "white"
|
||||
color: root.qrRows[matrixRow].charAt(matrixColumn) === "1" ? "#111111" : "transparent"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -124,7 +136,9 @@ PanelWindow {
|
||||
Text {
|
||||
visible: root.loading
|
||||
text: "Generating QR code…"
|
||||
color: root.bar.foreground
|
||||
color: Qt.darker(root.bar.foreground, 1.3)
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: Style.font.bodySmall
|
||||
Layout.fillWidth: true
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
@@ -133,15 +147,20 @@ PanelWindow {
|
||||
visible: root.error !== ""
|
||||
text: root.error
|
||||
color: root.bar.urgent
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: Style.font.bodySmall
|
||||
wrapMode: Text.Wrap
|
||||
Layout.fillWidth: true
|
||||
Layout.maximumWidth: Style.space(320)
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
visible: root.showingQr
|
||||
text: "Scan to join this network"
|
||||
color: root.bar.foreground
|
||||
color: Qt.darker(root.bar.foreground, 1.3)
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: Style.font.bodySmall
|
||||
Layout.fillWidth: true
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
@@ -157,6 +176,7 @@ PanelWindow {
|
||||
font.pixelSize: Style.font.bodySmall
|
||||
wrapMode: Text.WrapAnywhere
|
||||
Layout.fillWidth: true
|
||||
Layout.maximumWidth: Style.space(320)
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
|
||||
MouseArea {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"name": "Network",
|
||||
"version": "1.0.0",
|
||||
"author": "Omarchy",
|
||||
"description": "Wi-Fi list, connection state, and QR sharing",
|
||||
"description": "Wi-Fi list, connection state, QR sharing, and speed test",
|
||||
"kinds": [
|
||||
"bar-widget"
|
||||
],
|
||||
@@ -13,7 +13,7 @@
|
||||
},
|
||||
"barWidget": {
|
||||
"displayName": "Network",
|
||||
"description": "Wi-Fi list, connection state, and QR sharing",
|
||||
"description": "Wi-Fi list, connection state, QR sharing, and speed test",
|
||||
"category": "Network",
|
||||
"allowMultiple": false
|
||||
}
|
||||
|
||||
@@ -87,6 +87,7 @@ Panel {
|
||||
onLocationQueryChanged: {
|
||||
if (savingLocation) savingLocationQueryStarted = true
|
||||
forecastRetries = 0
|
||||
dailyForecastRetries = 0
|
||||
forecastProc.running = false
|
||||
dailyForecastProc.running = false
|
||||
Qt.callLater(refresh)
|
||||
@@ -112,6 +113,7 @@ Panel {
|
||||
}
|
||||
|
||||
property int forecastRetries: 0
|
||||
property int dailyForecastRetries: 0
|
||||
|
||||
// Click-to-edit state for the location label.
|
||||
property bool editingLocation: false
|
||||
@@ -148,6 +150,11 @@ Panel {
|
||||
readonly property string reportHumidity: current ? (current.humidity + "%") : ""
|
||||
|
||||
function refresh() {
|
||||
// Each full refresh cycle gets a fresh retry budget, so an earlier
|
||||
// exhausted round (e.g. waking with the network still down) doesn't
|
||||
// starve retries for the rest of the session.
|
||||
forecastRetries = 0
|
||||
dailyForecastRetries = 0
|
||||
if (!forecastProc.running) forecastProc.running = true
|
||||
if (root.locationQuery === "" && !locationProc.running) locationProc.running = true
|
||||
// With stored coordinates this fetches open-meteo right away — no need
|
||||
@@ -368,22 +375,42 @@ Panel {
|
||||
onTriggered: if (!forecastProc.running) forecastProc.running = true
|
||||
}
|
||||
|
||||
// With configured coordinates this fetch is the only thing that updates the
|
||||
// bar icon, so a dropped response (e.g. waking before the network is back)
|
||||
// must retry rather than wait out the refresh timer with a stale icon.
|
||||
function scheduleDailyForecastRetry() {
|
||||
if (dailyForecastRetries >= 3) return
|
||||
dailyForecastRetries++
|
||||
dailyForecastRetryTimer.restart()
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: dailyForecastRetryTimer
|
||||
interval: 2500
|
||||
onTriggered: root.refreshDailyForecast(null)
|
||||
}
|
||||
|
||||
Process {
|
||||
id: dailyForecastProc
|
||||
stdout: StdioCollector {
|
||||
waitForEnd: true
|
||||
onStreamFinished: {
|
||||
var raw = String(text || "").trim()
|
||||
if (!raw) return
|
||||
if (!raw) {
|
||||
root.scheduleDailyForecastRetry()
|
||||
return
|
||||
}
|
||||
try {
|
||||
var parsed = JSON.parse(raw)
|
||||
var parsedCurrent = Model.openMeteoCurrentCondition(parsed)
|
||||
root.dailyForecastReport = parsed
|
||||
root.label = Model.currentIcon(parsedCurrent, root.label)
|
||||
root.dailyForecastRetries = 0
|
||||
if (Model.weatherResponseCompletesSave(root.hasConfiguredCoordinates, "open-meteo"))
|
||||
root.finishSavingLocation()
|
||||
} catch (e) {
|
||||
// Keep last-good daily forecast on parse failure.
|
||||
// Keep last-good daily forecast visible, but try again shortly.
|
||||
root.scheduleDailyForecastRetry()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -418,6 +445,7 @@ Panel {
|
||||
if (!root.savingLocationQueryStarted) {
|
||||
root.savingLocationQueryStarted = true
|
||||
root.forecastRetries = 0
|
||||
root.dailyForecastRetries = 0
|
||||
forecastProc.running = false
|
||||
dailyForecastProc.running = false
|
||||
Qt.callLater(root.refresh)
|
||||
|
||||
@@ -138,7 +138,32 @@ assertDeepEqual(calendar.clockFormatRing('', '', []), ['HH:mm'], 'clock keeps a
|
||||
assertEqual(calendar.nextClockFormat(ring, ring[0]), ring[1], 'clock steps to the next format')
|
||||
assertEqual(calendar.nextClockFormat(ring, ring[ring.length - 1]), ring[0], 'clock wraps the format ring')
|
||||
assertEqual(calendar.nextClockFormat(ring, 'HH:mm:ss'), ring[0], 'clock starts at the top from a format outside the ring')
|
||||
assertEqual(calendar.clockFormats(true)[0], 'HH\n\u2014\nmm', 'clock keeps stacked formats for vertical bars')
|
||||
// Both rings, contents and order: a right click walks this list and writes
|
||||
// the result back to shell.json, so an inserted preset should have to be
|
||||
// acknowledged here.
|
||||
assertDeepEqual(
|
||||
calendar.clockFormats(false),
|
||||
[
|
||||
'dddd HH:mm', 'dddd h:mm AP',
|
||||
'HH:mm', 'h:mm AP',
|
||||
'ddd d MMM HH:mm', 'ddd d MMM h:mm AP',
|
||||
"d MMMM 'W'ww yyyy",
|
||||
// No twin: ISO 8601 writes time on a 24-hour clock, so an AM/PM variant
|
||||
// would contradict the one thing that format is for.
|
||||
'yyyy-MM-dd HH:mm'
|
||||
],
|
||||
'clock offers the horizontal presets in this order'
|
||||
)
|
||||
assertDeepEqual(
|
||||
calendar.clockFormats(true),
|
||||
['HH\n\u2014\nmm', 'h\n\u2014\nmm\nAP', "dd\nMMM\n'W'ww\n''yy", 'HH\nmm'],
|
||||
'clock offers the stacked presets in this order'
|
||||
)
|
||||
assertEqual(
|
||||
calendar.nextClockFormat(ring, 'dddd HH:mm'),
|
||||
'dddd h:mm AP',
|
||||
'clock reaches an AM/PM twin in one right click'
|
||||
)
|
||||
assertEqual(calendar.isoWeekLiteral(2026, 0, 5), '02', 'clock zero-pads the ISO week token')
|
||||
|
||||
// ---- widget wiring
|
||||
|
||||
@@ -104,7 +104,7 @@ source "$ROOT/migrations/1785617047.sh" >/dev/null
|
||||
grep -Fx "$omp_package omp" "$stub_log" >/dev/null || fail "Oh My Pi migration creates a working lazy stub"
|
||||
|
||||
: >"$stub_log"
|
||||
source "$ROOT/migrations/1785633225.sh" >/dev/null
|
||||
source "$ROOT/migrations/1785846769.sh" >/dev/null
|
||||
grep -Fx "$omp_package omp" "$stub_log" >/dev/null || fail "agent migration repairs the Oh My Pi lazy stub"
|
||||
grep -Fx "$grok_package grok" "$stub_log" >/dev/null || fail "agent migration creates the Grok lazy stub"
|
||||
grep -Fx "$crush_package" "$stub_log" >/dev/null || fail "agent migration creates the Crush lazy stub"
|
||||
@@ -114,7 +114,7 @@ touch "$test_home/.local/state/omarchy/preinstalls-removed"
|
||||
"$ROOT/bin/omarchy-mise-install" oh-my-pi omp
|
||||
: >"$stub_log"
|
||||
source "$ROOT/migrations/1785617047.sh" >/dev/null
|
||||
source "$ROOT/migrations/1785633225.sh" >/dev/null
|
||||
source "$ROOT/migrations/1785846769.sh" >/dev/null
|
||||
[[ ! -s $stub_log ]] || fail "agent migrations respect the preinstall opt-out"
|
||||
[[ ! -e $test_home/.local/bin/omp ]] || fail "agent migration removes the obsolete Oh My Pi wrapper after opt-out"
|
||||
rm "$test_home/.local/state/omarchy/preinstalls-removed"
|
||||
|
||||
@@ -14,7 +14,9 @@ fake_hyprctl="$test_tmp/hyprctl"
|
||||
signature="test-signature"
|
||||
runtime_dir="$run_root/1000"
|
||||
|
||||
mkdir -p "$runtime_dir/hypr/$signature"
|
||||
dead_signature="dead-signature"
|
||||
|
||||
mkdir -p "$runtime_dir/hypr/$signature" "$runtime_dir/hypr/$dead_signature"
|
||||
|
||||
cat >"$fake_hyprctl" <<'BASH'
|
||||
#!/bin/bash
|
||||
@@ -22,6 +24,10 @@ cat >"$fake_hyprctl" <<'BASH'
|
||||
printf '%s\t%s\n' "$XDG_RUNTIME_DIR" "$*" >>"$FAKE_HYPRCTL_LOG"
|
||||
|
||||
case "$*" in
|
||||
*'--instance dead-signature '*)
|
||||
printf "Couldn't connect to %s/hypr/dead-signature/.socket.sock. (4)\n" "$XDG_RUNTIME_DIR"
|
||||
exit 4
|
||||
;;
|
||||
*'getoption misc.disable_autoreload'*)
|
||||
printf '{"option":"misc.disable_autoreload","bool":%s,"set":true}\n' "${FAKE_DISABLE_AUTORELOAD:-false}"
|
||||
;;
|
||||
@@ -39,7 +45,7 @@ FAKE_HYPRCTL_LOG="$hyprctl_log" \
|
||||
HYPRCTL="$fake_hyprctl" \
|
||||
OMARCHY_HYPRLAND_RELOAD_GUARD_RUN_ROOT="$run_root" \
|
||||
OMARCHY_HYPRLAND_RELOAD_GUARD_STATE_DIR="$state_dir" \
|
||||
"$ROOT/bin/omarchy-hyprland-reload-guard" pause
|
||||
"$ROOT/bin/omarchy-hyprland-reload-guard" pause 2>"$test_tmp/pause-stderr"
|
||||
|
||||
state_file="$state_dir/$signature"
|
||||
[[ -f $state_file ]] || fail "reload guard stores Hyprland state on pause"
|
||||
@@ -48,6 +54,10 @@ grep -Fx "$expected_state" "$state_file" >/dev/null || fail "reload guard record
|
||||
grep -F 'hl.config({ misc = { disable_autoreload = true }, debug = { suppress_errors = true } })' "$hyprctl_log" >/dev/null || fail "reload guard pauses autoreload with hyprctl eval"
|
||||
pass "reload guard pauses live Hyprland reloads"
|
||||
|
||||
[[ ! -e $state_dir/$dead_signature ]] || fail "reload guard skips instances hyprctl cannot reach"
|
||||
[[ ! -s $test_tmp/pause-stderr ]] || fail "reload guard pauses dead Hyprland instances quietly" "$(cat "$test_tmp/pause-stderr")"
|
||||
pass "reload guard skips dead Hyprland instances quietly"
|
||||
|
||||
: >"$hyprctl_log"
|
||||
FAKE_HYPRCTL_LOG="$hyprctl_log" \
|
||||
HYPRCTL="$fake_hyprctl" \
|
||||
|
||||
@@ -10,7 +10,9 @@ mkdir -p "$tmp/bin"
|
||||
|
||||
cat >"$tmp/bin/nmcli" <<'EOF'
|
||||
#!/bin/bash
|
||||
if [[ $* == *GENERAL.CON-UUID* ]]; then
|
||||
if [[ $* == *"DEVICE,TYPE,STATE"* ]]; then
|
||||
printf 'eth0:ethernet:connected\nwlan0:wifi:connected\n'
|
||||
elif [[ $* == *GENERAL.CON-UUID* ]]; then
|
||||
echo test-uuid
|
||||
else
|
||||
printf '%s' "$QR_NMCLI_FIELDS"
|
||||
@@ -30,11 +32,12 @@ chmod +x "$tmp/bin/nmcli" "$tmp/bin/qrencode"
|
||||
|
||||
run_success_case() {
|
||||
local description=$1 fields=$2 expected_payload=$3
|
||||
shift 3
|
||||
local expected output payload
|
||||
|
||||
export QR_NMCLI_FIELDS=$fields
|
||||
export QR_PAYLOAD_FILE="$tmp/payload"
|
||||
output=$(PATH="$tmp/bin:$PATH" "$ROOT/bin/omarchy-network-qr" wlan0)
|
||||
output=$(PATH="$tmp/bin:$PATH" "$ROOT/bin/omarchy-network-qr" "$@")
|
||||
expected=$'100\n010\n001'
|
||||
[[ $output == "$expected" ]] || fail "$description emits a compact module matrix" "expected: $expected\nactual: $output"
|
||||
|
||||
@@ -46,24 +49,34 @@ run_success_case() {
|
||||
run_success_case \
|
||||
"network QR helper escapes WPA credentials through stdin" \
|
||||
$'Cafe;Guest\\5G\nwpa-psk\np,a:ss;word\\42\nno\n' \
|
||||
'WIFI:T:WPA;S:Cafe\;Guest\\5G;P:p\,a\:ss\;word\\42;;'
|
||||
'WIFI:T:WPA;S:Cafe\;Guest\\5G;P:p\,a\:ss\;word\\42;;' \
|
||||
wlan0
|
||||
|
||||
# With no interface argument the helper finds the connected Wi-Fi device.
|
||||
run_success_case \
|
||||
"network QR helper detects the Wi-Fi interface" \
|
||||
$'Cafe Detected\nwpa-psk\nsecret\nno\n' \
|
||||
'WIFI:T:WPA;S:Cafe Detected;P:secret;;'
|
||||
|
||||
run_success_case \
|
||||
"network QR helper supports open networks" \
|
||||
$'Cafe Open\nnone\n\nno\n' \
|
||||
'WIFI:T:nopass;S:Cafe Open;P:;;'
|
||||
'WIFI:T:nopass;S:Cafe Open;P:;;' \
|
||||
wlan0
|
||||
|
||||
run_success_case \
|
||||
"network QR helper marks hidden networks" \
|
||||
$'Hidden Network\nwpa-psk\nsecret\nyes\n' \
|
||||
'WIFI:T:WPA;S:Hidden Network;P:secret;H:true;;'
|
||||
'WIFI:T:WPA;S:Hidden Network;P:secret;H:true;;' \
|
||||
wlan0
|
||||
|
||||
# NetworkManager models WEP as key-mgmt "none" plus a wep-key, which must not
|
||||
# be mistaken for an open network.
|
||||
run_success_case \
|
||||
"network QR helper encodes WEP networks" \
|
||||
$'Old Router\nnone\n\nno\nwep-secret\n' \
|
||||
'WIFI:T:WEP;S:Old Router;P:wep-secret;;'
|
||||
'WIFI:T:WEP;S:Old Router;P:wep-secret;;' \
|
||||
wlan0
|
||||
|
||||
export QR_NMCLI_FIELDS=$'Enterprise\nwpa-eap\nsecret\nno\n'
|
||||
export QR_PAYLOAD_FILE="$tmp/enterprise-payload"
|
||||
|
||||
@@ -85,7 +85,6 @@ assertDeepEqual(
|
||||
|
||||
assertEqual(network.formatBytes(1536), '1.5 KB', 'network formats bytes')
|
||||
assertEqual(network.formatRate(1536), '1.5 KB/s', 'network formats rates')
|
||||
assertEqual(network.formatSpeedMbps('250.4'), '250 Mbps', 'network formats speed test results')
|
||||
assertEqual(network.formatPingLatency('2.54'), '2.5 ms', 'network formats low ping with precision')
|
||||
assertEqual(network.formatPingLatency('25.4'), '25 ms', 'network formats ping')
|
||||
assertEqual(network.formatPingLatency(''), 'Timeout', 'network formats missing ping as timeout')
|
||||
|
||||
@@ -234,13 +234,8 @@ pass "alert removal handles the older indicators key"
|
||||
fail "alert removal leaves an already-empty list alone" "$(jq -c '.bar.layout.right[1]' "$shell_config")"
|
||||
pass "alert removal leaves an already-empty list alone"
|
||||
|
||||
(($(wc -l <"$SHELL_RESTARTS") == 1)) || fail "alert removal restarts the shell"
|
||||
[[ ! -s $STATE_CALLS ]] || fail "a restarted shell needs no deferred restart" "$(cat "$STATE_CALLS")"
|
||||
pass "alert removal restarts the shell"
|
||||
|
||||
# Migrations run from a TTY or over ssh have no shell to restart, and stopping
|
||||
# there would strand every migration queued behind this one.
|
||||
reset_home
|
||||
SHELL_RESTART_STATUS=1 run_migration
|
||||
grep -Fxq 'set restart-shell-required' "$STATE_CALLS" || fail "an unavailable shell defers its restart" "$(cat "$STATE_CALLS")"
|
||||
pass "an unavailable shell defers its restart"
|
||||
# The running shell hot-reloads shell.json and the post-update restart is
|
||||
# unconditional, so the migration itself never touches the shell.
|
||||
[[ ! -s $SHELL_RESTARTS ]] || fail "alert removal leaves shell restarts to the update" "$(cat "$SHELL_RESTARTS")"
|
||||
[[ ! -s $STATE_CALLS ]] || fail "alert removal defers no shell restart" "$(cat "$STATE_CALLS")"
|
||||
pass "alert removal leaves shell restarts to the update"
|
||||
|
||||
Reference in New Issue
Block a user