Four things a Hyprland desktop can do that this one was not. Searching from the launcher needed no launcher work at all: Vicinae already models it, so this is a script command with one percent-encoded argument. Make it the fallback command and anything typed that matches nothing else offers to search it. Bangs come free -- they are a property of where the query is sent, not of the launcher -- so !yt reaches YouTube without a line of bang parsing. Suggestions could not be a script command. They need a view that reacts as you type, which is an extension: TypeScript, compiled, querying the same endpoint Firefox's address bar uses. It debounces, and aborts the request in flight on every keystroke -- typing is faster than the network, and an older answer landing after a newer one leaves the list describing a query that is no longer on screen. A bang skips suggestions entirely, because Google has no useful guesses about "!yt". The engine is now written down twice, once in each. The contract pins that they agree, since searching from the fallback and searching from the suggestions reaching different places is the kind of wrong that looks fine. Gestures mirror GNOME: three fingers sideways for workspaces, up for the overview, down to dismiss it. Open and close rather than toggle both ways -- toggling means swiping up from an open overview closes it, which is not what the fingers meant. Hyprland reads gesture registrations at startup so they cannot be a setting, but distance and direction can be, and are. Window swallowing is off by default and a preference like every other misc setting here. A terminal that vanishes when you did not ask for it is confusing rather than broken, which is worse. Claude-Session: https://claude.ai/code/session_01Q84axqUE5inJhf5Jz9CFy1
318 lines
12 KiB
QML
318 lines
12 KiB
QML
// Desktop & Dock.
|
|
//
|
|
// The dock timings used to be shown here as text -- "Instant", "250 ms" -- even
|
|
// though they were already stored, mutable integers. They are controls now.
|
|
// The window-layout card keeps text rows because those really are facts about
|
|
// how Panama tiles rather than settings: the adjustable parts of window
|
|
// appearance live on the Appearance page, next to the preview that explains
|
|
// them.
|
|
|
|
import QtQuick
|
|
import Quickshell
|
|
import qs.config
|
|
import qs.services
|
|
|
|
SettingsPage {
|
|
id: root
|
|
|
|
// Turning the last screen off would leave no dock anywhere and no obvious
|
|
// way back, so the final one cannot be removed -- it collapses to "every
|
|
// screen" instead, which is the same thing on one display and recoverable
|
|
// on several.
|
|
function toggleDockScreen(name: string): void {
|
|
const all = Quickshell.screens.map(screen => String(screen.name));
|
|
const current = Settings.dockScreens.length === 0
|
|
? all.slice()
|
|
: Settings.dockScreens.map(String);
|
|
const at = current.indexOf(name);
|
|
let next = current.slice();
|
|
if (at >= 0)
|
|
next.splice(at, 1);
|
|
else
|
|
next.push(name);
|
|
if (next.length === 0 || next.length === all.length)
|
|
next = [];
|
|
DesktopPreferences.set("dockScreens", next);
|
|
}
|
|
|
|
title: "Desktop & Dock"
|
|
lede: "Keep the shell instant, spatial, and out of your way."
|
|
|
|
SettingsCard {
|
|
title: "Dock"
|
|
|
|
ChoiceRow { setting: "dockPosition" }
|
|
|
|
// One row per connected screen. Nothing selected means every screen,
|
|
// which is stated rather than left as an empty list somebody has to
|
|
// interpret -- and it is what a single-monitor machine should do
|
|
// without being configured at all.
|
|
SettingRow {
|
|
label: "Screens"
|
|
detail: Settings.dockScreens.length === 0
|
|
? "On every display"
|
|
: "On " + Settings.dockScreens.length + " of "
|
|
+ Quickshell.screens.length + " displays"
|
|
visible: Quickshell.screens.length > 1
|
|
controlWidth: 260
|
|
|
|
Row {
|
|
anchors.right: parent.right
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
spacing: 7
|
|
|
|
Repeater {
|
|
model: Quickshell.screens
|
|
|
|
delegate: Rectangle {
|
|
id: screenPill
|
|
|
|
required property var modelData
|
|
|
|
readonly property string screenName: String(screenPill.modelData.name ?? "")
|
|
// An empty list means all, so every pill reads as on.
|
|
readonly property bool on: Settings.dockScreens.length === 0
|
|
|| Settings.dockScreens.indexOf(screenPill.screenName) >= 0
|
|
|
|
width: pillLabel.implicitWidth + 20
|
|
height: 28
|
|
radius: 8
|
|
color: screenPill.on ? Theme.alpha(Theme.accent, 0.22)
|
|
: Theme.alpha(Theme.fg, 0.06)
|
|
border.width: screenPill.on ? 1 : 0
|
|
border.color: Theme.alpha(Theme.accent, 0.5)
|
|
|
|
Text {
|
|
id: pillLabel
|
|
anchors.centerIn: parent
|
|
text: screenPill.screenName
|
|
color: screenPill.on ? Theme.fg : Theme.fgDim
|
|
font.family: Theme.fontFamily
|
|
font.pixelSize: Theme.fontSizeSmall
|
|
}
|
|
|
|
HoverHandler { cursorShape: Qt.PointingHandCursor }
|
|
TapHandler { onTapped: root.toggleDockScreen(screenPill.screenName) }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
ToggleRow { setting: "dockAutohide" }
|
|
SliderRow { setting: "dockRevealDelayMs"; zeroLabel: "Instant" }
|
|
SliderRow { setting: "dockHideDelayMs"; zeroLabel: "Instant" }
|
|
SliderRow { setting: "dockIconSize"; divider: false }
|
|
}
|
|
|
|
SettingsCard {
|
|
title: "Pinned applications"
|
|
subtitle: "What sits in the Dock whether or not it is running. Order here is the order on screen."
|
|
|
|
DockPinsEditor {
|
|
id: pins
|
|
width: parent.width
|
|
}
|
|
}
|
|
|
|
SettingsCard {
|
|
title: "Pin another application"
|
|
|
|
DockAppPicker {
|
|
width: parent.width
|
|
pinned: pins.pinned
|
|
onPicked: id => pins.add(id)
|
|
}
|
|
}
|
|
|
|
SettingsCard {
|
|
title: "Window layout"
|
|
subtitle: "Follows the Forge mental model, with native Hyprland tiling."
|
|
|
|
// These were two read-only rows reporting "Tiling" and "Dynamic", which
|
|
// described settings rather than facts -- both are ordinary Hyprland
|
|
// options that simply had no controls. TextRow's own documentation says
|
|
// a setting the user could reasonably change does not belong in it.
|
|
ChoiceRow { setting: "windowLayout" }
|
|
ToggleRow { setting: "preserveSplit" }
|
|
ChoiceRow { setting: "forceSplit" }
|
|
ToggleRow { setting: "windowSnapping" }
|
|
ActionRow {
|
|
label: "Gaps, corners, and effects"
|
|
detail: "Adjusted on the Appearance page, beside a live preview"
|
|
action: "Open Appearance"
|
|
divider: false
|
|
onTriggered: ShellState.openSettingsSection("appearance", "windows")
|
|
}
|
|
}
|
|
|
|
// GNOME's Multitasking panel, in Hyprland's terms.
|
|
// Only meaningful when the layout above is Master and stack. Hidden
|
|
// otherwise, because a card of settings that do nothing under the layout
|
|
// you are actually running is worse than not offering the layout at all.
|
|
SettingsCard {
|
|
visible: DesktopPreferences.get("windowLayout") === "master"
|
|
title: "Master and stack"
|
|
subtitle: "How the master area behaves. These apply only while the tiling layout above is Master and stack."
|
|
|
|
SliderRow { setting: "masterFactor" }
|
|
ChoiceRow { setting: "masterOrientation" }
|
|
ChoiceRow { setting: "masterNewStatus" }
|
|
ToggleRow { setting: "masterNewOnTop"; divider: false }
|
|
}
|
|
|
|
SettingsCard {
|
|
title: "Window edges"
|
|
subtitle: "How the pointer grabs a window's border, and how floating windows behave near each other and the screen edge."
|
|
|
|
ToggleRow { setting: "resizeOnBorder" }
|
|
SliderRow { setting: "borderGrabArea"; zeroLabel: "Border only" }
|
|
ToggleRow { setting: "hoverIconOnBorder" }
|
|
SliderRow { setting: "snapWindowGap"; zeroLabel: "Touching" }
|
|
SliderRow { setting: "snapMonitorGap"; zeroLabel: "Touching" }
|
|
ToggleRow { setting: "snapRespectGaps"; divider: false }
|
|
}
|
|
|
|
// Hyprland's own interruptions. Panama turns all four off, which is a
|
|
// defensible default and was not previously a decision anyone could
|
|
// reverse without editing looks.lua.
|
|
SettingsCard {
|
|
title: "Hyprland notices"
|
|
subtitle: "Panama hides all of these by default. They are the compositor's own, not Panama's."
|
|
|
|
ToggleRow { setting: "hyprlandLogo" }
|
|
ToggleRow { setting: "hyprlandSplash" }
|
|
ToggleRow { setting: "hyprlandUpdateNews" }
|
|
ToggleRow { setting: "hyprlandDonationNag"; divider: false }
|
|
}
|
|
|
|
SettingsCard {
|
|
title: "Workspaces & focus"
|
|
subtitle: "Hyprland's workspaces are created and destroyed as you use them, so there is no fixed count to set."
|
|
|
|
ToggleRow { setting: "workspaceBackAndForth" }
|
|
ToggleRow { setting: "allowWorkspaceCycles" }
|
|
ToggleRow { setting: "focusOnActivate" }
|
|
ToggleRow { setting: "mouseMoveFocusesMonitor" }
|
|
ToggleRow { setting: "windowSwallow"; divider: false }
|
|
}
|
|
|
|
SettingsCard {
|
|
title: "Focus"
|
|
|
|
SliderRow { setting: "focusDurationMinutes"; divider: false }
|
|
}
|
|
|
|
// Distinct from the snapshots below, which put THIS machine back as it
|
|
// was. This carries settings to a different one, and deliberately leaves
|
|
// behind anything that describes hardware.
|
|
SettingsCard {
|
|
title: "Carry settings to another machine"
|
|
subtitle: SettingsSync.lastError !== ""
|
|
? SettingsSync.lastError
|
|
: "Everything except what describes this machine: the display arrangement stays here."
|
|
|
|
ActionRow {
|
|
label: "Export"
|
|
detail: SettingsSync.lastAction === "export" && SettingsSync.carried > 0
|
|
? SettingsSync.carried + " settings written to " + SettingsSync.defaultPath
|
|
: "Writes " + SettingsSync.defaultPath
|
|
action: "Export"
|
|
enabled: !SettingsSync.busy
|
|
onTriggered: SettingsSync.exportTo(SettingsSync.defaultPath)
|
|
}
|
|
|
|
ActionRow {
|
|
label: "See what an import would change"
|
|
detail: SettingsSync.previewed
|
|
? SettingsSync.changes.length + " would change, "
|
|
+ SettingsSync.skipped.length + " skipped"
|
|
: "Reads " + SettingsSync.defaultPath + " without applying anything"
|
|
action: "Preview"
|
|
enabled: !SettingsSync.busy
|
|
onTriggered: SettingsSync.preview(SettingsSync.defaultPath)
|
|
}
|
|
|
|
// Only offered once a preview has said what it would do. Importing
|
|
// settings sight unseen is how somebody ends up wondering why their
|
|
// desktop changed.
|
|
ActionRow {
|
|
visible: SettingsSync.previewed && SettingsSync.changes.length > 0
|
|
label: "Apply those " + SettingsSync.changes.length + " changes"
|
|
detail: "Settings the file does not mention are left alone"
|
|
action: "Import"
|
|
enabled: !SettingsSync.busy
|
|
onTriggered: SettingsSync.importFrom(SettingsSync.defaultPath)
|
|
}
|
|
|
|
Repeater {
|
|
model: SettingsSync.previewed ? SettingsSync.skipped : []
|
|
|
|
delegate: TextRow {
|
|
required property var modelData
|
|
width: parent.width
|
|
label: String(modelData.key ?? "")
|
|
detail: "Skipped: " + String(modelData.reason ?? "")
|
|
value: ""
|
|
}
|
|
}
|
|
|
|
TextRow {
|
|
visible: SettingsSync.lastAction === "import" && SettingsSync.lastError === ""
|
|
label: SettingsSync.applied === 0
|
|
? "Nothing needed changing"
|
|
: SettingsSync.applied + " settings applied"
|
|
detail: "From " + (SettingsSync.exportedFrom || "the export")
|
|
value: ""
|
|
divider: false
|
|
}
|
|
}
|
|
|
|
SettingsCard {
|
|
title: "Snapshots"
|
|
subtitle: SettingsBackup.lastError !== ""
|
|
? SettingsBackup.lastError
|
|
: "Your whole desktop configuration is one file, so a snapshot is a copy of it. Restoring also snapshots what it replaces, so it is itself undoable."
|
|
|
|
ActionRow {
|
|
label: "Back up current settings"
|
|
detail: SettingsBackup.snapshots.length === 0
|
|
? "No snapshots yet"
|
|
: SettingsBackup.snapshots.length + (SettingsBackup.snapshots.length === 1 ? " snapshot kept" : " snapshots kept") + ", newest first"
|
|
action: "Back up now"
|
|
enabled: !SettingsBackup.busy
|
|
divider: SettingsBackup.snapshots.length > 0
|
|
onTriggered: SettingsBackup.save()
|
|
}
|
|
|
|
Repeater {
|
|
id: snapshotRows
|
|
model: SettingsBackup.snapshots
|
|
|
|
ActionRow {
|
|
required property var modelData
|
|
required property int index
|
|
|
|
label: modelData.when
|
|
detail: modelData.keys + " settings"
|
|
action: "Restore"
|
|
enabled: !SettingsBackup.busy
|
|
divider: index < snapshotRows.count - 1
|
|
onTriggered: SettingsBackup.restore(modelData.name)
|
|
}
|
|
}
|
|
}
|
|
|
|
SettingsCard {
|
|
title: "Reset"
|
|
subtitle: "Restores the appearance, dock, clock, focus, and display policy, and clears your Home accessory arrangement. Pinned applications, files, and paired devices are not changed."
|
|
|
|
ActionRow {
|
|
label: "Restore defaults"
|
|
detail: "Applies immediately, including to the compositor"
|
|
action: "Restore defaults"
|
|
divider: false
|
|
onTriggered: SystemSettings.restoreDefaults()
|
|
}
|
|
}
|
|
}
|