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 signature="$2"
|
||||||
local option="$3"
|
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() {
|
instances() {
|
||||||
|
|||||||
+16
-2
@@ -2,11 +2,25 @@
|
|||||||
|
|
||||||
# omarchy:summary=Generate a Wi-Fi QR matrix for the shell
|
# omarchy:summary=Generate a Wi-Fi QR matrix for the shell
|
||||||
# omarchy:group=network
|
# omarchy:group=network
|
||||||
# omarchy:args=<interface>
|
# omarchy:args=[interface]
|
||||||
|
|
||||||
set -euo pipefail
|
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)
|
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; }
|
[[ -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"
|
omarchy-restart-"$service"
|
||||||
fi
|
fi
|
||||||
done
|
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]
|
[scrollback]
|
||||||
lines=10000
|
lines=10000
|
||||||
|
multiplier=7.0
|
||||||
|
|
||||||
[cursor]
|
[cursor]
|
||||||
style=block
|
style=block
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ o.window({
|
|||||||
}, { tag = "+floating-window" })
|
}, { tag = "+floating-window" })
|
||||||
o.window("dev.tensaku.Tensaku", { float = true })
|
o.window("dev.tensaku.Tensaku", { float = true })
|
||||||
o.window("dev.tensaku.Tensaku", { center = true })
|
o.window("dev.tensaku.Tensaku", { center = true })
|
||||||
o.window("org.gnome.Calculator", { float = true })
|
o.window("omacalc", { float = true })
|
||||||
|
|
||||||
-- Fullscreen screensaver.
|
-- Fullscreen screensaver.
|
||||||
o.window("org.omarchy.screensaver", { fullscreen = true })
|
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("XF86PowerOff", "Power menu", "omarchy-menu toggle system", { locked = true })
|
||||||
o.bind("SUPER + K", "Show key bindings", "omarchy-menu-keybindings")
|
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("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_toggle("SUPER + SHIFT + SPACE", "Toggle top bar", "bar")
|
||||||
o.bind("SUPER + CTRL + SPACE", "Background switcher", "omarchy-menu toggle background")
|
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.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.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.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": {"icon":"","label":"Defaults","aliases":["default","defaults"]},
|
||||||
"setup.default.agent": {"icon":"","label":"Agent"},
|
"setup.default.agent": {"icon":"","label":"Agent"},
|
||||||
"setup.default.agent.claude": {"icon":"","label":"Claude","checked":"[[ \"$(omarchy-default-agent)\" == \"claude\" ]]","action":"omarchy-default-agent claude"},
|
"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`,
|
- Use helper commands such as `omarchy-cmd-present`, `omarchy-cmd-missing`,
|
||||||
`omarchy-pkg-add`, `omarchy-pkg-drop`, `omarchy-pkg-present`, and
|
`omarchy-pkg-add`, `omarchy-pkg-drop`, `omarchy-pkg-present`, and
|
||||||
`omarchy-pkg-missing` when appropriate.
|
`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:
|
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/current/` | user | Generated active theme, selected theme name, and current background symlink. |
|
||||||
| `~/.local/state/omarchy/migrations/` | user | Per-user migration markers. |
|
| `~/.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/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
|
## 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-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-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-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-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. |
|
| `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
|
foot
|
||||||
fzf
|
fzf
|
||||||
git
|
git
|
||||||
gnome-calculator
|
|
||||||
gnome-keyring
|
gnome-keyring
|
||||||
gnome-themes-extra
|
gnome-themes-extra
|
||||||
grim
|
grim
|
||||||
@@ -91,6 +90,7 @@ nss-mdns
|
|||||||
nvim
|
nvim
|
||||||
obs-studio
|
obs-studio
|
||||||
obsidian
|
obsidian
|
||||||
|
omacalc
|
||||||
omacut
|
omacut
|
||||||
omawrite
|
omawrite
|
||||||
omarchy-nvim
|
omarchy-nvim
|
||||||
@@ -109,7 +109,6 @@ qrencode
|
|||||||
quickshell-git
|
quickshell-git
|
||||||
ripgrep
|
ripgrep
|
||||||
ruby
|
ruby
|
||||||
rust
|
|
||||||
tensaku
|
tensaku
|
||||||
sddm
|
sddm
|
||||||
slurp
|
slurp
|
||||||
|
|||||||
@@ -5,5 +5,4 @@ if ! omarchy-pkg-present quickshell-git; then
|
|||||||
# quickshell package in place; packages depending on quickshell stay
|
# quickshell package in place; packages depending on quickshell stay
|
||||||
# satisfied through the provides.
|
# satisfied through the provides.
|
||||||
sudo pacman -S --noconfirm --ask 4 quickshell-git
|
sudo pacman -S --noconfirm --ask 4 quickshell-git
|
||||||
omarchy-state set restart-shell-required
|
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -37,5 +37,3 @@ if [[ -s $config_file ]]; then
|
|||||||
.bar.layout.center |= place_indicators_before_clock
|
.bar.layout.center |= place_indicators_before_clock
|
||||||
' "$config_file" >"$tmp" && mv "$tmp" "$config_file" || rm -f "$tmp"
|
' "$config_file" >"$tmp" && mv "$tmp" "$config_file" || rm -f "$tmp"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
omarchy-restart-shell
|
|
||||||
|
|||||||
@@ -118,7 +118,3 @@ if [[ -s $config_file ]] && grep -q 'TmuxAlert' "$config_file"; then
|
|||||||
|
|
||||||
rm -f "$tmp"
|
rm -f "$tmp"
|
||||||
fi
|
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
|
end
|
||||||
' "$config_file" >"$tmp" && mv "$tmp" "$config_file" || rm -f "$tmp"
|
' "$config_file" >"$tmp" && mv "$tmp" "$config_file" || rm -f "$tmp"
|
||||||
fi
|
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
|
foot_config="$HOME/.config/foot/foot.ini"
|
||||||
omarchy-mise-install github:can1357/oh-my-pi omp
|
|
||||||
omarchy-mise-install npm:@xai-official/grok grok
|
if [[ -f $foot_config ]] && ! grep -q '^multiplier=' "$foot_config"; then
|
||||||
omarchy-mise-install crush
|
if grep -qxF '[scrollback]' "$foot_config"; then
|
||||||
elif [[ -f $HOME/.local/bin/omp ]] && grep -Fq 'mise use -g "oh-my-pi"' "$HOME/.local/bin/omp"; then
|
tmp=$(mktemp)
|
||||||
rm -f "$HOME/.local/bin/omp"
|
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
|
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
|
// ---- 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
|
// 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 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 = [
|
var CLOCK_FORMATS = [
|
||||||
"dddd HH:mm",
|
"dddd HH:mm",
|
||||||
|
"dddd h:mm AP",
|
||||||
"HH:mm",
|
"HH:mm",
|
||||||
|
"h:mm AP",
|
||||||
"ddd d MMM HH:mm",
|
"ddd d MMM HH:mm",
|
||||||
|
"ddd d MMM h:mm AP",
|
||||||
"d MMMM 'W'ww yyyy",
|
"d MMMM 'W'ww yyyy",
|
||||||
"yyyy-MM-dd HH:mm"
|
"yyyy-MM-dd HH:mm"
|
||||||
]
|
]
|
||||||
|
|
||||||
// Vertical bars have room for a few stacked lines and nothing else, so the
|
// 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 = [
|
var VERTICAL_CLOCK_FORMATS = [
|
||||||
"HH\n—\nmm",
|
"HH\n—\nmm",
|
||||||
|
"h\n—\nmm\nAP",
|
||||||
"dd\nMMM\n'W'ww\n''yy",
|
"dd\nMMM\n'W'ww\n''yy",
|
||||||
"HH\nmm"
|
"HH\nmm"
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -250,12 +250,6 @@ function formatRate(bytesPerSec) {
|
|||||||
return formatBytes(bytesPerSec) + "/s"
|
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
|
// `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 "--"
|
// 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.
|
// so the grid doesn't reflow a second after the panel opens.
|
||||||
@@ -362,7 +356,6 @@ if (typeof module !== "undefined") {
|
|||||||
formatPacketLoss: formatPacketLoss,
|
formatPacketLoss: formatPacketLoss,
|
||||||
formatBytes: formatBytes,
|
formatBytes: formatBytes,
|
||||||
formatRate: formatRate,
|
formatRate: formatRate,
|
||||||
formatSpeedMbps: formatSpeedMbps,
|
|
||||||
formatPingLatency: formatPingLatency,
|
formatPingLatency: formatPingLatency,
|
||||||
wifiRow: wifiRow,
|
wifiRow: wifiRow,
|
||||||
sortWifiRows: sortWifiRows,
|
sortWifiRows: sortWifiRows,
|
||||||
|
|||||||
@@ -17,9 +17,28 @@ Panel {
|
|||||||
manageIpc: false
|
manageIpc: false
|
||||||
|
|
||||||
// Centralized close so callers can't forget to drop the passphrase prompt.
|
// 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() {
|
function close() {
|
||||||
root.controller.hide()
|
root.controller.hide()
|
||||||
cancelPasswordPrompt()
|
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() {
|
function cancelPasswordPrompt() {
|
||||||
@@ -83,8 +102,9 @@ Panel {
|
|||||||
property var bandAvailable: []
|
property var bandAvailable: []
|
||||||
property string pendingBand: ""
|
property string pendingBand: ""
|
||||||
property bool speedTestRunning: false
|
property bool speedTestRunning: false
|
||||||
property bool speedTestHasRun: false
|
property bool speedTestModalOpen: false
|
||||||
property bool speedTestExpectedStop: false
|
property bool speedTestExpectedStop: false
|
||||||
|
property bool pendingSpeedRun: false
|
||||||
property string speedTestPhase: ""
|
property string speedTestPhase: ""
|
||||||
property string speedTestStderr: ""
|
property string speedTestStderr: ""
|
||||||
property string speedTestDownloadMbps: ""
|
property string speedTestDownloadMbps: ""
|
||||||
@@ -111,6 +131,8 @@ Panel {
|
|||||||
property string qrError: ""
|
property string qrError: ""
|
||||||
property bool qrLoading: false
|
property bool qrLoading: false
|
||||||
property bool qrExpectedStop: false
|
property bool qrExpectedStop: false
|
||||||
|
property bool pendingQrShow: false
|
||||||
|
property bool pendingQrDetect: false
|
||||||
property string qrPassword: ""
|
property string qrPassword: ""
|
||||||
property bool qrPasswordVisible: false
|
property bool qrPasswordVisible: false
|
||||||
property string qrPasswordError: ""
|
property string qrPasswordError: ""
|
||||||
@@ -127,9 +149,9 @@ Panel {
|
|||||||
property bool cursorActive: false
|
property bool cursorActive: false
|
||||||
|
|
||||||
// Keyboard focus zone for the panel. j/k crosses row boundaries:
|
// 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.
|
// 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
|
property int headerIndex: 0
|
||||||
readonly property bool canDisconnect: !!connectedWifiNetwork
|
readonly property bool canDisconnect: !!connectedWifiNetwork
|
||||||
readonly property bool headerHasDisconnect: false
|
readonly property bool headerHasDisconnect: false
|
||||||
@@ -139,9 +161,11 @@ Panel {
|
|||||||
// "off" beside a perfectly live Ethernet connection.
|
// "off" beside a perfectly live Ethernet connection.
|
||||||
readonly property bool canToggleWifi: networkManagerAvailable && wifiStationAvailable
|
readonly property bool canToggleWifi: networkManagerAvailable && wifiStationAvailable
|
||||||
readonly property int qrHeaderIndex: canShareWifi ? 0 : -1
|
readonly property int qrHeaderIndex: canShareWifi ? 0 : -1
|
||||||
readonly property int toggleHeaderIndex: canToggleWifi ? (canShareWifi ? 1 : 0) : -1
|
readonly property int speedHeaderIndex: canRunSpeedTest ? (canShareWifi ? 1 : 0) : -1
|
||||||
readonly property int headerActionCount: (canShareWifi ? 1 : 0) + (canToggleWifi ? 1 : 0)
|
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 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 bool toggleHeaderHasCursor: cursorActive && focusSection === "header" && headerIndex === toggleHeaderIndex
|
||||||
readonly property string toggleHint: Networking.wifiEnabled ? "Turn Wi-Fi off" : "Turn Wi-Fi on"
|
readonly property string toggleHint: Networking.wifiEnabled ? "Turn Wi-Fi off" : "Turn Wi-Fi on"
|
||||||
readonly property var dnsProviders: ["DHCP", "Cloudflare", "Google", "Custom"]
|
readonly property var dnsProviders: ["DHCP", "Cloudflare", "Google", "Custom"]
|
||||||
@@ -167,8 +191,8 @@ Panel {
|
|||||||
readonly property bool bandPillsVisible: canSelectBand && bandPinned
|
readonly property bool bandPillsVisible: canSelectBand && bandPinned
|
||||||
readonly property string bandSectionTitle: Model.bandSectionTitle(bandEffective, bandCurrent)
|
readonly property string bandSectionTitle: Model.bandSectionTitle(bandEffective, bandCurrent)
|
||||||
readonly property bool bandBusy: pendingBand !== ""
|
readonly property bool bandBusy: pendingBand !== ""
|
||||||
// The speed test section only exists once there's an interface to test, so
|
// The speed test needs an interface to test, so its hero action only
|
||||||
// the Run button only joins the cursor chain then.
|
// appears once there is one.
|
||||||
readonly property bool canRunSpeedTest: !!info.iface
|
readonly property bool canRunSpeedTest: !!info.iface
|
||||||
property int bandIndex: 0
|
property int bandIndex: 0
|
||||||
// The band section has up to two cursor rows: the Automatic switch on the
|
// 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
|
// 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.
|
// nothing, so send it up to the switch that is still on screen.
|
||||||
onBandPillsVisibleChanged: {
|
onBandPillsVisibleChanged: {
|
||||||
@@ -226,10 +246,20 @@ Panel {
|
|||||||
function hide() { root.close() }
|
function hide() { root.close() }
|
||||||
function toggle() { root.toggle() }
|
function toggle() { root.toggle() }
|
||||||
function toggleNetwork() { root.toggleNetwork() }
|
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() {
|
function activateHeader() {
|
||||||
if (headerIndex === qrHeaderIndex) showWifiQr()
|
if (headerIndex === qrHeaderIndex) showWifiQr()
|
||||||
|
else if (headerIndex === speedHeaderIndex) showSpeedTest()
|
||||||
else if (headerIndex === toggleHeaderIndex) toggleNetwork()
|
else if (headerIndex === toggleHeaderIndex) toggleNetwork()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -427,14 +457,27 @@ Panel {
|
|||||||
|
|
||||||
readonly property string icon: Model.connectionIcon(kind, signalStrength)
|
readonly property string icon: Model.connectionIcon(kind, signalStrength)
|
||||||
|
|
||||||
function showWifiQr() {
|
function showWifiQr(forceDetect) {
|
||||||
if (qrProc.running || !info.iface || info.type !== "wifi") return
|
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
|
qrSize = 0
|
||||||
qrRows = []
|
qrRows = []
|
||||||
qrError = ""
|
qrError = ""
|
||||||
qrLoading = true
|
qrLoading = true
|
||||||
qrExpectedStop = false
|
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
|
qrProc.running = true
|
||||||
|
|
||||||
// Leave the compact network panel behind while the centered share card is open.
|
// Leave the compact network panel behind while the centered share card is open.
|
||||||
@@ -443,6 +486,7 @@ Panel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function hideWifiQr() {
|
function hideWifiQr() {
|
||||||
|
pendingQrShow = false
|
||||||
if (qrProc.running) {
|
if (qrProc.running) {
|
||||||
qrExpectedStop = true
|
qrExpectedStop = true
|
||||||
qrProc.running = false
|
qrProc.running = false
|
||||||
@@ -563,10 +607,6 @@ Panel {
|
|||||||
return Model.formatRate(bytesPerSec)
|
return Model.formatRate(bytesPerSec)
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatSpeedMbps(mbps) {
|
|
||||||
return Model.formatSpeedMbps(mbps)
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatPingLatency(ms) {
|
function formatPingLatency(ms) {
|
||||||
return Model.formatPingLatency(ms, hasInternetPing)
|
return Model.formatPingLatency(ms, hasInternetPing)
|
||||||
}
|
}
|
||||||
@@ -660,10 +700,42 @@ Panel {
|
|||||||
speedTestError = ""
|
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() {
|
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 = ""
|
speedTestError = ""
|
||||||
speedTestHasRun = true
|
speedTestDownloadMbps = ""
|
||||||
|
speedTestUploadMbps = ""
|
||||||
speedTestRunning = true
|
speedTestRunning = true
|
||||||
startSpeedTestPhase("down")
|
startSpeedTestPhase("down")
|
||||||
}
|
}
|
||||||
@@ -883,6 +955,12 @@ Panel {
|
|||||||
}
|
}
|
||||||
onExited: function(exitCode) {
|
onExited: function(exitCode) {
|
||||||
root.qrLoading = false
|
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 (root.qrExpectedStop) return
|
||||||
if (exitCode !== 0 || root.qrSize === 0) {
|
if (exitCode !== 0 || root.qrSize === 0) {
|
||||||
root.qrSize = 0
|
root.qrSize = 0
|
||||||
@@ -949,6 +1027,13 @@ Panel {
|
|||||||
onExited: function(exitCode) {
|
onExited: function(exitCode) {
|
||||||
speedTestPhaseTimer.stop()
|
speedTestPhaseTimer.stop()
|
||||||
|
|
||||||
|
if (root.pendingSpeedRun) {
|
||||||
|
root.pendingSpeedRun = false
|
||||||
|
root.speedTestExpectedStop = false
|
||||||
|
if (root.speedTestModalOpen) Qt.callLater(root.runSpeedTest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (!root.speedTestExpectedStop && exitCode !== 0) {
|
if (!root.speedTestExpectedStop && exitCode !== 0) {
|
||||||
root.speedTestError = root.speedTestStderr || "Speed test failed"
|
root.speedTestError = root.speedTestStderr || "Speed test failed"
|
||||||
root.speedTestPhase = ""
|
root.speedTestPhase = ""
|
||||||
@@ -1134,25 +1219,15 @@ Panel {
|
|||||||
root.focusSection = "header"
|
root.focusSection = "header"
|
||||||
root.headerIndex = 0
|
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) {
|
} else if (root.wifiNetworks.length > 0) {
|
||||||
root.focusSection = "wifi"
|
root.focusSection = "wifi"
|
||||||
if (root.selectedIndex < 0) root.selectedIndex = 0
|
if (root.selectedIndex < 0) root.selectedIndex = 0
|
||||||
}
|
}
|
||||||
} else { // wifi
|
} else { // wifi
|
||||||
// k from the top row escapes back up into the speed test's Run
|
// k from the top row escapes back up to the DNS row rather than
|
||||||
// button, or DNS when there is no speed test, rather than wrapping
|
// wrapping around to the bottom of the list.
|
||||||
// around to the bottom of the list.
|
|
||||||
if (dy < 0 && root.selectedIndex <= 0) {
|
if (dy < 0 && root.selectedIndex <= 0) {
|
||||||
root.focusSection = root.canRunSpeedTest ? "speed" : "dns"
|
root.focusSection = "dns"
|
||||||
root.wifiActionFocused = false
|
root.wifiActionFocused = false
|
||||||
}
|
}
|
||||||
else root.selectByDelta(dy)
|
else root.selectByDelta(dy)
|
||||||
@@ -1170,7 +1245,6 @@ Panel {
|
|||||||
if (root.focusSection === "header") root.activateHeader()
|
if (root.focusSection === "header") root.activateHeader()
|
||||||
else if (root.focusSection === "band") root.activateBand()
|
else if (root.focusSection === "band") root.activateBand()
|
||||||
else if (root.focusSection === "dns") root.activateDns()
|
else if (root.focusSection === "dns") root.activateDns()
|
||||||
else if (root.focusSection === "speed") root.runSpeedTest()
|
|
||||||
else root.activateSelected()
|
else root.activateSelected()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1229,6 +1303,22 @@ Panel {
|
|||||||
onClicked: root.showWifiQr()
|
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 {
|
ToggleSwitch {
|
||||||
id: powerSwitch
|
id: powerSwitch
|
||||||
visible: root.canToggleWifi
|
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).
|
// Wi-Fi networks (only if a Wi-Fi station is available).
|
||||||
PanelSeparator {
|
PanelSeparator {
|
||||||
visible: root.wifiStationAvailable
|
visible: root.wifiStationAvailable
|
||||||
@@ -1708,6 +1720,24 @@ Panel {
|
|||||||
onPasswordToggleRequested: root.toggleQrPassword()
|
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
|
// 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
|
// `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
|
// 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.Commons
|
||||||
import qs.Ui
|
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 {
|
PanelWindow {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
@@ -43,9 +45,11 @@ PanelWindow {
|
|||||||
WlrLayershell.layer: WlrLayer.Overlay
|
WlrLayershell.layer: WlrLayer.Overlay
|
||||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
|
WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
|
||||||
|
|
||||||
|
// Deep scrim: the floating code needs the backdrop to carry the contrast
|
||||||
|
// on any wallpaper.
|
||||||
Rectangle {
|
Rectangle {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
color: Qt.rgba(0, 0, 0, 0.45)
|
color: Qt.rgba(0, 0, 0, 0.78)
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
@@ -53,42 +57,49 @@ PanelWindow {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
BorderSurface {
|
Item {
|
||||||
width: Style.space(320)
|
id: keyCatcher
|
||||||
height: content.implicitHeight + Style.space(48)
|
anchors.fill: parent
|
||||||
anchors.centerIn: parent
|
focus: true
|
||||||
radius: Style.cornerRadius
|
|
||||||
color: Color.menu.background
|
|
||||||
borderSpec: Border.surfaceSpec("menu", "border", Color.popups.border, Math.max(1, Style.space(2)))
|
|
||||||
|
|
||||||
MouseArea { anchors.fill: parent; onClicked: {} }
|
Keys.onEscapePressed: root.closeRequested()
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: keyCatcher
|
anchors.centerIn: parent
|
||||||
anchors.fill: parent
|
width: content.implicitWidth
|
||||||
anchors.margins: Style.space(24)
|
height: content.implicitHeight
|
||||||
focus: true
|
// 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 {
|
ColumnLayout {
|
||||||
id: content
|
id: content
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
spacing: Style.space(12)
|
spacing: Style.space(16)
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
text: "Share " + (root.ssid || "Wi-Fi")
|
text: (root.ssid || "Wi-Fi").toUpperCase()
|
||||||
color: root.bar.foreground
|
color: Qt.darker(root.bar.foreground, 1.4)
|
||||||
font.family: root.bar.fontFamily
|
font.family: root.bar.fontFamily
|
||||||
font.pixelSize: Style.font.title
|
font.pixelSize: Style.font.caption
|
||||||
font.bold: true
|
font.bold: true
|
||||||
|
font.letterSpacing: 2
|
||||||
elide: Text.ElideRight
|
elide: Text.ElideRight
|
||||||
Layout.fillWidth: true
|
Layout.maximumWidth: Style.space(320)
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
horizontalAlignment: Text.AlignHCenter
|
horizontalAlignment: Text.AlignHCenter
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render every QR module as an integer-sized native rectangle. This
|
// 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 {
|
Rectangle {
|
||||||
id: qrCanvas
|
id: qrCanvas
|
||||||
readonly property int moduleSize: root.qrSize > 0
|
readonly property int moduleSize: root.qrSize > 0
|
||||||
@@ -99,6 +110,7 @@ PanelWindow {
|
|||||||
width: root.qrSize * moduleSize
|
width: root.qrSize * moduleSize
|
||||||
height: width
|
height: width
|
||||||
color: "white"
|
color: "white"
|
||||||
|
radius: Style.cornerRadius
|
||||||
Layout.alignment: Qt.AlignHCenter
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
|
||||||
Grid {
|
Grid {
|
||||||
@@ -115,7 +127,7 @@ PanelWindow {
|
|||||||
|
|
||||||
width: qrCanvas.moduleSize
|
width: qrCanvas.moduleSize
|
||||||
height: 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 {
|
Text {
|
||||||
visible: root.loading
|
visible: root.loading
|
||||||
text: "Generating QR code…"
|
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
|
Layout.fillWidth: true
|
||||||
horizontalAlignment: Text.AlignHCenter
|
horizontalAlignment: Text.AlignHCenter
|
||||||
}
|
}
|
||||||
@@ -133,15 +147,20 @@ PanelWindow {
|
|||||||
visible: root.error !== ""
|
visible: root.error !== ""
|
||||||
text: root.error
|
text: root.error
|
||||||
color: root.bar.urgent
|
color: root.bar.urgent
|
||||||
|
font.family: root.bar.fontFamily
|
||||||
|
font.pixelSize: Style.font.bodySmall
|
||||||
wrapMode: Text.Wrap
|
wrapMode: Text.Wrap
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
|
Layout.maximumWidth: Style.space(320)
|
||||||
horizontalAlignment: Text.AlignHCenter
|
horizontalAlignment: Text.AlignHCenter
|
||||||
}
|
}
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
visible: root.showingQr
|
visible: root.showingQr
|
||||||
text: "Scan to join this network"
|
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
|
Layout.fillWidth: true
|
||||||
horizontalAlignment: Text.AlignHCenter
|
horizontalAlignment: Text.AlignHCenter
|
||||||
}
|
}
|
||||||
@@ -157,6 +176,7 @@ PanelWindow {
|
|||||||
font.pixelSize: Style.font.bodySmall
|
font.pixelSize: Style.font.bodySmall
|
||||||
wrapMode: Text.WrapAnywhere
|
wrapMode: Text.WrapAnywhere
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
|
Layout.maximumWidth: Style.space(320)
|
||||||
horizontalAlignment: Text.AlignHCenter
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"name": "Network",
|
"name": "Network",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"author": "Omarchy",
|
"author": "Omarchy",
|
||||||
"description": "Wi-Fi list, connection state, and QR sharing",
|
"description": "Wi-Fi list, connection state, QR sharing, and speed test",
|
||||||
"kinds": [
|
"kinds": [
|
||||||
"bar-widget"
|
"bar-widget"
|
||||||
],
|
],
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
},
|
},
|
||||||
"barWidget": {
|
"barWidget": {
|
||||||
"displayName": "Network",
|
"displayName": "Network",
|
||||||
"description": "Wi-Fi list, connection state, and QR sharing",
|
"description": "Wi-Fi list, connection state, QR sharing, and speed test",
|
||||||
"category": "Network",
|
"category": "Network",
|
||||||
"allowMultiple": false
|
"allowMultiple": false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ Panel {
|
|||||||
onLocationQueryChanged: {
|
onLocationQueryChanged: {
|
||||||
if (savingLocation) savingLocationQueryStarted = true
|
if (savingLocation) savingLocationQueryStarted = true
|
||||||
forecastRetries = 0
|
forecastRetries = 0
|
||||||
|
dailyForecastRetries = 0
|
||||||
forecastProc.running = false
|
forecastProc.running = false
|
||||||
dailyForecastProc.running = false
|
dailyForecastProc.running = false
|
||||||
Qt.callLater(refresh)
|
Qt.callLater(refresh)
|
||||||
@@ -112,6 +113,7 @@ Panel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
property int forecastRetries: 0
|
property int forecastRetries: 0
|
||||||
|
property int dailyForecastRetries: 0
|
||||||
|
|
||||||
// Click-to-edit state for the location label.
|
// Click-to-edit state for the location label.
|
||||||
property bool editingLocation: false
|
property bool editingLocation: false
|
||||||
@@ -148,6 +150,11 @@ Panel {
|
|||||||
readonly property string reportHumidity: current ? (current.humidity + "%") : ""
|
readonly property string reportHumidity: current ? (current.humidity + "%") : ""
|
||||||
|
|
||||||
function refresh() {
|
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 (!forecastProc.running) forecastProc.running = true
|
||||||
if (root.locationQuery === "" && !locationProc.running) locationProc.running = true
|
if (root.locationQuery === "" && !locationProc.running) locationProc.running = true
|
||||||
// With stored coordinates this fetches open-meteo right away — no need
|
// With stored coordinates this fetches open-meteo right away — no need
|
||||||
@@ -368,22 +375,42 @@ Panel {
|
|||||||
onTriggered: if (!forecastProc.running) forecastProc.running = true
|
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 {
|
Process {
|
||||||
id: dailyForecastProc
|
id: dailyForecastProc
|
||||||
stdout: StdioCollector {
|
stdout: StdioCollector {
|
||||||
waitForEnd: true
|
waitForEnd: true
|
||||||
onStreamFinished: {
|
onStreamFinished: {
|
||||||
var raw = String(text || "").trim()
|
var raw = String(text || "").trim()
|
||||||
if (!raw) return
|
if (!raw) {
|
||||||
|
root.scheduleDailyForecastRetry()
|
||||||
|
return
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
var parsed = JSON.parse(raw)
|
var parsed = JSON.parse(raw)
|
||||||
var parsedCurrent = Model.openMeteoCurrentCondition(parsed)
|
var parsedCurrent = Model.openMeteoCurrentCondition(parsed)
|
||||||
root.dailyForecastReport = parsed
|
root.dailyForecastReport = parsed
|
||||||
root.label = Model.currentIcon(parsedCurrent, root.label)
|
root.label = Model.currentIcon(parsedCurrent, root.label)
|
||||||
|
root.dailyForecastRetries = 0
|
||||||
if (Model.weatherResponseCompletesSave(root.hasConfiguredCoordinates, "open-meteo"))
|
if (Model.weatherResponseCompletesSave(root.hasConfiguredCoordinates, "open-meteo"))
|
||||||
root.finishSavingLocation()
|
root.finishSavingLocation()
|
||||||
} catch (e) {
|
} 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) {
|
if (!root.savingLocationQueryStarted) {
|
||||||
root.savingLocationQueryStarted = true
|
root.savingLocationQueryStarted = true
|
||||||
root.forecastRetries = 0
|
root.forecastRetries = 0
|
||||||
|
root.dailyForecastRetries = 0
|
||||||
forecastProc.running = false
|
forecastProc.running = false
|
||||||
dailyForecastProc.running = false
|
dailyForecastProc.running = false
|
||||||
Qt.callLater(root.refresh)
|
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[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, 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.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')
|
assertEqual(calendar.isoWeekLiteral(2026, 0, 5), '02', 'clock zero-pads the ISO week token')
|
||||||
|
|
||||||
// ---- widget wiring
|
// ---- 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"
|
grep -Fx "$omp_package omp" "$stub_log" >/dev/null || fail "Oh My Pi migration creates a working lazy stub"
|
||||||
|
|
||||||
: >"$stub_log"
|
: >"$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 "$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 "$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"
|
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
|
"$ROOT/bin/omarchy-mise-install" oh-my-pi omp
|
||||||
: >"$stub_log"
|
: >"$stub_log"
|
||||||
source "$ROOT/migrations/1785617047.sh" >/dev/null
|
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"
|
[[ ! -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"
|
[[ ! -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"
|
rm "$test_home/.local/state/omarchy/preinstalls-removed"
|
||||||
|
|||||||
@@ -14,7 +14,9 @@ fake_hyprctl="$test_tmp/hyprctl"
|
|||||||
signature="test-signature"
|
signature="test-signature"
|
||||||
runtime_dir="$run_root/1000"
|
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'
|
cat >"$fake_hyprctl" <<'BASH'
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
@@ -22,6 +24,10 @@ cat >"$fake_hyprctl" <<'BASH'
|
|||||||
printf '%s\t%s\n' "$XDG_RUNTIME_DIR" "$*" >>"$FAKE_HYPRCTL_LOG"
|
printf '%s\t%s\n' "$XDG_RUNTIME_DIR" "$*" >>"$FAKE_HYPRCTL_LOG"
|
||||||
|
|
||||||
case "$*" in
|
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'*)
|
*'getoption misc.disable_autoreload'*)
|
||||||
printf '{"option":"misc.disable_autoreload","bool":%s,"set":true}\n' "${FAKE_DISABLE_AUTORELOAD:-false}"
|
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" \
|
HYPRCTL="$fake_hyprctl" \
|
||||||
OMARCHY_HYPRLAND_RELOAD_GUARD_RUN_ROOT="$run_root" \
|
OMARCHY_HYPRLAND_RELOAD_GUARD_RUN_ROOT="$run_root" \
|
||||||
OMARCHY_HYPRLAND_RELOAD_GUARD_STATE_DIR="$state_dir" \
|
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"
|
state_file="$state_dir/$signature"
|
||||||
[[ -f $state_file ]] || fail "reload guard stores Hyprland state on pause"
|
[[ -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"
|
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"
|
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"
|
: >"$hyprctl_log"
|
||||||
FAKE_HYPRCTL_LOG="$hyprctl_log" \
|
FAKE_HYPRCTL_LOG="$hyprctl_log" \
|
||||||
HYPRCTL="$fake_hyprctl" \
|
HYPRCTL="$fake_hyprctl" \
|
||||||
|
|||||||
@@ -10,7 +10,9 @@ mkdir -p "$tmp/bin"
|
|||||||
|
|
||||||
cat >"$tmp/bin/nmcli" <<'EOF'
|
cat >"$tmp/bin/nmcli" <<'EOF'
|
||||||
#!/bin/bash
|
#!/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
|
echo test-uuid
|
||||||
else
|
else
|
||||||
printf '%s' "$QR_NMCLI_FIELDS"
|
printf '%s' "$QR_NMCLI_FIELDS"
|
||||||
@@ -30,11 +32,12 @@ chmod +x "$tmp/bin/nmcli" "$tmp/bin/qrencode"
|
|||||||
|
|
||||||
run_success_case() {
|
run_success_case() {
|
||||||
local description=$1 fields=$2 expected_payload=$3
|
local description=$1 fields=$2 expected_payload=$3
|
||||||
|
shift 3
|
||||||
local expected output payload
|
local expected output payload
|
||||||
|
|
||||||
export QR_NMCLI_FIELDS=$fields
|
export QR_NMCLI_FIELDS=$fields
|
||||||
export QR_PAYLOAD_FILE="$tmp/payload"
|
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'
|
expected=$'100\n010\n001'
|
||||||
[[ $output == "$expected" ]] || fail "$description emits a compact module matrix" "expected: $expected\nactual: $output"
|
[[ $output == "$expected" ]] || fail "$description emits a compact module matrix" "expected: $expected\nactual: $output"
|
||||||
|
|
||||||
@@ -46,24 +49,34 @@ run_success_case() {
|
|||||||
run_success_case \
|
run_success_case \
|
||||||
"network QR helper escapes WPA credentials through stdin" \
|
"network QR helper escapes WPA credentials through stdin" \
|
||||||
$'Cafe;Guest\\5G\nwpa-psk\np,a:ss;word\\42\nno\n' \
|
$'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 \
|
run_success_case \
|
||||||
"network QR helper supports open networks" \
|
"network QR helper supports open networks" \
|
||||||
$'Cafe Open\nnone\n\nno\n' \
|
$'Cafe Open\nnone\n\nno\n' \
|
||||||
'WIFI:T:nopass;S:Cafe Open;P:;;'
|
'WIFI:T:nopass;S:Cafe Open;P:;;' \
|
||||||
|
wlan0
|
||||||
|
|
||||||
run_success_case \
|
run_success_case \
|
||||||
"network QR helper marks hidden networks" \
|
"network QR helper marks hidden networks" \
|
||||||
$'Hidden Network\nwpa-psk\nsecret\nyes\n' \
|
$'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
|
# NetworkManager models WEP as key-mgmt "none" plus a wep-key, which must not
|
||||||
# be mistaken for an open network.
|
# be mistaken for an open network.
|
||||||
run_success_case \
|
run_success_case \
|
||||||
"network QR helper encodes WEP networks" \
|
"network QR helper encodes WEP networks" \
|
||||||
$'Old Router\nnone\n\nno\nwep-secret\n' \
|
$'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_NMCLI_FIELDS=$'Enterprise\nwpa-eap\nsecret\nno\n'
|
||||||
export QR_PAYLOAD_FILE="$tmp/enterprise-payload"
|
export QR_PAYLOAD_FILE="$tmp/enterprise-payload"
|
||||||
|
|||||||
@@ -85,7 +85,6 @@ assertDeepEqual(
|
|||||||
|
|
||||||
assertEqual(network.formatBytes(1536), '1.5 KB', 'network formats bytes')
|
assertEqual(network.formatBytes(1536), '1.5 KB', 'network formats bytes')
|
||||||
assertEqual(network.formatRate(1536), '1.5 KB/s', 'network formats rates')
|
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('2.54'), '2.5 ms', 'network formats low ping with precision')
|
||||||
assertEqual(network.formatPingLatency('25.4'), '25 ms', 'network formats ping')
|
assertEqual(network.formatPingLatency('25.4'), '25 ms', 'network formats ping')
|
||||||
assertEqual(network.formatPingLatency(''), 'Timeout', 'network formats missing ping as timeout')
|
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")"
|
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"
|
pass "alert removal leaves an already-empty list alone"
|
||||||
|
|
||||||
(($(wc -l <"$SHELL_RESTARTS") == 1)) || fail "alert removal restarts the shell"
|
# The running shell hot-reloads shell.json and the post-update restart is
|
||||||
[[ ! -s $STATE_CALLS ]] || fail "a restarted shell needs no deferred restart" "$(cat "$STATE_CALLS")"
|
# unconditional, so the migration itself never touches the shell.
|
||||||
pass "alert removal restarts 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")"
|
||||||
# Migrations run from a TTY or over ssh have no shell to restart, and stopping
|
pass "alert removal leaves shell restarts to the update"
|
||||||
# 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"
|
|
||||||
|
|||||||
Reference in New Issue
Block a user