// 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"; 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() } } }