8 Commits

Author SHA1 Message Date
4aaa1d2d98 UI: Fixes Sizes 2026-09-05 22:16:10 +01:00
14b7e748d4 Feature: Add pinned tray for Bitwarden and Sunshine
Replaces the disabled generic system tray with a curated PinnedTray
showing just Bitwarden and Sunshine. Each shows its live tray icon
(and forwards clicks to it) while running; when not running it shows
a dimmed static icon and launches the app on click instead.

Right-clicking Sunshine specifically opens its web UI in the browser
rather than forwarding to secondaryActivate(), since that's how it's
actually configured/used.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-05 22:10:21 +01:00
a0b9ea29fe Feature: Added options for mullvad vpn 2026-09-05 21:56:42 +01:00
007843db6e Fix: Not to render on display with HEADLESS 2026-09-05 21:55:57 +01:00
764652d826 Feature: Added new window for steam big picture 2026-09-05 21:55:16 +01:00
8dedfe30fa Merge pull request 'Removal: Stopped Waybar from starting' (#7) from maintinance/remove_waybar into main
Reviewed-on: #7

granted
2026-08-27 15:02:44 +01:00
bc54a4b5b7 Removal: Stopped Waybar from starting 2026-08-27 15:01:45 +01:00
d306270dc8 Merge pull request 'features/adding_quickshell' (#6) from features/adding_quickshell into main
Reviewed-on: #6

tested on home laptop
tested in home PC

will need to make install scripts for process

approved
2026-08-27 14:55:29 +01:00
8 changed files with 623 additions and 36 deletions

View File

@@ -14,6 +14,9 @@ hl.on("hyprland.start", function()
hl.exec_cmd("dbus-update-activation-environment --systemd WAYLAND_DISPLAY XDG_CURRENT_DESKTOP") hl.exec_cmd("dbus-update-activation-environment --systemd WAYLAND_DISPLAY XDG_CURRENT_DESKTOP")
hl.exec_cmd("systemctl --user import-environment WAYLAND_DISPLAY XDG_CURRENT_DESKTOP") hl.exec_cmd("systemctl --user import-environment WAYLAND_DISPLAY XDG_CURRENT_DESKTOP")
-- Sunshine
hl.exec_cmd("sleep 1 && hyprctl output create headless")
-- Polkit (Polkit Gnome / KDE) -- Polkit (Polkit Gnome / KDE)
hl.exec_cmd(scriptsDir .. "/Polkit.sh") hl.exec_cmd(scriptsDir .. "/Polkit.sh")
@@ -23,7 +26,7 @@ hl.on("hyprland.start", function()
-- hl.exec_cmd("ags") -- hl.exec_cmd("ags")
-- hl.exec_cmd("blueman-applet") -- hl.exec_cmd("blueman-applet")
-- hl.exec_cmd("rog-control-center") -- hl.exec_cmd("rog-control-center")
hl.exec_cmd("waybar") -- hl.exec_cmd("waybar")
hl.exec_cmd("qs") -- quickshell AGS Desktop Overview alternative hl.exec_cmd("qs") -- quickshell AGS Desktop Overview alternative
-- clipboard manager -- clipboard manager
@@ -31,7 +34,10 @@ hl.on("hyprland.start", function()
hl.exec_cmd("wl-paste --type image --watch cliphist store") hl.exec_cmd("wl-paste --type image --watch cliphist store")
-- Starting hypridle to start hyprlock -- Starting hypridle to start hyprlock
hl.exec_cmd("hypridle") -- hl.exec_cmd("hypridle")
-- Sunshine for streaming media
h1.exec_cmd("sunshine")
-- Float + center the Bitwarden extension pop-out (window rules can't catch it — see the script) -- Float + center the Bitwarden extension pop-out (window rules can't catch it — see the script)
hl.exec_cmd(scriptsDir .. "/FloatBitwardenPopup.sh") hl.exec_cmd(scriptsDir .. "/FloatBitwardenPopup.sh")

View File

@@ -11,4 +11,5 @@
hl.monitor({ output = "desc:LG Electronics LG ULTRAWIDE 0x0002E68A", mode = "2560x1080@60.00000", position = "1920x-740", scale = 1.00000000, transform = 3, vrr = 0 }) hl.monitor({ output = "desc:LG Electronics LG ULTRAWIDE 0x0002E68A", mode = "2560x1080@60.00000", position = "1920x-740", scale = 1.00000000, transform = 3, vrr = 0 })
hl.monitor({ output = "desc:HP Inc. HP 524pf VNG447017D", mode = "1920x1080@100.00000", position = "-1920x0", scale = 1.00000000, transform = 0, vrr = 0 }) hl.monitor({ output = "desc:HP Inc. HP 524pf VNG447017D", mode = "1920x1080@100.00000", position = "-1920x0", scale = 1.00000000, transform = 0, vrr = 0 })
hl.monitor({ output = "desc:Acer Technologies VG240Y S 0x126191DE", mode = "1920x1080@165.00301", position = "0x0", scale = 1.00000000, transform = 0, vrr = 1, bitdepth = 10, cm = "wide" }) hl.monitor({ output = "desc:Acer Technologies VG240Y S 0x126191DE", mode = "1920x1080@165.00301", position = "0x0", scale = 1.00000000, transform = 0, vrr = 1, bitdepth = 10, cm = "wide" })
hl.monitor({ output = "HEADLESS-1", mode = "1728x1080@60.00000", position = "4480x0", scale = 1.00000000, transform = 0, vrr = 0 })
-- <<<<< TUI AUTO END -- <<<<< TUI AUTO END

View File

@@ -13,6 +13,7 @@
hl.workspace_rule({ workspace = 1, monitor = "DP-2", default = true }) hl.workspace_rule({ workspace = 1, monitor = "DP-2", default = true })
hl.workspace_rule({ workspace = 2, monitor = "DP-1", default = true }) hl.workspace_rule({ workspace = 2, monitor = "DP-1", default = true })
hl.workspace_rule({ workspace = 3, monitor = "HDMI-A-1", default = true }) hl.workspace_rule({ workspace = 3, monitor = "HDMI-A-1", default = true })
hl.workspace_rule({ workspace = 9, monitor = "HEADLESS-1", default = true })
-- hl.workspace_rule({ workspace = 4, monitor = "eDP-1" }) -- hl.workspace_rule({ workspace = 4, monitor = "eDP-1" })
-- hl.workspace_rule({ workspace = 5, monitor = "DP-2" }) -- hl.workspace_rule({ workspace = 5, monitor = "DP-2" })
-- hl.workspace_rule({ workspace = 6, monitor = "DP-2" }) -- hl.workspace_rule({ workspace = 6, monitor = "DP-2" })

View File

@@ -46,7 +46,7 @@ PanelWindow {
rightMargin: Appearance.spacing rightMargin: Appearance.spacing
} }
spacing: Appearance.spacing spacing: Appearance.spacing
//Tray {} PinnedTray {}
TailscaleIndicator { barWindow: bar } TailscaleIndicator { barWindow: bar }
VpnIndicator { barWindow: bar } VpnIndicator { barWindow: bar }
NetworkIndicator { barWindow: bar } NetworkIndicator { barWindow: bar }

View File

@@ -0,0 +1,101 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import Quickshell.Services.SystemTray
import Quickshell.Widgets
import "../../config"
// A curated stand-in for the full system tray (see Tray.qml) — just the
// couple of apps worth one click away, rather than every SNI item any
// background app happens to register. Each entry shows that app's own live
// tray icon (and forwards clicks to it, same as Tray.qml) while it's
// running; when it isn't, it falls back to the app's static icon, dimmed,
// and a click launches it instead of doing nothing.
RowLayout {
id: root
spacing: Appearance.spacing * 1
// name: matched case-insensitively against a tray item's id/title to
// find this app's live icon, once it's running.
// icon: themed icon name shown in place of a live tray icon.
// launch: command run on left-click while not running.
// secondary: optional command run on right-click, regardless of running
// state, in place of forwarding to the tray item's own
// secondaryActivate() (which for most apps' tray icons does
// nothing useful anyway).
readonly property var pinned: [
{
name: "Bitwarden",
icon: "bitwarden",
launch: ["bitwarden-desktop", "--ozone-platform-hint=auto", "--silent"],
},
{
name: "Sunshine",
icon: "dev.lizardbyte.app.Sunshine",
launch: ["systemctl", "--user", "start", "app-dev.lizardbyte.app.Sunshine.service"],
// Sunshine is configured entirely through its web UI — there's
// no separate desktop app to "open" here.
secondary: ["xdg-open", "https://localhost:47990"],
},
]
function findTrayItem(name) {
const needle = name.toLowerCase()
for (const item of SystemTray.items.values) {
if (item.id.toLowerCase().includes(needle) || item.title.toLowerCase().includes(needle))
return item
}
return null
}
Repeater {
model: root.pinned
delegate: Item {
id: slot
required property var modelData
readonly property var trayItem: root.findTrayItem(modelData.name)
readonly property bool running: trayItem !== null
implicitWidth: Appearance.iconSize
implicitHeight: Appearance.iconSize
IconImage {
anchors.fill: parent
source: slot.running ? slot.trayItem.icon : Quickshell.iconPath(slot.modelData.icon)
// Not-running is the only cue we give beyond the click
// behavior itself — dimmed rather than hidden or badged, so
// the icon stays put and clickable either way.
opacity: slot.running ? 1.0 : 0.5
}
Process { id: launcher }
Process { id: secondaryProc }
MouseArea {
anchors.fill: parent
anchors.margins: -4
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: (mouse) => {
if (mouse.button === Qt.RightButton) {
if (slot.modelData.secondary) {
if (!secondaryProc.running) {
secondaryProc.command = slot.modelData.secondary
secondaryProc.running = true
}
} else if (slot.running) {
slot.trayItem.secondaryActivate()
}
} else if (slot.running) {
slot.trayItem.activate()
} else if (!launcher.running) {
launcher.command = slot.modelData.launch
launcher.running = true
}
}
}
}
}
}

View File

@@ -5,7 +5,7 @@ import Quickshell.Widgets
import "../../config" import "../../config"
RowLayout { RowLayout {
spacing: Appearance.spacing * 0.6 spacing: Appearance.spacing
Repeater { Repeater {
model: SystemTray.items model: SystemTray.items

View File

@@ -45,9 +45,41 @@ Item {
// to connect once the current one has finished disconnecting. // to connect once the current one has finished disconnecting.
property string pendingConnect: "" property string pendingConnect: ""
// Surfaced under the header for a failed connect/disconnect; cleared // Surfaced under each provider's own row for a failed connect/
// automatically after a few seconds. // disconnect/settings change; cleared automatically after a few
property string actionStatus: "" // seconds. Kept separate per provider so an error from one doesn't
// show up hanging under the other's row.
property string mullvadActionStatus: ""
property string protonActionStatus: ""
// --- Mullvad advanced options (auto-connect, relay location, multihop) --
// ProtonVPN's CLI is plain-text-only and far less predictable across
// versions, so this extra panel is Mullvad-specific rather than a
// second ProviderRow feature.
property bool mullvadOptionsOpen: false
property bool mullvadAutoConnect: false
property bool mullvadMultihop: false
// Human-readable current relay constraint, e.g. "Any" or "Sweden" —
// resolved from `relay get`'s "country xx[ city yyy]" syntax against
// mullvadCountries once that list has loaded.
property string mullvadLocationLabel: "Any"
property string mullvadEntryLabel: "Any"
// [{name, code}], parsed from `mullvad relay list` and cached for the
// life of the panel — the full relay list rarely changes and re-parsing
// ~50 countries on every open would just be wasted work.
property var mullvadCountries: []
// "" | "exit" | "entry" — which location field's picker is open, if any.
property string mullvadPickerTarget: ""
property string mullvadPickerFilter: ""
readonly property var mullvadFilteredCountries: {
const f = root.mullvadPickerFilter.trim().toLowerCase()
if (!f) return root.mullvadCountries
return root.mullvadCountries.filter(function(c) {
return c.name.toLowerCase().indexOf(f) !== -1 || c.code.indexOf(f) !== -1
})
}
function shortLine(text) { function shortLine(text) {
const line = String(text || "").trim().split("\n")[0] || "" const line = String(text || "").trim().split("\n")[0] || ""
@@ -82,6 +114,95 @@ Item {
VpnExclusivity.protonConnected = root.protonState === "connected" VpnExclusivity.protonConnected = root.protonState === "connected"
} }
function applyMullvadAutoConnect(text) {
const m = String(text || "").match(/autoconnect:\s*(on|off)/i)
root.mullvadAutoConnect = !!m && m[1].toLowerCase() === "on"
}
// Resolves relay get's "any" / "country xx" / "country xx city yyy" /
// bare-hostname syntax into a display label. Falls back to the raw
// country code (or the raw string) when mullvadCountries hasn't loaded
// yet, rather than showing nothing.
function describeMullvadLocation(raw) {
const s = String(raw || "").trim()
if (!s || s === "any") return "Any"
const m = s.match(/^country\s+([a-z]{2})\b/i)
if (!m) return s
const code = m[1].toLowerCase()
const country = root.mullvadCountries.find(function(c) { return c.code === code })
return country ? country.name : code.toUpperCase()
}
function applyMullvadRelay(text) {
const body = String(text || "")
const locMatch = body.match(/^\s*Location:\s*(.+)$/mi)
const multihopMatch = body.match(/^\s*Multihop state:\s*(\w+)/mi)
const entryMatch = body.match(/^\s*Multihop entry:\s*(.+)$/mi)
root.mullvadMultihop = !!multihopMatch && multihopMatch[1].toLowerCase() === "enabled"
root.mullvadLocationLabel = root.describeMullvadLocation(locMatch ? locMatch[1].trim() : "")
root.mullvadEntryLabel = root.describeMullvadLocation(entryMatch ? entryMatch[1].trim() : "")
}
// Only country-level lines are of interest ("Sweden (se)", flush left) —
// city ("\tGothenburg (got) @ ...") and server lines are indented and
// their parenthesised code is never exactly two letters, so the regex
// alone is enough to tell them apart without checking indentation.
function applyMullvadCountries(text) {
const lines = String(text || "").split("\n")
const countries = []
for (const line of lines) {
const m = line.match(/^(\S.*) \(([a-z]{2})\)\s*$/)
if (m) countries.push({ name: m[1].trim(), code: m[2] })
}
countries.sort(function(a, b) { return a.name.localeCompare(b.name) })
root.mullvadCountries = countries
// Re-resolve any labels already showing raw codes now that names
// are available.
if (!mullvadRelayGetProc.running) mullvadRelayGetProc.running = true
}
function openMullvadOptions() {
root.mullvadOptionsOpen = !root.mullvadOptionsOpen
if (root.mullvadOptionsOpen) {
if (!mullvadAutoConnectGetProc.running) mullvadAutoConnectGetProc.running = true
if (!mullvadRelayGetProc.running) mullvadRelayGetProc.running = true
if (root.mullvadCountries.length === 0 && !mullvadRelayListProc.running) mullvadRelayListProc.running = true
} else {
root.mullvadPickerTarget = ""
}
}
function openMullvadPicker(target) {
root.mullvadPickerFilter = ""
root.mullvadPickerTarget = root.mullvadPickerTarget === target ? "" : target
}
function pickMullvadLocation(code) {
const target = root.mullvadPickerTarget
root.mullvadPickerTarget = ""
root.setMullvadLocation(target, code)
}
function setMullvadAutoConnect(on) {
if (mullvadAutoConnectSetProc.running) return
mullvadAutoConnectSetProc.command = ["mullvad", "auto-connect", "set", on ? "on" : "off"]
mullvadAutoConnectSetProc.running = true
}
function setMullvadMultihop(on) {
if (mullvadRelaySetProc.running) return
mullvadRelaySetProc.command = ["mullvad", "relay", "set", "multihop", on ? "on" : "off"]
mullvadRelaySetProc.running = true
}
function setMullvadLocation(target, code) {
if (mullvadRelaySetProc.running) return
mullvadRelaySetProc.command = target === "entry"
? ["mullvad", "relay", "set", "entry", "location", code]
: ["mullvad", "relay", "set", "location", code]
mullvadRelaySetProc.running = true
}
function refreshMullvad() { function refreshMullvad() {
if (!root.mullvadInstalled) { if (!root.mullvadInstalled) {
if (!mullvadWhichProc.running) mullvadWhichProc.running = true if (!mullvadWhichProc.running) mullvadWhichProc.running = true
@@ -197,8 +318,8 @@ Item {
stderr: StdioCollector { id: mullvadConnectErr } stderr: StdioCollector { id: mullvadConnectErr }
onExited: (exitCode, exitStatus) => { onExited: (exitCode, exitStatus) => {
if (exitCode !== 0) { if (exitCode !== 0) {
root.actionStatus = "Mullvad: " + root.shortLine(mullvadConnectErr.text || "connect failed") root.mullvadActionStatus = root.shortLine(mullvadConnectErr.text || "Connect failed")
actionStatusTimer.restart() mullvadActionStatusTimer.restart()
} else if (!tailscaleDownProc.running) { } else if (!tailscaleDownProc.running) {
tailscaleDownProc.running = true tailscaleDownProc.running = true
} }
@@ -213,8 +334,8 @@ Item {
stderr: StdioCollector { id: mullvadDisconnectErr } stderr: StdioCollector { id: mullvadDisconnectErr }
onExited: (exitCode, exitStatus) => { onExited: (exitCode, exitStatus) => {
if (exitCode !== 0) { if (exitCode !== 0) {
root.actionStatus = "Mullvad: " + root.shortLine(mullvadDisconnectErr.text || "disconnect failed") root.mullvadActionStatus = root.shortLine(mullvadDisconnectErr.text || "Disconnect failed")
actionStatusTimer.restart() mullvadActionStatusTimer.restart()
} }
root.refreshMullvad() root.refreshMullvad()
if (root.pendingConnect === "mullvad") { root.pendingConnect = ""; root.connectMullvad() } if (root.pendingConnect === "mullvad") { root.pendingConnect = ""; root.connectMullvad() }
@@ -229,6 +350,77 @@ Item {
onTriggered: if (mullvadStatusProc.running) mullvadStatusProc.running = false onTriggered: if (mullvadStatusProc.running) mullvadStatusProc.running = false
} }
// --- Mullvad advanced options: auto-connect, relay location, multihop --
// Only queried while the options section is open (see
// root.openMullvadOptions), not on the regular status poll — these
// don't change on their own the way tunnel state does.
Process {
id: mullvadAutoConnectGetProc
command: ["mullvad", "auto-connect", "get"]
stdout: StdioCollector {
id: mullvadAutoConnectGetCollector
onStreamFinished: root.applyMullvadAutoConnect(mullvadAutoConnectGetCollector.text)
}
}
Process {
id: mullvadAutoConnectSetProc
stdout: StdioCollector {}
stderr: StdioCollector { id: mullvadAutoConnectSetErr }
onExited: (exitCode, exitStatus) => {
if (exitCode !== 0) {
root.mullvadActionStatus = root.shortLine(mullvadAutoConnectSetErr.text || "Auto-connect change failed")
mullvadActionStatusTimer.restart()
}
if (!mullvadAutoConnectGetProc.running) mullvadAutoConnectGetProc.running = true
}
}
Process {
id: mullvadRelayGetProc
command: ["mullvad", "relay", "get"]
stdout: StdioCollector {
id: mullvadRelayGetCollector
onStreamFinished: root.applyMullvadRelay(mullvadRelayGetCollector.text)
}
}
Process {
id: mullvadRelayListProc
command: ["mullvad", "relay", "list"]
stdout: StdioCollector {
id: mullvadRelayListCollector
onStreamFinished: root.applyMullvadCountries(mullvadRelayListCollector.text)
}
}
Process {
id: mullvadRelaySetProc
stdout: StdioCollector {}
stderr: StdioCollector { id: mullvadRelaySetErr }
onExited: (exitCode, exitStatus) => {
if (exitCode !== 0) {
root.mullvadActionStatus = root.shortLine(mullvadRelaySetErr.text || "Relay change failed")
mullvadActionStatusTimer.restart()
}
if (!mullvadRelayGetProc.running) mullvadRelayGetProc.running = true
// Relay constraint changes don't take effect on an already-open
// tunnel by themselves — kick a reconnect so switching location/
// multihop while connected actually moves the tunnel, instead of
// silently only applying on the next manual reconnect.
if (root.mullvadState === "connected" && !mullvadReconnectProc.running) mullvadReconnectProc.running = true
}
}
Process {
id: mullvadReconnectProc
command: ["mullvad", "reconnect"]
stdout: StdioCollector {}
stderr: StdioCollector {}
onExited: (exitCode, exitStatus) => root.refreshMullvad()
}
// --- ProtonVPN --------------------------------------------------------- // --- ProtonVPN ---------------------------------------------------------
Process { Process {
@@ -261,8 +453,8 @@ Item {
stderr: StdioCollector { id: protonConnectErr } stderr: StdioCollector { id: protonConnectErr }
onExited: (exitCode, exitStatus) => { onExited: (exitCode, exitStatus) => {
if (exitCode !== 0) { if (exitCode !== 0) {
root.actionStatus = "ProtonVPN: " + root.shortLine(protonConnectErr.text || "connect failed") root.protonActionStatus = root.shortLine(protonConnectErr.text || "Connect failed")
actionStatusTimer.restart() protonActionStatusTimer.restart()
} else if (!tailscaleDownProc.running) { } else if (!tailscaleDownProc.running) {
tailscaleDownProc.running = true tailscaleDownProc.running = true
} }
@@ -277,8 +469,8 @@ Item {
stderr: StdioCollector { id: protonDisconnectErr } stderr: StdioCollector { id: protonDisconnectErr }
onExited: (exitCode, exitStatus) => { onExited: (exitCode, exitStatus) => {
if (exitCode !== 0) { if (exitCode !== 0) {
root.actionStatus = "ProtonVPN: " + root.shortLine(protonDisconnectErr.text || "disconnect failed") root.protonActionStatus = root.shortLine(protonDisconnectErr.text || "Disconnect failed")
actionStatusTimer.restart() protonActionStatusTimer.restart()
} }
root.refreshProton() root.refreshProton()
if (root.pendingConnect === "mullvad") { root.pendingConnect = ""; root.connectMullvad() } if (root.pendingConnect === "mullvad") { root.pendingConnect = ""; root.connectMullvad() }
@@ -305,10 +497,17 @@ Item {
} }
Timer { Timer {
id: actionStatusTimer id: mullvadActionStatusTimer
interval: 4000 interval: 4000
repeat: false repeat: false
onTriggered: root.actionStatus = "" onTriggered: root.mullvadActionStatus = ""
}
Timer {
id: protonActionStatusTimer
interval: 4000
repeat: false
onTriggered: root.protonActionStatus = ""
} }
// --- Bar icon ------------------------------------------------------------ // --- Bar icon ------------------------------------------------------------
@@ -416,7 +615,13 @@ Item {
property string detailText: "" property string detailText: ""
property string state: "disconnected" property string state: "disconnected"
property bool installed: true property bool installed: true
// When true, shows a chevron that expands a provider-specific
// options section below this row (used for Mullvad's advanced
// options; ProtonVPN's row leaves this false).
property bool expandable: false
property bool expanded: false
signal toggle() signal toggle()
signal expandToggle()
width: parent ? parent.width : 0 width: parent ? parent.width : 0
radius: 0 radius: 0
implicitHeight: rowContent.implicitHeight + 12 implicitHeight: rowContent.implicitHeight + 12
@@ -477,9 +682,91 @@ Item {
busy: provRow.state === "connecting" || provRow.state === "disconnecting" busy: provRow.state === "connecting" || provRow.state === "disconnecting"
onToggled: provRow.toggle() onToggled: provRow.toggle()
} }
Text {
visible: provRow.expandable
text: provRow.expanded ? "expand_less" : "expand_more"
font.family: Appearance.iconFontFamilyRounded
font.variableAxes: Appearance.iconFontAxes
font.pixelSize: 15
color: expandMouse.containsMouse ? Colors.foreground : Colors.muted
MouseArea {
id: expandMouse
anchors.fill: parent
anchors.margins: -5
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: provRow.expandToggle()
}
}
} }
} }
// A single clickable "Location: Sweden ›" row inside the Mullvad
// options section — used for both the exit location and (when multihop
// is on) the entry location. Clicking routes back to root, which opens
// the shared country picker below rather than each field owning one.
component LocationField: Item {
id: locField
property string fieldLabel: ""
property string valueText: "Any"
property bool open: false
signal clicked()
width: parent ? parent.width : 0
implicitHeight: locRow.implicitHeight
RowLayout {
id: locRow
width: parent.width
spacing: 6
Text {
text: locField.fieldLabel
font.family: Appearance.fontFamily
font.pixelSize: Appearance.fontSizeSmall
color: Colors.muted
}
Text {
Layout.fillWidth: true
horizontalAlignment: Text.AlignRight
elide: Text.ElideRight
text: locField.valueText
font.family: Appearance.fontFamily
font.pixelSize: Appearance.fontSizeSmall
font.bold: true
color: Colors.foreground
}
Text {
text: locField.open ? "expand_less" : "chevron_right"
font.family: Appearance.iconFontFamilyRounded
font.variableAxes: Appearance.iconFontAxes
font.pixelSize: 14
color: Colors.muted
}
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: locField.clicked()
}
}
// A failed connect/disconnect/settings-change message, shown directly
// under the provider row it came from rather than in one shared line
// for both providers — keeps an error attributed to whichever CLI it
// actually came from.
component StatusLine: Text {
property string message: ""
visible: message !== ""
width: parent ? parent.width : 0
text: message
font.family: Appearance.fontFamily
font.pixelSize: Appearance.fontSizeSmall
color: Colors.muted
wrapMode: Text.WordWrap
}
// --- Panel ----------------------------------------------------------- // --- Panel -----------------------------------------------------------
PopupWindow { PopupWindow {
@@ -494,7 +781,18 @@ Item {
NumberAnimation { duration: Appearance.animationDuration; easing.type: Easing.OutCubic } NumberAnimation { duration: Appearance.animationDuration; easing.type: Easing.OutCubic }
} }
onVisibleChanged: if (visible) root.refresh() // Leaving mullvadOptionsOpen itself alone across opens (it's a
// deliberate "keep this expanded" choice), but the country picker
// is transient state scoped to one visit — closing the panel with
// it open would otherwise leave a stale filter/list waiting.
onVisibleChanged: {
if (visible) {
root.refresh()
} else {
root.mullvadPickerTarget = ""
root.mullvadPickerFilter = ""
}
}
anchor { anchor {
window: root.barWindow window: root.barWindow
@@ -548,28 +846,202 @@ Item {
} }
} }
Text {
visible: root.actionStatus !== ""
width: parent.width
text: root.actionStatus
font.family: Appearance.fontFamily
font.pixelSize: Appearance.fontSizeSmall
color: Colors.muted
wrapMode: Text.WordWrap
}
Column { Column {
width: parent.width width: parent.width
spacing: 2 spacing: 2
ProviderRow { // Mullvad's connect row and its advanced options share a
label: "Mullvad" // single bordered card — the border wraps the whole
subIcon: "gpp_good" // section (row, status line, and options together) and
state: root.mullvadState // grows to include the options/picker as they open,
detailText: root.mullvadDetail // rather than the options having a separate box of
installed: root.mullvadInstalled // their own below an unbordered connect row.
onToggle: root.toggleMullvad() Rectangle {
width: parent.width
radius: 0
color: Colors.background
border.color: Colors.muted
border.width: 1
implicitHeight: mullvadSectionColumn.implicitHeight + 16
clip: true
// Without this, the card snapped to its new size
// instantly while the popup around it was still
// animating open, so the border visibly lagged
// behind (or cut off) content like the country
// picker appearing/disappearing beneath it.
Behavior on implicitHeight {
NumberAnimation { duration: Appearance.animationDuration; easing.type: Easing.OutCubic }
}
Column {
id: mullvadSectionColumn
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.margins: 8
spacing: 8
ProviderRow {
label: "Mullvad"
subIcon: "gpp_good"
state: root.mullvadState
detailText: root.mullvadDetail
installed: root.mullvadInstalled
expandable: root.mullvadInstalled
expanded: root.mullvadOptionsOpen
onToggle: root.toggleMullvad()
onExpandToggle: root.openMullvadOptions()
}
StatusLine { message: root.mullvadActionStatus }
// Advanced options — auto-connect, exit/entry
// relay location, multihop.
Column {
id: mullvadOptionsColumn
width: parent.width
visible: root.mullvadOptionsOpen && root.mullvadInstalled
spacing: 8
RowLayout {
width: parent.width
Text {
Layout.fillWidth: true
text: "Auto-connect"
font.family: Appearance.fontFamily
font.pixelSize: Appearance.fontSizeSmall
color: Colors.foreground
}
ToggleSwitch {
checked: root.mullvadAutoConnect
busy: mullvadAutoConnectSetProc.running
onToggled: root.setMullvadAutoConnect(!root.mullvadAutoConnect)
}
}
RowLayout {
width: parent.width
Text {
Layout.fillWidth: true
text: "Multihop"
font.family: Appearance.fontFamily
font.pixelSize: Appearance.fontSizeSmall
color: Colors.foreground
}
ToggleSwitch {
checked: root.mullvadMultihop
busy: mullvadRelaySetProc.running
onToggled: root.setMullvadMultihop(!root.mullvadMultihop)
}
}
LocationField {
fieldLabel: root.mullvadMultihop ? "Exit" : "Location"
valueText: root.mullvadLocationLabel
open: root.mullvadPickerTarget === "exit"
onClicked: root.openMullvadPicker("exit")
}
LocationField {
visible: root.mullvadMultihop
fieldLabel: "Entry"
valueText: root.mullvadEntryLabel
open: root.mullvadPickerTarget === "entry"
onClicked: root.openMullvadPicker("entry")
}
// Shared country picker for whichever LocationField
// was last clicked — filter box plus a capped,
// scrollable list ("Any" always pinned first).
Column {
width: parent.width
visible: root.mullvadPickerTarget !== ""
spacing: 6
Rectangle {
width: parent.width
implicitHeight: 24
radius: 0
color: Colors.surface
border.color: Colors.muted
border.width: 1
Text {
anchors.fill: parent
anchors.margins: 6
visible: mullvadFilterInput.text.length === 0
text: "Search country…"
font.family: Appearance.fontFamily
font.pixelSize: Appearance.fontSizeSmall
color: Colors.muted
}
TextInput {
id: mullvadFilterInput
anchors.fill: parent
anchors.margins: 6
color: Colors.foreground
font.family: Appearance.fontFamily
font.pixelSize: Appearance.fontSizeSmall
clip: true
focus: root.mullvadPickerTarget !== ""
text: root.mullvadPickerFilter
onTextChanged: root.mullvadPickerFilter = text
}
}
Flickable {
id: mullvadPickerFlick
width: parent.width
height: Math.min(mullvadPickerColumn.implicitHeight, 150)
contentWidth: width
contentHeight: mullvadPickerColumn.implicitHeight
clip: true
boundsBehavior: Flickable.StopAtBounds
Column {
id: mullvadPickerColumn
width: mullvadPickerFlick.width
spacing: 1
Repeater {
model: [{ name: "Any", code: "any" }].concat(root.mullvadFilteredCountries)
Rectangle {
required property var modelData
width: parent.width
implicitHeight: 22
radius: 0
color: countryMouse.containsMouse ? Colors.primary : "transparent"
Text {
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: 4
anchors.rightMargin: 4
elide: Text.ElideRight
text: modelData.name
font.family: Appearance.fontFamily
font.pixelSize: Appearance.fontSizeSmall
color: countryMouse.containsMouse ? Colors.primaryText : Colors.foreground
}
MouseArea {
id: countryMouse
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.pickMullvadLocation(modelData.code)
}
}
}
}
}
}
}
}
} }
ProviderRow { ProviderRow {
label: "ProtonVPN" label: "ProtonVPN"
subIcon: "gpp_good" subIcon: "gpp_good"
@@ -578,6 +1050,8 @@ Item {
installed: root.protonInstalled installed: root.protonInstalled
onToggle: root.toggleProton() onToggle: root.toggleProton()
} }
StatusLine { message: root.protonActionStatus }
} }
Text { Text {

View File

@@ -4,7 +4,11 @@ import "./modules/osd"
ShellRoot { ShellRoot {
Variants { Variants {
model: Quickshell.screens // Exclude the Sunshine headless virtual monitor — nobody ever sees a
// bar there, but PanelWindow's exclusiveZone still reserves space for
// it, which left a black strip cut out of Steam Big Picture's
// fullscreen area on that output.
model: Quickshell.screens.filter(s => !s.name.startsWith("HEADLESS"))
Bar { Bar {
required property var modelData required property var modelData