Watch
1
0
Fork
You've already forked souveraine
0

publish: the public projection begins here

This is a projection, not a development branch. The tree above was constructed
from the internal source named below under a manifest that decides which paths
may leave, then scanned as a whole tree rather than as a series of patches, and
only then published.

Public history starts here because the history before it was not admissible,
and neither was the tree. What used to stand in this repository included a
rescue copy of another machine, a directory of phone handoffs, deployment
wired to one house, and a submodule pointing at a forge no stranger can reach.
None of that was ever the product. It stays in the private forge, which is
allowed to hold the whole working organism, and this is what was deliberately
sent out instead.

Three mechanisms produced this tree, in decreasing order of trust. A top-level
path the manifest does not name never arrives at all, which is the one that
catches directories nobody has thought of yet. Named internal files inside
admitted roots are dropped. A short, reviewed table replaces deployment
defaults that a public build must not carry -- an endpoint aimed at one LAN, a
VPN profile belonging to one phone, packaging built from one checkout path.

Everything after this commit is an ordinary publication with the same three
trailers, so a force push stops being routine and starts meaning that
something deliberate happened. The trailers bind the projection to its source
without pretending the public SHA is the private one: same lineage, different
tree, and the record says so.

Source-Sha: 8f27b1e76a8fef560a336aba18e6990713ff1047
Policy-Sha: 6b261d2f3e6e1fb19874846ba4bb1dfe15565d25b8618c1c1afba0419c101d27
Tree-Digest: 18ec3563c5e5ef9a414993a9f6734b251ff9ed3cd56eebdd6cac01e45c6e3067
This commit is contained in:
Fimeg 2026-09-04 15:55:48 -04:00
commit 8f42fc953d
1476 changed files with 238455 additions and 0 deletions

View file

@ -0,0 +1,310 @@
pragma ComponentBehavior: Bound
import qs
import qs.services
import qs.modules.common
import qs.modules.common.widgets
import qs.modules.common.widgets.widgetCanvas
import qs.modules.common.functions as CF
import QtQuick
import QtQuick.Layouts
import Qt5Compat.GraphicalEffects
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import Quickshell.Hyprland
import qs.modules.ii.background.widgets
import qs.modules.ii.background.widgets.clock
import qs.modules.ii.background.widgets.weather
import qs.modules.ii.background.widgets.visualizer
import qs.modules.ii.background.widgets.stats
import qs.modules.ii.background.widgets.resources
Variants {
id: root
model: Quickshell.screens
PanelWindow {
id: bgRoot
required property var modelData
// Hide when fullscreen
property list<HyprlandWorkspace> workspacesForMonitor: Hyprland.workspaces.values.filter(workspace => workspace.monitor && workspace.monitor.name == monitor.name)
property var activeWorkspaceWithFullscreen: workspacesForMonitor.filter(workspace => ((workspace.toplevels.values.filter(window => window.wayland?.fullscreen)[0] != undefined) && workspace.active))[0]
visible: GlobalStates.screenLocked || (!(activeWorkspaceWithFullscreen != undefined)) || !Config?.options.background.hideWhenFullscreen
// Workspaces
property HyprlandMonitor monitor: Hyprland.monitorFor(modelData)
property list<var> relevantWindows: HyprlandData.windowList.filter(win => win.monitor == monitor?.id && win.workspace.id >= 0).sort((a, b) => a.workspace.id - b.workspace.id)
property int firstWorkspaceId: relevantWindows[0]?.workspace.id || 1
property int lastWorkspaceId: relevantWindows[relevantWindows.length - 1]?.workspace.id || 10
property int workspaceChunkSize: Config?.options.bar.workspaces.shown ?? 10
property int totalWorkspaces: Math.ceil(lastWorkspaceId / workspaceChunkSize) * workspaceChunkSize
// Wallpaper
property bool wallpaperIsVideo: Config.options.background.wallpaperPath.endsWith(".mp4") || Config.options.background.wallpaperPath.endsWith(".webm") || Config.options.background.wallpaperPath.endsWith(".mkv") || Config.options.background.wallpaperPath.endsWith(".avi") || Config.options.background.wallpaperPath.endsWith(".mov")
property string wallpaperPath: wallpaperIsVideo ? Config.options.background.thumbnailPath : Config.options.background.wallpaperPath
property bool wallpaperSafetyTriggered: {
const enabled = Config.options.workSafety.enable.wallpaper;
const sensitiveWallpaper = (CF.StringUtils.stringListContainsSubstring(wallpaperPath.toLowerCase(), Config.options.workSafety.triggerCondition.fileKeywords));
const sensitiveNetwork = (CF.StringUtils.stringListContainsSubstring(Network.networkName.toLowerCase(), Config.options.workSafety.triggerCondition.networkNameKeywords));
return enabled && sensitiveWallpaper && sensitiveNetwork;
}
readonly property real parallaxRation: Config.options.background.parallax.workspaceZoom
property real minSuitableScale: 1 // Some reasonable init, to be updated
property real effectiveWallpaperScale: minSuitableScale * parallaxRation
property int wallpaperWidth: modelData.width // Some reasonable init value, to be updated
property int wallpaperHeight: modelData.height // Some reasonable init value, to be updated
property real scaledWallpaperWidth: wallpaperWidth * effectiveWallpaperScale
property real scaledWallpaperHeight: wallpaperHeight * effectiveWallpaperScale
property real parallaxTotalPixelsX: Math.max(0, scaledWallpaperWidth - screen.width)
property real parallaxTotalPixelsY: Math.max(0, scaledWallpaperHeight - screen.height)
readonly property bool verticalParallax: (Config.options.background.parallax.autoVertical && wallpaperHeight > wallpaperWidth) || Config.options.background.parallax.vertical
// Colors
property bool shouldBlur: (GlobalStates.screenLocked && Config.options.lock.blur.enable)
property color dominantColor: Appearance.colors.colPrimary // Default, to be changed
property bool dominantColorIsDark: dominantColor.hslLightness < 0.5
property color colText: {
if (wallpaperSafetyTriggered)
return CF.ColorUtils.mix(Appearance.colors.colOnLayer0, Appearance.colors.colPrimary, 0.75);
return (GlobalStates.screenLocked && shouldBlur) ? Appearance.colors.colOnLayer0 : CF.ColorUtils.colorWithLightness(Appearance.colors.colPrimary, (dominantColorIsDark ? 0.8 : 0.12));
}
Behavior on colText {
animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this)
}
// Layer props
screen: modelData
exclusionMode: ExclusionMode.Ignore
WlrLayershell.layer: (GlobalStates.screenLocked && !scaleAnim.running) ? WlrLayer.Overlay : WlrLayer.Bottom
// WlrLayershell.layer: WlrLayer.Bottom
WlrLayershell.namespace: "quickshell:background"
anchors {
top: true
bottom: true
left: true
right: true
}
color: {
if (!bgRoot.wallpaperSafetyTriggered || bgRoot.wallpaperIsVideo)
return "transparent";
return CF.ColorUtils.mix(Appearance.colors.colLayer0, Appearance.colors.colPrimary, 0.75);
}
Behavior on color {
animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this)
}
onWallpaperPathChanged: {
bgRoot.updateZoomScale();
// Clock position gets updated after zoom scale is updated
}
// Wallpaper zoom scale
function updateZoomScale() {
getWallpaperSizeProc.path = bgRoot.wallpaperPath;
getWallpaperSizeProc.running = true;
}
Process {
id: getWallpaperSizeProc
property string path: bgRoot.wallpaperPath
command: ["magick", "identify", "-format", "%w %h", path]
stdout: StdioCollector {
id: wallpaperSizeOutputCollector
onStreamFinished: {
const output = wallpaperSizeOutputCollector.text;
const [width, height] = output.split(" ").map(Number);
const [screenWidth, screenHeight] = [bgRoot.screen.width, bgRoot.screen.height];
bgRoot.wallpaperWidth = width;
bgRoot.wallpaperHeight = height;
// Perfect image; scale = 1
// Small picture; scale > 1; will zoom in the picture
// Big picture; scale < 1; will zoom out the picture
// Choose max number so every side will fit
bgRoot.minSuitableScale = Math.max(screenWidth / width, screenHeight / height);
}
}
}
Item {
anchors.fill: parent
// Wallpaper
StyledImage {
id: wallpaper
visible: opacity > 0 && !blurLoader.active
opacity: (status === Image.Ready && !bgRoot.wallpaperIsVideo) ? 1 : 0
cache: false
smooth: false
property int workspaceIndex: (bgRoot.monitor.activeWorkspace?.id ?? 1) - 1
property real middleFraction: 0.5
property real fraction: {
// 0 - start of the picture
// 1 - end of the picture
if (bgRoot.totalWorkspaces <= 1) {
return middleFraction;
}
return Math.max(0, Math.min(1, workspaceIndex / (bgRoot.totalWorkspaces - 1)));
}
property real usedFractionX: {
let usedFraction = middleFraction;
if (Config.options.background.parallax.enableWorkspace && !bgRoot.verticalParallax) {
usedFraction = fraction;
}
if (Config.options.background.parallax.enableSidebar) {
let sidebarFraction = bgRoot.parallaxRation / bgRoot.workspaceChunkSize / 2;
usedFraction += (sidebarFraction * GlobalStates.sidebarRightOpen - sidebarFraction * GlobalStates.sidebarLeftOpen);
}
return Math.max(0, Math.min(1, usedFraction));
}
property real usedFractionY: {
let usedFraction = middleFraction;
if (Config.options.background.parallax.enableWorkspace && bgRoot.verticalParallax) {
usedFraction = fraction;
}
return Math.max(0, Math.min(1, usedFraction));
}
x: {
if (bgRoot.screen.width > width) {
// Center the picture
return (bgRoot.screen.width - width) / 2;
}
return - bgRoot.parallaxTotalPixelsX * usedFractionX;
}
y: {
if (bgRoot.screen.height > height) {
// Center the picture
return (bgRoot.screen.height - height) / 2;
}
return - bgRoot.parallaxTotalPixelsY * usedFractionY;
}
source: bgRoot.wallpaperSafetyTriggered ? "" : bgRoot.wallpaperPath
fillMode: Image.PreserveAspectCrop
Behavior on x {
NumberAnimation {
duration: 600
easing.type: Easing.OutCubic
}
}
Behavior on y {
NumberAnimation {
duration: 600
easing.type: Easing.OutCubic
}
}
width: bgRoot.scaledWallpaperWidth
height: bgRoot.scaledWallpaperHeight
}
Loader {
id: blurLoader
active: Config.options.lock.blur.enable && (GlobalStates.screenLocked || scaleAnim.running)
anchors.fill: wallpaper
scale: GlobalStates.screenLocked ? Config.options.lock.blur.extraZoom : 1
Behavior on scale {
NumberAnimation {
id: scaleAnim
duration: 400
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.animationCurves.expressiveDefaultSpatial
}
}
sourceComponent: GaussianBlur {
source: wallpaper
radius: GlobalStates.screenLocked ? Config.options.lock.blur.radius : 0
samples: radius * 2 + 1
Rectangle {
opacity: GlobalStates.screenLocked ? 1 : 0
anchors.fill: parent
color: CF.ColorUtils.transparentize(Appearance.colors.colLayer0, 0.7)
}
}
}
WidgetCanvas {
id: widgetCanvas
width: parent.width
height: parent.height
readonly property real parallaxFactor: {
var f = Config.options.background.parallax.widgetsFactor;
return f / bgRoot.parallaxRation;
}
readonly property real baseWallpaperOffsetX: (bgRoot.screen.width - wallpaper.width) / 2
readonly property real baseWallpaperOffsetY: (bgRoot.screen.height - wallpaper.height) / 2
readonly property real wallpaperTotalOffsetX: wallpaper.x - baseWallpaperOffsetX
readonly property real wallpaperTotalOffsetY: wallpaper.y - baseWallpaperOffsetY
readonly property bool locked: GlobalStates.screenLocked
x: wallpaperTotalOffsetX * parallaxFactor * !locked
y: wallpaperTotalOffsetY * parallaxFactor * !locked
transitions: Transition {
PropertyAnimation {
properties: "width,height"
duration: Appearance.animation.elementMove.duration
easing.type: Appearance.animation.elementMove.type
easing.bezierCurve: Appearance.animation.elementMove.bezierCurve
}
AnchorAnimation {
duration: Appearance.animation.elementMove.duration
easing.type: Appearance.animation.elementMove.type
easing.bezierCurve: Appearance.animation.elementMove.bezierCurve
}
}
FadeLoader {
shown: Config.options.background.widgets.weather.enable
sourceComponent: WeatherWidget {
screenWidth: bgRoot.screen.width
screenHeight: bgRoot.screen.height
scaledScreenWidth: bgRoot.screen.width
scaledScreenHeight: bgRoot.screen.height
wallpaperScale: 1
}
}
FadeLoader {
shown: Config.options.background.widgets.clock.enable
sourceComponent: ClockWidget {
screenWidth: bgRoot.screen.width
screenHeight: bgRoot.screen.height
scaledScreenWidth: bgRoot.screen.width
scaledScreenHeight: bgRoot.screen.height
wallpaperScale: 1
wallpaperSafetyTriggered: bgRoot.wallpaperSafetyTriggered
}
}
FadeLoader {
shown: Config.options.background.widgets.visualizer.enable
sourceComponent: VisualizerWidget {
screenWidth: bgRoot.screen.width; screenHeight: bgRoot.screen.height
scaledScreenWidth: bgRoot.screen.width; scaledScreenHeight: bgRoot.screen.height
wallpaperScale: 1
}
}
FadeLoader {
shown: Config.options.background.widgets.stats.enable
sourceComponent: StatsWidget {
screenWidth: bgRoot.screen.width; screenHeight: bgRoot.screen.height
scaledScreenWidth: bgRoot.screen.width; scaledScreenHeight: bgRoot.screen.height
wallpaperScale: 1
}
}
FadeLoader {
shown: Config.options.background.widgets.systemResources.enable
sourceComponent: SystemResourcesWidget {
screenWidth: bgRoot.screen.width; screenHeight: bgRoot.screen.height
scaledScreenWidth: bgRoot.screen.width; scaledScreenHeight: bgRoot.screen.height
wallpaperScale: 1
}
}
}
}
}
}

View file

@ -0,0 +1,102 @@
import QtQuick
import Quickshell
import Quickshell.Io
import qs
import qs.modules.common
import qs.modules.common.functions
import qs.modules.common.widgets.widgetCanvas
AbstractWidget {
id: root
required property string configEntryName
required property int screenWidth
required property int screenHeight
required property int scaledScreenWidth
required property int scaledScreenHeight
required property real wallpaperScale
property bool visibleWhenLocked: false
property var configEntry: Config.options.background.widgets[configEntryName]
property string placementStrategy: configEntry.placementStrategy
property real targetX: Math.max(0, Math.min(configEntry.x, scaledScreenWidth - width))
property real targetY : Math.max(0, Math.min(configEntry.y, scaledScreenHeight - height))
x: targetX
y: targetY
visible: opacity > 0
opacity: (GlobalStates.screenLocked && !visibleWhenLocked) ? 0 : 1
Behavior on opacity {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
}
scale: (draggable && containsPress) ? 1.05 : 1
Behavior on scale {
animation: Appearance.animation.elementResize.numberAnimation.createObject(this)
}
draggable: placementStrategy === "free"
onReleased: {
root.targetX = root.x;
root.targetY = root.y;
configEntry.x = root.targetX;
configEntry.y = root.targetY;
}
property bool needsColText: false
property color dominantColor: Appearance.colors.colPrimary
property bool dominantColorIsDark: dominantColor.hslLightness < 0.5
property color colText: {
const onNormalBackground = (GlobalStates.screenLocked && Config.options.lock.blur.enable)
const adaptiveColor = ColorUtils.colorWithLightness(Appearance.colors.colPrimary, (dominantColorIsDark ? 0.8 : 0.12))
return onNormalBackground ? Appearance.colors.colOnLayer0 : adaptiveColor;
}
property bool wallpaperIsVideo: Config.options.background.wallpaperPath.endsWith(".mp4") || Config.options.background.wallpaperPath.endsWith(".webm") || Config.options.background.wallpaperPath.endsWith(".mkv") || Config.options.background.wallpaperPath.endsWith(".avi") || Config.options.background.wallpaperPath.endsWith(".mov")
property string wallpaperPath: wallpaperIsVideo ? Config.options.background.thumbnailPath : Config.options.background.wallpaperPath
onWallpaperPathChanged: refreshPlacementIfNeeded()
onPlacementStrategyChanged: refreshPlacementIfNeeded()
Connections {
target: Config
function onReadyChanged() { refreshPlacementIfNeeded() }
}
function refreshPlacementIfNeeded() {
if (!Config.ready) return;
if (root.placementStrategy === "free" && !root.needsColText) return;
leastBusyRegionProc.wallpaperPath = root.wallpaperPath;
leastBusyRegionProc.running = false;
leastBusyRegionProc.running = true;
}
Process {
id: leastBusyRegionProc
property string wallpaperPath: root.wallpaperPath
// TODO: make these less arbitrary
property int contentWidth: 300
property int contentHeight: 300
property int horizontalPadding: 200
property int verticalPadding: 200
command: [Quickshell.shellPath("scripts/images/least-busy-region-venv.sh") // Comments to force the formatter to break lines
, "--screen-width", Math.round(root.scaledScreenWidth) //
, "--screen-height", Math.round(root.scaledScreenHeight) //
, "--width", contentWidth //
, "--height", contentHeight //
, "--horizontal-padding", horizontalPadding //
, "--vertical-padding", verticalPadding //
, wallpaperPath //
, ...(root.placementStrategy === "mostBusy" ? ["--busiest"] : [])
// "--visual-output",
]
stdout: StdioCollector {
id: leastBusyRegionOutputCollector
onStreamFinished: {
const output = leastBusyRegionOutputCollector.text;
// console.log("[Background] Least busy region output:", output)
if (output.length === 0) return;
const parsedContent = JSON.parse(output);
root.dominantColor = parsedContent.dominant_color || Appearance.colors.colPrimary;
if (root.placementStrategy === "free") return;
root.targetX = parsedContent.center_x * root.wallpaperScale - root.width / 2;
root.targetY = parsedContent.center_y * root.wallpaperScale - root.height / 2;
}
}
}
}

View file

@ -0,0 +1,19 @@
import qs.modules.common
import qs.modules.common.widgets
import QtQuick
import QtQuick.Layouts
StyledText {
Layout.fillWidth: true
font {
family: Appearance.font.family.expressive
pixelSize: 20
weight: 350
// Set empty to prevent conflicts, not meaningless
styleName: ""
variableAxes: ({})
}
style: Text.Raised
styleColor: Appearance.colors.colShadow
animateChange: Config.options.background.widgets.clock.digital.animateChange
}

View file

@ -0,0 +1,167 @@
import QtQuick
import QtQuick.Layouts
import qs
import qs.services
import qs.modules.common
import qs.modules.common.functions
import qs.modules.common.widgets
import qs.modules.common.widgets.widgetCanvas
import qs.modules.ii.background.widgets
AbstractBackgroundWidget {
id: root
configEntryName: "clock"
implicitHeight: contentColumn.implicitHeight
implicitWidth: contentColumn.implicitWidth
readonly property string clockStyle: GlobalStates.screenLocked ? Config.options.background.widgets.clock.styleLocked : Config.options.background.widgets.clock.style
readonly property bool forceCenter: (GlobalStates.screenLocked && Config.options.lock.centerClock)
readonly property bool shouldShow: (!Config.options.background.widgets.clock.showOnlyWhenLocked || GlobalStates.screenLocked)
property bool wallpaperSafetyTriggered: false
needsColText: clockStyle === "digital"
x: forceCenter ? ((root.screenWidth - root.width) / 2) : targetX
y: forceCenter ? ((root.screenHeight - root.height) / 2) : targetY
visibleWhenLocked: true
property var textHorizontalAlignment: {
if (!Config.options.background.widgets.clock.digital.adaptiveAlignment || root.forceCenter || Config.options.background.widgets.clock.digital.vertical)
return Text.AlignHCenter;
if (root.x < root.scaledScreenWidth / 3)
return Text.AlignLeft;
if (root.x > root.scaledScreenWidth * 2 / 3)
return Text.AlignRight;
return Text.AlignHCenter;
}
Column {
id: contentColumn
anchors.centerIn: parent
spacing: 10
FadeLoader {
id: cookieClockLoader
anchors.horizontalCenter: parent.horizontalCenter
shown: root.clockStyle === "cookie" && (root.shouldShow)
fade: false
sourceComponent: Column {
spacing: 10
CookieClock {
anchors.horizontalCenter: parent.horizontalCenter
}
FadeLoader {
anchors.horizontalCenter: parent.horizontalCenter
shown: Config.options.background.widgets.clock.quote.enable && Config.options.background.widgets.clock.quote.text !== ""
sourceComponent: CookieQuote {}
}
}
}
FadeLoader {
id: digitalClockLoader
anchors.horizontalCenter: parent.horizontalCenter
shown: root.clockStyle === "digital" && (root.shouldShow)
fade: false
sourceComponent: DigitalClock {
colText: root.colText
textHorizontalAlignment: root.textHorizontalAlignment
}
}
StatusRow {
anchors.horizontalCenter: parent.horizontalCenter
}
}
component StatusRow: Item {
id: statusText
implicitHeight: statusTextBg.implicitHeight
implicitWidth: statusTextBg.implicitWidth
StyledRectangularShadow {
target: statusTextBg
visible: statusTextBg.visible && root.clockStyle === "cookie"
opacity: statusTextBg.opacity
}
Rectangle {
id: statusTextBg
anchors.centerIn: parent
clip: true
opacity: (safetyStatusText.shown || lockStatusText.shown) ? 1 : 0
visible: opacity > 0
implicitHeight: statusTextRow.implicitHeight + 5 * 2
implicitWidth: statusTextRow.implicitWidth + 5 * 2
radius: Appearance.rounding.small
color: ColorUtils.transparentize(Appearance.colors.colSecondaryContainer, root.clockStyle === "cookie" ? 0 : 1)
Behavior on implicitWidth {
animation: Appearance.animation.elementResize.numberAnimation.createObject(this)
}
Behavior on implicitHeight {
animation: Appearance.animation.elementResize.numberAnimation.createObject(this)
}
Behavior on opacity {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
}
RowLayout {
id: statusTextRow
anchors.centerIn: parent
spacing: 14
Item {
Layout.fillWidth: root.textHorizontalAlignment !== Text.AlignLeft
implicitWidth: 1
}
ClockStatusText {
id: safetyStatusText
shown: root.wallpaperSafetyTriggered
statusIcon: "hide_image"
statusText: Translation.tr("Wallpaper safety enforced")
}
ClockStatusText {
id: lockStatusText
shown: GlobalStates.screenLocked && Config.options.lock.showLockedText
statusIcon: "lock"
statusText: Translation.tr("Locked")
}
Item {
Layout.fillWidth: root.textHorizontalAlignment !== Text.AlignRight
implicitWidth: 1
}
}
}
}
component ClockStatusText: Row {
id: statusTextRow
property alias statusIcon: statusIconWidget.text
property alias statusText: statusTextWidget.text
property bool shown: true
property color textColor: root.clockStyle === "cookie" ? Appearance.colors.colOnSecondaryContainer : root.colText
opacity: shown ? 1 : 0
visible: opacity > 0
Behavior on opacity {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
}
spacing: 4
MaterialSymbol {
id: statusIconWidget
anchors.verticalCenter: statusTextRow.verticalCenter
iconSize: Appearance.font.pixelSize.huge
color: statusTextRow.textColor
style: Text.Raised
styleColor: Appearance.colors.colShadow
}
ClockText {
id: statusTextWidget
color: statusTextRow.textColor
horizontalAlignment: root.textHorizontalAlignment
anchors.verticalCenter: statusTextRow.verticalCenter
font {
pixelSize: Appearance.font.pixelSize.large
weight: Font.Normal
}
style: Text.Raised
styleColor: Appearance.colors.colShadow
}
}
}

View file

@ -0,0 +1,211 @@
pragma ComponentBehavior: Bound
import qs.services
import qs.modules.common
import qs.modules.common.widgets
import qs.modules.common.functions
import QtQuick
import QtQuick.Layouts
import Qt5Compat.GraphicalEffects
import Quickshell.Io
import qs.modules.ii.background.widgets.clock.dateIndicator
import qs.modules.ii.background.widgets.clock.minuteMarks
Item {
id: root
readonly property string clockStyle: Config.options.background.widgets.clock.style
property real implicitSize: 230
property color colShadow: Appearance.colors.colShadow
property color colBackground: Appearance.colors.colPrimaryContainer
property color colOnBackground: ColorUtils.mix(Appearance.colors.colSecondary, Appearance.colors.colPrimaryContainer, 0.15)
property color colBackgroundInfo: ColorUtils.mix(Appearance.colors.colPrimary, Appearance.colors.colPrimaryContainer, 0.55)
property color colHourHand: Appearance.colors.colPrimary
property color colMinuteHand: Appearance.colors.colTertiary
property color colSecondHand: Appearance.colors.colPrimary
readonly property list<string> clockNumbers: DateTime.time.split(/[: ]/)
readonly property int clockHour: parseInt(clockNumbers[0]) % 12
readonly property int clockMinute: DateTime.clock.minutes
readonly property int clockSecond: DateTime.clock.seconds
implicitWidth: implicitSize
implicitHeight: implicitSize
function applyStyle(sides, dialStyle, hourHandStyle, minuteHandStyle, secondHandStyle, dateStyle) {
Config.options.background.widgets.clock.cookie.sides = sides
Config.options.background.widgets.clock.cookie.dialNumberStyle = dialStyle
Config.options.background.widgets.clock.cookie.hourHandStyle = hourHandStyle
Config.options.background.widgets.clock.cookie.minuteHandStyle = minuteHandStyle
Config.options.background.widgets.clock.cookie.secondHandStyle = secondHandStyle
Config.options.background.widgets.clock.cookie.dateStyle = dateStyle
}
function setClockPreset(category) {
if (!Config.options.background.widgets.clock.cookie.aiStyling) return;
if (category === "") return;
print("[Cookie clock] Setting clock preset for category: " + category)
// "abstract", "anime", "city", "minimalist", "landscape", "plants", "person", "space"
if (category == "abstract") {
applyStyle(9, "none", "fill", "medium", "dot", "bubble")
} else if (category == "anime") {
applyStyle(7, "none", "fill", "bold", "dot", "bubble")
} else if (category == "city" || category == "space") {
applyStyle(23, "full", "hollow", "thin", "classic", "bubble")
} else if (category == "minimalist") {
applyStyle(6, "none", "fill", "bold", "dot", "hide")
} else if (category == "landscape") {
applyStyle(14, "full", "hollow", "medium", "classic", "bubble")
} else if (category == "plants") {
applyStyle(9, "dots", "fill", "bold", "dot", "border")
} else if (category == "person") {
applyStyle(14, "full", "classic", "classic", "classic", "rect")
}
}
FileView {
id: categoryFileView
path: Config.ready ? Directories.generatedWallpaperCategoryPath : ""
watchChanges: true
onFileChanged: reload()
onLoaded: {
root.setClockPreset(categoryFileView.text().trim())
}
}
property bool useSineCookie: Config.options.background.widgets.clock.cookie.useSineCookie
StyledDropShadow {
target: root.useSineCookie ? sineCookieLoader : roundedPolygonCookieLoader
RotationAnimation on rotation {
running: Config.options.background.widgets.clock.cookie.constantlyRotate
duration: 30000
easing.type: Easing.Linear
loops: Animation.Infinite
from: 360
to: 0
}
}
Loader {
id: sineCookieLoader
z: 0
visible: false // The DropShadow already draws it
active: root.useSineCookie
sourceComponent: SineCookie {
implicitSize: root.implicitSize
sides: Config.options.background.widgets.clock.cookie.sides
color: root.colBackground
}
}
Loader {
id: roundedPolygonCookieLoader
z: 0
visible: false // The DropShadow already draws it
active: !root.useSineCookie
sourceComponent: MaterialCookie {
implicitSize: root.implicitSize
sides: Config.options.background.widgets.clock.cookie.sides
color: root.colBackground
}
}
// Hour/minutes numbers/dots/lines
MinuteMarks {
anchors.fill: parent
color: root.colOnBackground
}
// Stupid extra hour marks in the middle
FadeLoader {
id: hourMarksLoader
anchors.centerIn: parent
shown: Config.options.background.widgets.clock.cookie.hourMarks
sourceComponent: HourMarks {
implicitSize: 135 * (1.75 - 0.75 * hourMarksLoader.opacity)
color: root.colOnBackground
colOnBackground: ColorUtils.mix(root.colBackgroundInfo, root.colOnBackground, 0.5)
}
}
// Number column in the middle
FadeLoader {
id: timeColumnLoader
anchors.centerIn: parent
shown: Config.options.background.widgets.clock.cookie.timeIndicators
scale: 1.4 - 0.4 * timeColumnLoader.shown
Behavior on scale {
animation: Appearance.animation.elementResize.numberAnimation.createObject(this)
}
sourceComponent: TimeColumn {
color: root.colBackgroundInfo
}
}
// Minute hand
FadeLoader {
anchors.fill: parent
z: 1
shown: Config.options.background.widgets.clock.cookie.minuteHandStyle !== "hide"
sourceComponent: MinuteHand {
anchors.fill: parent
clockMinute: root.clockMinute
style: Config.options.background.widgets.clock.cookie.minuteHandStyle
color: root.colMinuteHand
}
}
// Hour hand
FadeLoader {
anchors.fill: parent
z: item?.style === "hollow" ? 0 : 2
shown: Config.options.background.widgets.clock.cookie.hourHandStyle !== "hide"
sourceComponent: HourHand {
clockHour: root.clockHour
clockMinute: root.clockMinute
style: Config.options.background.widgets.clock.cookie.hourHandStyle
color: root.colHourHand
}
}
// Second hand
FadeLoader {
id: secondHandLoader
z: (Config.options.background.widgets.clock.cookie.secondHandStyle === "line") ? 2 : 3
shown: Config.options.time.secondPrecision && Config.options.background.widgets.clock.cookie.secondHandStyle !== "hide"
anchors.fill: parent
sourceComponent: SecondHand {
id: secondHand
clockSecond: root.clockSecond
style: Config.options.background.widgets.clock.cookie.secondHandStyle
color: root.colSecondHand
}
}
// Center dot
FadeLoader {
z: 4
anchors.centerIn: parent
shown: Config.options.background.widgets.clock.cookie.minuteHandStyle !== "bold"
sourceComponent: Rectangle {
color: Config.options.background.widgets.clock.cookie.minuteHandStyle === "medium" ? root.colBackground : root.colMinuteHand
implicitWidth: 6
implicitHeight: implicitWidth
radius: width / 2
}
}
// Date
FadeLoader {
anchors.fill: parent
shown: Config.options.background.widgets.clock.cookie.dateStyle !== "hide"
sourceComponent: DateIndicator {
color: root.colBackgroundInfo
style: Config.options.background.widgets.clock.cookie.dateStyle
}
}
}

View file

@ -0,0 +1,59 @@
import qs.modules.common
import qs.modules.common.widgets
import QtQuick
import Qt5Compat.GraphicalEffects
Item {
id: root
readonly property string quoteText: Config.options.background.widgets.clock.quote.text
implicitWidth: quoteBox.implicitWidth
implicitHeight: quoteBox.implicitHeight
DropShadow {
source: quoteBox
anchors.fill: quoteBox
horizontalOffset: 0
verticalOffset: 2
radius: 12
samples: radius * 2 + 1
color: Appearance.colors.colShadow
transparentBorder: true
}
Rectangle {
id: quoteBox
implicitWidth: quoteRow.implicitWidth + 8 * 2
implicitHeight: quoteRow.implicitHeight + 4 * 2
radius: Appearance.rounding.small
color: Appearance.colors.colSecondaryContainer
Row {
id: quoteRow
anchors.centerIn: parent
spacing: 4
MaterialSymbol {
id: quoteIcon
anchors.top: parent.top
iconSize: Appearance.font.pixelSize.huge
text: "format_quote"
color: Appearance.colors.colOnSecondaryContainer
}
StyledText {
id: quoteStyledText
horizontalAlignment: Text.AlignLeft
text: Config.options.background.widgets.clock.quote.text
color: Appearance.colors.colOnSecondaryContainer
font {
family: Appearance.font.family.reading
pixelSize: Appearance.font.pixelSize.large
weight: Font.Normal
}
}
}
}
}

View file

@ -0,0 +1,71 @@
pragma ComponentBehavior: Bound
import qs.services
import qs.modules.common
import QtQuick
import QtQuick.Layouts
ColumnLayout {
id: clockColumn
spacing: 4
property bool isVertical: Config.options.background.widgets.clock.digital.vertical
property color colText: Appearance.colors.colOnSecondaryContainer
property var textHorizontalAlignment: Text.AlignHCenter
// Time
ClockText {
id: timeTextTop
text: clockColumn.isVertical ? DateTime.time.split(":")[0].padStart(2, "0") : DateTime.time
color: clockColumn.colText
horizontalAlignment: Text.AlignHCenter
font {
pixelSize: Config.options.background.widgets.clock.digital.font.size
weight: Config.options.background.widgets.clock.digital.font.weight
family: Config.options.background.widgets.clock.digital.font.family
variableAxes: ({
"wdth": Config.options.background.widgets.clock.digital.font.width,
"ROND": Config.options.background.widgets.clock.digital.font.roundness
})
}
}
Loader {
Layout.topMargin: -40
Layout.fillWidth: true
active: clockColumn.isVertical
visible: active
sourceComponent: ClockText {
id: timeTextBottom
text: DateTime.time.split(":")[1].split(" ")[0].padStart(2, "0")
color: clockColumn.colText
horizontalAlignment: clockColumn.textHorizontalAlignment
font {
pixelSize: timeTextTop.font.pixelSize
weight: timeTextTop.font.weight
family: timeTextTop.font.family
variableAxes: timeTextTop.font.variableAxes
}
}
}
// Date
ClockText {
visible: Config.options.background.widgets.clock.digital.showDate
Layout.topMargin: -20
Layout.fillWidth: true
text: DateTime.longDate
color: clockColumn.colText
horizontalAlignment: clockColumn.textHorizontalAlignment
}
// Quote
ClockText {
visible: Config.options.background.widgets.clock.quote.enable && Config.options.background.widgets.clock.quote.text.length > 0
font.pixelSize: Appearance.font.pixelSize.normal
text: Config.options.background.widgets.clock.quote.text
animateChange: false
color: clockColumn.colText
horizontalAlignment: clockColumn.textHorizontalAlignment
}
}

View file

@ -0,0 +1,45 @@
pragma ComponentBehavior: Bound
import qs.modules.common
import QtQuick
Item {
id: root
required property int clockHour
required property int clockMinute
property real handLength: 72
property real handWidth: 20
property string style: "fill"
property color color: Appearance.colors.colPrimary
property real fillColorAlpha: root.style === "hollow" ? 0 : 1
Behavior on fillColorAlpha {
animation: Appearance.animation.elementResize.numberAnimation.createObject(this)
}
rotation: -90 + (360 / 12) * (root.clockHour + root.clockMinute / 60)
Behavior on rotation {
animation: RotationAnimation {
direction: RotationAnimation.Clockwise
duration: 300
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.animationCurves.emphasized
}
}
Rectangle {
anchors.verticalCenter: parent.verticalCenter
x: (parent.width - root.handWidth) / 2 - 15 * (root.style === "classic")
width: root.handLength
height: root.style === "classic" ? 8 : root.handWidth
radius: root.style === "classic" ? 2 : root.handWidth / 2
color : Qt.rgba(root.color.r, root.color.g, root.color.b, root.fillColorAlpha)
border.color: root.color
border.width: 4
Behavior on x {
animation: Appearance.animation.elementResize.numberAnimation.createObject(this)
}
}
}

View file

@ -0,0 +1,50 @@
pragma ComponentBehavior: Bound
import qs.services
import qs.modules.common
import qs.modules.common.widgets
import qs.modules.common.functions
import QtQuick
Item {
id: root
property real implicitSize: 135
property real markLength: 12
property real markWidth: 4
property color color: Appearance.colors.colOnSecondaryContainer
property color colOnBackground: Appearance.colors.colSecondaryContainer
property real padding: 8
Rectangle {
color: root.color
anchors.centerIn: parent
implicitWidth: root.implicitSize
implicitHeight: root.implicitSize
radius: width / 2
// Hour mark lines
Repeater {
model: 12
Item {
required property int index
anchors.fill: parent
rotation: 360 / 12 * index
Rectangle {
anchors {
left: parent.left
verticalCenter: parent.verticalCenter
leftMargin: root.padding
}
implicitWidth: root.markLength
implicitHeight: root.markWidth
radius: width / 2
color: root.colOnBackground
}
}
}
}
}

View file

@ -0,0 +1,47 @@
pragma ComponentBehavior: Bound
import qs.modules.common
import QtQuick
Item {
id: root
anchors.fill: parent
required property int clockMinute
property string style: "medium"
property real handLength: 95
property real handWidth: style === "bold" ? 20 : style === "medium" ? 12 : 5
property color color: Appearance.colors.colTertiary
rotation: -90 + (360 / 60) * root.clockMinute
Behavior on rotation {
animation: RotationAnimation {
direction: RotationAnimation.Clockwise
duration: 300
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.animationCurves.emphasized
}
}
Rectangle {
anchors.verticalCenter: parent.verticalCenter
x: {
let position = parent.width / 2 - root.handWidth / 2;
if (root.style === "classic") position -= 15;
return position;
}
width: root.handLength
height: root.handWidth
radius: root.style === "classic" ? 2 : root.handWidth / 2
color: root.color
Behavior on height {
animation: Appearance.animation.elementResize.numberAnimation.createObject(this)
}
Behavior on x {
animation: Appearance.animation.elementResize.numberAnimation.createObject(this)
}
}
}

View file

@ -0,0 +1,71 @@
pragma ComponentBehavior: Bound
import qs.modules.common
import qs.modules.common.widgets
import QtQuick
Item {
id: root
anchors.fill: parent
required property int clockSecond
property real handWidth: 2
property real handLength: 95
property real dotSize: 20
property string style: "hide"
property color color: Appearance.colors.colSecondary
rotation: (360 / 60 * clockSecond) + 90
Behavior on rotation {
enabled: Config.options.background.widgets.clock.cookie.constantlyRotate // Animating every second is expensive...
animation: RotationAnimation {
direction: RotationAnimation.Clockwise
duration: 1000 // 1 second
easing.type: Easing.InOutQuad
}
}
Rectangle {
anchors {
left: parent.left
verticalCenter: parent.verticalCenter
leftMargin: 10 + (root.style === "dot" ? root.dotSize : 0)
}
implicitWidth: root.style === "dot" ? root.dotSize : root.handLength
implicitHeight: root.style === "dot" ? root.dotSize : root.handWidth
radius: Math.min(width, height) / 2
color: root.color
Behavior on implicitHeight {
animation: Appearance.animation.elementResize.numberAnimation.createObject(this)
}
Behavior on implicitWidth {
animation: Appearance.animation.elementResize.numberAnimation.createObject(this)
}
}
// Classic style dot in the middle of the hand
FadeLoader {
id: classicDotLoader
anchors {
left: parent.left
verticalCenter: parent.verticalCenter
}
shown: root.style === "classic"
Rectangle {
anchors {
left: parent.left
verticalCenter: parent.verticalCenter
leftMargin: 40
}
implicitWidth: root.style === "classic" ? 14 : 0
implicitHeight: implicitWidth
color: root.color
radius: Appearance.rounding.small
Behavior on implicitWidth {
animation: Appearance.animation.elementResize.numberAnimation.createObject(this)
}
}
}
}

View file

@ -0,0 +1,41 @@
pragma ComponentBehavior: Bound
import qs.services
import qs.modules.common
import qs.modules.common.widgets
import QtQuick
Column {
id: root
property list<string> clockNumbers: DateTime.time.split(/[: ]/)
property bool isEnabled: Config.options.background.widgets.clock.cookie.timeIndicators
property color color: Appearance.colors.colOnSecondaryContainer
property bool hourMarksEnabled: Config.options.background.widgets.clock.cookie.hourMarks
spacing: -16
Repeater {
model: root.clockNumbers
delegate: StyledText {
required property string modelData
text: modelData.padStart(2, "0")
property bool isAmPm: !text.match(/\d{2}/i)
property real numberSizeWithoutGlow: isAmPm ? 26 : 68
property real numberSizeWithGlow: isAmPm ? 20 : 40
property real numberSize: root.hourMarksEnabled ? numberSizeWithGlow : numberSizeWithoutGlow
anchors.horizontalCenter: root.horizontalCenter
color: root.color
font {
family: Appearance.font.family.expressive
weight: Font.Bold
pixelSize: numberSize
}
Behavior on numberSize {
animation: Appearance.animation.elementResize.numberAnimation.createObject(this)
}
}
}
}

View file

@ -0,0 +1,37 @@
pragma ComponentBehavior: Bound
import qs.services
import qs.modules.common
import qs.modules.common.widgets
import QtQuick
Item {
id: root
property bool isMonth: false
property real targetSize: 0
property alias text: bubbleText.text
text: Qt.locale().toString(DateTime.clock.date, root.isMonth ? "MM" : "d")
MaterialShape {
id: bubble
z: 5
// sides: root.isMonth ? 1 : 4
shape: root.isMonth ? MaterialShape.Shape.Pill : MaterialShape.Shape.Pentagon
anchors.centerIn: parent
color: root.isMonth ? Appearance.colors.colSecondaryContainer : Appearance.colors.colTertiaryContainer
implicitSize: targetSize
}
StyledText {
id: bubbleText
z: 6
anchors.centerIn: parent
color: root.isMonth ? Appearance.colors.colOnSecondaryContainer : Appearance.colors.colOnTertiaryContainer
font {
family: Appearance.font.family.expressive
pixelSize: 30
weight: Font.Black
}
}
}

View file

@ -0,0 +1,76 @@
pragma ComponentBehavior: Bound
import qs.modules.common
import qs.modules.common.widgets
import qs.modules.common.functions
import QtQuick
Item {
id: root
property string style: "bubble"
property color color: Appearance.colors.colOnSecondaryContainer
property real dateSquareSize: 64
// Rotating date
FadeLoader {
anchors.fill: parent
shown: Config.options.background.widgets.clock.cookie.dateStyle === "border"
sourceComponent: RotatingDate {
color: root.color
}
}
// Rectangle date (only today's number) in right side of the clock
FadeLoader {
id: rectLoader
shown: root.style === "rect"
anchors {
verticalCenter: parent.verticalCenter
right: parent.right
rightMargin: 40 - rectLoader.opacity * 30
}
sourceComponent: RectangleDate {
color: ColorUtils.mix(root.color, Appearance.colors.colSecondaryContainerHover, 0.5)
radius: Appearance.rounding.small
implicitWidth: 45 * rectLoader.opacity
implicitHeight: 30 * rectLoader.opacity
}
}
// Bubble style: day of month
FadeLoader {
id: dayBubbleLoader
shown: root.style === "bubble"
property real targetSize: root.dateSquareSize * opacity
anchors {
left: parent.left
top: parent.top
}
sourceComponent: BubbleDate {
implicitWidth: dayBubbleLoader.targetSize
implicitHeight: dayBubbleLoader.targetSize
isMonth: false
targetSize: dayBubbleLoader.targetSize
}
}
// Bubble style: month
FadeLoader {
id: monthBubbleLoader
shown: root.style === "bubble"
property real targetSize: root.dateSquareSize * opacity
anchors {
right: parent.right
bottom: parent.bottom
}
sourceComponent: BubbleDate {
implicitWidth: monthBubbleLoader.targetSize
implicitHeight: monthBubbleLoader.targetSize
isMonth: true
targetSize: monthBubbleLoader.targetSize
}
}
}

View file

@ -0,0 +1,21 @@
import qs.services
import qs.modules.common
import qs.modules.common.widgets
import QtQuick
Rectangle {
id: rect
readonly property string dialStyle: Config.options.background.widgets.clock.cookie.dialNumberStyle
StyledText {
anchors.centerIn: parent
color: Appearance.colors.colSecondaryHover
text: Qt.locale().toString(DateTime.clock.date, "dd")
font {
family: Appearance.font.family.expressive
pixelSize: 20
weight: 1000
}
}
}

View file

@ -0,0 +1,50 @@
pragma ComponentBehavior: Bound
import qs.services
import qs.modules.common
import qs.modules.common.widgets
import QtQuick
Item {
id: root
property string style: Config.options.background.widgets.clock.cookie.dateStyle
property color color: Appearance.colors.colOnSecondaryContainer
property real angleStep: 12 * Math.PI / 180
property string dateText: Qt.locale().toString(DateTime.clock.date, "ddd dd")
readonly property int clockSecond: DateTime.clock.seconds
readonly property string dialStyle: Config.options.background.widgets.clock.cookie.dialNumberStyle
readonly property bool timeIndicators: Config.options.background.widgets.clock.cookie.timeIndicators
property real radius: style === "border" ? 90 : 0
Behavior on radius {
animation: Appearance.animation.elementResize.numberAnimation.createObject(this)
}
rotation: {
if (!Config.options.time.secondPrecision) return 0
else return (360 / 60 * clockSecond) + 180 - (angleStep / Math.PI * 180 * dateText.length) / 2
}
Repeater {
model: root.dateText.length
delegate: Text {
required property int index
property real angle: index * root.angleStep - Math.PI / 2
x: root.width / 2 + root.radius * Math.cos(angle) - width / 2
y: root.height / 2 + root.radius * Math.sin(angle) - height / 2
rotation: angle * 180 / Math.PI + 90
color: root.color
font {
family: Appearance.font.family.title
pixelSize: 30
variableAxes: Appearance.font.variableAxes.title
}
text: root.dateText.charAt(index)
}
}
}

View file

@ -0,0 +1,49 @@
pragma ComponentBehavior: Bound
import qs.modules.common
import qs.modules.common.widgets
import QtQuick
Item {
id: root
property real numberSize: 80
property real margins: 10
property color color: Appearance.colors.colOnSecondaryContainer
property int hours: 12
property int numbers: 4
property int fontSize: 80
Repeater {
model: root.numbers
Item {
id: numberItem
required property int index
rotation: 360 / root.numbers * (index + 1)
anchors.fill: parent
Item {
implicitWidth: root.numberSize
implicitHeight: implicitWidth
anchors {
top: parent.top
horizontalCenter: parent.horizontalCenter
topMargin: root.margins
}
StyledText {
color: root.color
anchors.centerIn: parent
text: root.hours / root.numbers * (numberItem.index + 1)
rotation: -numberItem.rotation
font {
family: Appearance.font.family.reading
pixelSize: root.fontSize
weight: Font.Black
}
}
}
}
}
}

View file

@ -0,0 +1,34 @@
pragma ComponentBehavior: Bound
import qs.modules.common
import qs.modules.common.widgets
import QtQuick
Item {
id: root
property real implicitSize: 12
property real margins: 10
property color color: Appearance.colors.colOnSecondaryContainer
Repeater {
model: 12
Item {
required property int index
anchors.fill: parent // Ensures rotation works properly
rotation: 360 / 12 * index
Rectangle {
anchors {
left: parent.left
verticalCenter: parent.verticalCenter
leftMargin: root.margins
}
implicitWidth: root.implicitSize
implicitHeight: implicitWidth
radius: implicitWidth / 2
color: root.color
}
}
}
}

View file

@ -0,0 +1,66 @@
pragma ComponentBehavior: Bound
import qs.modules.common
import qs.modules.common.widgets
import QtQuick
Item {
id: root
property real numberSize: 80
property real margins: 10
property color color: Appearance.colors.colOnSecondaryContainer
property real hourLineSize: 4
property real minuteLineSize: 2
property real hourLineLength: 18
property real minuteLineLength: 7
property int hours: 12
property int minutes: 60
// Full dial style hour lines
Repeater {
model: root.hours
Item {
required property int index
rotation: 360 / root.hours * index
anchors.fill: parent
Rectangle {
anchors {
left: parent.left
verticalCenter: parent.verticalCenter
leftMargin: root.margins
}
implicitWidth: root.hourLineLength
implicitHeight: root.hourLineSize
radius: implicitWidth / 2
color: root.color
}
}
}
// Minute lines
Repeater {
model: root.minutes
Item {
required property int index
rotation: 360 / root.minutes * index
anchors.fill: parent
Rectangle {
anchors {
left: parent.left
verticalCenter: parent.verticalCenter
leftMargin: root.margins
}
implicitWidth: root.minuteLineLength
implicitHeight: root.minuteLineSize
radius: implicitWidth / 2
color: root.color
}
}
}
}

View file

@ -0,0 +1,54 @@
pragma ComponentBehavior: Bound
import qs.modules.common
import qs.modules.common.widgets
import QtQuick
Item {
id: root
property color color: Appearance.colors.colOnSecondaryContainer
property string style: Config.options.background.widgets.clock.cookie.dialNumberStyle // "dots", "numbers", "full", "hide"
property string dateStyle : Config.options.background.widgets.clock.cookie.dateStyle
// 12 Dots
FadeLoader {
id: dotsLoader
anchors {
fill: parent
margins: 10
}
shown: root.style === "dots"
sourceComponent: Dots {
color: root.color
margins: 46 - dotsLoader.opacity * 34
}
}
// 3-6-9-12 hour numbers (pls don't realize you can have more than 4 numbers)
FadeLoader {
id: bigHourNumbersLoader
anchors.fill: parent
shown: root.style === "numbers"
sourceComponent: BigHourNumbers {
numberSize: 80
color: root.color
margins: 20 - 10 * bigHourNumbersLoader.opacity
}
}
// Lines
FadeLoader {
id: linesLoader
anchors {
fill: parent
margins: 10
}
shown: root.style === "full"
sourceComponent: Lines {
color: root.color
margins: 46 - linesLoader.opacity * 34
}
}
}

View file

@ -0,0 +1,141 @@
import QtQuick
import Quickshell
import Quickshell.Io
import qs
import qs.services
import qs.modules.common
import qs.modules.common.functions
import qs.modules.common.widgets
import qs.modules.common.widgets.widgetCanvas
import qs.modules.ii.background.widgets
AbstractBackgroundWidget {
id: root
configEntryName: "systemResources"
implicitHeight: backgroundShape.implicitHeight
implicitWidth: backgroundShape.implicitWidth
property bool showGraphs: Config.options.background.widgets.systemResources.showGraphs || false
property int maxHistory: ResourceUsage.historyLength
property var gpuUsageHistory: []
property real currentGpuUsage: 0
Component.onCompleted: {
var arr = [];
for (var i = 0; i < maxHistory; i++) arr.push(0);
gpuUsageHistory = arr;
}
Timer { interval: 3000; running: true; repeat: true; triggeredOnStart: true; onTriggered: gpuProcess.running = true }
Process {
id: gpuProcess
running: false
command: ["bash", "-c",
"if command -v nvidia-smi &>/dev/null; then nvidia-smi --query-gpu=utilization.gpu --format=csv,noheader,nounits; " +
"elif [ -f /sys/class/drm/card0/device/gpu_busy_percent ]; then cat /sys/class/drm/card0/device/gpu_busy_percent; " +
"else echo 0; fi"]
stdout: SplitParser {
onRead: (line) => {
var v = parseFloat(line.trim());
if (!isNaN(v)) {
root.currentGpuUsage = v;
var arr = [...root.gpuUsageHistory, v/100.0];
if (arr.length > root.maxHistory) arr.shift();
root.gpuUsageHistory = arr;
}
}
}
}
StyledDropShadow { target: backgroundShape }
Rectangle {
id: backgroundShape
anchors.fill: parent
radius: Appearance.rounding.windowRounding
color: Appearance.colors.colPrimaryContainer
implicitWidth: 350
implicitHeight: contentCol.implicitHeight + 40
Column {
id: contentCol
anchors.centerIn: parent
spacing: 20
width: parent.width - 40
Row {
spacing: 15
MaterialSymbol { iconSize: 32; color: Appearance.colors.colOnPrimaryContainer; text: "memory"; anchors.verticalCenter: parent.verticalCenter }
StyledText { font.pixelSize: 18; font.weight: Font.Bold; color: Appearance.colors.colOnPrimaryContainer; text: "System Resources"; anchors.verticalCenter: parent.verticalCenter }
}
Row {
spacing: 30
anchors.horizontalCenter: parent.horizontalCenter
Column {
StyledText { text: "CPU"; font.pixelSize: 12; color: Appearance.colors.colPrimary }
StyledText { text: Math.round(ResourceUsage.cpuUsage * 100) + "%"; font.pixelSize: 18; color: Appearance.colors.colOnPrimaryContainer; font.weight: Font.Bold }
}
Column {
StyledText { text: "RAM"; font.pixelSize: 12; color: Appearance.colors.colPrimary }
StyledText { text: Math.round(ResourceUsage.memoryUsedPercentage * 100) + "%"; font.pixelSize: 18; color: Appearance.colors.colOnPrimaryContainer; font.weight: Font.Bold }
}
Column {
StyledText { text: "GPU"; font.pixelSize: 12; color: Appearance.colors.colPrimary }
StyledText { text: Math.round(root.currentGpuUsage) + "%"; font.pixelSize: 18; color: Appearance.colors.colOnPrimaryContainer; font.weight: Font.Bold }
}
}
Canvas {
id: graphCanvas
width: parent.width; height: 100; visible: root.showGraphs
onPaint: {
var ctx = getContext("2d");
ctx.clearRect(0, 0, width, height);
function drawSmooth(arr, color) {
if (!arr || arr.length < 2) return;
ctx.beginPath();
ctx.strokeStyle = color; ctx.lineWidth = 2;
ctx.lineCap = "round"; ctx.lineJoin = "round";
var n = arr.length, step = width/(n-1);
ctx.moveTo(0, height - arr[0]*(height-4) - 2);
for (var i=1; i<n; i++) {
var cpx = ((i-1)*step + i*step)/2;
var py = height - arr[i-1]*(height-4) - 2;
var cy = height - arr[i]*(height-4) - 2;
ctx.bezierCurveTo(cpx, py, cpx, cy, i*step, cy);
}
ctx.stroke();
}
drawSmooth(root.gpuUsageHistory, Appearance.colors.colError);
drawSmooth(ResourceUsage.memoryUsageHistory, Appearance.colors.colSecondary);
drawSmooth(ResourceUsage.cpuUsageHistory, Appearance.colors.colPrimary);
}
Timer { interval: 1000; running: root.showGraphs; repeat: true; onTriggered: parent.requestPaint() }
Row {
anchors.top: parent.bottom; anchors.topMargin: 5
anchors.horizontalCenter: parent.horizontalCenter; spacing: 15
Repeater {
model: [
{ label: "CPU", color: Appearance.colors.colPrimary },
{ label: "RAM", color: Appearance.colors.colSecondary },
{ label: "GPU", color: Appearance.colors.colError }
]
Row {
spacing: 5
required property var modelData
Rectangle { width: 10; height: 10; radius: 5; color: modelData.color; anchors.verticalCenter: parent.verticalCenter }
StyledText { text: modelData.label; font.pixelSize: 10; color: Appearance.colors.colOnPrimaryContainer }
}
}
}
}
Item { width: 1; height: root.showGraphs ? 20 : 0 }
}
}
}

View file

@ -0,0 +1,178 @@
import QtQuick
import Quickshell
import Quickshell.Io
import qs
import qs.services
import qs.modules.common
import qs.modules.common.functions
import qs.modules.common.widgets
import qs.modules.common.widgets.widgetCanvas
import qs.modules.ii.background.widgets
AbstractBackgroundWidget {
id: root
configEntryName: "stats"
implicitHeight: backgroundShape.implicitHeight
implicitWidth: backgroundShape.implicitWidth
property string githubUsername: Config.options.background.widgets.stats.githubUsername
property string codeforcesUsername: Config.options.background.widgets.stats.codeforcesUsername
property bool showGraphs: Config.options.background.widgets.stats.showGraphs || false
property int ghFollowers: 0
property int ghRepos: 0
property string cfRank: "--"
property int cfRating: 0
property var githubActivityArray: []
property var cfActivityArray: []
property int maxGithubActivity: 1
property int maxCfActivity: 1
function fetchGithub() {
if (!githubUsername) return;
var req = new XMLHttpRequest();
req.onreadystatechange = function() {
if (req.readyState === 4 && req.status === 200) {
var data = JSON.parse(req.responseText);
ghFollowers = data.followers || 0;
ghRepos = data.public_repos || 0;
}
}
req.open("GET", "https://api.github.com/users/" + githubUsername, true);
req.send();
if (showGraphs) {
var ereq = new XMLHttpRequest();
ereq.onreadystatechange = function() {
if (ereq.readyState !== 4 || ereq.status !== 200) return;
var events = JSON.parse(ereq.responseText);
var bins = new Array(30).fill(0), maxV = 0, now = new Date();
for (var i = 0; i < events.length; i++) {
var d = Math.floor(Math.abs(now - new Date(events[i].created_at)) / 86400000);
if (d < 30) { bins[29-d]++; if (bins[29-d] > maxV) maxV = bins[29-d]; }
}
maxGithubActivity = Math.max(1, maxV);
githubActivityArray = bins;
}
ereq.open("GET", "https://api.github.com/users/" + githubUsername + "/events/public?per_page=100", true);
ereq.send();
}
}
function fetchCodeforces() {
if (!codeforcesUsername) return;
var req = new XMLHttpRequest();
req.onreadystatechange = function() {
if (req.readyState === 4 && req.status === 200) {
var data = JSON.parse(req.responseText);
if (data.status === "OK" && data.result.length > 0) {
cfRank = data.result[0].rank || "--";
cfRating = data.result[0].rating || 0;
}
}
}
req.open("GET", "https://codeforces.com/api/user.info?handles=" + codeforcesUsername, true);
req.send();
if (showGraphs) {
var sreq = new XMLHttpRequest();
sreq.onreadystatechange = function() {
if (sreq.readyState !== 4 || sreq.status !== 200) return;
var data = JSON.parse(sreq.responseText);
if (data.status !== "OK") return;
var bins = new Array(30).fill(0), maxV = 0, nowS = Date.now()/1000;
for (var i = 0; i < data.result.length; i++) {
var d = Math.floor((nowS - data.result[i].creationTimeSeconds) / 86400);
if (d < 30) { bins[29-d]++; if (bins[29-d] > maxV) maxV = bins[29-d]; }
}
maxCfActivity = Math.max(1, maxV);
cfActivityArray = bins;
}
sreq.open("GET", "https://codeforces.com/api/user.status?handle=" + codeforcesUsername + "&from=1&count=200", true);
sreq.send();
}
}
Timer { interval: 600000; running: true; repeat: true; triggeredOnStart: true; onTriggered: { fetchGithub(); fetchCodeforces(); } }
onGithubUsernameChanged: fetchGithub()
onCodeforcesUsernameChanged: fetchCodeforces()
onShowGraphsChanged: { if (showGraphs) { fetchGithub(); fetchCodeforces(); } }
StyledDropShadow { target: backgroundShape }
Rectangle {
id: backgroundShape
anchors.fill: parent
radius: Appearance.rounding.windowRounding
color: Appearance.colors.colPrimaryContainer
implicitWidth: 320
implicitHeight: contentCol.implicitHeight + 40
Column {
id: contentCol
anchors.centerIn: parent
spacing: 20
width: parent.width - 40
// GitHub
Row {
spacing: 15
MaterialSymbol { iconSize: 40; color: Appearance.colors.colOnPrimaryContainer; text: "code"; anchors.verticalCenter: parent.verticalCenter }
Column {
StyledText { font.pixelSize: 18; font.weight: Font.Bold; color: Appearance.colors.colOnPrimaryContainer; text: "GitHub: " + (githubUsername || "Not set") }
StyledText { font.pixelSize: 14; color: Appearance.colors.colPrimary; text: ghFollowers + " Followers • " + ghRepos + " Repos" }
}
}
Canvas {
width: parent.width; height: root.showGraphs ? 40 : 0; visible: root.showGraphs
onPaint: {
var ctx = getContext("2d"); ctx.clearRect(0,0,width,height);
var arr = root.githubActivityArray;
if (!arr || arr.length < 2) return;
ctx.beginPath(); ctx.strokeStyle = Appearance.colors.colSecondary;
ctx.lineWidth = 2; ctx.lineCap = "round"; ctx.lineJoin = "round";
var step = width/(arr.length-1);
ctx.moveTo(0, height - (arr[0]/root.maxGithubActivity)*(height-4)-2);
for(var i=1;i<arr.length;i++){
var cpx=((i-1)*step+i*step)/2;
var py=height-(arr[i-1]/root.maxGithubActivity)*(height-4)-2;
var cy=height-(arr[i]/root.maxGithubActivity)*(height-4)-2;
ctx.bezierCurveTo(cpx,py,cpx,cy,i*step,cy);
}
ctx.stroke();
}
Timer { interval: 1000; running: root.showGraphs; repeat: true; onTriggered: parent.requestPaint() }
}
// Codeforces
Row {
spacing: 15
MaterialSymbol { iconSize: 40; color: Appearance.colors.colOnPrimaryContainer; text: "bar_chart"; anchors.verticalCenter: parent.verticalCenter }
Column {
StyledText { font.pixelSize: 18; font.weight: Font.Bold; color: Appearance.colors.colOnPrimaryContainer; text: "Codeforces: " + (codeforcesUsername || "Not set") }
StyledText { font.pixelSize: 14; color: Appearance.colors.colPrimary; text: "Rating: " + cfRating + " • " + cfRank }
}
}
Canvas {
width: parent.width; height: root.showGraphs ? 40 : 0; visible: root.showGraphs
onPaint: {
var ctx = getContext("2d"); ctx.clearRect(0,0,width,height);
var arr = root.cfActivityArray;
if (!arr || arr.length < 2) return;
ctx.beginPath(); ctx.strokeStyle = Appearance.colors.colError;
ctx.lineWidth = 2; ctx.lineCap = "round"; ctx.lineJoin = "round";
var step = width/(arr.length-1);
ctx.moveTo(0, height - (arr[0]/root.maxCfActivity)*(height-4)-2);
for(var i=1;i<arr.length;i++){
var cpx=((i-1)*step+i*step)/2;
var py=height-(arr[i-1]/root.maxCfActivity)*(height-4)-2;
var cy=height-(arr[i]/root.maxCfActivity)*(height-4)-2;
ctx.bezierCurveTo(cpx,py,cpx,cy,i*step,cy);
}
ctx.stroke();
}
Timer { interval: 1000; running: root.showGraphs; repeat: true; onTriggered: parent.requestPaint() }
}
}
}
}

View file

@ -0,0 +1,215 @@
import QtQuick
import Quickshell
import Quickshell.Io
import qs
import qs.services
import qs.modules.common
import qs.modules.common.functions
import qs.modules.common.widgets
import qs.modules.common.widgets.widgetCanvas
import qs.modules.ii.background.widgets
AbstractBackgroundWidget {
id: root
configEntryName: "visualizer"
implicitHeight: backgroundShape.implicitHeight
implicitWidth: backgroundShape.implicitWidth
property int numBars: {
// cava requires even bar count in stereo mode — force even
var raw = Config.options.background.widgets.visualizer.bars;
return raw % 2 === 0 ? raw : raw + 1;
}
property bool isVertical: Config.options.background.widgets.visualizer.vertical
property string style: Config.options.background.widgets.visualizer.style
property var amplitudeArray: []
property bool cavaReady: false
// Cava config path — /tmp is cleared on reboot, we always regenerate it
readonly property string cavaConfigPath: "/tmp/quickshell_cava_config"
function buildCavaConfig() {
return "[general]\nbars = " + root.numBars + "\nchannels = mono\n" +
"[output]\nmethod = raw\nraw_target = /dev/stdout\n" +
"data_format = ascii\nascii_max_range = 100\n";
}
// Souveraine (idle-power task 4): cava only earns its keep while the
// widget can actually be seen. Gate the watchdog on visibility + display
// activity, and tear cava down when either goes away.
readonly property bool wantCava: parent.visible && GlobalStates.displayActive
onWantCavaChanged: {
if (!wantCava && cavaProcess.running) {
root.cavaReady = false;
cavaProcess.running = false;
}
}
// Watchdog: every 2s, if cava isn't running, write config and start it
Timer {
id: watchdog
interval: 2000
running: root.wantCava
repeat: true
triggeredOnStart: true
onTriggered: {
if (!cavaProcess.running) {
root.cavaReady = false;
writeConfigProcess.running = false;
writeConfigProcess.running = true;
}
}
}
// Step 1: Write the cava config to /tmp
Process {
id: writeConfigProcess
running: false
command: ["bash", "-c",
"printf '%s' '" + root.buildCavaConfig().replace(/'/g, "'\\''") + "' > " + root.cavaConfigPath]
onExited: (code) => {
if (code === 0) startDelay.start();
}
}
Timer {
id: startDelay
interval: 100
repeat: false
onTriggered: {
cavaProcess.running = true;
root.cavaReady = true;
}
}
// Step 2: Run cava, read its ascii stdout
Process {
id: cavaProcess
running: false
command: ["cava", "-p", root.cavaConfigPath]
stdout: SplitParser {
onRead: (line) => {
var vals = line.split(';')
.map(s => parseInt(s, 10))
.filter(v => !isNaN(v) && v >= 0);
if (vals.length > 0) root.amplitudeArray = vals;
}
}
onRunningChanged: {
if (!running) root.cavaReady = false;
}
}
// When bar count changes, restart cava with new config
onNumBarsChanged: {
cavaProcess.running = false;
writeConfigProcess.running = false;
writeConfigProcess.running = true;
var arr = [];
for (let i = 0; i < numBars; i++) arr.push(0);
amplitudeArray = arr;
}
Rectangle {
id: backgroundShape
anchors.fill: parent
color: "transparent"
implicitWidth: isVertical ? 200 : (root.numBars * 10 + 40)
implicitHeight: isVertical ? (root.numBars * 10 + 40) : 200
Item {
anchors.fill: parent
anchors.margins: 20
// Horizontal base line
Rectangle {
visible: !isVertical
color: Appearance.colors.colPrimary
height: 4; width: parent.width
anchors.bottom: parent.bottom
radius: 2
}
// Vertical base line
Rectangle {
visible: isVertical
color: Appearance.colors.colPrimary
width: 4; height: parent.height
anchors.left: parent.left
radius: 2
}
// Horizontal bars
Row {
anchors.fill: parent; spacing: 4
visible: !isVertical && root.style === "bars"
Repeater {
model: root.amplitudeArray
Rectangle {
required property int modelData
width: Math.max(1, parent.width / root.amplitudeArray.length - parent.spacing)
height: Math.max(2, (modelData / 100) * parent.height)
anchors.bottom: parent.bottom
color: Appearance.colors.colPrimary; radius: 2
Behavior on height { NumberAnimation { duration: 60; easing.type: Easing.OutSine } }
}
}
}
// Vertical bars
Column {
anchors.fill: parent; spacing: 4
visible: isVertical && root.style === "bars"
Repeater {
model: root.amplitudeArray
Rectangle {
required property int modelData
height: Math.max(1, parent.height / root.amplitudeArray.length - parent.spacing)
width: Math.max(2, (modelData / 100) * parent.width)
anchors.left: parent.left
color: Appearance.colors.colPrimary; radius: 2
Behavior on width { NumberAnimation { duration: 60; easing.type: Easing.OutSine } }
}
}
}
// Wave — smooth bezier curve
Canvas {
anchors.fill: parent
visible: root.style === "wave"
onPaint: {
var ctx = getContext("2d");
ctx.clearRect(0, 0, width, height);
var arr = root.amplitudeArray;
if (arr.length < 2) return;
ctx.beginPath();
ctx.strokeStyle = Appearance.colors.colPrimary;
ctx.lineWidth = 4; ctx.lineCap = "round"; ctx.lineJoin = "round";
if (!isVertical) {
var stepX = width / (arr.length - 1);
ctx.moveTo(0, height - (arr[0]/100)*height);
for(var i=1; i<arr.length; i++) {
var cpx = ((i-1)*stepX + i*stepX) / 2;
ctx.bezierCurveTo(cpx, height-(arr[i-1]/100)*height, cpx, height-(arr[i]/100)*height, i*stepX, height-(arr[i]/100)*height);
}
} else {
var stepY = height / (arr.length - 1);
ctx.moveTo((arr[0]/100)*width, 0);
for(var j=1; j<arr.length; j++) {
var cpy = ((j-1)*stepY + j*stepY) / 2;
ctx.bezierCurveTo((arr[j-1]/100)*width, cpy, (arr[j]/100)*width, cpy, (arr[j]/100)*width, j*stepY);
}
}
ctx.stroke();
}
// 30fps repaint only while this canvas is actually visible.
// Was `running: root.style === "wave"` — but the Canvas's own
// `visible` (line above) already encodes that, AND collapses to
// false when the widget subtree is hidden. Gating the timer on
// `parent.visible` stops the continuous redraw whenever the
// canvas isn't shown, instead of painting behind a hidden
// background. No new import needed (pure QtQuick `visible`).
Timer { interval: 33; running: parent.visible; repeat: true; onTriggered: parent.requestPaint() }
}
}
}
}

View file

@ -0,0 +1,58 @@
import QtQuick
import qs
import qs.services
import qs.modules.common
import qs.modules.common.functions
import qs.modules.common.widgets
import qs.modules.common.widgets.widgetCanvas
import qs.modules.ii.background.widgets
AbstractBackgroundWidget {
id: root
configEntryName: "weather"
implicitHeight: backgroundShape.implicitHeight
implicitWidth: backgroundShape.implicitWidth
StyledDropShadow {
target: backgroundShape
}
MaterialShape {
id: backgroundShape
anchors.fill: parent
shape: MaterialShape.Shape.Pill
color: Appearance.colors.colPrimaryContainer
implicitSize: 200
StyledText {
font {
pixelSize: 80
family: Appearance.font.family.expressive
weight: Font.Medium
}
color: Appearance.colors.colPrimary
text: Weather.data?.temp.substring(0,Weather.data?.temp.length - 1) ?? "--°"
anchors {
right: parent.right
top: parent.top
rightMargin: 16
topMargin: 20
}
}
MaterialSymbol {
iconSize: 80
color: Appearance.colors.colOnPrimaryContainer
text: Icons.getWeatherIcon(Weather.data.wCode) ?? "cloud"
anchors {
left: parent.left
bottom: parent.bottom
leftMargin: 16
bottomMargin: 20
}
}
}
}

View file

@ -0,0 +1,66 @@
import qs.services
import qs.modules.common
import qs.modules.common.widgets
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Wayland
import Quickshell.Hyprland
Item {
id: root
readonly property HyprlandMonitor monitor: Hyprland.monitorFor(root.QsWindow.window?.screen)
readonly property Toplevel activeWindow: ToplevelManager.activeToplevel
property bool focusingThisMonitor: HyprlandData.activeWorkspace?.monitor == monitor?.name
property var hyprlandDataMonitor: HyprlandData.monitors.find(m => m.id === root.monitor?.id)
property bool activeIsSpecialWorkspace: Boolean(hyprlandDataMonitor?.specialWorkspace.id)
property var currentWorkspaceID: activeIsSpecialWorkspace? hyprlandDataMonitor?.specialWorkspace.id : hyprlandDataMonitor?.activeWorkspace.id
property var currentWorkspaceName: activeIsSpecialWorkspace?
capitalize(hyprlandDataMonitor?.specialWorkspace.name.replace("special:", "")) :
hyprlandDataMonitor?.activeWorkspace.name
property var biggestWindow: HyprlandData.biggestWindowForWorkspace(currentWorkspaceID)
function capitalize(s) {
return s.charAt(0).toUpperCase() + s.slice(1);
}
implicitWidth: colLayout.implicitWidth
ColumnLayout {
id: colLayout
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.right: parent.right
spacing: -4
StyledText {
Layout.fillWidth: true
font.pixelSize: Appearance.font.pixelSize.smaller
color: Appearance.colors.colSubtext
elide: Text.ElideRight
text: root.focusingThisMonitor && root.activeWindow?.activated && HyprlandData.activeWindow?.workspace?.id === root.currentWorkspaceID ?
HyprlandData.activeWindow.class :
root.biggestWindow?.class ??
(activeIsSpecialWorkspace ?
Translation.tr("Scratchpad") :
root.biggestWindow?.class ?? Translation.tr("Desktop"))
}
StyledText {
Layout.fillWidth: true
font.pixelSize: Appearance.font.pixelSize.small
color: Appearance.colors.colOnLayer0
elide: Text.ElideRight
text: root.focusingThisMonitor && root.activeWindow?.activated && HyprlandData.activeWindow?.workspace?.id === root.currentWorkspaceID
? HyprlandData.activeWindow.title
: root.biggestWindow?.title ??
(activeIsSpecialWorkspace
? `${Translation.tr("Workspace")} ${currentWorkspaceName}`
: `${Translation.tr("Workspace")} ${monitor?.activeWorkspace?.id ?? 1}`)
}
}
}

View file

@ -0,0 +1,269 @@
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import Quickshell.Hyprland
import qs
import qs.services
import qs.modules.common
import qs.modules.common.widgets
Scope {
id: bar
property bool showBarBackground: Config.options.bar.showBackground
Variants {
// For each monitor
model: {
const screens = Quickshell.screens;
const list = Config.options.bar.screenList;
if (!list || list.length === 0)
return screens;
return screens.filter(screen => list.includes(screen.name));
}
LazyLoader {
id: barLoader
active: GlobalStates.barOpen && !GlobalStates.screenLocked
required property ShellScreen modelData
component: PanelWindow { // Bar window
id: barRoot
screen: barLoader.modelData
Timer {
id: showBarTimer
interval: (Config?.options.bar.autoHide.showWhenPressingSuper.delay ?? 100)
repeat: false
onTriggered: {
barRoot.superShow = true
}
}
Connections {
target: GlobalStates
function onSuperDownChanged() {
if (!Config?.options.bar.autoHide.showWhenPressingSuper.enable) return;
if (GlobalStates.superDown) showBarTimer.restart();
else {
showBarTimer.stop();
barRoot.superShow = false;
}
}
}
property bool superShow: false
property bool mustShow: hoverRegion.containsMouse || superShow
// Normal, not Ignore. Ignore tells the compositor to disregard
// the exclusive zone entirely, so the careful expression below
// computed a height that was then thrown away and every window
// was laid out underneath the bar. The bar is not app space
// when it is visible; that is the whole point of a zone.
//
// The auto-hide case still yields 0 below, so a bar configured
// to hide still gives its space back — the mode says "honour
// what I claim", and claiming nothing remains available.
exclusionMode: ExclusionMode.Normal
exclusiveZone: (Config?.options.bar.autoHide.enable && (!mustShow || !Config?.options.bar.autoHide.pushWindows)) ? 0 :
Appearance.sizes.baseBarHeight + (Config.options.bar.cornerStyle === 1 ? Appearance.sizes.hyprlandGapsOut : 0)
WlrLayershell.namespace: "quickshell:bar"
implicitHeight: Appearance.sizes.barHeight + Appearance.rounding.screenRounding
mask: Region {
item: hoverMaskRegion
}
color: "transparent"
// Positioning
anchors {
top: !Config.options.bar.bottom
bottom: Config.options.bar.bottom
left: true
right: true
}
margins {
right: (Config.options.interactions.deadPixelWorkaround.enable && barRoot.anchors.right) * -1
bottom: (Config.options.interactions.deadPixelWorkaround.enable && barRoot.anchors.bottom) * -1
}
// Include in focus grab
Component.onCompleted: {
GlobalFocusGrab.addPersistent(barRoot);
}
Component.onDestruction: {
GlobalFocusGrab.removePersistent(barRoot);
}
MouseArea {
id: hoverRegion
hoverEnabled: true
anchors {
fill: parent
rightMargin: (Config.options.interactions.deadPixelWorkaround.enable && barRoot.anchors.right) * 1
bottomMargin: (Config.options.interactions.deadPixelWorkaround.enable && barRoot.anchors.bottom) * 1
}
Item {
id: hoverMaskRegion
anchors {
fill: barContent
topMargin: -Config.options.bar.autoHide.hoverRegionWidth
bottomMargin: -Config.options.bar.autoHide.hoverRegionWidth
}
}
BarContent {
id: barContent
implicitHeight: Appearance.sizes.barHeight
anchors {
right: parent.right
left: parent.left
top: parent.top
bottom: undefined
topMargin: (Config?.options.bar.autoHide.enable && !mustShow) ? -Appearance.sizes.barHeight : 0
bottomMargin: (Config.options.interactions.deadPixelWorkaround.enable && barRoot.anchors.bottom) * -1
rightMargin: (Config.options.interactions.deadPixelWorkaround.enable && barRoot.anchors.right) * -1
}
Behavior on anchors.topMargin {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
}
Behavior on anchors.bottomMargin {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
}
states: State {
name: "bottom"
when: Config.options.bar.bottom
AnchorChanges {
target: barContent
anchors {
right: parent.right
left: parent.left
top: undefined
bottom: parent.bottom
}
}
PropertyChanges {
target: barContent
anchors.topMargin: 0
anchors.bottomMargin: (Config?.options.bar.autoHide.enable && !mustShow) ? -Appearance.sizes.barHeight : 0
}
}
}
// Round decorators
Loader {
id: roundDecorators
anchors {
left: parent.left
right: parent.right
top: barContent.bottom
bottom: undefined
}
height: Appearance.rounding.screenRounding
active: showBarBackground && Config.options.bar.cornerStyle === 0 // Hug
states: State {
name: "bottom"
when: Config.options.bar.bottom
AnchorChanges {
target: roundDecorators
anchors {
right: parent.right
left: parent.left
top: undefined
bottom: barContent.top
}
}
}
sourceComponent: Item {
implicitHeight: Appearance.rounding.screenRounding
RoundCorner {
id: leftCorner
anchors {
top: parent.top
bottom: parent.bottom
left: parent.left
}
implicitSize: Appearance.rounding.screenRounding
color: showBarBackground ? Appearance.colors.colLayer0 : "transparent"
corner: RoundCorner.CornerEnum.TopLeft
states: State {
name: "bottom"
when: Config.options.bar.bottom
PropertyChanges {
leftCorner.corner: RoundCorner.CornerEnum.BottomLeft
}
}
}
RoundCorner {
id: rightCorner
anchors {
right: parent.right
top: !Config.options.bar.bottom ? parent.top : undefined
bottom: Config.options.bar.bottom ? parent.bottom : undefined
}
implicitSize: Appearance.rounding.screenRounding
color: showBarBackground ? Appearance.colors.colLayer0 : "transparent"
corner: RoundCorner.CornerEnum.TopRight
states: State {
name: "bottom"
when: Config.options.bar.bottom
PropertyChanges {
rightCorner.corner: RoundCorner.CornerEnum.BottomRight
}
}
}
}
}
}
}
}
}
IpcHandler {
target: "bar"
function toggle(): void {
GlobalStates.barOpen = !GlobalStates.barOpen
}
function close(): void {
GlobalStates.barOpen = false
}
function open(): void {
GlobalStates.barOpen = true
}
}
GlobalShortcut {
name: "barToggle"
description: "Toggles bar on press"
onPressed: {
GlobalStates.barOpen = !GlobalStates.barOpen;
}
}
GlobalShortcut {
name: "barOpen"
description: "Opens bar on press"
onPressed: {
GlobalStates.barOpen = true;
}
}
GlobalShortcut {
name: "barClose"
description: "Closes bar on press"
onPressed: {
GlobalStates.barOpen = false;
}
}
}

View file

@ -0,0 +1,357 @@
import qs.modules.ii.bar.weather
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Services.UPower
import qs
import qs.services
import qs.modules.common
import qs.modules.common.widgets
import qs.modules.common.functions
Item { // Bar content region
id: root
property var screen: root.QsWindow.window?.screen
property var brightnessMonitor: Brightness.getMonitorForScreen(screen)
property real useShortenedForm: (Appearance.sizes.barHellaShortenScreenWidthThreshold >= screen?.width) ? 2 : (Appearance.sizes.barShortenScreenWidthThreshold >= screen?.width) ? 1 : 0
readonly property int centerSideModuleWidth: (useShortenedForm == 2) ? Appearance.sizes.barCenterSideModuleWidthHellaShortened : (useShortenedForm == 1) ? Appearance.sizes.barCenterSideModuleWidthShortened : Appearance.sizes.barCenterSideModuleWidth
component VerticalBarSeparator: Rectangle {
Layout.topMargin: Appearance.sizes.baseBarHeight / 3
Layout.bottomMargin: Appearance.sizes.baseBarHeight / 3
Layout.fillHeight: true
implicitWidth: 1
color: Appearance.colors.colOutlineVariant
}
// Background shadow
Loader {
active: Config.options.bar.showBackground && Config.options.bar.cornerStyle === 1 && Config.options.bar.floatStyleShadow
anchors.fill: barBackground
sourceComponent: StyledRectangularShadow {
anchors.fill: undefined // The loader's anchors act on this, and this should not have any anchor
target: barBackground
}
}
// Background
Rectangle {
id: barBackground
anchors {
fill: parent
margins: Config.options.bar.cornerStyle === 1 ? (Appearance.sizes.hyprlandGapsOut) : 0 // idk why but +1 is needed
}
color: Config.options.bar.showBackground ? Appearance.colors.colLayer0 : "transparent"
radius: Config.options.bar.cornerStyle === 1 ? Appearance.rounding.windowRounding : 0
border.width: Config.options.bar.cornerStyle === 1 ? 1 : 0
border.color: Appearance.colors.colLayer0Border
}
FocusedScrollMouseArea { // Left side | scroll to change brightness
id: barLeftSideMouseArea
anchors {
top: parent.top
bottom: parent.bottom
left: parent.left
right: middleSection.left
}
implicitWidth: leftSectionRowLayout.implicitWidth
implicitHeight: Appearance.sizes.baseBarHeight
onScrollDown: Brightness.decreaseBrightness()
onScrollUp: Brightness.increaseBrightness()
onMovedAway: GlobalStates.osdBrightnessOpen = false
onPressed: event => {
if (event.button === Qt.LeftButton)
GlobalStates.sidebarLeftOpen = !GlobalStates.sidebarLeftOpen;
}
// Visual content
ScrollHint {
reveal: barLeftSideMouseArea.hovered
icon: Hyprsunset.gamma === 100 ? "light_mode" : "wb_twilight"
tooltipText: Translation.tr("Scroll to change brightness")
side: "left"
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
}
RowLayout {
id: leftSectionRowLayout
anchors.fill: parent
spacing: 0
LeftSidebarButton { // Left sidebar button
id: leftSidebarButton
Layout.alignment: Qt.AlignVCenter
Layout.leftMargin: Appearance.rounding.screenRounding
colBackground: barLeftSideMouseArea.hovered ? Appearance.colors.colLayer1Hover : ColorUtils.transparentize(Appearance.colors.colLayer1Hover, 1)
}
ActiveWindow {
Layout.leftMargin: 10 + (leftSidebarButton.visible ? 0 : Appearance.rounding.screenRounding)
Layout.rightMargin: Appearance.rounding.screenRounding
Layout.fillWidth: true
Layout.fillHeight: true
visible: root.useShortenedForm === 0
}
}
}
Row { // Middle section
id: middleSection
anchors {
top: parent.top
bottom: parent.bottom
horizontalCenter: parent.horizontalCenter
}
spacing: 4
BarGroup {
id: leftCenterGroup
anchors.verticalCenter: parent.verticalCenter
Resources {
alwaysShowAllResources: root.useShortenedForm === 2
Layout.fillWidth: root.useShortenedForm === 2
}
Media {
visible: root.useShortenedForm < 2
Layout.fillWidth: true
}
Loader {
active: Config.options.bar.claudeUsage.enable
visible: root.useShortenedForm < 2 && active
sourceComponent: ClaudeUsageBar {}
}
}
VerticalBarSeparator {
visible: Config.options?.bar.borderless
}
BarGroup {
id: middleCenterGroup
anchors.verticalCenter: parent.verticalCenter
padding: workspacesWidget.widgetPadding
Workspaces {
id: workspacesWidget
Layout.fillHeight: true
MouseArea {
// Right-click to toggle overview
anchors.fill: parent
acceptedButtons: Qt.RightButton
onPressed: event => {
if (event.button === Qt.RightButton) {
GlobalStates.overviewOpen = !GlobalStates.overviewOpen;
}
}
}
}
}
VerticalBarSeparator {
visible: Config.options?.bar.borderless
}
MouseArea {
id: rightCenterGroup
anchors.verticalCenter: parent.verticalCenter
implicitWidth: rightCenterGroupContent.implicitWidth
implicitHeight: rightCenterGroupContent.implicitHeight
onPressed: {
GlobalStates.sidebarRightOpen = !GlobalStates.sidebarRightOpen;
}
BarGroup {
id: rightCenterGroupContent
anchors.fill: parent
ClockWidget {
showDate: (Config.options.bar.verbose && root.useShortenedForm < 2)
Layout.alignment: Qt.AlignVCenter
Layout.fillWidth: true
}
UtilButtons {
visible: (Config.options.bar.verbose && root.useShortenedForm === 0)
Layout.alignment: Qt.AlignVCenter
}
BatteryIndicator {
visible: (root.useShortenedForm < 2 && Battery.available)
Layout.alignment: Qt.AlignVCenter
}
}
}
}
FocusedScrollMouseArea { // Right side | scroll to change volume
id: barRightSideMouseArea
anchors {
top: parent.top
bottom: parent.bottom
left: middleSection.right
right: parent.right
}
implicitWidth: rightSectionRowLayout.implicitWidth
implicitHeight: Appearance.sizes.baseBarHeight
onScrollDown: Audio.decrementVolume();
onScrollUp: Audio.incrementVolume();
onMovedAway: GlobalStates.osdVolumeOpen = false;
onPressed: event => {
if (event.button === Qt.LeftButton) {
GlobalStates.sidebarRightOpen = !GlobalStates.sidebarRightOpen;
}
}
// Visual content
ScrollHint {
reveal: barRightSideMouseArea.hovered
icon: "volume_up"
tooltipText: Translation.tr("Scroll to change volume")
side: "right"
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
}
RowLayout {
id: rightSectionRowLayout
anchors.fill: parent
spacing: 5
layoutDirection: Qt.RightToLeft
RippleButton { // Right sidebar button
id: rightSidebarButton
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
Layout.rightMargin: Appearance.rounding.screenRounding
Layout.fillWidth: false
implicitWidth: indicatorsRowLayout.implicitWidth + 10 * 2
implicitHeight: indicatorsRowLayout.implicitHeight + 5 * 2
buttonRadius: Appearance.rounding.full
colBackground: barRightSideMouseArea.hovered ? Appearance.colors.colLayer1Hover : ColorUtils.transparentize(Appearance.colors.colLayer1Hover, 1)
colBackgroundHover: Appearance.colors.colLayer1Hover
colRipple: Appearance.colors.colLayer1Active
colBackgroundToggled: Appearance.colors.colSecondaryContainer
colBackgroundToggledHover: Appearance.colors.colSecondaryContainerHover
colRippleToggled: Appearance.colors.colSecondaryContainerActive
toggled: GlobalStates.sidebarRightOpen
property color colText: toggled ? Appearance.m3colors.m3onSecondaryContainer : Appearance.colors.colOnLayer0
Behavior on colText {
animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this)
}
onPressed: {
GlobalStates.sidebarRightOpen = !GlobalStates.sidebarRightOpen;
}
RowLayout {
id: indicatorsRowLayout
anchors.centerIn: parent
property real realSpacing: 15
spacing: 0
Revealer {
reveal: Audio.sink?.audio?.muted ?? false
Layout.fillHeight: true
Layout.rightMargin: reveal ? indicatorsRowLayout.realSpacing : 0
Behavior on Layout.rightMargin {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
}
MaterialSymbol {
text: "volume_off"
iconSize: Appearance.font.pixelSize.larger
color: rightSidebarButton.colText
}
}
Revealer {
reveal: Audio.source?.audio?.muted ?? false
Layout.fillHeight: true
Layout.rightMargin: reveal ? indicatorsRowLayout.realSpacing : 0
Behavior on Layout.rightMargin {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
}
MaterialSymbol {
text: "mic_off"
iconSize: Appearance.font.pixelSize.larger
color: rightSidebarButton.colText
}
}
HyprlandXkbIndicator {
Layout.alignment: Qt.AlignVCenter
Layout.rightMargin: indicatorsRowLayout.realSpacing
color: rightSidebarButton.colText
}
Revealer {
reveal: Notifications.silent || Notifications.unread > 0
Layout.fillHeight: true
Layout.rightMargin: reveal ? indicatorsRowLayout.realSpacing : 0
implicitHeight: reveal ? notificationUnreadCount.implicitHeight : 0
implicitWidth: reveal ? notificationUnreadCount.implicitWidth : 0
Behavior on Layout.rightMargin {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
}
NotificationUnreadCount {
id: notificationUnreadCount
}
}
MaterialSymbol {
text: Network.materialSymbol
iconSize: Appearance.font.pixelSize.larger
color: rightSidebarButton.colText
}
MaterialSymbol {
Layout.leftMargin: indicatorsRowLayout.realSpacing
visible: BluetoothStatus.available
text: BluetoothStatus.connected ? "bluetooth_connected" : BluetoothStatus.enabled ? "bluetooth" : "bluetooth_disabled"
iconSize: Appearance.font.pixelSize.larger
color: rightSidebarButton.colText
}
}
}
Revealer {
reveal: TimerService.pomodoroRunning
Layout.fillHeight: true
PomodoroBarIndicator {
anchors.verticalCenter: parent.verticalCenter
}
}
SysTray {
visible: root.useShortenedForm === 0
Layout.fillWidth: false
Layout.fillHeight: true
invertSide: Config?.options.bar.bottom
}
Item {
Layout.fillWidth: true
Layout.fillHeight: true
}
// Weather
Loader {
Layout.leftMargin: 4
active: Config.options.bar.weather.enable
sourceComponent: BarGroup {
WeatherBar {}
}
}
}
}
}

View file

@ -0,0 +1,41 @@
import qs.modules.common
import QtQuick
import QtQuick.Layouts
Item {
id: root
property bool vertical: false
property real padding: 5
implicitWidth: vertical ? Appearance.sizes.baseVerticalBarWidth : (gridLayout.implicitWidth + padding * 2)
implicitHeight: vertical ? (gridLayout.implicitHeight + padding * 2) : Appearance.sizes.baseBarHeight
default property alias items: gridLayout.children
Rectangle {
id: background
anchors {
fill: parent
topMargin: root.vertical ? 0 : 4
bottomMargin: root.vertical ? 0 : 4
leftMargin: root.vertical ? 4 : 0
rightMargin: root.vertical ? 4 : 0
}
color: Config.options?.bar.borderless ? "transparent" : Appearance.colors.colLayer1
radius: Appearance.rounding.small
}
GridLayout {
id: gridLayout
columns: root.vertical ? 1 : -1
anchors {
verticalCenter: root.vertical ? undefined : parent.verticalCenter
horizontalCenter: root.vertical ? parent.horizontalCenter : undefined
left: root.vertical ? undefined : parent.left
right: root.vertical ? undefined : parent.right
top: root.vertical ? parent.top : undefined
bottom: root.vertical ? parent.bottom : undefined
margins: root.padding
}
columnSpacing: 4
rowSpacing: 12
}
}

View file

@ -0,0 +1,63 @@
import qs.modules.common
import qs.modules.common.widgets
import qs.services
import QtQuick
import QtQuick.Layouts
MouseArea {
id: root
property bool borderless: Config.options.bar.borderless
readonly property var chargeState: Battery.chargeState
readonly property bool isCharging: Battery.isCharging
readonly property bool isPluggedIn: Battery.isPluggedIn
readonly property real percentage: Battery.percentage
readonly property bool isLow: percentage <= Config.options.battery.low / 100
implicitWidth: batteryProgress.implicitWidth
implicitHeight: Appearance.sizes.barHeight
hoverEnabled: !Config.options.bar.tooltips.clickToShow
ClippedProgressBar {
id: batteryProgress
anchors.centerIn: parent
value: percentage
highlightColor: (isLow && !isCharging) ? Appearance.m3colors.m3error : Appearance.colors.colOnSecondaryContainer
Item {
anchors.centerIn: parent
width: batteryProgress.valueBarWidth
height: batteryProgress.valueBarHeight
RowLayout {
anchors {
horizontalCenter: parent.horizontalCenter
bottom: parent.bottom
bottomMargin: (parent.height - height) / 2
}
spacing: 0
MaterialSymbol {
id: boltIcon
Layout.alignment: Qt.AlignVCenter
Layout.leftMargin: -2
Layout.rightMargin: -2
fill: 1
text: "bolt"
iconSize: Appearance.font.pixelSize.smaller
visible: isCharging && percentage < 1 // TODO: animation
}
StyledText {
Layout.alignment: Qt.AlignVCenter
font: batteryProgress.font
text: batteryProgress.text
}
}
}
}
BatteryPopup {
id: batteryPopup
hoverTarget: root
}
}

View file

@ -0,0 +1,72 @@
import qs.modules.common
import qs.modules.common.widgets
import qs.services
import QtQuick
import QtQuick.Layouts
StyledPopup {
id: root
ColumnLayout {
id: columnLayout
anchors.centerIn: parent
spacing: 4
// Header
StyledPopupHeaderRow {
icon: "battery_android_full"
label: Translation.tr("Battery")
}
StyledPopupValueRow {
visible: {
let timeValue = Battery.isCharging ? Battery.timeToFull : Battery.timeToEmpty;
let power = Battery.energyRate;
return !(Battery.chargeState == 4 || timeValue <= 0 || power <= 0.01);
}
icon: "schedule"
label: Battery.isCharging ? Translation.tr("Time to full:") : Translation.tr("Time to empty:")
value: {
function formatTime(seconds) {
var h = Math.floor(seconds / 3600);
var m = Math.floor((seconds % 3600) / 60);
if (h > 0)
return `${h}h, ${m}m`;
else
return `${m}m`;
}
if (Battery.isCharging)
return formatTime(Battery.timeToFull);
else
return formatTime(Battery.timeToEmpty);
}
}
StyledPopupValueRow {
visible: !(Battery.chargeState != 4 && Battery.energyRate == 0)
icon: "bolt"
label: {
if (Battery.chargeState == 4) {
return Translation.tr("Fully charged");
} else if (Battery.chargeState == 1) {
return Translation.tr("Charging:");
} else {
return Translation.tr("Discharging:");
}
}
value: {
if (Battery.chargeState == 4) {
return "";
} else {
return `${Battery.energyRate.toFixed(2)}W`;
}
}
}
StyledPopupValueRow {
icon: "heart_check"
label: Translation.tr("Health:")
value: `${(Battery.health).toFixed(1)}%`
}
}
}

View file

@ -0,0 +1,15 @@
import qs.modules.common
import qs.modules.common.widgets
import QtQuick
RippleButton {
id: button
required default property Item content
property bool extraActiveCondition: false
implicitHeight: Math.max(content.implicitHeight, 26, content.implicitHeight)
implicitWidth: implicitHeight
contentItem: content
}

View file

@ -0,0 +1,153 @@
import qs
import qs.modules.common
import qs.modules.common.widgets
import qs.services
import QtQuick
import QtQuick.Layouts
MouseArea {
id: root
property bool borderless: Config.options.bar.borderless
implicitWidth: rowLayout.implicitWidth + rowLayout.anchors.leftMargin + rowLayout.anchors.rightMargin
implicitHeight: Appearance.sizes.barHeight
hoverEnabled: !Config.options.bar.tooltips.clickToShow
acceptedButtons: Qt.LeftButton | Qt.MiddleButton
// Which window the single gauge shows; toggled by clicking.
property bool showingWeekly: Config.options.bar.claudeUsage.defaultWeekly
onPressed: event => {
if (event.button === Qt.MiddleButton)
ClaudeUsage.getData(); // middle-click to refresh now
else
root.showingWeekly = !root.showingWeekly; // click to switch session <-> week
}
// A single Claude-usage gauge: icon inside a ring + percentage number.
component UsageGauge: Item {
id: gauge
required property string iconName
required property real percentage // 0..1
property string label: "" // short window identifier, e.g. "5h" / "7d"
property int warningThreshold: Config.options.bar.claudeUsage.warningThreshold
property bool warning: percentage * 100 >= warningThreshold
implicitWidth: gaugeRow.implicitWidth
implicitHeight: Appearance.sizes.barHeight
RowLayout {
id: gaugeRow
spacing: 2
anchors.verticalCenter: parent.verticalCenter
ClippedFilledCircularProgress {
Layout.alignment: Qt.AlignVCenter
lineWidth: Appearance.rounding.unsharpen
value: Math.max(0, Math.min(1, gauge.percentage))
implicitSize: 20
colPrimary: gauge.warning ? Appearance.colors.colError : Appearance.colors.colOnSecondaryContainer
accountForLightBleeding: !gauge.warning
enableAnimation: false
Item {
anchors.centerIn: parent
width: 20
height: 20
MaterialSymbol {
anchors.centerIn: parent
font.weight: Font.DemiBold
fill: 1
text: gauge.iconName
iconSize: Appearance.font.pixelSize.normal
color: Appearance.m3colors.m3onSecondaryContainer
}
}
}
StyledText {
Layout.alignment: Qt.AlignVCenter
color: Appearance.colors.colOnLayer1
font.pixelSize: Appearance.font.pixelSize.small
text: ClaudeUsage.available ? `${Math.round(gauge.percentage * 100)}` : "–"
}
StyledText { // which window is being shown
Layout.alignment: Qt.AlignVCenter
visible: gauge.label.length > 0
color: Appearance.colors.colSubtext
font.pixelSize: Appearance.font.pixelSize.smaller
text: gauge.label
}
}
}
RowLayout {
id: rowLayout
spacing: 6
anchors.fill: parent
anchors.leftMargin: 4
anchors.rightMargin: 4
UsageGauge { // Click the module to switch between the two windows
iconName: root.showingWeekly ? "calendar_month" : "auto_awesome"
percentage: (root.showingWeekly ? ClaudeUsage.sevenDay : ClaudeUsage.fiveHour) / 100
label: root.showingWeekly ? "7d" : "5h"
}
}
StyledPopup {
hoverTarget: root
ColumnLayout {
anchors.centerIn: parent
spacing: 8
StyledPopupHeaderRow {
icon: "auto_awesome"
label: ClaudeUsage.subscriptionType.length > 0 ? `Claude ${ClaudeUsage.subscriptionType.charAt(0).toUpperCase() + ClaudeUsage.subscriptionType.slice(1)}` : "Claude usage"
}
ColumnLayout {
spacing: 4
visible: ClaudeUsage.available
StyledPopupValueRow {
icon: "timer"
label: Translation.tr("Session (5h):")
value: `${Math.round(ClaudeUsage.fiveHour)}% · ${ClaudeUsage.timeUntil(ClaudeUsage.fiveHourReset)}`
}
StyledPopupValueRow {
icon: "calendar_month"
label: Translation.tr("Week (7d):")
value: `${Math.round(ClaudeUsage.sevenDay)}% · ${ClaudeUsage.timeUntil(ClaudeUsage.sevenDayReset)}`
}
StyledPopupValueRow {
visible: ClaudeUsage.sevenDayOpus >= 0
icon: "neurology"
label: Translation.tr("Week · Opus:")
value: `${Math.round(ClaudeUsage.sevenDayOpus)}%`
}
StyledPopupValueRow {
visible: ClaudeUsage.sevenDaySonnet >= 0
icon: "graph_3"
label: Translation.tr("Week · Sonnet:")
value: `${Math.round(ClaudeUsage.sevenDaySonnet)}%`
}
StyledPopupValueRow {
visible: ClaudeUsage.extraEnabled && ClaudeUsage.extraMonthlyLimit > 0
icon: "paid"
label: Translation.tr("Extra credits:")
value: `${ClaudeUsage.extraUsedCredits.toFixed(2)} / ${ClaudeUsage.extraMonthlyLimit} ${ClaudeUsage.extraCurrency}`
}
}
StyledText {
visible: !ClaudeUsage.available
color: Appearance.colors.colOnSurfaceVariant
font.pixelSize: Appearance.font.pixelSize.small
text: ClaudeUsage.lastError.length > 0 ? Translation.tr("Unavailable: %1").arg(ClaudeUsage.lastError) : Translation.tr("Loading…")
}
}
}
}

View file

@ -0,0 +1,49 @@
import qs.modules.common
import qs.modules.common.widgets
import qs.services
import QtQuick
import QtQuick.Layouts
Item {
id: root
property bool borderless: Config.options.bar.borderless
property bool showDate: Config.options.bar.verbose
implicitWidth: rowLayout.implicitWidth
implicitHeight: Appearance.sizes.barHeight
RowLayout {
id: rowLayout
anchors.centerIn: parent
spacing: 4
StyledText {
font.pixelSize: Appearance.font.pixelSize.large
color: Appearance.colors.colOnLayer1
text: DateTime.time
}
StyledText {
visible: root.showDate
font.pixelSize: Appearance.font.pixelSize.small
color: Appearance.colors.colOnLayer1
text: "•"
}
StyledText {
visible: root.showDate
font.pixelSize: Appearance.font.pixelSize.small
color: Appearance.colors.colOnLayer1
text: DateTime.longDate
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: !Config.options.bar.tooltips.clickToShow
ClockWidgetPopup {
hoverTarget: mouseArea
}
}
}

View file

@ -0,0 +1,70 @@
import qs.modules.common
import qs.modules.common.widgets
import qs.services
import QtQuick
import QtQuick.Layouts
StyledPopup {
id: root
property string formattedDate: Qt.locale().toString(DateTime.clock.date, "dddd, MMMM dd, yyyy")
property string formattedTime: DateTime.time
property string formattedUptime: DateTime.uptime
property string todosSection: getUpcomingTodos()
function getUpcomingTodos() {
const unfinishedTodos = Todo.list.filter(function (item) {
return !item.done;
});
if (unfinishedTodos.length === 0) {
return Translation.tr("No pending tasks");
}
// Limit to first 5 todos to keep popup manageable
const limitedTodos = unfinishedTodos.slice(0, 5);
let todoText = limitedTodos.map(function (item, index) {
return ` ${index + 1}. ${item.content}`;
}).join('\n');
if (unfinishedTodos.length > 5) {
todoText += `\n ${Translation.tr("... and %1 more").arg(unfinishedTodos.length - 5)}`;
}
return todoText;
}
ColumnLayout {
id: columnLayout
anchors.centerIn: parent
spacing: 4
StyledPopupHeaderRow {
icon: "calendar_month"
label: root.formattedDate
}
StyledPopupValueRow {
icon: "timelapse"
label: Translation.tr("System uptime:")
value: root.formattedUptime
}
// Tasks
Column {
spacing: 0
Layout.fillWidth: true
StyledPopupValueRow {
icon: "checklist"
label: Translation.tr("To Do:")
value: ""
}
StyledText {
horizontalAlignment: Text.AlignLeft
wrapMode: Text.Wrap
color: Appearance.colors.colOnSurfaceVariant
text: root.todosSection
}
}
}
}

View file

@ -0,0 +1,34 @@
import QtQuick
import qs.services
import qs.modules.common
import qs.modules.common.widgets
Loader {
id: root
property bool vertical: false
property color color: Appearance.colors.colOnSurfaceVariant
active: HyprlandXkb.layoutCodes.length > 1
visible: active
function abbreviateLayoutCode(fullCode) {
return fullCode.split(':').map(layout => {
const baseLayout = layout.split('-')[0];
return baseLayout.slice(0, 4);
}).join('\n');
}
sourceComponent: Item {
implicitWidth: root.vertical ? null : layoutCodeText.implicitWidth
implicitHeight: root.vertical ? layoutCodeText.implicitHeight : null
StyledText {
id: layoutCodeText
anchors.centerIn: parent
horizontalAlignment: Text.AlignHCenter
text: abbreviateLayoutCode(HyprlandXkb.currentLayoutCode)
font.pixelSize: text.includes("\n") ? Appearance.font.pixelSize.smallie : Appearance.font.pixelSize.small
color: root.color
animateChange: true
}
}
}

View file

@ -0,0 +1,83 @@
import QtQuick
import qs
import qs.services
import qs.modules.common
import qs.modules.common.widgets
RippleButton {
id: root
property bool showPing: false
property bool aiChatEnabled: Config.options.policies.ai !== 0
property bool translatorEnabled: Config.options.sidebar.translator.enable
property bool animeEnabled: Config.options.policies.weeb !== 0
visible: aiChatEnabled || translatorEnabled || animeEnabled
property real buttonPadding: 5
implicitWidth: distroIcon.width + buttonPadding * 2
implicitHeight: distroIcon.height + buttonPadding * 2
buttonRadius: Appearance.rounding.full
colBackgroundHover: Appearance.colors.colLayer1Hover
colRipple: Appearance.colors.colLayer1Active
colBackgroundToggled: Appearance.colors.colSecondaryContainer
colBackgroundToggledHover: Appearance.colors.colSecondaryContainerHover
colRippleToggled: Appearance.colors.colSecondaryContainerActive
toggled: GlobalStates.sidebarLeftOpen
onPressed: {
GlobalStates.sidebarLeftOpen = !GlobalStates.sidebarLeftOpen;
}
Connections {
target: Ai
function onResponseFinished() {
if (GlobalStates.sidebarLeftOpen) return;
root.showPing = true;
}
}
Connections {
target: Booru
function onResponseFinished() {
if (GlobalStates.sidebarLeftOpen) return;
root.showPing = true;
}
}
Connections {
target: GlobalStates
function onSidebarLeftOpenChanged() {
root.showPing = false;
}
}
CustomIcon {
id: distroIcon
anchors.centerIn: parent
width: 19.5
height: 19.5
source: Config.options.bar.topLeftIcon == 'distro' ? SystemInfo.distroIcon : `${Config.options.bar.topLeftIcon}-symbolic`
colorize: true
color: Appearance.colors.colOnLayer0
Rectangle {
opacity: root.showPing ? 1 : 0
visible: opacity > 0
anchors {
bottom: parent.bottom
right: parent.right
bottomMargin: -2
rightMargin: -2
}
implicitWidth: 8
implicitHeight: 8
radius: Appearance.rounding.full
color: Appearance.colors.colTertiary
Behavior on opacity {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
}
}
}
}

View file

@ -0,0 +1,89 @@
import qs.modules.common
import qs.modules.common.widgets
import qs.services
import qs
import qs.modules.common.functions
import QtQuick
import QtQuick.Layouts
import Quickshell.Services.Mpris
import Quickshell.Hyprland
Item {
id: root
property bool borderless: Config.options.bar.borderless
readonly property MprisPlayer activePlayer: MprisController.activePlayer
readonly property string cleanedTitle: StringUtils.cleanMusicTitle(activePlayer?.trackTitle) || Translation.tr("No media")
Layout.fillHeight: true
implicitWidth: rowLayout.implicitWidth + rowLayout.spacing * 2
implicitHeight: Appearance.sizes.barHeight
Timer {
running: activePlayer?.playbackState == MprisPlaybackState.Playing
interval: Config.options.resources.updateInterval
repeat: true
onTriggered: activePlayer.positionChanged()
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.MiddleButton | Qt.BackButton | Qt.ForwardButton | Qt.RightButton | Qt.LeftButton
onPressed: (event) => {
if (event.button === Qt.MiddleButton) {
activePlayer.togglePlaying();
} else if (event.button === Qt.BackButton) {
activePlayer.previous();
} else if (event.button === Qt.ForwardButton || event.button === Qt.RightButton) {
activePlayer.next();
} else if (event.button === Qt.LeftButton) {
GlobalStates.mediaControlsOpen = !GlobalStates.mediaControlsOpen
}
}
}
RowLayout { // Real content
id: rowLayout
spacing: 4
anchors.fill: parent
ClippedFilledCircularProgress {
id: mediaCircProg
Layout.alignment: Qt.AlignVCenter
lineWidth: Appearance.rounding.unsharpen
value: activePlayer?.position / activePlayer?.length
implicitSize: 20
colPrimary: Appearance.colors.colOnSecondaryContainer
enableAnimation: false
Item {
anchors.centerIn: parent
width: mediaCircProg.implicitSize
height: mediaCircProg.implicitSize
MaterialSymbol {
anchors.centerIn: parent
fill: 1
text: activePlayer?.isPlaying ? "pause" : "music_note"
iconSize: Appearance.font.pixelSize.normal
color: Appearance.m3colors.m3onSecondaryContainer
}
}
}
StyledText {
visible: Config.options.bar.verbose
width: rowLayout.width - (CircularProgress.size + rowLayout.spacing * 2)
Layout.alignment: Qt.AlignVCenter
Layout.fillWidth: true // Ensures the text takes up available space
Layout.rightMargin: rowLayout.spacing
horizontalAlignment: Text.AlignHCenter
elide: Text.ElideRight // Truncates the text on the right
color: Appearance.colors.colOnLayer1
text: `${cleanedTitle}${activePlayer?.trackArtist ? ' • ' + activePlayer.trackArtist : ''}`
}
}
}

View file

@ -0,0 +1,38 @@
import QtQuick
import qs.services
import qs.modules.common
import qs.modules.common.widgets
MaterialSymbol {
id: root
readonly property bool showUnreadCount: Config.options.bar.indicators.notifications.showUnreadCount
text: Notifications.silent ? "notifications_paused" : "notifications"
iconSize: Appearance.font.pixelSize.larger
color: rightSidebarButton.colText
Rectangle {
id: notifPing
visible: !Notifications.silent && Notifications.unread > 0
anchors {
right: parent.right
top: parent.top
rightMargin: root.showUnreadCount ? 0 : 1
topMargin: root.showUnreadCount ? 0 : 3
}
radius: Appearance.rounding.full
color: Appearance.colors.colOnLayer0
z: 1
implicitHeight: root.showUnreadCount ? Math.max(notificationCounterText.implicitWidth, notificationCounterText.implicitHeight) : 8
implicitWidth: implicitHeight
StyledText {
id: notificationCounterText
visible: root.showUnreadCount
anchors.centerIn: parent
font.pixelSize: Appearance.font.pixelSize.smallest
color: Appearance.colors.colLayer0
text: Notifications.unread
}
}
}

View file

@ -0,0 +1,113 @@
import qs
import qs.modules.common
import qs.modules.common.widgets
import qs.services
import QtQuick
import QtQuick.Layouts
RippleButton {
id: root
property bool vertical: false
readonly property color stateColor: TimerService.pomodoroBreak ? Appearance.colors.colTertiaryContainer : Appearance.colors.colSecondaryContainer
readonly property color stateColorHover: TimerService.pomodoroBreak ? Appearance.colors.colTertiaryContainerHover : Appearance.colors.colSecondaryContainerHover
readonly property color stateColorActive: TimerService.pomodoroBreak ? Appearance.colors.colTertiaryContainerActive : Appearance.colors.colSecondaryContainerActive
readonly property color stateTextColor: TimerService.pomodoroBreak ? Appearance.colors.colOnTertiaryContainer : Appearance.colors.colOnSecondaryContainer
implicitWidth: vertical ? Appearance.sizes.verticalBarWidth - 10 : 104
implicitHeight: vertical ? 54 : Appearance.sizes.baseBarHeight - 8
buttonRadius: Appearance.rounding.full
toggled: TimerService.pomodoroRunning
colBackground: stateColor
colBackgroundHover: stateColorHover
colRipple: stateColorActive
colBackgroundToggled: stateColor
colBackgroundToggledHover: stateColorHover
colRippleToggled: stateColorActive
function pomodoroTimeText() {
const minutes = Math.floor(TimerService.pomodoroSecondsLeft / 60).toString().padStart(2, "0");
const seconds = Math.floor(TimerService.pomodoroSecondsLeft % 60).toString().padStart(2, "0");
return `${minutes}:${seconds}`;
}
function compactMinutesText() {
return Math.ceil(TimerService.pomodoroSecondsLeft / 60).toString().padStart(2, "0");
}
onClicked: {
Persistent.states.sidebar.bottomGroup.collapsed = false;
Persistent.states.sidebar.bottomGroup.tab = 2;
GlobalStates.sidebarRightOpen = true;
}
contentItem: Item {
anchors.fill: parent
RowLayout {
id: contentRow
anchors.centerIn: parent
visible: !root.vertical
spacing: 5
MaterialSymbol {
Layout.alignment: Qt.AlignVCenter
text: TimerService.pomodoroBreak ? "coffee" : "search_activity"
iconSize: Appearance.font.pixelSize.larger
color: root.stateTextColor
}
StyledText {
Layout.alignment: Qt.AlignVCenter
Layout.preferredWidth: 48
horizontalAlignment: Text.AlignHCenter
text: root.pomodoroTimeText()
font.family: Appearance.font.family.monospace
font.pixelSize: Appearance.font.pixelSize.normal
font.weight: Font.DemiBold
color: root.stateTextColor
}
}
ColumnLayout {
anchors.centerIn: parent
visible: root.vertical
spacing: 1
ClippedFilledCircularProgress {
Layout.alignment: Qt.AlignHCenter
implicitSize: 28
lineWidth: 3
value: TimerService.pomodoroSecondsLeft / TimerService.pomodoroLapDuration
colPrimary: root.stateTextColor
colSecondary: root.stateColorHover
enableAnimation: true
textMask: Item {
width: 28
height: 28
StyledText {
anchors.centerIn: parent
text: root.compactMinutesText()
font.family: Appearance.font.family.monospace
font.pixelSize: 11
font.weight: Font.DemiBold
}
}
}
MaterialSymbol {
Layout.alignment: Qt.AlignHCenter
text: TimerService.pomodoroBreak ? "coffee" : "search_activity"
iconSize: 14
color: root.stateTextColor
}
}
}
StyledToolTip {
text: TimerService.pomodoroLongBreak ? Translation.tr("Long break") : TimerService.pomodoroBreak ? Translation.tr("Break") : Translation.tr("Pomodoro")
}
}

View file

@ -0,0 +1,92 @@
import qs.modules.common
import qs.modules.common.widgets
import QtQuick
import QtQuick.Layouts
Item {
id: root
required property string iconName
required property double percentage
property int warningThreshold: 100
property bool shown: true
clip: true
visible: width > 0 && height > 0
implicitWidth: resourceRowLayout.x < 0 ? 0 : resourceRowLayout.implicitWidth
implicitHeight: Appearance.sizes.barHeight
property bool warning: percentage * 100 >= warningThreshold
RowLayout {
id: resourceRowLayout
spacing: 2
x: shown ? 0 : -resourceRowLayout.width
anchors {
verticalCenter: parent.verticalCenter
}
ClippedFilledCircularProgress {
id: resourceCircProg
Layout.alignment: Qt.AlignVCenter
lineWidth: Appearance.rounding.unsharpen
value: percentage
implicitSize: 20
colPrimary: root.warning ? Appearance.colors.colError : Appearance.colors.colOnSecondaryContainer
accountForLightBleeding: !root.warning
enableAnimation: false
Item {
anchors.centerIn: parent
width: resourceCircProg.implicitSize
height: resourceCircProg.implicitSize
MaterialSymbol {
anchors.centerIn: parent
font.weight: Font.DemiBold
fill: 1
text: iconName
iconSize: Appearance.font.pixelSize.normal
color: Appearance.m3colors.m3onSecondaryContainer
}
}
}
Item {
Layout.alignment: Qt.AlignVCenter
implicitWidth: fullPercentageTextMetrics.width
implicitHeight: percentageText.implicitHeight
TextMetrics {
id: fullPercentageTextMetrics
text: "100"
font.pixelSize: Appearance.font.pixelSize.small
}
StyledText {
id: percentageText
anchors.centerIn: parent
color: Appearance.colors.colOnLayer1
font.pixelSize: Appearance.font.pixelSize.small
text: `${Math.round(percentage * 100).toString()}`
}
}
Behavior on x {
animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.NoButton
enabled: resourceRowLayout.x >= 0 && root.width > 0 && root.visible
}
Behavior on implicitWidth {
NumberAnimation {
duration: Appearance.animation.elementMove.duration
easing.type: Appearance.animation.elementMove.type
easing.bezierCurve: Appearance.animation.elementMove.bezierCurve
}
}
}

View file

@ -0,0 +1,138 @@
import qs.modules.common
import qs.modules.common.widgets
import qs.services
import QtQuick
import QtQuick.Layouts
MouseArea {
id: root
property bool borderless: Config.options.bar.borderless
property bool alwaysShowAllResources: false
implicitWidth: rowLayout.implicitWidth + rowLayout.anchors.leftMargin + rowLayout.anchors.rightMargin
implicitHeight: Appearance.sizes.barHeight
hoverEnabled: !Config.options.bar.tooltips.clickToShow
RowLayout {
id: rowLayout
spacing: 0
anchors.fill: parent
anchors.leftMargin: 4
anchors.rightMargin: 4
Item {
visible: NetworkTraffic.available
implicitWidth: speedMeasure.implicitWidth
implicitHeight: Appearance.sizes.barHeight
Layout.rightMargin: visible ? 16 : 0
clip: true
TextMetrics {
id: speedTextMetrics
text: "8888G/s"
font.pixelSize: Appearance.font.pixelSize.small
font.family: Appearance.font.family.main
font.variableAxes: Appearance.font.variableAxes.main
}
RowLayout {
id: speedMeasure
visible: false
MaterialSymbol {
text: "south"
iconSize: Appearance.font.pixelSize.normal
}
Item {
implicitWidth: speedTextMetrics.width
implicitHeight: 1
}
Item {
implicitWidth: 2
implicitHeight: 1
}
MaterialSymbol {
text: "north"
iconSize: Appearance.font.pixelSize.normal
}
Item {
implicitWidth: speedTextMetrics.width
implicitHeight: 1
}
}
RowLayout {
id: speedRow
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: 2
MaterialSymbol {
text: "south"
iconSize: Appearance.font.pixelSize.normal
color: Appearance.colors.colOnLayer1
}
StyledText {
width: speedTextMetrics.width
text: NetworkTraffic.downloadSpeedCompactText
font.pixelSize: Appearance.font.pixelSize.small
color: Appearance.colors.colOnLayer1
horizontalAlignment: Text.AlignRight
elide: Text.ElideLeft
}
MaterialSymbol {
text: "north"
iconSize: Appearance.font.pixelSize.normal
color: Appearance.colors.colOnLayer1
Layout.leftMargin: 2
}
StyledText {
width: speedTextMetrics.width
text: NetworkTraffic.uploadSpeedCompactText
font.pixelSize: Appearance.font.pixelSize.small
color: Appearance.colors.colOnLayer1
horizontalAlignment: Text.AlignRight
elide: Text.ElideLeft
}
}
}
Resource {
iconName: "memory"
percentage: ResourceUsage.memoryUsedPercentage
warningThreshold: Config.options.bar.resources.memoryWarningThreshold
}
Resource {
iconName: "swap_horiz"
percentage: ResourceUsage.swapUsedPercentage
shown: (Config.options.bar.resources.alwaysShowSwap && percentage > 0) ||
(MprisController.activePlayer?.trackTitle == null) ||
root.alwaysShowAllResources
Layout.leftMargin: shown ? 6 : 0
warningThreshold: Config.options.bar.resources.swapWarningThreshold
}
Resource {
iconName: "planner_review"
percentage: ResourceUsage.cpuUsage
shown: Config.options.bar.resources.alwaysShowCpu ||
!(MprisController.activePlayer?.trackTitle?.length > 0) ||
root.alwaysShowAllResources
Layout.leftMargin: shown ? 6 : 0
warningThreshold: Config.options.bar.resources.cpuWarningThreshold
}
}
ResourcesPopup {
hoverTarget: root
}
}

View file

@ -0,0 +1,94 @@
import qs.modules.common
import qs.modules.common.widgets
import qs.services
import QtQuick
import QtQuick.Layouts
StyledPopup {
id: root
// Helper function to format KB to GB
function formatKB(kb) {
return (kb / (1024 * 1024)).toFixed(1) + " GB";
}
Row {
anchors.centerIn: parent
spacing: 12
Column {
anchors.top: parent.top
spacing: 8
StyledPopupHeaderRow {
icon: "memory"
label: "RAM"
}
Column {
spacing: 4
StyledPopupValueRow {
icon: "clock_loader_60"
label: Translation.tr("Used:")
value: root.formatKB(ResourceUsage.memoryUsed)
}
StyledPopupValueRow {
icon: "check_circle"
label: Translation.tr("Free:")
value: root.formatKB(ResourceUsage.memoryFree)
}
StyledPopupValueRow {
icon: "empty_dashboard"
label: Translation.tr("Total:")
value: root.formatKB(ResourceUsage.memoryTotal)
}
}
}
Column {
visible: ResourceUsage.swapTotal > 0
anchors.top: parent.top
spacing: 8
StyledPopupHeaderRow {
icon: "swap_horiz"
label: "Swap"
}
Column {
spacing: 4
StyledPopupValueRow {
icon: "clock_loader_60"
label: Translation.tr("Used:")
value: root.formatKB(ResourceUsage.swapUsed)
}
StyledPopupValueRow {
icon: "check_circle"
label: Translation.tr("Free:")
value: root.formatKB(ResourceUsage.swapFree)
}
StyledPopupValueRow {
icon: "empty_dashboard"
label: Translation.tr("Total:")
value: root.formatKB(ResourceUsage.swapTotal)
}
}
}
Column {
anchors.top: parent.top
spacing: 8
StyledPopupHeaderRow {
icon: "planner_review"
label: "CPU"
}
Column {
spacing: 4
StyledPopupValueRow {
icon: "bolt"
label: Translation.tr("Load:")
value: `${Math.round(ResourceUsage.cpuUsage * 100)}%`
}
}
}
}
}

View file

@ -0,0 +1,64 @@
import qs.modules.common
import qs.modules.common.widgets
import QtQuick
import Quickshell
Revealer { // Scroll hint
id: root
property string icon
property string side: "left"
property string tooltipText: ""
MouseArea {
id: mouseArea
anchors.right: root.side === "left" ? parent.right : undefined
anchors.left: root.side === "right" ? parent.left : undefined
implicitWidth: contentColumn.implicitWidth
implicitHeight: contentColumn.implicitHeight
property bool hovered: false
hoverEnabled: true
onEntered: hovered = true
onExited: hovered = false
acceptedButtons: Qt.NoButton
property bool showHintTimedOut: false
onHoveredChanged: showHintTimedOut = false
Timer {
running: mouseArea.hovered
interval: 500
onTriggered: mouseArea.showHintTimedOut = true
}
PopupToolTip {
extraVisibleCondition: (tooltipText.length > 0 && mouseArea.showHintTimedOut)
text: tooltipText
anchorEdges: Config.options.bar.bottom ? Edges.Top : Edges.Bottom
anchorGravity: anchorEdges
verticalMargin: 8
}
Column {
id: contentColumn
anchors {
fill: parent
}
spacing: -5
MaterialSymbol {
text: "keyboard_arrow_up"
iconSize: 14
color: Appearance.colors.colSubtext
}
MaterialSymbol {
text: root.icon
iconSize: 14
color: Appearance.colors.colSubtext
}
MaterialSymbol {
text: "keyboard_arrow_down"
iconSize: 14
color: Appearance.colors.colSubtext
}
}
}
}

View file

@ -0,0 +1,86 @@
import qs.modules.common
import qs.modules.common.widgets
import qs.modules.common.functions
import QtQuick
import QtQuick.Effects
import Quickshell
import Quickshell.Wayland
LazyLoader {
id: root
property Item hoverTarget
default property Item contentItem
property real popupBackgroundMargin: 0
active: hoverTarget && hoverTarget.containsMouse
component: PanelWindow {
id: popupWindow
color: "transparent"
anchors.left: !Config.options.bar.vertical || (Config.options.bar.vertical && !Config.options.bar.bottom)
anchors.right: Config.options.bar.vertical && Config.options.bar.bottom
anchors.top: Config.options.bar.vertical || (!Config.options.bar.vertical && !Config.options.bar.bottom)
anchors.bottom: !Config.options.bar.vertical && Config.options.bar.bottom
implicitWidth: popupBackground.implicitWidth + Appearance.sizes.elevationMargin * 2 + root.popupBackgroundMargin
implicitHeight: popupBackground.implicitHeight + Appearance.sizes.elevationMargin * 2 + root.popupBackgroundMargin
mask: Region {
item: popupBackground
}
exclusionMode: ExclusionMode.Ignore
exclusiveZone: 0
margins {
left: {
if (!Config.options.bar.vertical) {
const rawX = root.QsWindow?.mapFromItem(
root.hoverTarget,
(root.hoverTarget.width - popupBackground.implicitWidth) / 2, 0
).x ?? 0;
const screenWidth = root.QsWindow?.window?.screen?.width ?? 0;
const maxX = screenWidth - popupBackground.implicitWidth - Appearance.sizes.elevationMargin * 2;
return screenWidth > 0 ? Math.max(0, Math.min(rawX, maxX)) : rawX;
}
return Appearance.sizes.verticalBarWidth
}
top: {
if (!Config.options.bar.vertical) return Appearance.sizes.barHeight;
return root.QsWindow?.mapFromItem(
root.hoverTarget,
(root.hoverTarget.height - popupBackground.implicitHeight) / 2, 0
).y;
}
right: Appearance.sizes.verticalBarWidth
bottom: Appearance.sizes.barHeight
}
WlrLayershell.namespace: "quickshell:popup"
WlrLayershell.layer: WlrLayer.Overlay
StyledRectangularShadow {
target: popupBackground
}
Rectangle {
id: popupBackground
readonly property real margin: 10
anchors {
fill: parent
leftMargin: Appearance.sizes.elevationMargin + root.popupBackgroundMargin * (!popupWindow.anchors.left)
rightMargin: Appearance.sizes.elevationMargin + root.popupBackgroundMargin * (!popupWindow.anchors.right)
topMargin: Appearance.sizes.elevationMargin + root.popupBackgroundMargin * (!popupWindow.anchors.top)
bottomMargin: Appearance.sizes.elevationMargin + root.popupBackgroundMargin * (!popupWindow.anchors.bottom)
}
implicitWidth: root.contentItem.implicitWidth + margin * 2
implicitHeight: root.contentItem.implicitHeight + margin * 2
color: Appearance.m3colors.m3surfaceContainer
radius: Appearance.rounding.small
children: [root.contentItem]
border.width: 1
border.color: Appearance.colors.colLayer0Border
}
}
}

View file

@ -0,0 +1,30 @@
import QtQuick
import QtQuick.Layouts
import qs.modules.common
import qs.modules.common.widgets
Row {
id: root
required property var icon
required property var label
spacing: 5
MaterialSymbol {
anchors.verticalCenter: parent.verticalCenter
fill: 0
font.weight: Font.DemiBold
text: root.icon
iconSize: Appearance.font.pixelSize.large
color: Appearance.colors.colOnSurfaceVariant
}
StyledText {
anchors.verticalCenter: parent.verticalCenter
text: root.label
font {
weight: Font.DemiBold
pixelSize: Appearance.font.pixelSize.normal
}
color: Appearance.colors.colOnSurfaceVariant
}
}

View file

@ -0,0 +1,29 @@
import QtQuick
import QtQuick.Layouts
import qs.modules.common
import qs.modules.common.widgets
RowLayout {
id: root
required property string icon
required property string label
required property string value
spacing: 4
MaterialSymbol {
text: root.icon
color: Appearance.colors.colOnSurfaceVariant
iconSize: Appearance.font.pixelSize.large
}
StyledText {
text: root.label
color: Appearance.colors.colOnSurfaceVariant
}
StyledText {
Layout.fillWidth: true
horizontalAlignment: Text.AlignRight
visible: root.value !== ""
color: Appearance.colors.colOnSurfaceVariant
text: root.value
}
}

View file

@ -0,0 +1,157 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Hyprland
import Quickshell.Services.SystemTray
import qs.services
import qs.modules.common
import qs.modules.common.widgets
Item {
id: root
implicitWidth: gridLayout.implicitWidth
implicitHeight: gridLayout.implicitHeight
property bool vertical: false
property bool invertSide: false
property bool trayOverflowOpen: false
property bool showSeparator: true
property bool showOverflowMenu: true
property var activeMenu: null
property list<var> pinnedItems: TrayService.pinnedItems
property list<var> unpinnedItems: TrayService.unpinnedItems
onUnpinnedItemsChanged: {
if (unpinnedItems.length == 0) root.closeOverflowMenu();
}
function grabFocus() {
focusGrab.active = true;
}
function setExtraWindowAndGrabFocus(window) {
if (root.activeMenu && root.activeMenu !== window) {
if (typeof root.activeMenu.close === "function")
root.activeMenu.close();
root.activeMenu = null;
}
root.activeMenu = window;
root.grabFocus();
}
function releaseFocus() {
focusGrab.active = false;
}
function closeOverflowMenu() {
focusGrab.active = false;
}
onTrayOverflowOpenChanged: {
if (root.trayOverflowOpen) {
root.grabFocus();
}
}
HyprlandFocusGrab {
id: focusGrab
active: false
windows: [trayOverflowLayout.QsWindow?.window, root.activeMenu]
onCleared: {
root.trayOverflowOpen = false;
if (root.activeMenu) {
root.activeMenu.close();
root.activeMenu = null;
}
}
}
GridLayout {
id: gridLayout
columns: root.vertical ? 1 : -1
anchors.fill: parent
rowSpacing: 8
columnSpacing: 15
RippleButton {
id: trayOverflowButton
visible: root.showOverflowMenu && root.unpinnedItems.length > 0
toggled: root.trayOverflowOpen
property bool containsMouse: hovered
downAction: () => root.trayOverflowOpen = !root.trayOverflowOpen
Layout.fillHeight: !root.vertical
Layout.fillWidth: root.vertical
background.implicitWidth: 24
background.implicitHeight: 24
background.anchors.centerIn: this
colBackgroundToggled: Appearance.colors.colSecondaryContainer
colBackgroundToggledHover: Appearance.colors.colSecondaryContainerHover
colRippleToggled: Appearance.colors.colSecondaryContainerActive
contentItem: MaterialSymbol {
anchors.centerIn: parent
iconSize: Appearance.font.pixelSize.larger
text: "expand_more"
horizontalAlignment: Text.AlignHCenter
color: root.trayOverflowOpen ? Appearance.colors.colOnSecondaryContainer : Appearance.colors.colOnLayer2
rotation: (root.trayOverflowOpen ? 180 : 0) - (90 * root.vertical) + (180 * root.invertSide)
Behavior on rotation {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
}
}
StyledPopup {
id: overflowPopup
hoverTarget: trayOverflowButton
active: root.trayOverflowOpen && root.unpinnedItems.length > 0
GridLayout {
id: trayOverflowLayout
anchors.centerIn: parent
columns: Math.ceil(Math.sqrt(root.unpinnedItems.length))
columnSpacing: 10
rowSpacing: 10
Repeater {
model: root.unpinnedItems
delegate: SysTrayItem {
required property SystemTrayItem modelData
item: modelData
Layout.fillHeight: !root.vertical
Layout.fillWidth: root.vertical
onMenuClosed: root.releaseFocus();
onMenuOpened: (qsWindow) => root.setExtraWindowAndGrabFocus(qsWindow);
}
}
}
}
}
Repeater {
model: ScriptModel {
values: root.pinnedItems
}
delegate: SysTrayItem {
required property SystemTrayItem modelData
item: modelData
Layout.fillHeight: !root.vertical
Layout.fillWidth: root.vertical
onMenuClosed: root.releaseFocus();
onMenuOpened: (qsWindow) => {
root.setExtraWindowAndGrabFocus(qsWindow);
}
}
}
StyledText {
Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter
font.pixelSize: Appearance.font.pixelSize.larger
color: Appearance.colors.colSubtext
text: "•"
visible: root.showSeparator && SystemTray.items.values.length > 0
}
}
}

View file

@ -0,0 +1,106 @@
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Services.SystemTray
import Quickshell.Widgets
import Qt5Compat.GraphicalEffects
import qs.services
import qs.modules.common
import qs.modules.common.widgets
import qs.modules.common.functions
MouseArea {
id: root
required property SystemTrayItem item
property bool targetMenuOpen: false
signal menuOpened(qsWindow: var)
signal menuClosed()
hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.RightButton
implicitWidth: 20
implicitHeight: 20
onPressed: (event) => {
switch (event.button) {
case Qt.LeftButton:
item.activate();
break;
case Qt.RightButton:
if (item.hasMenu)
if (menu.active && menu.item && typeof menu.item.close === "function")
menu.item.close();
else
menu.open();
break;
}
event.accepted = true;
}
onEntered: {
tooltip.text = TrayService.getTooltipForItem(root.item);
}
Loader {
id: menu
function open() {
menu.active = true;
}
active: false
sourceComponent: SysTrayMenu {
Component.onCompleted: this.open();
trayItemMenuHandle: root.item.menu
trayItemId: root.item.id
anchor {
window: root.QsWindow.window
item: root
gravity: Config.options.bar.vertical
? (Config.options.bar.bottom ? Edges.Left : Edges.Right)
: (Config.options.bar.bottom ? Edges.Top : Edges.Bottom)
edges: Config.options.bar.vertical
? (Config.options.bar.bottom ? Edges.Left : Edges.Right)
: (Config.options.bar.bottom ? Edges.Top : Edges.Bottom)
}
onMenuOpened: (window) => root.menuOpened(window);
onMenuClosed: {
root.menuClosed();
menu.active = false;
}
}
}
IconImage {
id: trayIcon
visible: !Config.options.tray.monochromeIcons
source: root.item.icon
anchors.centerIn: parent
width: parent.width
height: parent.height
}
Loader {
active: Config.options.tray.monochromeIcons
anchors.fill: trayIcon
sourceComponent: Item {
Desaturate {
id: desaturatedIcon
visible: false // There's already color overlay
anchors.fill: parent
source: trayIcon
desaturation: 0.8 // 1.0 means fully grayscale
}
ColorOverlay {
anchors.fill: desaturatedIcon
source: desaturatedIcon
color: ColorUtils.transparentize(Appearance.colors.colOnLayer0, 0.9)
}
}
}
PopupToolTip {
id: tooltip
extraVisibleCondition: root.containsMouse
alternativeVisibleCondition: extraVisibleCondition
anchorEdges: (!Config.options.bar.bottom && !Config.options.bar.vertical) ? Edges.Bottom : Edges.Top
}
}

View file

@ -0,0 +1,262 @@
pragma ComponentBehavior: Bound
import qs.services
import qs.modules.common
import qs.modules.common.widgets
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
PopupWindow {
id: root
required property QsMenuHandle trayItemMenuHandle
property string trayItemId: ""
property real popupBackgroundMargin: 0
signal menuClosed
signal menuOpened(qsWindow: var) // Correct type is QsWindow, but QML does not like that
color: "transparent"
property real padding: Appearance.sizes.elevationMargin
implicitHeight: {
let result = 0;
for (let child of stackView.children) {
result = Math.max(child.implicitHeight, result);
}
return result + popupBackground.padding * 2 + root.padding * 2;
}
implicitWidth: {
let result = 0;
for (let child of stackView.children) {
result = Math.max(child.implicitWidth, result);
}
return result + popupBackground.padding * 2 + root.padding * 2;
}
function open() {
root.visible = true;
root.menuOpened(root);
}
function close() {
root.visible = false;
while (stackView.depth > 1)
stackView.pop();
root.menuClosed();
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.BackButton | Qt.RightButton
onPressed: event => {
if ((event.button === Qt.BackButton || event.button === Qt.RightButton) && stackView.depth > 1)
stackView.pop();
}
StyledRectangularShadow {
target: popupBackground
opacity: popupBackground.opacity
}
Rectangle {
id: popupBackground
readonly property real padding: 4
anchors {
left: parent.left
right: parent.right
verticalCenter: Config.options.bar.vertical ? parent.verticalCenter : undefined
top: Config.options.bar.vertical ? undefined : Config.options.bar.bottom ? undefined : parent.top
bottom: Config.options.bar.vertical ? undefined : Config.options.bar.bottom ? parent.bottom : undefined
margins: root.padding
}
color: Appearance.colors.colLayer0
radius: Appearance.rounding.windowRounding
border.width: 1
border.color: Appearance.colors.colLayer0Border
clip: true
opacity: 0
Component.onCompleted: opacity = 1
implicitWidth: stackView.implicitWidth + popupBackground.padding * 2
implicitHeight: stackView.implicitHeight + popupBackground.padding * 2
Behavior on opacity {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
}
Behavior on implicitHeight {
animation: Appearance.animation.elementResize.numberAnimation.createObject(this)
}
Behavior on implicitWidth {
animation: Appearance.animation.elementResize.numberAnimation.createObject(this)
}
StackView {
id: stackView
anchors {
fill: parent
margins: popupBackground.padding
}
pushEnter: NoAnim {}
pushExit: NoAnim {}
popEnter: NoAnim {}
popExit: NoAnim {}
implicitWidth: currentItem.implicitWidth
implicitHeight: currentItem.implicitHeight
initialItem: SubMenu {
handle: root.trayItemMenuHandle
}
}
}
}
component NoAnim: Transition {
NumberAnimation {
duration: 0
}
}
component SubMenu: ColumnLayout {
id: submenu
required property QsMenuHandle handle
property bool isSubMenu: false
property bool shown: false
opacity: shown ? 1 : 0
Behavior on opacity {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
}
Component.onCompleted: shown = true
StackView.onActivating: shown = true
StackView.onDeactivating: shown = false
StackView.onRemoved: destroy()
QsMenuOpener {
id: menuOpener
menu: submenu.handle
}
spacing: 0
Loader {
Layout.fillWidth: true
visible: submenu.isSubMenu
active: visible
sourceComponent: RippleButton {
id: backButton
buttonRadius: popupBackground.radius - popupBackground.padding
horizontalPadding: 12
implicitWidth: contentItem.implicitWidth + horizontalPadding * 2
implicitHeight: 36
downAction: () => stackView.pop()
contentItem: RowLayout {
anchors {
verticalCenter: parent.verticalCenter
left: parent.left
right: parent.right
leftMargin: backButton.horizontalPadding
rightMargin: backButton.horizontalPadding
}
spacing: 8
MaterialSymbol {
iconSize: 20
text: "chevron_left"
}
StyledText {
Layout.fillWidth: true
text: Translation.tr("Back")
}
}
}
}
RippleButton {
id: pinEntry
buttonRadius: popupBackground.radius - popupBackground.padding
horizontalPadding: 12
implicitWidth: contentItem.implicitWidth + horizontalPadding * 2
implicitHeight: 36
Layout.topMargin: 0
Layout.bottomMargin: 0
Layout.fillWidth: true
visible: root.trayItemId !== undefined && root.trayItemId.length > 0 && stackView.depth === 1
releaseAction: () => TrayService.togglePin(root.trayItemId);
contentItem: RowLayout {
anchors {
verticalCenter: parent.verticalCenter
left: parent.left
right: parent.right
leftMargin: pinEntry.horizontalPadding
rightMargin: pinEntry.horizontalPadding
}
spacing: 8
MaterialSymbol {
iconSize: 18
text: "push_pin"
}
StyledText {
Layout.fillWidth: true
text: TrayService.isPinned(root.trayItemId) ? Translation.tr("Unpin") : Translation.tr("Pin")
}
}
}
Rectangle {
Layout.fillWidth: true
implicitHeight: 1
color: Appearance.colors.colSubtext
Layout.topMargin: 4
Layout.bottomMargin: 4
}
Repeater {
id: menuEntriesRepeater
property bool iconColumnNeeded: {
for (let i = 0; i < menuOpener.children.values.length; i++) {
if (menuOpener.children.values[i].icon.length > 0)
return true;
}
return false;
}
property bool specialInteractionColumnNeeded: {
for (let i = 0; i < menuOpener.children.values.length; i++) {
if (menuOpener.children.values[i].buttonType !== QsMenuButtonType.None)
return true;
}
return false;
}
model: menuOpener.children
delegate: SysTrayMenuEntry {
required property QsMenuEntry modelData
forceIconColumn: menuEntriesRepeater.iconColumnNeeded
forceSpecialInteractionColumn: menuEntriesRepeater.specialInteractionColumnNeeded
menuEntry: modelData
buttonRadius: popupBackground.radius - popupBackground.padding
onDismiss: root.close()
onOpenSubmenu: handle => {
stackView.push(subMenuComponent.createObject(null, {
handle: handle,
isSubMenu: true
}));
}
}
}
}
Component {
id: subMenuComponent
SubMenu {}
}
}

View file

@ -0,0 +1,126 @@
pragma ComponentBehavior: Bound
import qs.modules.common
import qs.modules.common.widgets
import qs.modules.common.functions
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Widgets
RippleButton {
id: root
required property QsMenuEntry menuEntry
property bool forceIconColumn: false
property bool forceSpecialInteractionColumn: false
readonly property bool hasIcon: menuEntry.icon.length > 0
readonly property bool hasSpecialInteraction: menuEntry.buttonType !== QsMenuButtonType.None
signal dismiss()
signal openSubmenu(handle: QsMenuHandle)
colBackground: menuEntry.isSeparator ? Appearance.m3colors.m3outlineVariant : ColorUtils.transparentize(Appearance.colors.colLayer0)
enabled: !menuEntry.isSeparator
opacity: 1
horizontalPadding: 12
implicitWidth: contentItem.implicitWidth + horizontalPadding * 2
implicitHeight: menuEntry.isSeparator ? 1 : 36
Layout.topMargin: menuEntry.isSeparator ? 4 : 0
Layout.bottomMargin: menuEntry.isSeparator ? 4 : 0
Layout.fillWidth: true
Component.onCompleted: {
if (menuEntry.isSeparator) {
root.buttonColor = root.colBackground;
}
}
releaseAction: () => {
if (menuEntry.hasChildren) {
root.openSubmenu(root.menuEntry);
return;
}
menuEntry.triggered();
root.dismiss();
}
altAction: (event) => { // Not hog right-click
event.accepted = false;
}
contentItem: RowLayout {
id: contentItem
anchors {
verticalCenter: parent.verticalCenter
left: parent.left
right: parent.right
leftMargin: root.horizontalPadding
rightMargin: root.horizontalPadding
}
spacing: 8
visible: !root.menuEntry.isSeparator
// Interaction: checkbox or radio button
Item {
visible: root.hasSpecialInteraction || root.forceSpecialInteractionColumn
implicitWidth: 20
implicitHeight: 20
Loader {
anchors.fill: parent
active: root.menuEntry.buttonType === QsMenuButtonType.RadioButton
sourceComponent: StyledRadioButton {
enabled: false
padding: 0
checked: root.menuEntry.checkState === Qt.Checked
}
}
Loader {
anchors.fill: parent
active: root.menuEntry.buttonType === QsMenuButtonType.CheckBox && root.menuEntry.checkState !== Qt.Unchecked
sourceComponent: MaterialSymbol {
text: root.menuEntry.checkState === Qt.PartiallyChecked ? "check_indeterminate_small" : "check"
iconSize: 20
}
}
}
// Button icon
Item {
visible: root.hasIcon || root.forceIconColumn
implicitWidth: 20
implicitHeight: 20
Loader {
anchors.centerIn: parent
active: root.menuEntry.icon.length > 0
sourceComponent: IconImage {
asynchronous: true
source: root.menuEntry.icon
implicitSize: 20
mipmap: true
}
}
}
StyledText {
id: label
text: root.menuEntry.text
font.pixelSize: Appearance.font.pixelSize.smallie
Layout.fillWidth: true
}
Loader {
active: root.menuEntry.hasChildren
sourceComponent: MaterialSymbol {
text: "chevron_right"
iconSize: 20
}
}
}
}

View file

@ -0,0 +1,158 @@
import qs
import qs.modules.common
import qs.modules.common.widgets
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Hyprland
import Quickshell.Services.Pipewire
import Quickshell.Services.UPower
Item {
id: root
property bool borderless: Config.options.bar.borderless
implicitWidth: rowLayout.implicitWidth + rowLayout.spacing * 2
implicitHeight: rowLayout.implicitHeight
RowLayout {
id: rowLayout
spacing: 4
anchors.centerIn: parent
Loader {
active: Config.options.bar.utilButtons.showScreenSnip
visible: Config.options.bar.utilButtons.showScreenSnip
sourceComponent: CircleUtilButton {
Layout.alignment: Qt.AlignVCenter
onClicked: Quickshell.execDetached(["qs", "-p", Quickshell.shellPath(""), "ipc", "call", "region", "screenshot"]);
MaterialSymbol {
horizontalAlignment: Qt.AlignHCenter
fill: 1
text: "screenshot_region"
iconSize: Appearance.font.pixelSize.large
color: Appearance.colors.colOnLayer2
}
}
}
Loader {
active: Config.options.bar.utilButtons.showScreenRecord
visible: Config.options.bar.utilButtons.showScreenRecord
sourceComponent: CircleUtilButton {
Layout.alignment: Qt.AlignVCenter
onClicked: Quickshell.execDetached([Directories.recordScriptPath])
MaterialSymbol {
horizontalAlignment: Qt.AlignHCenter
fill: 1
text: "videocam"
iconSize: Appearance.font.pixelSize.large
color: Appearance.colors.colOnLayer2
}
}
}
Loader {
active: Config.options.bar.utilButtons.showColorPicker
visible: Config.options.bar.utilButtons.showColorPicker
sourceComponent: CircleUtilButton {
Layout.alignment: Qt.AlignVCenter
onClicked: Quickshell.execDetached(["hyprpicker", "-a"])
MaterialSymbol {
horizontalAlignment: Qt.AlignHCenter
fill: 1
text: "colorize"
iconSize: Appearance.font.pixelSize.large
color: Appearance.colors.colOnLayer2
}
}
}
Loader {
active: Config.options.bar.utilButtons.showKeyboardToggle
visible: Config.options.bar.utilButtons.showKeyboardToggle
sourceComponent: CircleUtilButton {
Layout.alignment: Qt.AlignVCenter
onClicked: GlobalStates.oskOpen = !GlobalStates.oskOpen
MaterialSymbol {
horizontalAlignment: Qt.AlignHCenter
fill: 0
text: "keyboard"
iconSize: Appearance.font.pixelSize.large
color: Appearance.colors.colOnLayer2
}
}
}
Loader {
active: Config.options.bar.utilButtons.showMicToggle
visible: Config.options.bar.utilButtons.showMicToggle
sourceComponent: CircleUtilButton {
Layout.alignment: Qt.AlignVCenter
onClicked: Quickshell.execDetached(["wpctl", "set-mute", "@DEFAULT_SOURCE@", "toggle"])
MaterialSymbol {
horizontalAlignment: Qt.AlignHCenter
fill: 0
text: Pipewire.defaultAudioSource?.audio?.muted ? "mic_off" : "mic"
iconSize: Appearance.font.pixelSize.large
color: Appearance.colors.colOnLayer2
}
}
}
Loader {
active: Config.options.bar.utilButtons.showDarkModeToggle
visible: Config.options.bar.utilButtons.showDarkModeToggle
sourceComponent: CircleUtilButton {
Layout.alignment: Qt.AlignVCenter
onClicked: event => {
if (Appearance.m3colors.darkmode) {
Quickshell.execDetached(["bash", "-c", `${Directories.wallpaperSwitchScriptPath} --mode light --noswitch`])
} else {
Quickshell.execDetached(["bash", "-c", `${Directories.wallpaperSwitchScriptPath} --mode dark --noswitch`])
}
}
MaterialSymbol {
horizontalAlignment: Qt.AlignHCenter
fill: 0
text: Appearance.m3colors.darkmode ? "light_mode" : "dark_mode"
iconSize: Appearance.font.pixelSize.large
color: Appearance.colors.colOnLayer2
}
}
}
Loader {
active: Config.options.bar.utilButtons.showPerformanceProfileToggle
visible: Config.options.bar.utilButtons.showPerformanceProfileToggle
sourceComponent: CircleUtilButton {
Layout.alignment: Qt.AlignVCenter
onClicked: event => {
if (PowerProfiles.hasPerformanceProfile) {
switch(PowerProfiles.profile) {
case PowerProfile.PowerSaver: PowerProfiles.profile = PowerProfile.Balanced
break;
case PowerProfile.Balanced: PowerProfiles.profile = PowerProfile.Performance
break;
case PowerProfile.Performance: PowerProfiles.profile = PowerProfile.PowerSaver
break;
}
} else {
PowerProfiles.profile = PowerProfiles.profile == PowerProfile.Balanced ? PowerProfile.PowerSaver : PowerProfile.Balanced
}
}
MaterialSymbol {
horizontalAlignment: Qt.AlignHCenter
fill: 0
text: switch(PowerProfiles.profile) {
case PowerProfile.PowerSaver: return "energy_savings_leaf"
case PowerProfile.Balanced: return "airwave"
case PowerProfile.Performance: return "local_fire_department"
}
iconSize: Appearance.font.pixelSize.large
color: Appearance.colors.colOnLayer2
}
}
}
}
}

View file

@ -0,0 +1,320 @@
import qs
import qs.services
import qs.modules.common
import qs.modules.common.models
import qs.modules.common.widgets
import qs.modules.common.functions
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Wayland
import Quickshell.Hyprland
import Quickshell.Widgets
import Qt5Compat.GraphicalEffects
Item {
id: root
property bool vertical: false
property bool borderless: Config.options.bar.borderless
readonly property HyprlandMonitor monitor: Hyprland.monitorFor(root.QsWindow.window?.screen)
readonly property Toplevel activeWindow: ToplevelManager.activeToplevel
readonly property int effectiveActiveWorkspaceId: monitor?.activeWorkspace?.id ?? 1
readonly property int workspacesShown: Config.options.bar.workspaces.shown
readonly property int workspaceGroup: Math.floor((effectiveActiveWorkspaceId - 1) / root.workspacesShown)
property list<bool> workspaceOccupied: []
property int widgetPadding: 4
property int workspaceButtonWidth: 26
property real activeWorkspaceMargin: 2
property real workspaceIconSize: workspaceButtonWidth * 0.69
property real workspaceIconSizeShrinked: workspaceButtonWidth * 0.55
property real workspaceIconOpacityShrinked: 1
property real workspaceIconMarginShrinked: -4
property int workspaceIndexInGroup: (effectiveActiveWorkspaceId - 1) % root.workspacesShown
property bool showNumbers: false
Timer {
id: showNumbersTimer
interval: (Config?.options.bar.autoHide.showWhenPressingSuper.delay ?? 100)
repeat: false
onTriggered: {
root.showNumbers = true
}
}
Connections {
target: GlobalStates
function onSuperDownChanged() {
if (!Config?.options.bar.autoHide.showWhenPressingSuper.enable) return;
if (GlobalStates.superDown) showNumbersTimer.restart();
else {
showNumbersTimer.stop();
root.showNumbers = false;
}
}
function onSuperReleaseMightTriggerChanged() {
showNumbersTimer.stop()
}
}
// Function to update workspaceOccupied
function updateWorkspaceOccupied() {
workspaceOccupied = Array.from({ length: root.workspacesShown }, (_, i) => {
return Hyprland.workspaces.values.some(ws => ws.id === workspaceGroup * root.workspacesShown + i + 1);
})
}
// Occupied workspace updates
Component.onCompleted: updateWorkspaceOccupied()
Connections {
target: Hyprland.workspaces
function onValuesChanged() {
updateWorkspaceOccupied();
}
}
Connections {
target: Hyprland
function onFocusedWorkspaceChanged() {
updateWorkspaceOccupied();
}
}
onWorkspaceGroupChanged: {
updateWorkspaceOccupied();
}
implicitWidth: root.vertical ? Appearance.sizes.verticalBarWidth : (root.workspaceButtonWidth * root.workspacesShown)
implicitHeight: root.vertical ? (root.workspaceButtonWidth * root.workspacesShown) : Appearance.sizes.barHeight
// Scroll to switch workspaces
WheelHandler {
onWheel: (event) => {
if (event.angleDelta.y < 0)
Hyprland.dispatch(`hl.dsp.focus({workspace = "r+1"})`);
else if (event.angleDelta.y > 0)
Hyprland.dispatch(`hl.dsp.focus({workspace = "r-1"})`);
}
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.BackButton
onPressed: (event) => {
if (event.button === Qt.BackButton) {
Hyprland.dispatch(`hl.dsp.workspace.toggle_special("special")`);
}
}
}
// Workspaces - background
Grid {
z: 1
anchors.centerIn: parent
rowSpacing: 0
columnSpacing: 0
columns: root.vertical ? 1 : root.workspacesShown
rows: root.vertical ? root.workspacesShown : 1
Repeater {
model: root.workspacesShown
Rectangle {
z: 1
implicitWidth: workspaceButtonWidth
implicitHeight: workspaceButtonWidth
radius: (width / 2)
property var previousOccupied: (workspaceOccupied[index-1] && !(!activeWindow?.activated && root.effectiveActiveWorkspaceId === index))
property var rightOccupied: (workspaceOccupied[index+1] && !(!activeWindow?.activated && root.effectiveActiveWorkspaceId === index+2))
property var radiusPrev: previousOccupied ? 0 : (width / 2)
property var radiusNext: rightOccupied ? 0 : (width / 2)
topLeftRadius: radiusPrev
bottomLeftRadius: root.vertical ? radiusNext : radiusPrev
topRightRadius: root.vertical ? radiusPrev : radiusNext
bottomRightRadius: radiusNext
color: ColorUtils.transparentize(Appearance.m3colors.m3secondaryContainer, 0.4)
opacity: (workspaceOccupied[index] && !(!activeWindow?.activated && root.effectiveActiveWorkspaceId === index+1)) ? 1 : 0
Behavior on opacity {
animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
}
Behavior on radiusPrev {
animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
}
Behavior on radiusNext {
animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
}
}
}
}
// Active workspace
Rectangle {
z: 2
// Make active ws indicator, which has a brighter color, smaller to look like it is of the same size as ws occupied highlight
radius: Appearance.rounding.full
color: Appearance.colors.colPrimary
anchors {
verticalCenter: vertical ? undefined : parent.verticalCenter
horizontalCenter: vertical ? parent.horizontalCenter : undefined
}
AnimatedTabIndexPair {
id: idxPair
index: root.workspaceIndexInGroup
}
property real indicatorPosition: Math.min(idxPair.idx1, idxPair.idx2) * workspaceButtonWidth + root.activeWorkspaceMargin
property real indicatorLength: Math.abs(idxPair.idx1 - idxPair.idx2) * workspaceButtonWidth + workspaceButtonWidth - root.activeWorkspaceMargin * 2
property real indicatorThickness: workspaceButtonWidth - root.activeWorkspaceMargin * 2
x: root.vertical ? null : indicatorPosition
implicitWidth: root.vertical ? indicatorThickness : indicatorLength
y: root.vertical ? indicatorPosition : null
implicitHeight: root.vertical ? indicatorLength : indicatorThickness
}
// Workspaces - numbers
Grid {
z: 3
columns: root.vertical ? 1 : root.workspacesShown
rows: root.vertical ? root.workspacesShown : 1
columnSpacing: 0
rowSpacing: 0
anchors.fill: parent
Repeater {
model: root.workspacesShown
Button {
id: button
property int workspaceValue: workspaceGroup * root.workspacesShown + index + 1
implicitHeight: vertical ? Appearance.sizes.verticalBarWidth : Appearance.sizes.barHeight
implicitWidth: vertical ? Appearance.sizes.verticalBarWidth : Appearance.sizes.verticalBarWidth
onPressed: Hyprland.dispatch(`hl.dsp.focus({ workspace = ${workspaceValue}})`)
width: vertical ? undefined : root.workspaceButtonWidth
height: vertical ? root.workspaceButtonWidth : undefined
background: Item {
id: workspaceButtonBackground
implicitWidth: workspaceButtonWidth
implicitHeight: workspaceButtonWidth
property var biggestWindow: HyprlandData.biggestWindowForWorkspace(button.workspaceValue)
property var mainAppIconSource: Quickshell.iconPath(AppSearch.guessIcon(biggestWindow?.class), "image-missing")
StyledText { // Workspace number text
opacity: root.showNumbers
|| ((Config.options?.bar.workspaces.alwaysShowNumbers && (!Config.options?.bar.workspaces.showAppIcons || !workspaceButtonBackground.biggestWindow || root.showNumbers))
|| (root.showNumbers && !Config.options?.bar.workspaces.showAppIcons)
) ? 1 : 0
z: 3
anchors.centerIn: parent
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
font {
pixelSize: Appearance.font.pixelSize.small - ((text.length - 1) * (text !== "10") * 2)
family: Config.options?.bar.workspaces.useNerdFont ? Appearance.font.family.iconNerd : defaultFont
}
text: Config.options?.bar.workspaces.numberMap[button.workspaceValue - 1] || button.workspaceValue
elide: Text.ElideRight
color: (root.effectiveActiveWorkspaceId == button.workspaceValue) ?
Appearance.m3colors.m3onPrimary :
(workspaceOccupied[index] ? Appearance.m3colors.m3onSecondaryContainer :
Appearance.colors.colOnLayer1Inactive)
Behavior on opacity {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
}
}
Rectangle { // Dot instead of ws number
id: wsDot
opacity: (Config.options?.bar.workspaces.alwaysShowNumbers
|| root.showNumbers
|| (Config.options?.bar.workspaces.showAppIcons && workspaceButtonBackground.biggestWindow)
) ? 0 : 1
visible: opacity > 0
anchors.centerIn: parent
width: workspaceButtonWidth * 0.18
height: width
radius: width / 2
color: (root.effectiveActiveWorkspaceId == button.workspaceValue) ?
Appearance.m3colors.m3onPrimary :
(workspaceOccupied[index] ? Appearance.m3colors.m3onSecondaryContainer :
Appearance.colors.colOnLayer1Inactive)
Behavior on opacity {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
}
}
Item { // Main app icon
anchors.centerIn: parent
width: workspaceButtonWidth
height: workspaceButtonWidth
opacity: !Config.options?.bar.workspaces.showAppIcons ? 0 :
(workspaceButtonBackground.biggestWindow && !root.showNumbers && Config.options?.bar.workspaces.showAppIcons) ?
1 : workspaceButtonBackground.biggestWindow ? workspaceIconOpacityShrinked : 0
visible: opacity > 0
IconImage {
id: mainAppIcon
anchors.bottom: parent.bottom
anchors.right: parent.right
anchors.bottomMargin: (!root.showNumbers && Config.options?.bar.workspaces.showAppIcons) ?
(workspaceButtonWidth - workspaceIconSize) / 2 : workspaceIconMarginShrinked
anchors.rightMargin: (!root.showNumbers && Config.options?.bar.workspaces.showAppIcons) ?
(workspaceButtonWidth - workspaceIconSize) / 2 : workspaceIconMarginShrinked
source: workspaceButtonBackground.mainAppIconSource
implicitSize: (!root.showNumbers && Config.options?.bar.workspaces.showAppIcons) ? workspaceIconSize : workspaceIconSizeShrinked
Behavior on opacity {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
}
Behavior on anchors.bottomMargin {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
}
Behavior on anchors.rightMargin {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
}
Behavior on implicitSize {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
}
}
Loader {
active: Config.options.bar.workspaces.monochromeIcons
anchors.fill: mainAppIcon
sourceComponent: Item {
Desaturate {
id: desaturatedIcon
visible: false // There's already color overlay
anchors.fill: parent
source: mainAppIcon
desaturation: 0.8
}
ColorOverlay {
anchors.fill: desaturatedIcon
source: desaturatedIcon
color: ColorUtils.transparentize(wsDot.color, 0.9)
}
}
}
}
}
}
}
}
}

View file

@ -0,0 +1,56 @@
pragma ComponentBehavior: Bound
import qs.modules.common
import qs.modules.common.widgets
import qs.services
import Quickshell
import QtQuick
import QtQuick.Layouts
MouseArea {
id: root
property bool hovered: false
implicitWidth: rowLayout.implicitWidth + 10 * 2
implicitHeight: Appearance.sizes.barHeight
acceptedButtons: Qt.LeftButton | Qt.RightButton
hoverEnabled: !Config.options.bar.tooltips.clickToShow
onPressed: {
if (mouse.button === Qt.RightButton) {
Weather.getData();
Quickshell.execDetached(["notify-send",
Translation.tr("Weather"),
Translation.tr("Refreshing (manually triggered)")
, "-a", "Shell"
])
mouse.accepted = false
}
}
RowLayout {
id: rowLayout
anchors.centerIn: parent
MaterialSymbol {
fill: 0
text: Icons.getWeatherIcon(Weather.data.wCode) ?? "cloud"
iconSize: Appearance.font.pixelSize.large
color: Appearance.colors.colOnLayer1
Layout.alignment: Qt.AlignVCenter
}
StyledText {
visible: true
font.pixelSize: Appearance.font.pixelSize.small
color: Appearance.colors.colOnLayer1
text: Weather.data?.temp ?? "--°"
Layout.alignment: Qt.AlignVCenter
}
}
WeatherPopup {
id: weatherPopup
hoverTarget: root
}
}

View file

@ -0,0 +1,44 @@
import QtQuick
import QtQuick.Layouts
import qs.modules.common
import qs.modules.common.widgets
Rectangle {
id: root
radius: Appearance.rounding.small
color: Appearance.colors.colSurfaceContainerHigh
implicitWidth: columnLayout.implicitWidth + 14 * 2
implicitHeight: columnLayout.implicitHeight + 14 * 2
Layout.fillWidth: parent
property alias title: title.text
property alias value: value.text
property alias symbol: symbol.text
ColumnLayout {
id: columnLayout
anchors.fill: parent
spacing: -10
RowLayout {
Layout.alignment: Qt.AlignHCenter
MaterialSymbol {
id: symbol
fill: 0
iconSize: Appearance.font.pixelSize.normal
color: Appearance.colors.colOnSurfaceVariant
}
StyledText {
id: title
font.pixelSize: Appearance.font.pixelSize.smaller
color: Appearance.colors.colOnSurfaceVariant
}
}
StyledText {
id: value
Layout.alignment: Qt.AlignHCenter
font.pixelSize: Appearance.font.pixelSize.small
color: Appearance.colors.colOnSurfaceVariant
}
}
}

View file

@ -0,0 +1,115 @@
import qs.services
import qs.modules.common
import qs.modules.common.widgets
import QtQuick
import QtQuick.Layouts
import qs.modules.ii.bar
StyledPopup {
id: root
ColumnLayout {
id: columnLayout
anchors.centerIn: parent
implicitWidth: Math.max(header.implicitWidth, gridLayout.implicitWidth)
implicitHeight: gridLayout.implicitHeight
spacing: 5
// Header
ColumnLayout {
id: header
Layout.alignment: Qt.AlignHCenter
spacing: 2
RowLayout {
Layout.alignment: Qt.AlignHCenter
spacing: 6
MaterialSymbol {
fill: 0
font.weight: Font.Medium
text: "location_on"
iconSize: Appearance.font.pixelSize.large
color: Appearance.colors.colOnSurfaceVariant
}
StyledText {
text: Weather.data.city
font {
weight: Font.Medium
pixelSize: Appearance.font.pixelSize.normal
}
color: Appearance.colors.colOnSurfaceVariant
}
}
StyledText {
id: temp
font.pixelSize: Appearance.font.pixelSize.smaller
color: Appearance.colors.colOnSurfaceVariant
text: Weather.data.temp + " • " + Translation.tr("Feels like %1").arg(Weather.data.tempFeelsLike)
}
}
// Metrics grid
GridLayout {
id: gridLayout
columns: 2
rowSpacing: 5
columnSpacing: 5
uniformCellWidths: true
WeatherCard {
title: Translation.tr("UV Index")
symbol: "wb_sunny"
value: Weather.data.uv
}
WeatherCard {
title: Translation.tr("Wind")
symbol: "air"
value: `(${Weather.data.windDir}) ${Weather.data.wind}`
}
WeatherCard {
title: Translation.tr("Precipitation")
symbol: "rainy_light"
value: Weather.data.precip
}
WeatherCard {
title: Translation.tr("Humidity")
symbol: "humidity_low"
value: Weather.data.humidity
}
WeatherCard {
title: Translation.tr("Visibility")
symbol: "visibility"
value: Weather.data.visib
}
WeatherCard {
title: Translation.tr("Pressure")
symbol: "readiness_score"
value: Weather.data.press
}
WeatherCard {
title: Translation.tr("Sunrise")
symbol: "wb_twilight"
value: Weather.data.sunrise
}
WeatherCard {
title: Translation.tr("Sunset")
symbol: "bedtime"
value: Weather.data.sunset
}
}
// Footer: last refresh
StyledText {
Layout.alignment: Qt.AlignHCenter
text: Translation.tr("Last refresh: %1").arg(Weather.data.lastRefresh)
font {
weight: Font.Medium
pixelSize: Appearance.font.pixelSize.smaller
}
color: Appearance.colors.colOnSurfaceVariant
}
}
}

View file

@ -0,0 +1,409 @@
import qs.services
import qs.modules.common
import qs.modules.common.widgets
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Qt.labs.synchronizer
import Qt5Compat.GraphicalEffects
import Quickshell.Io
import Quickshell
import Quickshell.Wayland
import Quickshell.Hyprland
Scope { // Scope
id: root
property var tabButtonList: [
{
"icon": "keyboard",
"name": Translation.tr("Keybinds")
},
{
"icon": "experiment",
"name": Translation.tr("Elements")
},
]
Loader {
id: cheatsheetLoader
active: false
sourceComponent: PanelWindow { // Window
id: cheatsheetRoot
visible: cheatsheetLoader.active
anchors {
top: true
bottom: true
left: true
right: true
}
function hide() {
cheatsheetLoader.active = false;
}
property bool keyboardReady: false
exclusiveZone: 0
implicitWidth: cheatsheetBackground.width + Appearance.sizes.elevationMargin * 2
implicitHeight: cheatsheetBackground.height + Appearance.sizes.elevationMargin * 2
WlrLayershell.namespace: "quickshell:cheatsheet"
WlrLayershell.layer: WlrLayer.Overlay
//--> It dont take that sweet time to open Anymore!
WlrLayershell.keyboardFocus: cheatsheetRoot.keyboardReady ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None
color: "transparent"
mask: Region {
item: cheatsheetBackground
}
Timer {
id: lockWidthTimer
interval: 150
repeat: false
onTriggered: {
if (cheatsheetBackground.lockedWidth === 0) {
const contentWidth = Math.max(
headerColumn.implicitWidth,
keybindsPage.implicitWidth,
periodicTablePage.implicitWidth,
);
cheatsheetBackground.lockedWidth = contentWidth + cheatsheetBackground.padding * 2;
}
}
}
Timer {
id: keyboardFocusTimer
interval: 150
repeat: false
onTriggered: {
if (cheatsheetRoot.visible) {
cheatsheetRoot.keyboardReady = true;
}
}
}
Component.onCompleted: {
GlobalFocusGrab.addDismissable(cheatsheetRoot);
}
Component.onDestruction: {
GlobalFocusGrab.removeDismissable(cheatsheetRoot);
}
Connections {
target: cheatsheetRoot
function onVisibleChanged() {
if (cheatsheetRoot.visible) {
cheatsheetRoot.keyboardReady = false;
lockWidthTimer.restart();
keyboardFocusTimer.restart();
} else {
keyboardFocusTimer.stop();
cheatsheetRoot.keyboardReady = false;
cheatsheetBackground.lockedWidth = 0;
cheatsheetBackground.collapseSearch();
}
}
}
Connections {
target: GlobalFocusGrab
function onDismissed() {
cheatsheetRoot.hide();
}
}
// Background
StyledRectangularShadow {
target: cheatsheetBackground
}
Rectangle {
id: cheatsheetBackground
anchors.centerIn: parent
focus: cheatsheetRoot.visible
color: Appearance.colors.colLayer0
border.width: 1
border.color: Appearance.colors.colLayer0Border
radius: Appearance.rounding.windowRounding
property real padding: 20
property real lockedWidth: 0
property bool searchExpanded: false
function collapseSearch() {
searchExpanded = false;
searchField.focus = false;
if (searchField.text !== "") {
searchField.text = "";
}
cheatsheetBackground.forceActiveFocus();
GlobalFocusGrab.addDismissable(cheatsheetRoot);
}
function expandSearch() {
GlobalFocusGrab.removeDismissable(cheatsheetRoot);
searchExpanded = true;
searchField.forceActiveFocus();
}
implicitWidth: lockedWidth > 0 ? lockedWidth : cheatsheetColumnLayout.implicitWidth + padding * 2
implicitHeight: cheatsheetColumnLayout.implicitHeight + padding * 2
Keys.onPressed: event => {
if (event.key === Qt.Key_Escape) {
event.accepted = true;
if (searchField.text.length > 0 || searchExpanded) {
clearSearchButton.clicked();
} else {
cheatsheetRoot.hide();
}
return;
}
if (event.modifiers === Qt.ControlModifier) {
if (event.key === Qt.Key_PageDown) {
tabBar.incrementCurrentIndex();
event.accepted = true;
} else if (event.key === Qt.Key_PageUp) {
tabBar.decrementCurrentIndex();
event.accepted = true;
} else if (event.key === Qt.Key_Tab) {
tabBar.setCurrentIndex((tabBar.currentIndex + 1) % root.tabButtonList.length);
event.accepted = true;
} else if (event.key === Qt.Key_Backtab) {
tabBar.setCurrentIndex((tabBar.currentIndex - 1 + root.tabButtonList.length) % root.tabButtonList.length);
event.accepted = true;
}
return;
}
if (searchField.activeFocus && cheatsheetBackground.searchExpanded) {
event.accepted = false;
return;
}
if (event.key === Qt.Key_Backspace) {
if (!cheatsheetBackground.searchExpanded) {
event.accepted = true;
return;
}
if (searchField.text.length > 0) {
searchField.text = searchField.text.substring(0, searchField.text.length - 1);
}
event.accepted = true;
return;
}
if (event.text.length > 0) {
expandSearch();
searchField.text += event.text;
searchField.cursorPosition = searchField.text.length;
searchField.forceActiveFocus();
event.accepted = true;
}
}
RippleButton { // Close button
id: closeButton
implicitWidth: 40
implicitHeight: 40
buttonRadius: Appearance.rounding.full
anchors {
top: parent.top
right: parent.right
topMargin: 20
rightMargin: 20
}
onClicked: {
cheatsheetRoot.hide();
}
contentItem: MaterialSymbol {
anchors.centerIn: parent
horizontalAlignment: Text.AlignHCenter
font.pixelSize: Appearance.font.pixelSize.title
text: "close"
}
}
ColumnLayout { // Real content
id: cheatsheetColumnLayout
anchors.centerIn: parent
spacing: 10
ColumnLayout {
id: headerColumn
Layout.alignment: Qt.AlignHCenter
spacing: 6
Toolbar {
id: tabToolbar
Layout.alignment: Qt.AlignHCenter
enableShadow: false
ToolbarTabBar {
id: tabBar
tabButtonList: root.tabButtonList
Synchronizer on currentIndex {
property alias source: swipeView.currentIndex
}
}
}
Item {
Layout.alignment: Qt.AlignHCenter
Layout.preferredWidth: Math.max(keybindsPage.implicitWidth, periodicTablePage.implicitWidth)
implicitHeight: 30
MouseArea {
anchors.fill: parent
enabled: !cheatsheetBackground.searchExpanded
cursorShape: Qt.IBeamCursor
onClicked: cheatsheetBackground.expandSearch()
}
RowLayout {
id: searchHintRow
anchors.centerIn: parent
spacing: 4
visible: !cheatsheetBackground.searchExpanded
MaterialSymbol {
Layout.alignment: Qt.AlignVCenter
font.pixelSize: Appearance.font.pixelSize.smaller
color: Appearance.colors.colSubtext
text: "search"
}
StyledText {
Layout.alignment: Qt.AlignVCenter
color: Appearance.colors.colSubtext
font.pixelSize: Appearance.font.pixelSize.smaller
text: Translation.tr("Type or click here to search…")
}
}
Item {
id: searchFieldContainer
anchors.centerIn: parent
width: Math.max(tabBar.implicitWidth, 220)
height: 30
visible: cheatsheetBackground.searchExpanded
enabled: cheatsheetBackground.searchExpanded
ToolbarTextField {
id: searchField
anchors.fill: parent
enabled: cheatsheetBackground.searchExpanded
padding: 6
rightPadding: 32
placeholderText: ""
font.pixelSize: Appearance.font.pixelSize.smaller
Keys.onPressed: event => {
if (event.key === Qt.Key_Escape) {
event.accepted = true;
if (cheatsheetBackground.searchExpanded || text.length > 0) {
clearSearchButton.clicked();
}
}
}
onTextChanged: {
if (text.length === 0 && cheatsheetBackground.searchExpanded) {
cheatsheetBackground.collapseSearch();
}
}
}
IconToolbarButton {
id: clearSearchButton
anchors {
right: parent.right
verticalCenter: parent.verticalCenter
rightMargin: 2
}
implicitWidth: 26
implicitHeight: 26
text: "close"
onClicked: cheatsheetBackground.collapseSearch()
}
}
}
}
SwipeView { // Content pages
id: swipeView
Layout.topMargin: 5
spacing: 10
currentIndex: Persistent.states.cheatsheet.tabIndex
onCurrentIndexChanged: {
Persistent.states.cheatsheet.tabIndex = currentIndex;
}
implicitWidth: Math.max.apply(null, contentChildren.map(child => child.implicitWidth || 0))
implicitHeight: Math.max.apply(null, contentChildren.map(child => child.implicitHeight || 0))
clip: true
layer.enabled: true
layer.effect: OpacityMask {
maskSource: Rectangle {
width: swipeView.width
height: swipeView.height
radius: Appearance.rounding.small
}
}
CheatsheetKeybinds {
id: keybindsPage
searchQuery: searchField.text
}
CheatsheetPeriodicTable {
id: periodicTablePage
searchQuery: searchField.text
}
}
}
}
}
}
IpcHandler {
target: "cheatsheet"
function toggle(): void {
cheatsheetLoader.active = !cheatsheetLoader.active;
}
function close(): void {
cheatsheetLoader.active = false;
}
function open(): void {
cheatsheetLoader.active = true;
}
}
GlobalShortcut {
name: "cheatsheetToggle"
description: "Toggles cheatsheet on press"
onPressed: {
cheatsheetLoader.active = !cheatsheetLoader.active;
}
}
GlobalShortcut {
name: "cheatsheetOpen"
description: "Opens cheatsheet on press"
onPressed: {
cheatsheetLoader.active = true;
}
}
GlobalShortcut {
name: "cheatsheetClose"
description: "Closes cheatsheet on press"
onPressed: {
cheatsheetLoader.active = false;
}
}
}

View file

@ -0,0 +1,78 @@
pragma ComponentBehavior: Bound
import "cheatsheet_search.js" as CheatsheetSearch
import qs.services
import qs.modules.common
import qs.modules.common.functions
import qs.modules.common.widgets
import QtQuick
import QtQuick.Layouts
import Quickshell
Item {
id: root
property string searchQuery: ""
property bool noMatches: false
property real padding: 4
implicitWidth: QsWindow?.window?.screen.width * 0.7 ?? 0
implicitHeight: QsWindow?.window?.screen.height * 0.7 ?? 0
onSearchQueryChanged: matchUpdateTimer.restart()
Timer {
id: matchUpdateTimer
interval: 0
onTriggered: {
if (!root.searchQuery.trim()) {
root.noMatches = false;
return;
}
let any = false;
for (let i = 0; i < flow.children.length; i++) {
if (flow.children[i].visible) {
any = true;
break;
}
}
root.noMatches = !any;
}
}
StyledFlickable {
id: flickable
clip: true
anchors.fill: parent
anchors.margins: Appearance.rounding.small
contentHeight: height
contentWidth: flow.implicitWidth
Flow {
id: flow
height: flickable.height
flow: Flow.TopToBottom
spacing: 10
Repeater {
model: [...HyprlandKeybinds.keybindCategories, ""]
delegate: CheatsheetKeybindsCategory {
required property var modelData
categoryName: modelData
searchQuery: root.searchQuery
}
}
}
}
ScrollEdgeFade {
target: flickable
vertical: false
color: Appearance.colors.colLayer0Base
}
StyledText {
anchors.centerIn: parent
visible: root.noMatches
font.pixelSize: Appearance.font.pixelSize.large
color: Appearance.colors.colSubtext
text: Translation.tr("No matching keybinds")
}
}

View file

@ -0,0 +1,232 @@
pragma ComponentBehavior: Bound
import "cheatsheet_search.js" as CheatsheetSearch
import qs.services
import qs.modules.common
import qs.modules.common.functions
import qs.modules.common.widgets
import QtQuick
import QtQuick.Layouts
import Quickshell
// Notes:
// We deal with keybinds being numbered 1, 2, etc by discarding 2+, keeping 1 and replacing it with a generic "<Number>"
Column {
id: root
required property string categoryName
property string searchQuery: ""
readonly property bool isCategorized: categoryName?.length > 0
property int maxBindWidth: 0
property real columnSpacing: 40
property real titleSpacing: 7
// Excellent symbol explaination and source :
// http://xahlee.info/comp/unicode_computing_symbols.html
// https://www.nerdfonts.com/cheat-sheet
property var macSymbolMap: ({
"Ctrl": "󰘴",
"Alt": "󰘵",
"Shift": "󰘶",
"Space": "󱁐",
"Tab": "↹",
"Equal": "󰇼",
"Minus": "",
"Print": "",
"BackSpace": "󰭜",
"Delete": "⌦",
"Return": "󰌑",
"Period": ".",
"Escape": "⎋"
})
property var functionSymbolMap: ({
"F1": "󱊫",
"F2": "󱊬",
"F3": "󱊭",
"F4": "󱊮",
"F5": "󱊯",
"F6": "󱊰",
"F7": "󱊱",
"F8": "󱊲",
"F9": "󱊳",
"F10": "󱊴",
"F11": "󱊵",
"F12": "󱊶",
})
property var mouseSymbolMap: ({
"mouse_up": "󱕐",
"mouse_down": "󱕑",
"mouse:272": "L󰍽",
"mouse:273": "R󰍽",
"Scroll ↑/↓": "󱕒",
"Page_↑/↓": "⇞/⇟",
})
property var keyBlacklist: ["SUPER_L", "SUPER_R"]
property var keySubstitutions: Object.assign({
"Super": "",
"mouse_up": "Scroll ↓", // ikr, weird
"mouse_down": "Scroll ↑", // trust me bro
"mouse:272": "LMB",
"mouse:273": "RMB",
"mouse:275": "MouseBack",
"Slash": "/",
"Hash": "#",
"Return": "Enter",
// "Shift": "",
},
!!Config.options.cheatsheet.superKey ? {
"Super": Config.options.cheatsheet.superKey,
}: {},
Config.options.cheatsheet.useMacSymbol ? macSymbolMap : {},
Config.options.cheatsheet.useFnSymbol ? functionSymbolMap : {},
Config.options.cheatsheet.useMouseSymbol ? mouseSymbolMap : {},
)
function modMaskToStringList(modMask: int): list<string> {
var list = [];
// Funny mathematical order but we wanna have this natural user-facing order
if (modMask & (1 << 2)) { list.push("Ctrl"); }
if (modMask & (1 << 6)) { list.push("Super"); }
if (modMask & (1 << 0)) { list.push("Shift"); }
if (modMask & (1 << 3)) { list.push("Alt"); }
if (modMask & (1 << 1)) { list.push("Caps"); }
if (modMask & (1 << 4)) { list.push("Mod2"); }
if (modMask & (1 << 5)) { list.push("Mod3"); }
if (modMask & (1 << 7)) { list.push("Mod5"); }
return list;
}
visible: repeater.model.length > 0
spacing: titleSpacing
StyledText {
text: root.isCategorized ? root.categoryName : "Uncategorized"
font.pixelSize: Appearance.font.pixelSize.title
}
function hasDescription(bind) {
return bind.description?.length > 0;
}
function isCategory(bind, categoryName) {
return bind.description.substring(0, bind.description.indexOf(":")) === categoryName;
}
function isUncategorized(bind) {
return bind.description.indexOf(":") === -1;
}
function containsNonFirstRepetitive(bind) {
const key = bind.key;
if (key.includes("mouse") || key.includes("page")) return false;
// Contains non-1 number
if (/\d/.test(key) && !key.includes("1")) return true;
// Contains non-left direction
if (/^(right|up|down)\b/i.test(key)) return true;
return false;
}
function containsFirstRepetitive(bind) {
const key = bind.key;
return key.includes("1") || /left/i.test(key);
}
function transformKey(key) {
const replaced = root.keySubstitutions[key] || key;
const denumbered = replaced.replace("1", "<Number>");
const dedirectioned = denumbered.replace("Left", "<Direction>");
return dedirectioned;
}
function transformDescription(bind, categoryName) {
const description = bind.description
const regex = new RegExp("\\s*" + categoryName + "\\s*:\\s*");
const decategorized = description.replace(regex, "");
if (!containsFirstRepetitive(bind)) return decategorized;
const denumbered = decategorized.replace("1", "<Number>");
const dedirectioned = denumbered.replace(/ \b(left|right|up|down)\b/i, " <Direction>");
return dedirectioned;
}
Column {
spacing: 4
Repeater {
id: repeater
model: {
let binds;
if (!root.isCategorized) {
binds = HyprlandKeybinds.keybinds.filter(bind => root.hasDescription(bind) && root.isUncategorized(bind) && !root.containsNonFirstRepetitive(bind));
} else {
binds = HyprlandKeybinds.keybinds.filter(bind => root.hasDescription(bind) && root.isCategory(bind, root.categoryName) && !root.containsNonFirstRepetitive(bind));
}
if (root.searchQuery.trim().length === 0) return binds;
const category = root.isCategorized ? root.categoryName : "";
return binds.filter(bind => CheatsheetSearch.matchesQuery(
CheatsheetSearch.keybindHaystack(bind, root.keySubstitutions, category),
root.searchQuery,
));
}
delegate: BindLine {
required property var modelData
keyData: modelData
categoryName: root.categoryName
}
}
}
component BindLine: Row {
id: bindLine
required property var keyData
property string categoryName: ""
Row {
spacing: 16
Row {
id: modRow
Component.onCompleted: root.maxBindWidth = Math.max(root.maxBindWidth, implicitWidth)
width: root.maxBindWidth
spacing: 4
Repeater {
model: {
const modList = root.modMaskToStringList(bindLine.keyData.modmask).map(mod => root.keySubstitutions[mod] || mod)
if (modList.length == 0) return []
if (Config.options.cheatsheet.splitButtons) return modList;
return [modList.join(" ")]
}
delegate: KeyboardKey {
required property var modelData
key: root.transformKey(modelData)
pixelSize: Config.options.cheatsheet.fontSize.key
}
}
StyledText {
id: keybindPlus
anchors.verticalCenter: parent.verticalCenter
visible: !keyBlacklist.includes(bindLine.keyData.key) && bindLine.keyData.modmask > 0
text: "+"
}
KeyboardKey {
id: keybindKey
anchors.verticalCenter: parent.verticalCenter
visible: !keyBlacklist.includes(bindLine.keyData.key)
key: root.transformKey(bindLine.keyData.key)
pixelSize: Config.options.cheatsheet.fontSize.key
color: Appearance.colors.colOnLayer0
}
}
Item {
anchors.verticalCenter: parent.verticalCenter
implicitWidth: commentText.implicitWidth + root.columnSpacing
implicitHeight: commentText.implicitHeight
StyledText {
id: commentText
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
font.pixelSize: Config.options.cheatsheet.fontSize.comment || Appearance.font.pixelSize.smaller
text: root.transformDescription(bindLine.keyData, bindLine.categoryName)
}
}
}
}
}

View file

@ -0,0 +1,66 @@
import "periodic_table.js" as PTable
import QtQuick
Item {
id: root
property string searchQuery: ""
readonly property var elements: PTable.elements
readonly property var series: PTable.series
property real spacing: 6
implicitWidth: mainLayout.implicitWidth
implicitHeight: mainLayout.implicitHeight
Column {
id: mainLayout
anchors.centerIn: parent
spacing: root.spacing
Repeater { // Main table rows
model: root.elements
delegate: Row { // Table cells
id: tableRow
spacing: root.spacing
required property var modelData
Repeater {
model: tableRow.modelData
delegate: ElementTile {
required property var modelData
element: modelData
searchQuery: root.searchQuery
}
}
}
}
Item {
id: gap
implicitHeight: 20
}
Repeater { // Main table rows
model: root.series
delegate: Row { // Table cells
id: seriesTableRow
spacing: root.spacing
required property var modelData
Repeater {
model: seriesTableRow.modelData
delegate: ElementTile {
required property var modelData
element: modelData
searchQuery: root.searchQuery
}
}
}
}
}
}

View file

@ -0,0 +1,86 @@
import "cheatsheet_search.js" as CheatsheetSearch
import qs.modules.common
import qs.modules.common.functions
import qs.modules.common.widgets
import QtQuick
RippleButton {
id: root
required property var element
property string searchQuery: ""
readonly property bool dimmed: searchQuery.trim().length > 0
&& element.type !== "empty"
&& !CheatsheetSearch.matchesElement(element, searchQuery)
opacity: element.type != "empty" ? (dimmed ? 0.28 : 1) : 0
enabled: element.type === "empty" || !dimmed
implicitHeight: 70
implicitWidth: 70
colBackground: Appearance.colors.colLayer2
buttonRadius: Appearance.rounding.small
Rectangle {
anchors {
top: parent.top
left: parent.left
topMargin: 4
leftMargin: 4
}
color: ColorUtils.transparentize(Appearance.colors.colLayer2)
radius: Appearance.rounding.full
implicitWidth: Math.max(20, elementNumber.implicitWidth)
implicitHeight: Math.max(20, elementNumber.implicitHeight)
width: height
StyledText {
id: elementNumber
anchors.left: parent.left
color: Appearance.colors.colOnLayer2
text: root.element.number
font.pixelSize: Appearance.font.pixelSize.smallest
}
}
Rectangle {
anchors {
top: parent.top
right: parent.right
topMargin: 4
rightMargin: 4
}
color: ColorUtils.transparentize(Appearance.colors.colLayer2)
radius: Appearance.rounding.full
implicitWidth: Math.max(20, elementWeight.implicitWidth)
implicitHeight: Math.max(20, elementWeight.implicitHeight)
width: height
StyledText {
id: elementWeight
anchors.right: parent.right
color: Appearance.colors.colOnLayer2
text: root.element.weight
font.pixelSize: Appearance.font.pixelSize.smallest
}
}
StyledText {
id: elementSymbol
anchors.centerIn: parent
color: Appearance.colors.colSecondary
font.pixelSize: Appearance.font.pixelSize.huge
text: root.element.symbol
}
StyledText {
id: elementName
anchors {
horizontalCenter: parent.horizontalCenter
bottom: parent.bottom
bottomMargin: 4
}
font.pixelSize: Appearance.font.pixelSize.smallest
color: Appearance.colors.colOnLayer2
text: root.element.name
}
}

View file

@ -0,0 +1,88 @@
.pragma library
function queryTokens(query) {
if (!query) return [];
return query.trim().toLowerCase().split(/\s+/).filter(token => token.length > 0);
}
function matchesQuery(haystack, query) {
const tokens = queryTokens(query);
if (tokens.length === 0) return true;
return tokens.every(token => String(haystack).toLowerCase().includes(token));
}
function lookupSubstitution(substitutions, key) {
if (!key) return "";
if (substitutions[key]) return substitutions[key];
const title = key.charAt(0).toUpperCase() + key.slice(1).toLowerCase();
if (substitutions[title]) return substitutions[title];
if (substitutions[key.toUpperCase()]) return substitutions[key.toUpperCase()];
if (substitutions[key.toLowerCase()]) return substitutions[key.toLowerCase()];
return key;
}
function modMaskToStringList(modMask) {
const list = [];
if (modMask & (1 << 2)) list.push("Ctrl");
if (modMask & (1 << 6)) list.push("Super");
if (modMask & (1 << 0)) list.push("Shift");
if (modMask & (1 << 3)) list.push("Alt");
if (modMask & (1 << 1)) list.push("Caps");
if (modMask & (1 << 4)) list.push("Mod2");
if (modMask & (1 << 5)) list.push("Mod3");
if (modMask & (1 << 7)) list.push("Mod5");
return list;
}
function containsFirstRepetitive(key) {
return key.includes("1") || /left/i.test(key);
}
function transformKey(key, substitutions) {
const replaced = lookupSubstitution(substitutions, key);
return replaced.replace("1", "<Number>").replace("Left", "<Direction>");
}
function transformDescription(bind, categoryName) {
const description = bind.description || "";
const decategorized = categoryName
? description.replace(new RegExp("\\s*" + categoryName + "\\s*:\\s*"), "")
: description;
const key = bind.key || "";
if (!containsFirstRepetitive(key)) return decategorized;
const denumbered = decategorized.replace("1", "<Number>");
return denumbered.replace(/ \b(left|right|up|down)\b/i, " <Direction>");
}
function mouseKeySearchTerms(rawKey) {
switch (rawKey) {
case "mouse:272": return "LMB left mouse";
case "mouse:273": return "RMB right mouse";
case "mouse:275": return "MouseBack mouse back";
case "mouse_up": return "Scroll Down scroll down mouse up";
case "mouse_down": return "Scroll Up scroll up mouse down";
default: return "";
}
}
function keybindHaystack(bind, substitutions, categoryName) {
const modParts = [];
for (const mod of modMaskToStringList(bind.modmask)) {
modParts.push(mod);
modParts.push(lookupSubstitution(substitutions, mod));
}
const rawKey = bind.key || "";
const displayKey = transformKey(rawKey, substitutions);
const description = bind.description || "";
const displayDescription = transformDescription(bind, categoryName);
const categoryPrefix = categoryName || description.substring(0, description.indexOf(":"));
const keyTerms = [rawKey, displayKey, mouseKeySearchTerms(rawKey)].join(" ");
return [modParts.join(" "), keyTerms, description, displayDescription, categoryPrefix].join(" ");
}
function matchesElement(element, query) {
if (!element || element.type === "empty") return false;
if (queryTokens(query).length === 0) return true;
const haystack = [element.name, element.symbol, String(element.weight)].join(" ");
return matchesQuery(haystack, query);
}

View file

@ -0,0 +1,196 @@
// List of rows
const elements = [
[
{ name: 'Hydrogen', symbol: 'H', number: 1, weight: 1.01, type: 'nonmetal' },
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
{ name: 'Helium', symbol: 'He', number: 2, weight: 4.00, type: 'noblegas' },
],
[
{ name: 'Lithium', symbol: 'Li', number: 3, weight: 6.94, type: 'metal' },
{ name: 'Beryllium', symbol: 'Be', number: 4, weight: 9.01, type: 'metal' },
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
{ name: 'Boron', symbol: 'B', number: 5, weight: 10.81, type: 'nonmetal' },
{ name: 'Carbon', symbol: 'C', number: 6, weight: 12.01, type: 'nonmetal' },
{ name: 'Nitrogen', symbol: 'N', number: 7, weight: 14.01, type: 'nonmetal' },
{ name: 'Oxygen', symbol: 'O', number: 8, weight: 16, type: 'nonmetal' },
{ name: 'Fluorine', symbol: 'F', number: 9, weight: 19, type: 'nonmetal' },
{ name: 'Neon', symbol: 'Ne', number: 10, weight: 20.18, type: 'noblegas' },
],
[
{ name: 'Sodium', symbol: 'Na', number: 11, weight: 22.99, type: 'metal' },
{ name: 'Magnesium', symbol: 'Mg', number: 12, weight: 24.31, type: 'metal' },
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
{ name: 'Aluminum', symbol: 'Al', number: 13, weight: 26.98, type: 'metal' },
{ name: 'Silicon', symbol: 'Si', number: 14, weight: 28.09, type: 'nonmetal' },
{ name: 'Phosphorus', symbol: 'P', number: 15, weight: 30.97, type: 'nonmetal' },
{ name: 'Sulfur', symbol: 'S', number: 16, weight: 32.07, type: 'nonmetal' },
{ name: 'Chlorine', symbol: 'Cl', number: 17, weight: 35.45, type: 'nonmetal' },
{ name: 'Argon', symbol: 'Ar', number: 18, weight: 39.95, type: 'noblegas' },
],
[
{ name: 'Potassium', symbol: 'K', number: 19, weight: 39.098, type: 'metal' },
{ name: 'Calcium', symbol: 'Ca', number: 20, weight: 40.078, type: 'metal' },
{ name: 'Scandium', symbol: 'Sc', number: 21, weight: 44.956, type: 'metal' },
{ name: 'Titanium', symbol: 'Ti', number: 22, weight: 47.87, type: 'metal' },
{ name: 'Vanadium', symbol: 'V', number: 23, weight: 50.94, type: 'metal' },
{ name: 'Chromium', symbol: 'Cr', number: 24, weight: 52, type: 'metal'/*, icon: 'chromium-browser'*/ },
{ name: 'Manganese', symbol: 'Mn', number: 25, weight: 54.94, type: 'metal' },
{ name: 'Iron', symbol: 'Fe', number: 26, weight: 55.85, type: 'metal' },
{ name: 'Cobalt', symbol: 'Co', number: 27, weight: 58.93, type: 'metal' },
{ name: 'Nickel', symbol: 'Ni', number: 28, weight: 58.69, type: 'metal' },
{ name: 'Copper', symbol: 'Cu', number: 29, weight: 63.55, type: 'metal' },
{ name: 'Zinc', symbol: 'Zn', number: 30, weight: 65.38, type: 'metal' },
{ name: 'Gallium', symbol: 'Ga', number: 31, weight: 69.72, type: 'metal' },
{ name: 'Germanium', symbol: 'Ge', number: 32, weight: 72.63, type: 'metal' },
{ name: 'Arsenic', symbol: 'As', number: 33, weight: 74.92, type: 'nonmetal' },
{ name: 'Selenium', symbol: 'Se', number: 34, weight: 78.96, type: 'nonmetal' },
{ name: 'Bromine', symbol: 'Br', number: 35, weight: 79.904, type: 'nonmetal' },
{ name: 'Krypton', symbol: 'Kr', number: 36, weight: 83.8, type: 'noblegas' },
],
[
{ name: 'Rubidium', symbol: 'Rb', number: 37, weight: 85.47, type: 'metal' },
{ name: 'Strontium', symbol: 'Sr', number: 38, weight: 87.62, type: 'metal' },
{ name: 'Yttrium', symbol: 'Y', number: 39, weight: 88.91, type: 'metal' },
{ name: 'Zirconium', symbol: 'Zr', number: 40, weight: 91.22, type: 'metal' },
{ name: 'Niobium', symbol: 'Nb', number: 41, weight: 92.91, type: 'metal' },
{ name: 'Molybdenum', symbol: 'Mo', number: 42, weight: 95.94, type: 'metal' },
{ name: 'Technetium', symbol: 'Tc', number: 43, weight: 98, type: 'metal' },
{ name: 'Ruthenium', symbol: 'Ru', number: 44, weight: 101.07, type: 'metal' },
{ name: 'Rhodium', symbol: 'Rh', number: 45, weight: 102.91, type: 'metal' },
{ name: 'Palladium', symbol: 'Pd', number: 46, weight: 106.42, type: 'metal' },
{ name: 'Silver', symbol: 'Ag', number: 47, weight: 107.87, type: 'metal' },
{ name: 'Cadmium', symbol: 'Cd', number: 48, weight: 112.41, type: 'metal' },
{ name: 'Indium', symbol: 'In', number: 49, weight: 114.82, type: 'metal' },
{ name: 'Tin', symbol: 'Sn', number: 50, weight: 118.71, type: 'metal' },
{ name: 'Antimony', symbol: 'Sb', number: 51, weight: 121.76, type: 'metal' },
{ name: 'Tellurium', symbol: 'Te', number: 52, weight: 127.6, type: 'nonmetal' },
{ name: 'Iodine', symbol: 'I', number: 53, weight: 126.9, type: 'nonmetal' },
{ name: 'Xenon', symbol: 'Xe', number: 54, weight: 131.29, type: 'noblegas' },
],
[
{ name: 'Cesium', symbol: 'Cs', number: 55, weight: 132.91, type: 'metal' },
{ name: 'Barium', symbol: 'Ba', number: 56, weight: 137.33, type: 'metal' },
{ name: 'Lanthanum', symbol: 'La', number: 57, weight: 138.91, type: 'lanthanum' },
{ name: 'Hafnium', symbol: 'Hf', number: 72, weight: 178.49, type: 'metal' },
{ name: 'Tantalum', symbol: 'Ta', number: 73, weight: 180.95, type: 'metal' },
{ name: 'Tungsten', symbol: 'W', number: 74, weight: 183.84, type: 'metal' },
{ name: 'Rhenium', symbol: 'Re', number: 75, weight: 186.21, type: 'metal' },
{ name: 'Osmium', symbol: 'Os', number: 76, weight: 190.23, type: 'metal' },
{ name: 'Iridium', symbol: 'Ir', number: 77, weight: 192.22, type: 'metal' },
{ name: 'Platinum', symbol: 'Pt', number: 78, weight: 195.09, type: 'metal' },
{ name: 'Gold', symbol: 'Au', number: 79, weight: 196.97, type: 'metal' },
{ name: 'Mercury', symbol: 'Hg', number: 80, weight: 200.59, type: 'metal' },
{ name: 'Thallium', symbol: 'Tl', number: 81, weight: 204.38, type: 'metal' },
{ name: 'Lead', symbol: 'Pb', number: 82, weight: 207.2, type: 'metal' },
{ name: 'Bismuth', symbol: 'Bi', number: 83, weight: 208.98, type: 'metal' },
{ name: 'Polonium', symbol: 'Po', number: 84, weight: 209, type: 'metal' },
{ name: 'Astatine', symbol: 'At', number: 85, weight: 210, type: 'nonmetal' },
{ name: 'Radon', symbol: 'Rn', number: 86, weight: 222, type: 'noblegas' },
],
[
{ name: 'Francium', symbol: 'Fr', number: 87, weight: 223, type: 'metal' },
{ name: 'Radium', symbol: 'Ra', number: 88, weight: 226, type: 'metal' },
{ name: 'Actinium', symbol: 'Ac', number: 89, weight: 227, type: 'actinium' },
{ name: 'Rutherfordium', symbol: 'Rf', number: 104, weight: 267, type: 'metal' },
{ name: 'Dubnium', symbol: 'Db', number: 105, weight: 268, type: 'metal' },
{ name: 'Seaborgium', symbol: 'Sg', number: 106, weight: 271, type: 'metal' },
{ name: 'Bohrium', symbol: 'Bh', number: 107, weight: 272, type: 'metal' },
{ name: 'Hassium', symbol: 'Hs', number: 108, weight: 277, type: 'metal' },
{ name: 'Meitnerium', symbol: 'Mt', number: 109, weight: 278, type: 'metal' },
{ name: 'Darmstadtium', symbol: 'Ds', number: 110, weight: 281, type: 'metal' },
{ name: 'Roentgenium', symbol: 'Rg', number: 111, weight: 280, type: 'metal' },
{ name: 'Copernicium', symbol: 'Cn', number: 112, weight: 285, type: 'metal' },
{ name: 'Nihonium', symbol: 'Nh', number: 113, weight: 286, type: 'metal' },
{ name: 'Flerovium', symbol: 'Fl', number: 114, weight: 289, type: 'metal' },
{ name: 'Moscovium', symbol: 'Mc', number: 115, weight: 290, type: 'metal' },
{ name: 'Livermorium', symbol: 'Lv', number: 116, weight: 293, type: 'metal' },
{ name: 'Tennessine', symbol: 'Ts', number: 117, weight: 294, type: 'metal' },
{ name: 'Oganesson', symbol: 'Og', number: 118, weight: 294, type: 'noblegas' },
],
]
const series = [
[
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
{ name: 'Cerium', symbol: 'Ce', number: 58, weight: 140.12, type: 'lanthanum' },
{ name: 'Praseodymium', symbol: 'Pr', number: 59, weight: 140.91, type: 'lanthanum' },
{ name: 'Neodymium', symbol: 'Nd', number: 60, weight: 144.24, type: 'lanthanum' },
{ name: 'Promethium', symbol: 'Pm', number: 61, weight: 145, type: 'lanthanum' },
{ name: 'Samarium', symbol: 'Sm', number: 62, weight: 150.36, type: 'lanthanum' },
{ name: 'Europium', symbol: 'Eu', number: 63, weight: 151.96, type: 'lanthanum' },
{ name: 'Gadolinium', symbol: 'Gd', number: 64, weight: 157.25, type: 'lanthanum' },
{ name: 'Terbium', symbol: 'Tb', number: 65, weight: 158.93, type: 'lanthanum' },
{ name: 'Dysprosium', symbol: 'Dy', number: 66, weight: 162.5, type: 'lanthanum' },
{ name: 'Holmium', symbol: 'Ho', number: 67, weight: 164.93, type: 'lanthanum' },
{ name: 'Erbium', symbol: 'Er', number: 68, weight: 167.26, type: 'lanthanum' },
{ name: 'Thulium', symbol: 'Tm', number: 69, weight: 168.93, type: 'lanthanum' },
{ name: 'Ytterbium', symbol: 'Yb', number: 70, weight: 173.04, type: 'lanthanum' },
{ name: 'Lutetium', symbol: 'Lu', number: 71, weight: 174.97, type: 'lanthanum' },
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
],
[
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
{ name: 'Thorium', symbol: 'Th', number: 90, weight: 232.04, type: 'actinium' },
{ name: 'Protactinium', symbol: 'Pa', number: 91, weight: 231.04, type: 'actinium' },
{ name: 'Uranium', symbol: 'U', number: 92, weight: 238.03, type: 'actinium' },
{ name: 'Neptunium', symbol: 'Np', number: 93, weight: 237, type: 'actinium' },
{ name: 'Plutonium', symbol: 'Pu', number: 94, weight: 244, type: 'actinium' },
{ name: 'Americium', symbol: 'Am', number: 95, weight: 243, type: 'actinium' },
{ name: 'Curium', symbol: 'Cm', number: 96, weight: 247, type: 'actinium' },
{ name: 'Berkelium', symbol: 'Bk', number: 97, weight: 247, type: 'actinium' },
{ name: 'Californium', symbol: 'Cf', number: 98, weight: 251, type: 'actinium' },
{ name: 'Einsteinium', symbol: 'Es', number: 99, weight: 252, type: 'actinium' },
{ name: 'Fermium', symbol: 'Fm', number: 100, weight: 257, type: 'actinium' },
{ name: 'Mendelevium', symbol: 'Md', number: 101, weight: 258, type: 'actinium' },
{ name: 'Nobelium', symbol: 'No', number: 102, weight: 259, type: 'actinium' },
{ name: 'Lawrencium', symbol: 'Lr', number: 103, weight: 262, type: 'actinium' },
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
],
];
const niceTypes = {
'metal': "Metal",
'nonmetal': "Nonmetal",
'noblegas': "Noble gas",
'lanthanum': "Lanthanum",
'actinium': "Actinium"
}

View file

@ -0,0 +1,148 @@
import qs
import qs.services
import qs.modules.common
import qs.modules.common.widgets
import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import QtQuick.Layouts
import Quickshell.Io
import Quickshell
import Quickshell.Widgets
import Quickshell.Wayland
import Quickshell.Hyprland
Scope { // Scope
id: root
property bool pinned: Config.options?.dock.pinnedOnStartup ?? false
Variants {
// For each monitor
model: Quickshell.screens
PanelWindow {
id: dockRoot
// Window
required property var modelData
screen: modelData
visible: !GlobalStates.screenLocked
property bool reveal: root.pinned || (Config.options?.dock.hoverToReveal && dockMouseArea.containsMouse) || dockApps.requestDockShow || (!ToplevelManager.activeToplevel?.activated)
anchors {
bottom: true
left: true
right: true
}
exclusiveZone: root.pinned ? implicitHeight - (Appearance.sizes.hyprlandGapsOut) - (Appearance.sizes.elevationMargin - Appearance.sizes.hyprlandGapsOut) : 0
implicitWidth: dockBackground.implicitWidth
WlrLayershell.namespace: "quickshell:dock"
color: "transparent"
implicitHeight: (Config.options?.dock.height ?? 70) + Appearance.sizes.elevationMargin + Appearance.sizes.hyprlandGapsOut
mask: Region {
item: dockMouseArea
}
MouseArea {
id: dockMouseArea
height: parent.height
anchors {
top: parent.top
topMargin: dockRoot.reveal ? 0 : Config.options?.dock.hoverToReveal ? (dockRoot.implicitHeight - Config.options.dock.hoverRegionHeight) : (dockRoot.implicitHeight + 1)
horizontalCenter: parent.horizontalCenter
}
implicitWidth: dockHoverRegion.implicitWidth + Appearance.sizes.elevationMargin * 2
hoverEnabled: true
Behavior on anchors.topMargin {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
}
Item {
id: dockHoverRegion
anchors.fill: parent
implicitWidth: dockBackground.implicitWidth
Item { // Wrapper for the dock background
id: dockBackground
anchors {
top: parent.top
bottom: parent.bottom
horizontalCenter: parent.horizontalCenter
}
implicitWidth: dockRow.implicitWidth + 5 * 2
height: parent.height - Appearance.sizes.elevationMargin - Appearance.sizes.hyprlandGapsOut
StyledRectangularShadow {
target: dockVisualBackground
}
Rectangle { // The real rectangle that is visible
id: dockVisualBackground
property real margin: Appearance.sizes.elevationMargin
anchors.fill: parent
anchors.topMargin: Appearance.sizes.elevationMargin
anchors.bottomMargin: Appearance.sizes.hyprlandGapsOut
color: Appearance.colors.colLayer0
border.width: 1
border.color: Appearance.colors.colLayer0Border
radius: Appearance.rounding.large
}
RowLayout {
id: dockRow
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
spacing: 3
property real padding: 5
VerticalButtonGroup {
Layout.topMargin: Appearance.sizes.hyprlandGapsOut // why does this work
GroupButton {
// Pin button
baseWidth: 35
baseHeight: 35
clickedWidth: baseWidth
clickedHeight: baseHeight + 20
buttonRadius: Appearance.rounding.normal
toggled: root.pinned
onClicked: root.pinned = !root.pinned
contentItem: MaterialSymbol {
text: "keep"
horizontalAlignment: Text.AlignHCenter
iconSize: Appearance.font.pixelSize.larger
color: root.pinned ? Appearance.m3colors.m3onPrimary : Appearance.colors.colOnLayer0
}
}
}
DockSeparator {}
DockApps {
id: dockApps
buttonPadding: dockRow.padding
}
DockSeparator {}
DockButton {
Layout.fillHeight: true
onClicked: GlobalStates.overviewOpen = !GlobalStates.overviewOpen
topInset: Appearance.sizes.hyprlandGapsOut + dockRow.padding
bottomInset: Appearance.sizes.hyprlandGapsOut + dockRow.padding
contentItem: MaterialSymbol {
anchors.fill: parent
horizontalAlignment: Text.AlignHCenter
font.pixelSize: parent.width / 2
text: "apps"
color: Appearance.colors.colOnLayer0
}
}
}
}
}
}
}
}
}

View file

@ -0,0 +1,140 @@
import qs.services
import qs.modules.common
import qs.modules.common.functions
import Qt5Compat.GraphicalEffects
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Widgets
DockButton {
id: root
property var appToplevel
property var appListRoot
property int lastFocused: -1
property real iconSize: 35
property real countDotWidth: 10
property real countDotHeight: 4
property bool appIsActive: appToplevel.toplevels.find(t => (t.activated == true)) !== undefined
readonly property bool isSeparator: appToplevel.appId === "SEPARATOR"
property var desktopEntry: DesktopEntries.heuristicLookup(appToplevel.appId)
enabled: !isSeparator
implicitWidth: isSeparator ? 1 : implicitHeight - topInset - bottomInset
Connections {
target: DesktopEntries
function onApplicationsChanged() {
root.desktopEntry = DesktopEntries.heuristicLookup(appToplevel.appId);
}
}
Loader {
active: isSeparator
anchors {
fill: parent
topMargin: dockVisualBackground.margin + dockRow.padding + Appearance.rounding.normal
bottomMargin: dockVisualBackground.margin + dockRow.padding + Appearance.rounding.normal
}
sourceComponent: DockSeparator {}
}
Loader {
anchors.fill: parent
active: appToplevel.toplevels.length > 0
sourceComponent: MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.NoButton
onEntered: {
appListRoot.lastHoveredButton = root
appListRoot.buttonHovered = true
lastFocused = appToplevel.toplevels.length - 1
}
onExited: {
if (appListRoot.lastHoveredButton === root) {
appListRoot.buttonHovered = false
}
}
}
}
onClicked: {
if (appToplevel.toplevels.length === 0) {
root.desktopEntry?.execute();
return;
}
lastFocused = (lastFocused + 1) % appToplevel.toplevels.length
appToplevel.toplevels[lastFocused].activate()
}
middleClickAction: () => {
root.desktopEntry?.execute();
}
altAction: () => {
TaskbarApps.togglePin(appToplevel.appId);
}
contentItem: Loader {
active: !isSeparator
sourceComponent: Item {
anchors.centerIn: parent
Loader {
id: iconImageLoader
anchors {
left: parent.left
right: parent.right
verticalCenter: parent.verticalCenter
}
active: !root.isSeparator
sourceComponent: IconImage {
source: Quickshell.iconPath(AppSearch.guessIcon(appToplevel.appId), "image-missing")
implicitSize: root.iconSize
}
}
Loader {
active: Config.options.dock.monochromeIcons
anchors.fill: iconImageLoader
sourceComponent: Item {
Desaturate {
id: desaturatedIcon
visible: false // There's already color overlay
anchors.fill: parent
source: iconImageLoader
desaturation: 0.8
}
ColorOverlay {
anchors.fill: desaturatedIcon
source: desaturatedIcon
color: ColorUtils.transparentize(Appearance.colors.colPrimary, 0.9)
}
}
}
RowLayout {
spacing: 3
anchors {
top: iconImageLoader.bottom
topMargin: 2
horizontalCenter: parent.horizontalCenter
}
Repeater {
model: Math.min(appToplevel.toplevels.length, 3)
delegate: Rectangle {
required property int index
radius: Appearance.rounding.full
implicitWidth: (appToplevel.toplevels.length <= 3) ?
root.countDotWidth : root.countDotHeight // Circles when too many
implicitHeight: root.countDotHeight
color: appIsActive ? Appearance.colors.colPrimary : ColorUtils.transparentize(Appearance.colors.colOnLayer0, 0.4)
}
}
}
}
}
}

View file

@ -0,0 +1,228 @@
pragma ComponentBehavior: Bound
import Qt5Compat.GraphicalEffects
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Widgets
import Quickshell.Wayland
import qs.services
import qs.modules.common
import qs.modules.common.widgets
import qs.modules.common.functions
Item {
id: root
property real maxWindowPreviewHeight: 200
property real maxWindowPreviewWidth: 300
property real windowControlsHeight: 30
property real buttonPadding: 5
property Item lastHoveredButton: null
property bool buttonHovered: false
property bool requestDockShow: previewPopup.show
Layout.fillHeight: true
Layout.topMargin: Appearance.sizes.hyprlandGapsOut
implicitWidth: listView.implicitWidth
function popupCenterXForButton(button) {
if (!button || !root.QsWindow)
return 0;
return root.QsWindow.mapFromItem(button, button.width / 2, 0).x;
}
StyledListView {
id: listView
spacing: 2
orientation: ListView.Horizontal
anchors {
top: parent.top
bottom: parent.bottom
}
implicitWidth: contentWidth
Behavior on implicitWidth {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
}
model: ScriptModel {
objectProp: "appId"
values: TaskbarApps.apps
}
delegate: DockAppButton {
required property var modelData
appToplevel: modelData
appListRoot: root
topInset: Appearance.sizes.hyprlandGapsOut + root.buttonPadding
bottomInset: Appearance.sizes.hyprlandGapsOut + root.buttonPadding
}
}
PopupWindow {
id: previewPopup
property var appTopLevel: root.lastHoveredButton?.appToplevel
property bool shouldShow: (popupMouseArea.containsMouse || root.buttonHovered) && appTopLevel && appTopLevel.toplevels && appTopLevel.toplevels.length > 0
property bool show: false
property real cachedCenterX: 0
Connections {
target: root
function onLastHoveredButtonChanged() {
if (root.lastHoveredButton && root.QsWindow)
previewPopup.cachedCenterX = root.popupCenterXForButton(root.lastHoveredButton);
}
function onButtonHoveredChanged() {
if (root.buttonHovered && root.lastHoveredButton && root.QsWindow)
previewPopup.cachedCenterX = root.popupCenterXForButton(root.lastHoveredButton);
updateTimer.restart();
}
}
onShouldShowChanged: {
updateTimer.restart();
}
Timer {
id: updateTimer
interval: 100
onTriggered: {
previewPopup.show = previewPopup.shouldShow;
}
}
anchor {
window: root.QsWindow.window
adjustment: PopupAdjustment.None
gravity: Edges.Top | Edges.Right
edges: Edges.Top | Edges.Left
}
visible: popupBackground.opacity > 0
color: "transparent"
implicitWidth: root.QsWindow.window?.width ?? 1
implicitHeight: popupMouseArea.implicitHeight + root.windowControlsHeight + Appearance.sizes.elevationMargin * 2
MouseArea {
id: popupMouseArea
anchors.bottom: parent.bottom
implicitWidth: popupBackground.implicitWidth + Appearance.sizes.elevationMargin * 2
implicitHeight: root.maxWindowPreviewHeight + root.windowControlsHeight + Appearance.sizes.elevationMargin * 2
hoverEnabled: true
x: previewPopup.cachedCenterX - width / 2
StyledRectangularShadow {
target: popupBackground
opacity: previewPopup.show ? 1 : 0
visible: opacity > 0
Behavior on opacity {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
}
}
Rectangle {
id: popupBackground
property real padding: 5
opacity: previewPopup.show ? 1 : 0
visible: opacity > 0
Behavior on opacity {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
}
clip: true
color: Appearance.m3colors.m3surfaceContainer
radius: Appearance.rounding.normal
anchors.bottom: parent.bottom
anchors.bottomMargin: Appearance.sizes.elevationMargin
anchors.horizontalCenter: parent.horizontalCenter
implicitHeight: previewRowLayout.implicitHeight + padding * 2
implicitWidth: previewRowLayout.implicitWidth + padding * 2
Behavior on implicitWidth {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
}
Behavior on implicitHeight {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
}
RowLayout {
id: previewRowLayout
anchors.centerIn: parent
Repeater {
model: ScriptModel {
values: previewPopup.appTopLevel?.toplevels ?? []
}
RippleButton {
id: windowButton
Layout.fillHeight: true
required property var modelData
padding: 0
middleClickAction: () => {
windowButton.modelData?.close();
}
onClicked: {
windowButton.modelData?.activate();
}
contentItem: ColumnLayout {
implicitWidth: screencopyView.implicitWidth
implicitHeight: screencopyView.implicitHeight
ButtonGroup {
contentWidth: parent.width - anchors.margins * 2
StyledText {
Layout.margins: 5
Layout.fillWidth: true
font.pixelSize: Appearance.font.pixelSize.small
text: windowButton.modelData?.title
elide: Text.ElideRight
color: Appearance.m3colors.m3onSurface
}
GroupButton {
id: closeButton
colBackground: ColorUtils.transparentize(Appearance.colors.colSurfaceContainer)
baseWidth: root.windowControlsHeight
baseHeight: root.windowControlsHeight
buttonRadius: Appearance.rounding.full
contentItem: MaterialSymbol {
anchors.centerIn: parent
horizontalAlignment: Text.AlignHCenter
text: "close"
iconSize: Appearance.font.pixelSize.normal
color: Appearance.m3colors.m3onSurface
}
onClicked: {
windowButton.modelData?.close();
}
}
}
Item {
Layout.fillWidth: true
Layout.fillHeight: true
implicitHeight: screencopyView.height
implicitWidth: screencopyView.width
ScreencopyView {
id: screencopyView
anchors.centerIn: parent
captureSource: windowButton.modelData
live: true
paintCursor: true
constraintSize: Qt.size(root.maxWindowPreviewWidth, root.maxWindowPreviewHeight)
layer.enabled: true
layer.effect: OpacityMask {
maskSource: Rectangle {
width: screencopyView.width
height: screencopyView.height
radius: Appearance.rounding.small
}
}
}
}
}
}
}
}
}
}
}
}

View file

@ -0,0 +1,13 @@
import qs.modules.common
import qs.modules.common.widgets
import QtQuick
import QtQuick.Layouts
RippleButton {
Layout.fillHeight: true
Layout.topMargin: Appearance.sizes.elevationMargin - Appearance.sizes.hyprlandGapsOut
implicitWidth: implicitHeight - topInset - bottomInset
buttonRadius: Appearance.rounding.normal
background.implicitHeight: 50
}

View file

@ -0,0 +1,11 @@
import qs.modules.common
import QtQuick
import QtQuick.Layouts
Rectangle {
Layout.topMargin: Appearance.sizes.elevationMargin + dockRow.padding + Appearance.rounding.normal
Layout.bottomMargin: Appearance.sizes.hyprlandGapsOut + dockRow.padding + Appearance.rounding.normal
Layout.fillHeight: true
implicitWidth: 1
color: Appearance.colors.colOutlineVariant
}

View file

@ -0,0 +1,77 @@
pragma ComponentBehavior: Bound
import qs
import qs.services
import qs.modules.common
import qs.modules.common.functions
import qs.modules.common.panels.lock
import QtQuick
import Quickshell
import Quickshell.Hyprland
LockScreen {
id: root
// Monitor name -> workspace id to restore on unlock (set when locking)
property var savedWorkspaces: ({})
Timer {
id: restoreTimer
interval: 150
repeat: false
onTriggered: {
var batch = ""
for (var j = 0; j < Quickshell.screens.length; ++j) {
var monName = Quickshell.screens[j].name
var wsId = root.savedWorkspaces[monName]
if (wsId !== undefined) {
batch += `hyprctl dispatch 'hl.dsp.focus({monitor="${monName}"})'; hyprctl dispatch 'hl.dsp.focus({workspace=${wsId}})';`
}
}
if (batch.length > 0) {
Quickshell.execDetached(["bash", "-c", batch])
}
}
}
lockSurface: LockSurface {
context: root.context
}
// Single batch for lock and unlock so we don't race multiple hyprctl calls
Connections {
target: GlobalStates
function onScreenLockedChanged() {
if (GlobalStates.screenLocked) {
// Lock: save workspace per monitor and move all to temp workspace in one batch
var next = {}
var batch = "keyword animation workspaces,1,7,menu_decel,slidevert; "
for (var i = 0; i < Quickshell.screens.length; ++i) {
var mon = Quickshell.screens[i].name
var mData = HyprlandData.monitors.find(m => m.name === mon)
if (mData?.activeWorkspace == undefined) {
return;
}
var ws = (mData?.activeWorkspace?.id ?? 1)
next[mon] = ws
batch += `hyprctl dispatch 'hl.dsp.focus({monitor="${mon}"})'; hyprctl dispatch 'hl.dsp.focus({workspace=${2147483647 - ws}})';`
}
root.savedWorkspaces = next
Quickshell.execDetached(["bash", "-c", batch])
} else {
restoreTimer.start()
}
}
}
// Push everything down (visual only; workspace switch is in Connections above)
Variants {
model: Quickshell.screens
delegate: Scope {
required property ShellScreen modelData
property bool shouldPush: GlobalStates.screenLocked
property string targetMonitorName: modelData?.name ?? ""
property int verticalMovementDistance: modelData?.height ?? 0
property int horizontalSqueeze: (modelData?.width ?? 0) * 0.2
}
}
}

View file

@ -0,0 +1,500 @@
import QtQuick
import QtQuick.Layouts
import Qt5Compat.GraphicalEffects
import Quickshell.Services.UPower
import qs
import qs.services
import qs.modules.common
import qs.modules.common.widgets
import qs.modules.common.functions
import qs.modules.common.panels.lock
import qs.modules.ii.bar as Bar
import Quickshell
import Quickshell.Io
import Quickshell.Services.SystemTray
import qs.modules.ii.mediaControls
MouseArea {
id: root
required property LockContext context
property bool active: false
property bool showInputField: active || context.currentText.length > 0
readonly property bool requirePasswordToPower: Config.options.lock.security.requirePasswordToPower
// Whether a player with a title is currently active
readonly property bool mediaPlayerAvailable: MprisController.activePlayer !== null && MprisController.activePlayer.trackTitle
// Gate that keeps the Loader alive during exit animation.
// Set to true when player appears, set to false only after exit anim finishes.
property bool mediaLoaderActive: false
onMediaPlayerAvailableChanged: {
if (mediaPlayerAvailable) {
// Player appeared: activate loader (entry anim fires via onLoaded)
mediaLoaderActive = true
} else {
// Player disappeared: run exit anim, deactivate loader when done
if (lockscreenMediaController.item) {
entryAnim.stop()
mediaExitAnim.restart()
} else {
mediaLoaderActive = false
}
}
}
// Visualizer data for lockscreen media controls
property list<real> visualizerPoints: []
Process {
id: cavaProc
running: root.mediaLoaderActive && root.mediaPlayerAvailable
onRunningChanged: {
if (!cavaProc.running)
root.visualizerPoints = [];
}
command: ["cava", "-p", `${FileUtils.trimFileProtocol(Directories.scriptPath)}/cava/raw_output_config.txt`]
stdout: SplitParser {
onRead: data => {
let points = data.split(";").map(p => parseFloat(p.trim())).filter(p => !isNaN(p));
root.visualizerPoints = points;
}
}
}
// Force focus on entry
function forceFieldFocus() {
passwordBox.forceActiveFocus();
}
Connections {
target: context
function onShouldReFocus() {
forceFieldFocus();
}
}
hoverEnabled: true
acceptedButtons: Qt.LeftButton
onPressed: mouse => {
forceFieldFocus();
}
onPositionChanged: mouse => {
forceFieldFocus();
}
// Toolbar appearing animation
property real toolbarScale: 0.9
property real toolbarOpacity: 0
Behavior on toolbarScale {
NumberAnimation {
duration: Appearance.animation.elementMove.duration
easing.type: Appearance.animation.elementMove.type
easing.bezierCurve: Appearance.animationCurves.expressiveFastSpatial
}
}
Behavior on toolbarOpacity {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
}
// Init
Component.onCompleted: {
forceFieldFocus();
toolbarScale = 1;
toolbarOpacity = 1;
}
// Key presses
property bool ctrlHeld: false
Keys.onPressed: event => {
root.context.resetClearTimer();
if (event.key === Qt.Key_Control) {
root.ctrlHeld = true;
}
if (event.key === Qt.Key_Escape) { // Esc to clear
root.context.currentText = "";
}
forceFieldFocus();
}
Keys.onReleased: event => {
if (event.key === Qt.Key_Control) {
root.ctrlHeld = false;
}
forceFieldFocus();
}
// RippleButton {
// anchors {
// top: parent.top
// left: parent.left
// leftMargin: 10
// topMargin: 10
// }
// implicitHeight: 40
// colBackground: Appearance.colors.colLayer2
// onClicked: {
// context.unlocked(LockContext.ActionEnum.Unlock);
// GlobalStates.screenLocked = false;
// }
// contentItem: StyledText {
// text: "[[ DEBUG BYPASS ]]"
// }
// }
Loader {
id: lockscreenMediaController
// Controlled manually via mediaLoaderActive so exit anim can finish
// before the item is destroyed.
active: root.mediaLoaderActive
anchors {
horizontalCenter: parent.horizontalCenter
bottom: mainIsland.top
bottomMargin: 15
}
// Driven imperatively — no Behavior, so start values apply instantly.
property real mediaScale: 0.85
property real mediaOpacity: 0.0
scale: mediaScale * root.toolbarScale
opacity: mediaOpacity * root.toolbarOpacity
// Entry: fires after sourceComponent is fully instantiated.
onLoaded: {
mediaScale = 0.85
mediaOpacity = 0.0
entryAnim.restart()
}
ParallelAnimation {
id: entryAnim
NumberAnimation {
target: lockscreenMediaController
property: "mediaScale"
to: 1.0
duration: Appearance.animation.elementMove.duration * 1.2
easing.type: Easing.OutBack
easing.overshoot: 1.2
}
NumberAnimation {
target: lockscreenMediaController
property: "mediaOpacity"
to: 1.0
duration: Appearance.animation.elementMoveFast.duration
easing.type: Easing.OutCubic
}
}
// Exit: deactivates loader when animation finishes.
SequentialAnimation {
id: mediaExitAnim
ParallelAnimation {
NumberAnimation {
target: lockscreenMediaController
property: "mediaScale"
to: 0.85
duration: Appearance.animation.elementMoveFast.duration
easing.type: Easing.InBack
easing.overshoot: 1.2
}
NumberAnimation {
target: lockscreenMediaController
property: "mediaOpacity"
to: 0.0
duration: Appearance.animation.elementMoveFast.duration
easing.type: Easing.InCubic
}
}
// Only destroy the item after the animation has fully completed.
ScriptAction {
script: root.mediaLoaderActive = false
}
}
sourceComponent: PlayerControl {
player: MprisController.activePlayer
visualizerPoints: root.visualizerPoints
implicitWidth: Appearance.sizes.mediaControlsWidth
implicitHeight: Appearance.sizes.mediaControlsHeight
radius: Appearance.rounding.screenRounding - Appearance.sizes.hyprlandGapsOut + 1
}
}
// Main toolbar: password box
Toolbar {
id: mainIsland
anchors {
horizontalCenter: parent.horizontalCenter
bottom: parent.bottom
bottomMargin: 20
}
Behavior on anchors.bottomMargin {
animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
}
scale: root.toolbarScale
opacity: root.toolbarOpacity
// Fingerprint
Loader {
Layout.leftMargin: 10
Layout.rightMargin: 6
Layout.alignment: Qt.AlignVCenter
active: root.context.fingerprintsConfigured
visible: active
sourceComponent: MaterialSymbol {
id: fingerprintIcon
fill: 1
text: "fingerprint"
iconSize: Appearance.font.pixelSize.hugeass
color: Appearance.colors.colOnSurfaceVariant
}
}
ToolbarTextField {
id: passwordBox
Layout.rightMargin: -Layout.leftMargin
placeholderText: GlobalStates.screenUnlockFailed ? Translation.tr("Incorrect password") : Translation.tr("Enter password")
// Style
clip: true
font.pixelSize: Appearance.font.pixelSize.small
selectedTextColor: materialShapeChars ? "transparent" : Appearance.colors.colOnSecondaryContainer
selectionColor: materialShapeChars ? "transparent" : Appearance.colors.colSecondaryContainer
// Password
enabled: !root.context.unlockInProgress
echoMode: TextInput.Password
inputMethodHints: Qt.ImhSensitiveData
// Synchronizing (across monitors) and unlocking
onTextChanged: root.context.currentText = this.text
onAccepted: {
root.context.tryUnlock(ctrlHeld);
}
Connections {
target: root.context
function onCurrentTextChanged() {
passwordBox.text = root.context.currentText;
}
}
Keys.onPressed: event => {
root.context.resetClearTimer();
}
layer.enabled: true
layer.effect: OpacityMask {
maskSource: Rectangle {
width: passwordBox.width - 8
height: passwordBox.height
radius: height / 2
}
}
// Shake when wrong password
ErrorShakeAnimation {
id: wrongPasswordShakeAnim
target: passwordBox
}
Connections {
target: GlobalStates
function onScreenUnlockFailedChanged() {
if (GlobalStates.screenUnlockFailed) wrongPasswordShakeAnim.restart();
}
}
// We're drawing dots manually
property bool materialShapeChars: Config.options.lock.materialShapeChars
color: ColorUtils.transparentize(Appearance.colors.colOnLayer1, materialShapeChars ? 1 : 0)
Loader {
active: passwordBox.materialShapeChars
anchors {
fill: parent
leftMargin: passwordBox.padding
rightMargin: passwordBox.padding
}
sourceComponent: PasswordChars {
length: root.context.currentText.length
selectionStart: passwordBox.selectionStart
selectionEnd: passwordBox.selectionEnd
cursorPosition: passwordBox.cursorPosition
}
}
}
ToolbarButton {
id: confirmButton
implicitWidth: height
toggled: true
enabled: !root.context.unlockInProgress
colBackgroundToggled: Appearance.colors.colPrimary
onClicked: root.context.tryUnlock()
contentItem: MaterialSymbol {
anchors.centerIn: parent
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
iconSize: 24
text: {
if (root.context.targetAction === LockContext.ActionEnum.Unlock) {
return root.ctrlHeld ? "coffee" : "arrow_right_alt";
} else if (root.context.targetAction === LockContext.ActionEnum.Poweroff) {
return "power_settings_new";
} else if (root.context.targetAction === LockContext.ActionEnum.Reboot) {
return "restart_alt";
}
}
color: confirmButton.enabled ? Appearance.colors.colOnPrimary : Appearance.colors.colSubtext
}
}
}
// Left toolbar
Toolbar {
id: leftIsland
anchors {
right: mainIsland.left
top: mainIsland.top
bottom: mainIsland.bottom
rightMargin: 10
}
scale: root.toolbarScale
opacity: root.toolbarOpacity
// Username
IconAndTextPair {
Layout.leftMargin: 8
icon: "account_circle"
text: SystemInfo.username
}
// Keyboard layout (Xkb)
Loader {
Layout.rightMargin: 8
Layout.fillHeight: true
active: true
visible: active
sourceComponent: Row {
spacing: 8
MaterialSymbol {
id: keyboardIcon
anchors.verticalCenter: parent.verticalCenter
fill: 1
text: "keyboard_alt"
iconSize: Appearance.font.pixelSize.huge
color: Appearance.colors.colOnSurfaceVariant
}
Loader {
anchors.verticalCenter: parent.verticalCenter
sourceComponent: StyledText {
text: HyprlandXkb.currentLayoutCode
color: Appearance.colors.colOnSurfaceVariant
animateChange: true
}
}
}
}
// Keyboard layout (Fcitx)
Bar.SysTray {
Layout.rightMargin: 10
Layout.alignment: Qt.AlignVCenter
showSeparator: false
showOverflowMenu: false
pinnedItems: SystemTray.items.values.filter(i => i.id == "Fcitx")
visible: pinnedItems.length > 0
}
}
// Right toolbar
Toolbar {
id: rightIsland
anchors {
left: mainIsland.right
top: mainIsland.top
bottom: mainIsland.bottom
leftMargin: 10
}
scale: root.toolbarScale
opacity: root.toolbarOpacity
IconAndTextPair {
visible: Battery.available
icon: Battery.isCharging ? "bolt" : "battery_android_full"
text: Math.round(Battery.percentage * 100)
color: (Battery.isLow && !Battery.isCharging) ? Appearance.colors.colError : Appearance.colors.colOnSurfaceVariant
}
IconToolbarButton {
id: sleepButton
onClicked: Session.suspend()
text: "dark_mode"
}
PasswordGuardedIconToolbarButton {
id: powerButton
text: "power_settings_new"
targetAction: LockContext.ActionEnum.Poweroff
}
PasswordGuardedIconToolbarButton {
id: rebootButton
text: "restart_alt"
targetAction: LockContext.ActionEnum.Reboot
}
}
component PasswordGuardedIconToolbarButton: IconToolbarButton {
id: guardedBtn
required property var targetAction
toggled: root.context.targetAction === guardedBtn.targetAction
onClicked: {
if (!root.requirePasswordToPower) {
root.context.unlocked(guardedBtn.targetAction);
return;
}
if (root.context.targetAction === guardedBtn.targetAction) {
root.context.resetTargetAction();
} else {
root.context.targetAction = guardedBtn.targetAction;
root.context.shouldReFocus();
}
}
}
component IconAndTextPair: Row {
id: pair
required property string icon
required property string text
property color color: Appearance.colors.colOnSurfaceVariant
spacing: 4
Layout.fillHeight: true
Layout.leftMargin: 10
Layout.rightMargin: 10
MaterialSymbol {
anchors.verticalCenter: parent.verticalCenter
fill: 1
text: pair.icon
iconSize: Appearance.font.pixelSize.huge
animateChange: true
color: pair.color
}
StyledText {
anchors.verticalCenter: parent.verticalCenter
text: pair.text
color: pair.color
}
}
}

View file

@ -0,0 +1,128 @@
pragma ComponentBehavior: Bound
import QtQuick
import qs
import qs.services
import qs.modules.common
import qs.modules.common.widgets
import qs.modules.common.functions
import Quickshell
StyledFlickable {
id: root
required property int length
property int selectionStart
property int selectionEnd
property int cursorPosition
property color color: Appearance.colors.colPrimary
property color selectedTextColor: Appearance.colors.colOnSecondaryContainer
property color selectionColor: Appearance.colors.colSecondaryContainer
property int charSize: 20
contentWidth: dotsRow.implicitWidth
contentX: (Math.max(contentWidth - width, 0))
Behavior on contentX {
animation: Appearance.animation.elementMoveEnter.numberAnimation.createObject(this)
}
Rectangle {
id: cursor
anchors {
verticalCenter: parent.verticalCenter
left: parent.left
leftMargin: root.charSize * root.cursorPosition
}
color: root.color
implicitWidth: 2
implicitHeight: root.charSize
Behavior on anchors.leftMargin {
animation: Appearance.animation.elementMoveEnter.numberAnimation.createObject(cursor)
}
}
Row {
id: dotsRow
anchors {
left: parent.left
verticalCenter: parent.verticalCenter
leftMargin: 4 - 5 // -5 to account for spacing being simulated by char item width
}
spacing: 0
Repeater {
model: ScriptModel { // TODO: use proper custom object model to insert new char at the correct pos
values: Array(root.length)
}
delegate: Rectangle {
id: charItem
required property int index
implicitWidth: root.charSize
implicitHeight: root.charSize
property bool selected: index >= root.selectionStart && index < root.selectionEnd
color: ColorUtils.transparentize(root.selectionColor, selected ? 0 : 1)
MaterialShape {
id: materialShape
anchors.centerIn: parent
property list<var> charShapes: [
MaterialShape.Shape.Clover4Leaf,
MaterialShape.Shape.Arrow,
MaterialShape.Shape.Pill,
MaterialShape.Shape.SoftBurst,
MaterialShape.Shape.Diamond,
MaterialShape.Shape.ClamShell,
MaterialShape.Shape.Pentagon,
]
shape: charShapes[charItem.index % charShapes.length]
// Animate on appearance
color: charItem.selected ? root.selectedTextColor : root.color
implicitSize: 0
opacity: 0
scale: 0.5
Component.onCompleted: {
appearAnim.start();
}
ParallelAnimation {
id: appearAnim
NumberAnimation {
target: materialShape
properties: "opacity"
to: 1
duration: 50
easing.type: Appearance.animation.elementMoveFast.type
easing.bezierCurve: Appearance.animation.elementMoveFast.bezierCurve
}
NumberAnimation {
target: materialShape
properties: "scale"
to: 1
duration: 200
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.animationCurves.expressiveFastSpatial
}
NumberAnimation {
target: materialShape
properties: "implicitSize"
to: 18
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.animationCurves.expressiveFastSpatial
}
ColorAnimation {
target: materialShape
properties: "color"
from: Appearance.colors.colPrimary
to: Appearance.colors.colOnLayer1
duration: 1000
easing.type: Appearance.animation.elementMoveFast.type
easing.bezierCurve: Appearance.animation.elementMoveFast.bezierCurve
}
}
}
}
}
}
}

View file

@ -0,0 +1,294 @@
pragma ComponentBehavior: Bound
import qs
import qs.services
import qs.modules.common
import qs.modules.common.widgets
import qs.modules.common.functions
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import Quickshell.Services.Mpris
import Quickshell.Wayland
import Quickshell.Hyprland
Scope {
id: root
property bool visible: false
readonly property MprisPlayer activePlayer: MprisController.activePlayer
readonly property var realPlayers: MprisController.players
// Hide ghost players: Stopped AND no title. Chromium (and others) leave
// an MPRIS registration on D-Bus after a tab closes — still owns the
// bus name, playbackState=Stopped, no title/artist, sometimes a stale
// mpris:artUrl. Filtering on Stopped alone is too aggressive: real
// players (e.g. pear-desktop / youtube-music) transition through
// Stopped during track changes while keeping the previous trackTitle
// set, and filtering them produces a brief "No active player"
// placeholder flash mid-skip.
readonly property var _rawMeaningfulPlayers: filterDuplicatePlayers(
realPlayers.filter(p => !(p?.playbackState === MprisPlaybackState.Stopped && !p?.trackTitle))
)
// Stabilized view: holds the previous non-empty list for a brief grace
// window when the raw list goes empty, so we don't destroy/recreate
// PlayerControl delegates (and shrink/grow the panel) during normal
// track changes. Real "all players gone" states get through after the
// timer fires.
property var meaningfulPlayers: _rawMeaningfulPlayers
property bool noActivePlayers: false
on_RawMeaningfulPlayersChanged: {
if (_rawMeaningfulPlayers.length > 0) {
meaningfulPlayers = _rawMeaningfulPlayers;
noActivePlayers = false;
noActivePlayerDelay.stop();
} else {
noActivePlayerDelay.restart();
}
}
Timer {
id: noActivePlayerDelay
interval: 800
repeat: false
onTriggered: {
if (root._rawMeaningfulPlayers.length === 0) {
root.meaningfulPlayers = [];
root.noActivePlayers = true;
}
}
}
readonly property real osdWidth: Appearance.sizes.osdWidth
readonly property real widgetWidth: Appearance.sizes.mediaControlsWidth
readonly property real widgetHeight: Appearance.sizes.mediaControlsHeight
property real popupRounding: Appearance.rounding.screenRounding - Appearance.sizes.hyprlandGapsOut + 1
property list<real> visualizerPoints: []
function filterDuplicatePlayers(players) {
let filtered = [];
let used = new Set();
// Higher rank wins when picking the canonical player from a duplicate
// group. A stale Stopped player with a non-empty artUrl should never
// mask a live Playing one (Chromium leaves behind such ghosts).
const playRank = p => (p?.playbackState === MprisPlaybackState.Playing) ? 2
: (p?.playbackState === MprisPlaybackState.Paused) ? 1 : 0;
const hasArt = p => !!(p?.trackArtUrl && p.trackArtUrl.length > 0);
for (let i = 0; i < players.length; ++i) {
if (used.has(i))
continue;
let p1 = players[i];
let group = [i];
// Find duplicates: matching titles, OR same playback state plus
// near-identical position/length (plasma-browser-integration vs
// the native browser bus). Use absolute differences — the
// original `<= 2` on raw subtraction is true for any negative
// diff, so a Stopped player at position=0/length=0 incorrectly
// matches every Playing player.
for (let j = i + 1; j < players.length; ++j) {
let p2 = players[j];
const titleMatch = p1.trackTitle && p2.trackTitle &&
(p1.trackTitle.includes(p2.trackTitle) || p2.trackTitle.includes(p1.trackTitle));
const motionMatch = p1.playbackState === p2.playbackState &&
Math.abs(p1.position - p2.position) <= 2 &&
Math.abs(p1.length - p2.length) <= 2;
if (titleMatch || motionMatch) {
group.push(j);
}
}
// Tiebreaker: highest play-state rank, then non-empty trackArtUrl,
// then first encountered.
let chosenIdx = group[0];
for (let k = 1; k < group.length; ++k) {
const cand = players[group[k]];
const best = players[chosenIdx];
if (playRank(cand) > playRank(best)) chosenIdx = group[k];
else if (playRank(cand) === playRank(best) && hasArt(cand) && !hasArt(best)) chosenIdx = group[k];
}
filtered.push(players[chosenIdx]);
group.forEach(idx => used.add(idx));
}
return filtered;
}
Process {
id: cavaProc
running: mediaControlsLoader.active
onRunningChanged: {
if (!cavaProc.running) {
root.visualizerPoints = [];
}
}
command: ["cava", "-p", `${FileUtils.trimFileProtocol(Directories.scriptPath)}/cava/raw_output_config.txt`]
stdout: SplitParser {
onRead: data => {
// Parse `;`-separated values into the visualizerPoints array
let points = data.split(";").map(p => parseFloat(p.trim())).filter(p => !isNaN(p));
root.visualizerPoints = points;
}
}
}
Loader {
id: mediaControlsLoader
active: GlobalStates.mediaControlsOpen
onActiveChanged: {
if (!mediaControlsLoader.active && root.realPlayers.length === 0) {
GlobalStates.mediaControlsOpen = false;
}
}
sourceComponent: PanelWindow {
id: panelWindow
visible: true
exclusionMode: ExclusionMode.Ignore
exclusiveZone: 0
implicitWidth: root.widgetWidth
implicitHeight: playerColumnLayout.implicitHeight
color: "transparent"
WlrLayershell.namespace: "quickshell:mediaControls"
anchors {
top: !Config.options.bar.bottom || Config.options.bar.vertical
bottom: Config.options.bar.bottom && !Config.options.bar.vertical
left: !(Config.options.bar.vertical && Config.options.bar.bottom)
right: Config.options.bar.vertical && Config.options.bar.bottom
}
margins {
top: Config.options.bar.vertical ? ((panelWindow.screen.height / 2) - widgetHeight * 1.5) : Appearance.sizes.barHeight
bottom: Appearance.sizes.barHeight
left: Config.options.bar.vertical ? Appearance.sizes.barHeight : ((panelWindow.screen.width / 2) - (osdWidth / 2) - widgetWidth)
right: Appearance.sizes.barHeight
}
mask: Region {
item: playerColumnLayout
}
Component.onCompleted: {
GlobalFocusGrab.addDismissable(panelWindow);
}
Component.onDestruction: {
GlobalFocusGrab.removeDismissable(panelWindow);
}
Connections {
target: GlobalFocusGrab
function onDismissed() {
GlobalStates.mediaControlsOpen = false;
}
}
ColumnLayout {
id: playerColumnLayout
anchors.fill: parent
spacing: -Appearance.sizes.elevationMargin // Shadow overlap okay
Repeater {
model: ScriptModel {
values: root.meaningfulPlayers
}
delegate: PlayerControl {
required property MprisPlayer modelData
player: modelData
visualizerPoints: root.visualizerPoints
implicitWidth: root.widgetWidth
implicitHeight: root.widgetHeight
radius: root.popupRounding
}
}
Item {
// No player placeholder
Layout.alignment: {
if (panelWindow.anchors.left)
return Qt.AlignLeft;
if (panelWindow.anchors.right)
return Qt.AlignRight;
return Qt.AlignHCenter;
}
Layout.leftMargin: Appearance.sizes.hyprlandGapsOut
Layout.rightMargin: Appearance.sizes.hyprlandGapsOut
visible: root.noActivePlayers
implicitWidth: placeholderBackground.implicitWidth + Appearance.sizes.elevationMargin
implicitHeight: placeholderBackground.implicitHeight + Appearance.sizes.elevationMargin
StyledRectangularShadow {
target: placeholderBackground
}
Rectangle {
id: placeholderBackground
anchors.centerIn: parent
color: Appearance.colors.colLayer0
radius: root.popupRounding
property real padding: 20
implicitWidth: placeholderLayout.implicitWidth + padding * 2
implicitHeight: placeholderLayout.implicitHeight + padding * 2
ColumnLayout {
id: placeholderLayout
anchors.centerIn: parent
StyledText {
text: Translation.tr("No active player")
font.pixelSize: Appearance.font.pixelSize.large
}
StyledText {
color: Appearance.colors.colSubtext
text: Translation.tr("Make sure your player has MPRIS support\nor try turning off duplicate player filtering")
font.pixelSize: Appearance.font.pixelSize.small
}
}
}
}
}
}
}
IpcHandler {
target: "mediaControls"
function toggle(): void {
mediaControlsLoader.active = !mediaControlsLoader.active;
if (mediaControlsLoader.active)
Notifications.timeoutAll();
}
function close(): void {
mediaControlsLoader.active = false;
}
function open(): void {
mediaControlsLoader.active = true;
Notifications.timeoutAll();
}
}
GlobalShortcut {
name: "mediaControlsToggle"
description: "Toggles media controls on press"
onPressed: {
GlobalStates.mediaControlsOpen = !GlobalStates.mediaControlsOpen;
}
}
GlobalShortcut {
name: "mediaControlsOpen"
description: "Opens media controls on press"
onPressed: {
GlobalStates.mediaControlsOpen = true;
}
}
GlobalShortcut {
name: "mediaControlsClose"
description: "Closes media controls on press"
onPressed: {
GlobalStates.mediaControlsOpen = false;
}
}
}

View file

@ -0,0 +1,318 @@
pragma ComponentBehavior: Bound
import qs.modules.common
import qs.modules.common.models
import qs.modules.common.widgets
import qs.services
import qs.modules.common.functions
import Qt5Compat.GraphicalEffects
import QtQuick
import QtQuick.Effects
import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import Quickshell.Services.Mpris
Item { // Player instance
id: root
required property MprisPlayer player
property color artDominantColor: ColorUtils.mix((colorQuantizer?.colors[0] ?? Appearance.colors.colPrimary), Appearance.colors.colPrimaryContainer, 0.8) || Appearance.m3colors.m3secondaryContainer
property list<real> visualizerPoints: []
property real maxVisualizerValue: 1000 // Max value in the data points
property int visualizerSmoothing: 2 // Number of points to average for smoothing
property real radius
// Cover art is downloaded centrally by MprisController (per-player Process
// that runs regardless of panel state), and tracked there as the
// highest-quality variant seen for each (player, track) pair. We just
// read that cache. This survives both:
// - panel close/reopen (we weren't here to receive emissions; the
// central downloader was)
// - players that emit multiple sizes per track (Firefox: 544x544 then
// 60x60 thumbnail; Chromium: several near-identical tmp files) —
// bestArtByPlayer keeps the largest.
// Cache the last non-empty trackTitle/trackArtist. Some players
// (pear-desktop / youtube-music) clear metadata briefly during a track
// change before emitting the new track's values; binding the UI
// directly to player.trackTitle would make the title flash to
// "Untitled" mid-skip and would also invalidate the trackKey, dropping
// the cover image.
property string _stableTitle: ""
property string _stableArtist: ""
Connections {
target: root.player
function onTrackTitleChanged() {
if (root.player?.trackTitle) root._stableTitle = root.player.trackTitle;
}
function onTrackArtistChanged() {
if (root.player?.trackArtist) root._stableArtist = root.player.trackArtist;
}
}
Component.onCompleted: {
if (root.player?.trackTitle) _stableTitle = root.player.trackTitle;
if (root.player?.trackArtist) _stableArtist = root.player.trackArtist;
}
// title|artist only; see MprisController._trackKeyOf for why album is omitted.
readonly property string _trackKey: `${_stableTitle}|${_stableArtist}`
readonly property string displayedArtFilePath: {
const entry = MprisController.getBestArt(player, _trackKey);
return entry ? Qt.resolvedUrl(entry.artFilePath) : "";
}
component TrackChangeButton: RippleButton {
implicitWidth: 24
implicitHeight: 24
property var iconName
colBackground: ColorUtils.transparentize(blendedColors.colSecondaryContainer, 1)
colBackgroundHover: blendedColors.colSecondaryContainerHover
colRipple: blendedColors.colSecondaryContainerActive
contentItem: MaterialSymbol {
iconSize: Appearance.font.pixelSize.huge
fill: 1
horizontalAlignment: Text.AlignHCenter
color: blendedColors.colOnSecondaryContainer
text: iconName
Behavior on color {
animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this)
}
}
}
Timer { // Force update for revision
running: root.player?.playbackState == MprisPlaybackState.Playing
interval: Config.options.resources.updateInterval
repeat: true
onTriggered: {
root.player.positionChanged()
}
}
ColorQuantizer {
id: colorQuantizer
source: root.displayedArtFilePath
depth: 0 // 2^0 = 1 color
rescaleSize: 1 // Rescale to 1x1 pixel for faster processing
}
property QtObject blendedColors: AdaptedMaterialScheme {
color: artDominantColor
}
StyledRectangularShadow {
target: background
}
Rectangle { // Background
id: background
anchors.fill: parent
anchors.margins: Appearance.sizes.elevationMargin
color: ColorUtils.applyAlpha(blendedColors.colLayer0, 1)
radius: root.radius
layer.enabled: true
layer.effect: OpacityMask {
maskSource: Rectangle {
width: background.width
height: background.height
radius: background.radius
}
}
StyledImage {
id: blurredArt
anchors.fill: parent
source: root.displayedArtFilePath
fillMode: Image.PreserveAspectCrop
cache: false
antialiasing: true
asynchronous: true
layer.enabled: true
layer.effect: StyledBlurEffect {
source: blurredArt
}
Rectangle {
anchors.fill: parent
color: ColorUtils.transparentize(blendedColors.colLayer0, 0.3)
radius: root.radius
}
}
WaveVisualizer {
id: visualizerCanvas
anchors.fill: parent
live: root.player?.isPlaying
points: root.visualizerPoints
maxVisualizerValue: root.maxVisualizerValue
smoothing: root.visualizerSmoothing
color: blendedColors.colPrimary
}
RowLayout {
anchors.fill: parent
anchors.margins: 13
spacing: 15
Rectangle { // Art background
id: artBackground
Layout.fillHeight: true
implicitWidth: height
radius: Appearance.rounding.verysmall
color: ColorUtils.transparentize(blendedColors.colLayer1, 0.5)
layer.enabled: true
layer.effect: OpacityMask {
maskSource: Rectangle {
width: artBackground.width
height: artBackground.height
radius: artBackground.radius
}
}
StyledImage { // Art image
id: mediaArt
property int size: parent.height
anchors.fill: parent
source: root.displayedArtFilePath
fillMode: Image.PreserveAspectCrop
cache: false
antialiasing: true
width: size
height: size
}
}
ColumnLayout { // Info & controls
Layout.fillHeight: true
spacing: 2
StyledText {
id: trackTitle
Layout.fillWidth: true
font.pixelSize: Appearance.font.pixelSize.large
color: blendedColors.colOnLayer0
elide: Text.ElideRight
text: StringUtils.cleanMusicTitle(root._stableTitle) || "Untitled"
animateChange: true
animationDistanceX: 6
animationDistanceY: 0
}
StyledText {
id: trackArtist
Layout.fillWidth: true
font.pixelSize: Appearance.font.pixelSize.smaller
color: blendedColors.colSubtext
elide: Text.ElideRight
text: root._stableArtist
animateChange: true
animationDistanceX: 6
animationDistanceY: 0
}
Item { Layout.fillHeight: true }
Item {
Layout.fillWidth: true
implicitHeight: trackTime.implicitHeight + sliderRow.implicitHeight
StyledText {
id: trackTime
anchors.bottom: sliderRow.top
anchors.bottomMargin: 5
anchors.left: parent.left
font.pixelSize: Appearance.font.pixelSize.small
color: blendedColors.colSubtext
elide: Text.ElideRight
text: `${StringUtils.friendlyTimeForSeconds(root.player?.position)} / ${StringUtils.friendlyTimeForSeconds(root.player?.length)}`
}
RowLayout {
id: sliderRow
anchors {
bottom: parent.bottom
left: parent.left
right: parent.right
}
TrackChangeButton {
iconName: "skip_previous"
downAction: () => root.player?.previous()
}
Item {
id: progressBarContainer
Layout.fillWidth: true
implicitHeight: Math.max(sliderLoader.implicitHeight, progressBarLoader.implicitHeight)
Loader {
id: sliderLoader
anchors.fill: parent
active: root.player?.canSeek ?? false
sourceComponent: StyledSlider {
configuration: StyledSlider.Configuration.Wavy
highlightColor: blendedColors.colPrimary
trackColor: blendedColors.colSecondaryContainer
handleColor: blendedColors.colPrimary
value: root.player?.position / root.player?.length
onMoved: {
root.player.position = value * root.player.length;
}
}
}
Loader {
id: progressBarLoader
anchors {
verticalCenter: parent.verticalCenter
left: parent.left
right: parent.right
}
active: !(root.player?.canSeek ?? false)
sourceComponent: StyledProgressBar {
wavy: root.player?.isPlaying
highlightColor: blendedColors.colPrimary
trackColor: blendedColors.colSecondaryContainer
value: root.player?.position / root.player?.length
}
}
}
TrackChangeButton {
iconName: "skip_next"
downAction: () => root.player?.next()
}
}
RippleButton {
id: playPauseButton
anchors.right: parent.right
anchors.bottom: sliderRow.top
anchors.bottomMargin: 5
property real size: 44
implicitWidth: size
implicitHeight: size
downAction: () => root.player.togglePlaying();
buttonRadius: root.player?.isPlaying ? Appearance?.rounding.normal : size / 2
colBackground: root.player?.isPlaying ? blendedColors.colPrimary : blendedColors.colSecondaryContainer
colBackgroundHover: root.player?.isPlaying ? blendedColors.colPrimaryHover : blendedColors.colSecondaryContainerHover
colRipple: root.player?.isPlaying ? blendedColors.colPrimaryActive : blendedColors.colSecondaryContainerActive
contentItem: MaterialSymbol {
iconSize: Appearance.font.pixelSize.huge
fill: 1
horizontalAlignment: Text.AlignHCenter
color: root.player?.isPlaying ? blendedColors.colOnPrimary : blendedColors.colOnSecondaryContainer
text: root.player?.isPlaying ? "pause" : "play_arrow"
Behavior on color {
animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this)
}
}
}
}
}
}
}
}

View file

@ -0,0 +1,49 @@
import qs
import qs.modules.common
import qs.modules.common.widgets
import qs.services
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Wayland
import Quickshell.Hyprland
Scope {
id: notificationPopup
PanelWindow {
id: root
visible: (Notifications.popupList.length > 0) && !GlobalStates.screenLocked
screen: Quickshell.screens.find(s => Config.options.notifications.forceMonitor.enable ? s.name === Config.options.notifications.forceMonitor.name : s.name === Hyprland.focusedMonitor?.name) ?? null
WlrLayershell.namespace: "quickshell:notificationPopup"
WlrLayershell.layer: WlrLayer.Overlay
exclusiveZone: 0
anchors {
top: true
right: true
bottom: true
}
mask: Region {
item: listview.contentItem
}
color: "transparent"
implicitWidth: Appearance.sizes.notificationPopupWidth
NotificationListView {
id: listview
anchors {
top: parent.top
bottom: parent.bottom
right: parent.right
rightMargin: 4
topMargin: 4
}
implicitWidth: parent.width - Appearance.sizes.elevationMargin * 2
popup: true
}
}
}

View file

@ -0,0 +1,237 @@
import qs
import qs.services
import qs.modules.common
import qs.modules.common.widgets
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import Quickshell.Hyprland
Scope {
id: root
property string protectionMessage: ""
property var focusedScreen: Quickshell.screens.find(s => s.name === Hyprland.focusedMonitor?.name)
property string currentIndicator: "volume"
property var indicators: [
{
id: "volume",
sourceUrl: "indicators/VolumeIndicator.qml"
},
{
id: "brightness",
sourceUrl: "indicators/BrightnessIndicator.qml"
},
{
id: "gamma",
sourceUrl: "indicators/GammaIndicator.qml"
},
]
function triggerOsd() {
GlobalStates.osdVolumeOpen = true;
osdTimeout.restart();
}
Timer {
id: osdTimeout
interval: Config.options.osd.timeout
repeat: false
running: false
onTriggered: {
GlobalStates.osdVolumeOpen = false;
root.protectionMessage = "";
}
}
Connections {
target: Brightness
function onBrightnessChanged() {
root.protectionMessage = "";
root.currentIndicator = "brightness";
root.triggerOsd();
}
}
Connections {
target: Hyprsunset
function onGammaChangeAttempt() {
root.protectionMessage = "";
root.currentIndicator = "gamma";
root.triggerOsd();
}
}
Connections {
// Listen to volume changes
target: Audio.sink?.audio ?? null
function onVolumeChanged() {
if (!Audio.ready)
return;
root.currentIndicator = "volume";
root.triggerOsd();
}
function onMutedChanged() {
if (!Audio.ready)
return;
root.currentIndicator = "volume";
root.triggerOsd();
}
}
Connections {
// Listen to protection triggers
target: Audio
function onSinkProtectionTriggered(reason) {
root.protectionMessage = reason;
root.currentIndicator = "volume";
root.triggerOsd();
}
}
Loader {
id: osdLoader
active: GlobalStates.osdVolumeOpen
sourceComponent: PanelWindow {
id: osdRoot
color: "transparent"
Connections {
target: root
function onFocusedScreenChanged() {
osdRoot.screen = root.focusedScreen;
}
}
WlrLayershell.namespace: "quickshell:onScreenDisplay"
WlrLayershell.layer: WlrLayer.Overlay
anchors {
top: !Config.options.bar.bottom
bottom: Config.options.bar.bottom
}
mask: Region {
item: osdValuesWrapper
}
exclusionMode: ExclusionMode.Ignore
exclusiveZone: 0
margins {
top: Appearance.sizes.barHeight
bottom: Appearance.sizes.barHeight
}
implicitWidth: columnLayout.implicitWidth
implicitHeight: columnLayout.implicitHeight
visible: osdLoader.active
ColumnLayout {
id: columnLayout
anchors.horizontalCenter: parent.horizontalCenter
Item {
id: osdValuesWrapper
// Extra space for shadow
implicitHeight: contentColumnLayout.implicitHeight
implicitWidth: contentColumnLayout.implicitWidth
clip: true
MouseArea {
anchors.fill: parent
hoverEnabled: true
onEntered: GlobalStates.osdVolumeOpen = false
}
Column {
id: contentColumnLayout
anchors {
top: parent.top
left: parent.left
right: parent.right
}
spacing: 0
Loader {
id: osdIndicatorLoader
source: root.indicators.find(i => i.id === root.currentIndicator)?.sourceUrl
}
Item {
id: protectionMessageWrapper
anchors.horizontalCenter: parent.horizontalCenter
implicitHeight: protectionMessageBackground.implicitHeight
implicitWidth: protectionMessageBackground.implicitWidth
opacity: root.protectionMessage !== "" ? 1 : 0
StyledRectangularShadow {
target: protectionMessageBackground
}
Rectangle {
id: protectionMessageBackground
anchors.centerIn: parent
color: Appearance.m3colors.m3error
property real padding: 10
implicitHeight: protectionMessageRowLayout.implicitHeight + padding * 2
implicitWidth: protectionMessageRowLayout.implicitWidth + padding * 2
radius: Appearance.rounding.normal
RowLayout {
id: protectionMessageRowLayout
anchors.centerIn: parent
MaterialSymbol {
id: protectionMessageIcon
text: "dangerous"
iconSize: Appearance.font.pixelSize.hugeass
color: Appearance.m3colors.m3onError
}
StyledText {
id: protectionMessageTextWidget
horizontalAlignment: Text.AlignHCenter
color: Appearance.m3colors.m3onError
wrapMode: Text.Wrap
text: root.protectionMessage
}
}
}
}
}
}
}
}
}
IpcHandler {
target: "osdVolume"
function trigger() {
root.triggerOsd();
}
function hide() {
GlobalStates.osdVolumeOpen = false;
}
function toggle() {
GlobalStates.osdVolumeOpen = !GlobalStates.osdVolumeOpen;
}
}
GlobalShortcut {
name: "osdVolumeTrigger"
description: "Triggers volume OSD on press"
onPressed: {
root.triggerOsd();
}
}
GlobalShortcut {
name: "osdVolumeHide"
description: "Hides volume OSD on press"
onPressed: {
GlobalStates.osdVolumeOpen = false;
}
}
}

View file

@ -0,0 +1,106 @@
import qs.modules.common
import qs.modules.common.widgets
import QtQuick
import QtQuick.Layouts
import Quickshell.Widgets
Item {
id: root
required property real value
required property string icon
required property string name
property bool rotateIcon: false
property bool scaleIcon: false
property alias from: valueProgressBar.from
property alias to: valueProgressBar.to
property real valueIndicatorVerticalPadding: 9
property real valueIndicatorLeftPadding: 10
property real valueIndicatorRightPadding: 20 // An icon is circle ish, a column isn't, hence the extra padding
implicitWidth: Appearance.sizes.osdWidth + 2 * Appearance.sizes.elevationMargin
implicitHeight: valueIndicator.implicitHeight + 2 * Appearance.sizes.elevationMargin
StyledRectangularShadow {
target: valueIndicator
}
Rectangle {
id: valueIndicator
anchors {
fill: parent
margins: Appearance.sizes.elevationMargin
}
radius: Appearance.rounding.full
color: Appearance.colors.colLayer0
implicitWidth: valueRow.implicitWidth
implicitHeight: valueRow.implicitHeight
RowLayout { // Icon on the left, stuff on the right
id: valueRow
Layout.margins: 10
anchors.fill: parent
spacing: 10
Item {
implicitWidth: 30
implicitHeight: 30
Layout.alignment: Qt.AlignVCenter
Layout.leftMargin: valueIndicatorLeftPadding
Layout.topMargin: valueIndicatorVerticalPadding
Layout.bottomMargin: valueIndicatorVerticalPadding
MaterialSymbol { // Icon
anchors {
centerIn: parent
alignWhenCentered: !root.rotateIcon
}
color: Appearance.colors.colOnLayer0
renderType: Text.QtRendering
text: root.icon
iconSize: 20 + 10 * (root.scaleIcon ? value : 1)
rotation: 180 * (root.rotateIcon ? value : 0)
Behavior on iconSize {
animation: Appearance.animation.elementMoveEnter.numberAnimation.createObject(this)
}
Behavior on rotation {
animation: Appearance.animation.elementMoveEnter.numberAnimation.createObject(this)
}
}
}
ColumnLayout { // Stuff
Layout.alignment: Qt.AlignVCenter
Layout.rightMargin: valueIndicatorRightPadding
spacing: 5
RowLayout { // Name fill left, value on the right end
Layout.leftMargin: valueProgressBar.height / 2 // Align text with progressbar radius curve's left end
Layout.rightMargin: valueProgressBar.height / 2 // Align text with progressbar radius curve's left end
StyledText {
color: Appearance.colors.colOnLayer0
font.pixelSize: Appearance.font.pixelSize.small
Layout.fillWidth: true
text: root.name
}
StyledText {
color: Appearance.colors.colOnLayer0
font.pixelSize: Appearance.font.pixelSize.small
Layout.fillWidth: false
text: Math.round(root.value * 100)
}
}
StyledProgressBar {
id: valueProgressBar
Layout.fillWidth: true
value: root.value
}
}
}
}
}

View file

@ -0,0 +1,17 @@
import qs.services
import QtQuick
import Quickshell
import Quickshell.Hyprland
import qs.modules.ii.onScreenDisplay
OsdValueIndicator {
id: root
property var focusedScreen: Quickshell.screens.find(s => s.name === Hyprland.focusedMonitor?.name)
property var brightnessMonitor: Brightness.getMonitorForScreen(focusedScreen)
icon: Hyprsunset.temperatureActive ? "routine" : "light_mode"
rotateIcon: true
scaleIcon: true
name: Translation.tr("Brightness")
value: root.brightnessMonitor?.brightness ?? 50
}

View file

@ -0,0 +1,14 @@
import qs.services
import QtQuick
import Quickshell
import Quickshell.Hyprland
import qs.modules.ii.onScreenDisplay
OsdValueIndicator {
id: rotateIcon
icon: "wb_twilight"
name: Translation.tr("Gamma")
from: Hyprsunset.gammaLowerLimit / 100
value: Hyprsunset.gamma / 100 ?? 0.5
}

View file

@ -0,0 +1,10 @@
import qs.services
import QtQuick
import qs.modules.ii.onScreenDisplay
OsdValueIndicator {
id: osdValues
value: Audio.sink?.audio.volume ?? 0
icon: Audio.sink?.audio.muted ? "volume_off" : "volume_up"
name: Translation.tr("Volume")
}

View file

@ -0,0 +1,174 @@
import qs
import qs.services
import qs.modules.common
import qs.modules.common.widgets
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell.Io
import Quickshell
import Quickshell.Wayland
import Quickshell.Hyprland
Scope { // Scope
id: root
property bool pinned: Config.options?.osk.pinnedOnStartup ?? false
component OskControlButton: GroupButton { // Pin button
baseWidth: 40
baseHeight: 40
clickedWidth: baseWidth
clickedHeight: baseHeight + 10
buttonRadius: Appearance.rounding.normal
}
Loader {
id: oskLoader
active: GlobalStates.oskOpen
onActiveChanged: {
PhysicalKeyboard.active = oskLoader.active;
if (!oskLoader.active) {
Ydotool.releaseAllKeys();
}
}
sourceComponent: PanelWindow { // Window
id: oskRoot
visible: oskLoader.active && !GlobalStates.screenLocked
anchors {
bottom: true
left: true
right: true
}
function hide() {
GlobalStates.oskOpen = false
}
exclusiveZone: root.pinned ? implicitHeight - Appearance.sizes.hyprlandGapsOut : 0
implicitWidth: oskBackground.width + Appearance.sizes.elevationMargin * 2
implicitHeight: oskBackground.height + Appearance.sizes.elevationMargin * 2
WlrLayershell.namespace: "quickshell:osk"
WlrLayershell.layer: WlrLayer.Overlay
// Hyprland 0.49: Focus is always exclusive and setting this breaks mouse focus grab
// WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
color: "transparent"
mask: Region {
item: oskBackground
}
// Make it usable with other panels
Component.onCompleted: {
GlobalFocusGrab.addPersistent(oskRoot);
}
Component.onDestruction: {
GlobalFocusGrab.removePersistent(oskRoot);
}
// Background
StyledRectangularShadow {
target: oskBackground
}
Rectangle {
id: oskBackground
anchors.centerIn: parent
color: Appearance.colors.colLayer0
radius: Appearance.rounding.windowRounding
property real padding: 10
implicitWidth: oskRowLayout.implicitWidth + padding * 2
implicitHeight: oskRowLayout.implicitHeight + padding * 2
Keys.onPressed: (event) => { // Esc to close
if (event.key === Qt.Key_Escape) {
oskRoot.hide()
}
}
RowLayout {
id: oskRowLayout
anchors.centerIn: parent
spacing: 5
VerticalButtonGroup {
OskControlButton { // Pin button
toggled: root.pinned
downAction: () => root.pinned = !root.pinned
contentItem: MaterialSymbol {
text: "keep"
horizontalAlignment: Text.AlignHCenter
iconSize: Appearance.font.pixelSize.larger
color: root.pinned ? Appearance.m3colors.m3onPrimary : Appearance.colors.colOnLayer0
}
}
OskControlButton {
onClicked: () => {
oskRoot.hide()
}
contentItem: MaterialSymbol {
horizontalAlignment: Text.AlignHCenter
text: "keyboard_hide"
iconSize: Appearance.font.pixelSize.larger
}
}
}
Rectangle {
Layout.topMargin: 20
Layout.bottomMargin: 20
Layout.fillHeight: true
implicitWidth: 1
color: Appearance.colors.colOutlineVariant
}
OskContent {
id: oskContent
Layout.fillWidth: true
}
}
}
}
}
IpcHandler {
target: "osk"
function toggle(): void {
GlobalStates.oskOpen = !GlobalStates.oskOpen;
}
function close(): void {
GlobalStates.oskOpen = false
}
function open(): void {
GlobalStates.oskOpen = true
}
}
GlobalShortcut {
name: "oskToggle"
description: "Toggles on screen keyboard on press"
onPressed: {
GlobalStates.oskOpen = !GlobalStates.oskOpen;
}
}
GlobalShortcut {
name: "oskOpen"
description: "Opens on screen keyboard on press"
onPressed: {
GlobalStates.oskOpen = true
}
}
GlobalShortcut {
name: "oskClose"
description: "Closes on screen keyboard on press"
onPressed: {
GlobalStates.oskOpen = false
}
}
}

View file

@ -0,0 +1,41 @@
import qs.modules.common
import "layouts.js" as Layouts
import QtQuick
import QtQuick.Layouts
Item {
id: root
property var layouts: Layouts.byName
property var activeLayoutName: (layouts.hasOwnProperty(Config.options?.osk.layout))
? Config.options?.osk.layout
: Layouts.defaultLayout
property var currentLayout: layouts[activeLayoutName]
implicitWidth: keyRows.implicitWidth
implicitHeight: keyRows.implicitHeight
ColumnLayout {
id: keyRows
anchors.fill: parent
spacing: 5
Repeater {
model: root.currentLayout.keys
delegate: RowLayout {
id: keyRow
required property var modelData
spacing: 5
Repeater {
model: modelData
// A normal key looks like this: {label: "a", labelShift: "A", shape: "normal", keycode: 30, type: "normal"}
delegate: OskKey {
required property var modelData
keyData: modelData
}
}
}
}
}
}

View file

@ -0,0 +1,137 @@
import qs.services
import qs.modules.common
import qs.modules.common.widgets
import qs.modules.common.functions
import QtQuick
import QtQuick.Layouts
RippleButton {
id: root
property var keyData
property string key: keyData.label
property string type: keyData.keytype
property var keycode: keyData.keycode
property string shape: keyData.shape
property bool isShift: Ydotool.shiftKeys.includes(keycode)
property bool isBackspace: (key.toLowerCase() == "backspace")
property bool isEnter: (key.toLowerCase() == "enter" || key.toLowerCase() == "return")
property real baseWidth: 45
property real baseHeight: 45
property var widthMultiplier: ({
"normal": 1,
"fn": 1,
"tab": 1.6,
"caps": 1.9,
"shift": 2.5,
"control": 1.3
})
property var heightMultiplier: ({
"normal": 1,
"fn": 0.7,
"tab": 1,
"caps": 1,
"shift": 1,
"control": 1
})
toggled: isShift ? Ydotool.shiftMode : false
enabled: shape != "empty"
colBackground: shape == "empty" ? ColorUtils.transparentize(Appearance.colors.colLayer1) : Appearance.colors.colLayer1
buttonRadius: Appearance.rounding.small
implicitWidth: baseWidth * widthMultiplier[shape] || baseWidth
implicitHeight: baseHeight * heightMultiplier[shape] || baseHeight
Layout.fillWidth: shape == "space" || shape == "expand"
Connections {
target: Ydotool
enabled: isShift
function onShiftModeChanged() {
if (Ydotool.shiftMode == 0) {
capsLockTimer.hasStarted = false;
}
}
}
// Visual feedback for physical keyboard presses
Connections {
target: PhysicalKeyboard
enabled: root.enabled && root.keycode !== undefined
function onKeyPressed(keycode) {
if (keycode === root.keycode) {
root.startRipple(root.width / 2, root.height / 2);
}
}
function onKeyReleased(keycode) {
if (keycode === root.keycode) {
root.fadeRipple();
}
}
}
Timer {
id: capsLockTimer
property bool hasStarted: false
property bool canCaps: false
interval: 300
function startWaiting() {
hasStarted = true;
canCaps = true;
start();
}
onTriggered: {
canCaps = false;
}
}
downAction: () => {
Ydotool.press(root.keycode);
if (isShift && Ydotool.shiftMode == 0) Ydotool.shiftMode = 1;
}
releaseAction: () => {
if (root.type == "normal") {
Ydotool.release(root.keycode);
if (Ydotool.shiftMode == 1) {
Ydotool.releaseShiftKeys()
}
} else if (isShift) {
if (Ydotool.shiftMode == 1) {
if (!capsLockTimer.hasStarted) {
capsLockTimer.startWaiting();
} else {
if (capsLockTimer.canCaps) {
Ydotool.shiftMode = 2; // Caps lock mode
} else {
Ydotool.releaseShiftKeys()
}
}
} else if (Ydotool.shiftMode == 2) {
Ydotool.releaseShiftKeys();
}
} else if (root.type == "modkey") {
root.toggled = !root.toggled;
if (!root.toggled) {
if (isShift) {
Ydotool.releaseShiftKeys();
} else {
Ydotool.release(root.keycode);
}
}
}
}
contentItem: StyledText {
id: keyText
anchors.fill: parent
font.family: (isBackspace || isEnter) ? Appearance.font.family.iconMaterial : Appearance.font.family.main
font.pixelSize: root.shape == "fn" ? Appearance.font.pixelSize.small :
(isBackspace || isEnter) ? Appearance.font.pixelSize.huge :
Appearance.font.pixelSize.large
horizontalAlignment: Text.AlignHCenter
color: root.toggled ? Appearance.m3colors.m3onPrimary : Appearance.colors.colOnLayer1
text: root.isBackspace ? "backspace" : root.isEnter ? "subdirectory_arrow_left" :
Ydotool.shiftMode == 2 ? (root.keyData.labelCaps || root.keyData.labelShift || root.keyData.label) :
Ydotool.shiftMode == 1 ? (root.keyData.labelShift || root.keyData.label) :
root.keyData.label
}
}

View file

@ -0,0 +1,594 @@
// We're going to use ydotool
// See /usr/include/linux/input-event-codes.h for keycodes
const defaultLayout = "English (US)";
const byName = {
"English (US)": {
name_short: "US",
description: "QWERTY - Full",
comment: "Like physical keyboard",
// A key looks like this: { k: "a", ks: "A", t: "normal" } (key, key-shift, type)
// key types are: normal, tab, caps, shift, control, fn (normal w/ half height), space, expand
// keys: [
// [{ k: "Esc", t: "fn" }, { k: "F1", t: "fn" }, { k: "F2", t: "fn" }, { k: "F3", t: "fn" }, { k: "F4", t: "fn" }, { k: "F5", t: "fn" }, { k: "F6", t: "fn" }, { k: "F7", t: "fn" }, { k: "F8", t: "fn" }, { k: "F9", t: "fn" }, { k: "F10", t: "fn" }, { k: "F11", t: "fn" }, { k: "F12", t: "fn" }, { k: "PrtSc", t: "fn" }, { k: "Del", t: "fn" }],
// [{ k: "`", ks: "~", t: "normal" }, { k: "1", ks: "!", t: "normal" }, { k: "2", ks: "@", t: "normal" }, { k: "3", ks: "#", t: "normal" }, { k: "4", ks: "$", t: "normal" }, { k: "5", ks: "%", t: "normal" }, { k: "6", ks: "^", t: "normal" }, { k: "7", ks: "&", t: "normal" }, { k: "8", ks: "*", t: "normal" }, { k: "9", ks: "(", t: "normal" }, { k: "0", ks: ")", t: "normal" }, { k: "-", ks: "_", t: "normal" }, { k: "=", ks: "+", t: "normal" }, { k: "Backspace", t: "shift" }],
// [{ k: "Tab", t: "tab" }, { k: "q", ks: "Q", t: "normal" }, { k: "w", ks: "W", t: "normal" }, { k: "e", ks: "E", t: "normal" }, { k: "r", ks: "R", t: "normal" }, { k: "t", ks: "T", t: "normal" }, { k: "y", ks: "Y", t: "normal" }, { k: "u", ks: "U", t: "normal" }, { k: "i", ks: "I", t: "normal" }, { k: "o", ks: "O", t: "normal" }, { k: "p", ks: "P", t: "normal" }, { k: "[", ks: "{", t: "normal" }, { k: "]", ks: "}", t: "normal" }, { k: "\\", ks: "|", t: "expand" }],
// [{ k: "Caps", t: "caps" }, { k: "a", ks: "A", t: "normal" }, { k: "s", ks: "S", t: "normal" }, { k: "d", ks: "D", t: "normal" }, { k: "f", ks: "F", t: "normal" }, { k: "g", ks: "G", t: "normal" }, { k: "h", ks: "H", t: "normal" }, { k: "j", ks: "J", t: "normal" }, { k: "k", ks: "K", t: "normal" }, { k: "l", ks: "L", t: "normal" }, { k: ";", ks: ":", t: "normal" }, { k: "'", ks: '"', t: "normal" }, { k: "Enter", t: "expand" }],
// [{ k: "Shift", t: "shift" }, { k: "z", ks: "Z", t: "normal" }, { k: "x", ks: "X", t: "normal" }, { k: "c", ks: "C", t: "normal" }, { k: "v", ks: "V", t: "normal" }, { k: "b", ks: "B", t: "normal" }, { k: "n", ks: "N", t: "normal" }, { k: "m", ks: "M", t: "normal" }, { k: ",", ks: "<", t: "normal" }, { k: ".", ks: ">", t: "normal" }, { k: "/", ks: "?", t: "normal" }, { k: "Shift", t: "expand" }],
// [{ k: "Ctrl", t: "control" }, { k: "Fn", t: "normal" }, { k: "Win", t: "normal" }, { k: "Alt", t: "normal" }, { k: "Space", t: "space" }, { k: "Alt", t: "normal" }, { k: "Menu", t: "normal" }, { k: "Ctrl", t: "control" }]
// ]
// A normal key looks like this: {label: "a", labelShift: "A", shape: "normal", keycode: 30, type: "normal"}
// A modkey looks like this: {label: "Ctrl", shape: "control", keycode: 29, type: "modkey"}
// key types are: normal, tab, caps, shift, control, fn (normal w/ half height), space, expand
keys: [
[
{ keytype: "normal", label: "Esc", shape: "fn", keycode: 1 },
{ keytype: "normal", label: "F1", shape: "fn", keycode: 59 },
{ keytype: "normal", label: "F2", shape: "fn", keycode: 60 },
{ keytype: "normal", label: "F3", shape: "fn", keycode: 61 },
{ keytype: "normal", label: "F4", shape: "fn", keycode: 62 },
{ keytype: "normal", label: "F5", shape: "fn", keycode: 63 },
{ keytype: "normal", label: "F6", shape: "fn", keycode: 64 },
{ keytype: "normal", label: "F7", shape: "fn", keycode: 65 },
{ keytype: "normal", label: "F8", shape: "fn", keycode: 66 },
{ keytype: "normal", label: "F9", shape: "fn", keycode: 67 },
{ keytype: "normal", label: "F10", shape: "fn", keycode: 68 },
{ keytype: "normal", label: "F11", shape: "fn", keycode: 87 },
{ keytype: "normal", label: "F12", shape: "fn", keycode: 88 },
{ keytype: "normal", label: "PrtSc", shape: "fn", keycode: 99 },
{ keytype: "normal", label: "Del", shape: "fn", keycode: 111 }
],
[
{ keytype: "normal", label: "`", labelShift: "~", shape: "normal", keycode: 41 },
{ keytype: "normal", label: "1", labelShift: "!", shape: "normal", keycode: 2 },
{ keytype: "normal", label: "2", labelShift: "@", shape: "normal", keycode: 3 },
{ keytype: "normal", label: "3", labelShift: "#", shape: "normal", keycode: 4 },
{ keytype: "normal", label: "4", labelShift: "$", shape: "normal", keycode: 5 },
{ keytype: "normal", label: "5", labelShift: "%", shape: "normal", keycode: 6 },
{ keytype: "normal", label: "6", labelShift: "^", shape: "normal", keycode: 7 },
{ keytype: "normal", label: "7", labelShift: "&", shape: "normal", keycode: 8 },
{ keytype: "normal", label: "8", labelShift: "*", shape: "normal", keycode: 9 },
{ keytype: "normal", label: "9", labelShift: "(", shape: "normal", keycode: 10 },
{ keytype: "normal", label: "0", labelShift: ")", shape: "normal", keycode: 11 },
{ keytype: "normal", label: "-", labelShift: "_", shape: "normal", keycode: 12 },
{ keytype: "normal", label: "=", labelShift: "+", shape: "normal", keycode: 13 },
{ keytype: "normal", label: "Backspace", shape: "expand", keycode: 14 }
],
[
{ keytype: "normal", label: "Tab", shape: "tab", keycode: 15 },
{ keytype: "normal", label: "q", labelShift: "Q", shape: "normal", keycode: 16 },
{ keytype: "normal", label: "w", labelShift: "W", shape: "normal", keycode: 17 },
{ keytype: "normal", label: "e", labelShift: "E", shape: "normal", keycode: 18 },
{ keytype: "normal", label: "r", labelShift: "R", shape: "normal", keycode: 19 },
{ keytype: "normal", label: "t", labelShift: "T", shape: "normal", keycode: 20 },
{ keytype: "normal", label: "y", labelShift: "Y", shape: "normal", keycode: 21 },
{ keytype: "normal", label: "u", labelShift: "U", shape: "normal", keycode: 22 },
{ keytype: "normal", label: "i", labelShift: "I", shape: "normal", keycode: 23 },
{ keytype: "normal", label: "o", labelShift: "O", shape: "normal", keycode: 24 },
{ keytype: "normal", label: "p", labelShift: "P", shape: "normal", keycode: 25 },
{ keytype: "normal", label: "[", labelShift: "{", shape: "normal", keycode: 26 },
{ keytype: "normal", label: "]", labelShift: "}", shape: "normal", keycode: 27 },
{ keytype: "normal", label: "\\", labelShift: "|", shape: "expand", keycode: 43 }
],
[
//{ keytype: "normal", label: "Caps", shape: "caps", keycode: 58 }, // not needed as double-pressing shift does that
{ keytype: "spacer", label: "", shape: "empty" },
{ keytype: "spacer", label: "", shape: "empty" },
{ keytype: "normal", label: "a", labelShift: "A", shape: "normal", keycode: 30 },
{ keytype: "normal", label: "s", labelShift: "S", shape: "normal", keycode: 31 },
{ keytype: "normal", label: "d", labelShift: "D", shape: "normal", keycode: 32 },
{ keytype: "normal", label: "f", labelShift: "F", shape: "normal", keycode: 33 },
{ keytype: "normal", label: "g", labelShift: "G", shape: "normal", keycode: 34 },
{ keytype: "normal", label: "h", labelShift: "H", shape: "normal", keycode: 35 },
{ keytype: "normal", label: "j", labelShift: "J", shape: "normal", keycode: 36 },
{ keytype: "normal", label: "k", labelShift: "K", shape: "normal", keycode: 37 },
{ keytype: "normal", label: "l", labelShift: "L", shape: "normal", keycode: 38 },
{ keytype: "normal", label: ";", labelShift: ":", shape: "normal", keycode: 39 },
{ keytype: "normal", label: "'", labelShift: '"', shape: "normal", keycode: 40 },
{ keytype: "normal", label: "Enter", shape: "expand", keycode: 28 }
],
[
{ keytype: "modkey", label: "Shift", labelShift: "Shift", labelCaps: "Caps", shape: "shift", keycode: 42 },
{ keytype: "normal", label: "z", labelShift: "Z", shape: "normal", keycode: 44 },
{ keytype: "normal", label: "x", labelShift: "X", shape: "normal", keycode: 45 },
{ keytype: "normal", label: "c", labelShift: "C", shape: "normal", keycode: 46 },
{ keytype: "normal", label: "v", labelShift: "V", shape: "normal", keycode: 47 },
{ keytype: "normal", label: "b", labelShift: "B", shape: "normal", keycode: 48 },
{ keytype: "normal", label: "n", labelShift: "N", shape: "normal", keycode: 49 },
{ keytype: "normal", label: "m", labelShift: "M", shape: "normal", keycode: 50 },
{ keytype: "normal", label: ",", labelShift: "<", shape: "normal", keycode: 51 },
{ keytype: "normal", label: ".", labelShift: ">", shape: "normal", keycode: 52 },
{ keytype: "normal", label: "/", labelShift: "?", shape: "normal", keycode: 53 },
{ keytype: "modkey", label: "Shift", labelShift: "Shift", labelCaps: "Caps", shape: "expand", keycode: 54 } // optional
],
[
{ keytype: "modkey", label: "Ctrl", shape: "control", keycode: 29 },
// { label: "Super", shape: "normal", keycode: 125 }, // dangerous
{ keytype: "modkey", label: "Alt", shape: "normal", keycode: 56 },
{ keytype: "normal", label: "Space", shape: "space", keycode: 57 },
{ keytype: "modkey", label: "Alt", shape: "normal", keycode: 100 },
// { label: "Super", shape: "normal", keycode: 126 }, // dangerous
{ keytype: "normal", label: "Menu", shape: "normal", keycode: 139 },
{ keytype: "modkey", label: "Ctrl", shape: "control", keycode: 97 }
]
]
},
"German": {
name_short: "DE",
description: "QWERTZ - Full",
comment: "Keyboard layout commonly used in German-speaking countries",
keys: [
[
{ keytype: "normal", label: "Esc", shape: "fn", keycode: 1 },
{ keytype: "normal", label: "F1", shape: "fn", keycode: 59 },
{ keytype: "normal", label: "F2", shape: "fn", keycode: 60 },
{ keytype: "normal", label: "F3", shape: "fn", keycode: 61 },
{ keytype: "normal", label: "F4", shape: "fn", keycode: 62 },
{ keytype: "normal", label: "F5", shape: "fn", keycode: 63 },
{ keytype: "normal", label: "F6", shape: "fn", keycode: 64 },
{ keytype: "normal", label: "F7", shape: "fn", keycode: 65 },
{ keytype: "normal", label: "F8", shape: "fn", keycode: 66 },
{ keytype: "normal", label: "F9", shape: "fn", keycode: 67 },
{ keytype: "normal", label: "F10", shape: "fn", keycode: 68 },
{ keytype: "normal", label: "F11", shape: "fn", keycode: 87 },
{ keytype: "normal", label: "F12", shape: "fn", keycode: 88 },
{ keytype: "normal", label: "Druck", shape: "fn", keycode: 99 },
{ keytype: "normal", label: "Entf", shape: "fn", keycode: 111 }
],
[
{ keytype: "normal", label: "^", labelShift: "°", labelAlt: "′", shape: "normal", keycode: 41 },
{ keytype: "normal", label: "1", labelShift: "!", labelAlt: "¹", shape: "normal", keycode: 2 },
{ keytype: "normal", label: "2", labelShift: "\"", labelAlt: "²", shape: "normal", keycode: 3 },
{ keytype: "normal", label: "3", labelShift: "§", labelAlt: "³", shape: "normal", keycode: 4 },
{ keytype: "normal", label: "4", labelShift: "$", labelAlt: "¼", shape: "normal", keycode: 5 },
{ keytype: "normal", label: "5", labelShift: "%", labelAlt: "½", shape: "normal", keycode: 6 },
{ keytype: "normal", label: "6", labelShift: "&", labelAlt: "¬", shape: "normal", keycode: 7 },
{ keytype: "normal", label: "7", labelShift: "/", labelAlt: "{", shape: "normal", keycode: 8 },
{ keytype: "normal", label: "8", labelShift: "(", labelAlt: "[", shape: "normal", keycode: 9 },
{ keytype: "normal", label: "9", labelShift: ")", labelAlt: "]", shape: "normal", keycode: 10 },
{ keytype: "normal", label: "0", labelShift: "=", labelAlt: "}", shape: "normal", keycode: 11 },
{ keytype: "normal", label: "ß", labelShift: "?", labelAlt: "\\", shape: "normal", keycode: 12 },
{ keytype: "normal", label: "´", labelShift: "`", labelAlt: "¸", shape: "normal", keycode: 13 },
{ keytype: "normal", label: "⟵", shape: "expand", keycode: 14 }
],
[
{ keytype: "normal", label: "Tab ⇆", shape: "tab", keycode: 15 },
{ keytype: "normal", label: "q", labelShift: "Q", labelAlt: "@", shape: "normal", keycode: 16 },
{ keytype: "normal", label: "w", labelShift: "W", labelAlt: "ſ", shape: "normal", keycode: 17 },
{ keytype: "normal", label: "e", labelShift: "E", labelAlt: "€", shape: "normal", keycode: 18 },
{ keytype: "normal", label: "r", labelShift: "R", labelAlt: "¶", shape: "normal", keycode: 19 },
{ keytype: "normal", label: "t", labelShift: "T", labelAlt: "ŧ", shape: "normal", keycode: 20 },
{ keytype: "normal", label: "z", labelShift: "Z", labelAlt: "←", shape: "normal", keycode: 21 },
{ keytype: "normal", label: "u", labelShift: "U", labelAlt: "↓", shape: "normal", keycode: 22 },
{ keytype: "normal", label: "i", labelShift: "I", labelAlt: "→", shape: "normal", keycode: 23 },
{ keytype: "normal", label: "o", labelShift: "O", labelAlt: "ø", shape: "normal", keycode: 24 },
{ keytype: "normal", label: "p", labelShift: "P", labelAlt: "þ", shape: "normal", keycode: 25 },
{ keytype: "normal", label: "ü", labelShift: "Ü", labelAlt: "¨", shape: "normal", keycode: 26 },
{ keytype: "normal", label: "+", labelShift: "*", labelAlt: "~", shape: "normal", keycode: 27 },
{ keytype: "normal", label: "↵", shape: "expand", keycode: 28 }
],
[
//{ keytype: "normal", label: "Umschalt ⇩", shape: "caps", keycode: 58 },
{ keytype: "spacer", label: "", shape: "empty" },
{ keytype: "spacer", label: "", shape: "empty" },
{ keytype: "normal", label: "a", labelShift: "A", labelAlt: "æ", shape: "normal", keycode: 30 },
{ keytype: "normal", label: "s", labelShift: "S", labelAlt: "ſ", shape: "normal", keycode: 31 },
{ keytype: "normal", label: "d", labelShift: "D", labelAlt: "ð", shape: "normal", keycode: 32 },
{ keytype: "normal", label: "f", labelShift: "F", labelAlt: "đ", shape: "normal", keycode: 33 },
{ keytype: "normal", label: "g", labelShift: "G", labelAlt: "ŋ", shape: "normal", keycode: 34 },
{ keytype: "normal", label: "h", labelShift: "H", labelAlt: "ħ", shape: "normal", keycode: 35 },
{ keytype: "normal", label: "j", labelShift: "J", labelAlt: "", shape: "normal", keycode: 36 },
{ keytype: "normal", label: "k", labelShift: "K", labelAlt: "ĸ", shape: "normal", keycode: 37 },
{ keytype: "normal", label: "l", labelShift: "L", labelAlt: "ł", shape: "normal", keycode: 38 },
{ keytype: "normal", label: "ö", labelShift: "Ö", labelAlt: "˝", shape: "normal", keycode: 39 },
{ keytype: "normal", label: "ä", labelShift: 'Ä', labelAlt: "^", shape: "normal", keycode: 40 },
{ keytype: "normal", label: "#", labelShift: '\'', labelAlt: "’", shape: "normal", keycode: 43 },
{ keytype: "spacer", label: "", shape: "empty" },
//{ keytype: "normal", label: "↵", shape: "expand", keycode: 28 }
],
[
{ keytype: "modkey", label: "Shift", labelShift: "Shift ⇧", labelCaps: "Locked ⇩", shape: "shift", keycode: 42 },
{ keytype: "normal", label: "<", labelShift: ">", labelAlt: "|", shape: "normal", keycode: 86 },
{ keytype: "normal", label: "y", labelShift: "Y", labelAlt: "»", shape: "normal", keycode: 44 },
{ keytype: "normal", label: "x", labelShift: "X", labelAlt: "«", shape: "normal", keycode: 45 },
{ keytype: "normal", label: "c", labelShift: "C", labelAlt: "¢", shape: "normal", keycode: 46 },
{ keytype: "normal", label: "v", labelShift: "V", labelAlt: "„", shape: "normal", keycode: 47 },
{ keytype: "normal", label: "b", labelShift: "B", labelAlt: "“", shape: "normal", keycode: 48 },
{ keytype: "normal", label: "n", labelShift: "N", labelAlt: "”", shape: "normal", keycode: 49 },
{ keytype: "normal", label: "m", labelShift: "M", labelAlt: "µ", shape: "normal", keycode: 50 },
{ keytype: "normal", label: ",", labelShift: ";", labelAlt: "·", shape: "normal", keycode: 51 },
{ keytype: "normal", label: ".", labelShift: ":", labelAlt: "…", shape: "normal", keycode: 52 },
{ keytype: "normal", label: "-", labelShift: "_", labelAlt: "–", shape: "normal", keycode: 53 },
{ keytype: "modkey", label: "Shift", labelShift: "Shift ⇧", labelCaps: "Locked ⇩", shape: "expand", keycode: 54 }, // optional
],
[
{ keytype: "modkey", label: "Strg", shape: "control", keycode: 29 },
//{ keytype: "normal", label: "", shape: "normal", keycode: 125 }, // dangerous
{ keytype: "modkey", label: "Alt", shape: "normal", keycode: 56 },
{ keytype: "normal", label: "Leertaste", shape: "space", keycode: 57 },
{ keytype: "modkey", label: "Alt Gr", shape: "normal", keycode: 100 },
// { label: "Super", shape: "normal", keycode: 126 }, // dangerous
//{ keytype: "normal", label: "Menu", shape: "normal", keycode: 139 }, // doesn't work?
{ keytype: "modkey", label: "Strg", shape: "control", keycode: 97 },
{ keytype: "normal", label: "⇦", shape: "normal", keycode: 105 },
{ keytype: "normal", label: "⇨", shape: "normal", keycode: 106 },
]
]
},
"Russian": {
name_short: "RU",
description: "ЙЦУКЕН - Full",
comment: "Standard Russian keyboard layout",
keys: [
[
{ keytype: "normal", label: "Esc", shape: "fn", keycode: 1 },
{ keytype: "normal", label: "F1", shape: "fn", keycode: 59 },
{ keytype: "normal", label: "F2", shape: "fn", keycode: 60 },
{ keytype: "normal", label: "F3", shape: "fn", keycode: 61 },
{ keytype: "normal", label: "F4", shape: "fn", keycode: 62 },
{ keytype: "normal", label: "F5", shape: "fn", keycode: 63 },
{ keytype: "normal", label: "F6", shape: "fn", keycode: 64 },
{ keytype: "normal", label: "F7", shape: "fn", keycode: 65 },
{ keytype: "normal", label: "F8", shape: "fn", keycode: 66 },
{ keytype: "normal", label: "F9", shape: "fn", keycode: 67 },
{ keytype: "normal", label: "F10", shape: "fn", keycode: 68 },
{ keytype: "normal", label: "F11", shape: "fn", keycode: 87 },
{ keytype: "normal", label: "F12", shape: "fn", keycode: 88 },
{ keytype: "normal", label: "PrtSc", shape: "fn", keycode: 99 },
{ keytype: "normal", label: "Del", shape: "fn", keycode: 111 }
],
[
{ keytype: "normal", label: "ё", labelShift: "Ё", shape: "normal", keycode: 41 },
{ keytype: "normal", label: "1", labelShift: "!", shape: "normal", keycode: 2 },
{ keytype: "normal", label: "2", labelShift: "\"", shape: "normal", keycode: 3 },
{ keytype: "normal", label: "3", labelShift: "№", shape: "normal", keycode: 4 },
{ keytype: "normal", label: "4", labelShift: ";", shape: "normal", keycode: 5 },
{ keytype: "normal", label: "5", labelShift: "%", shape: "normal", keycode: 6 },
{ keytype: "normal", label: "6", labelShift: ":", shape: "normal", keycode: 7 },
{ keytype: "normal", label: "7", labelShift: "?", shape: "normal", keycode: 8 },
{ keytype: "normal", label: "8", labelShift: "*", shape: "normal", keycode: 9 },
{ keytype: "normal", label: "9", labelShift: "(", shape: "normal", keycode: 10 },
{ keytype: "normal", label: "0", labelShift: ")", shape: "normal", keycode: 11 },
{ keytype: "normal", label: "-", labelShift: "_", shape: "normal", keycode: 12 },
{ keytype: "normal", label: "=", labelShift: "+", shape: "normal", keycode: 13 },
{ keytype: "normal", label: "Backspace", shape: "expand", keycode: 14 }
],
[
{ keytype: "normal", label: "Tab", shape: "tab", keycode: 15 },
{ keytype: "normal", label: "й", labelShift: "Й", shape: "normal", keycode: 16 },
{ keytype: "normal", label: "ц", labelShift: "Ц", shape: "normal", keycode: 17 },
{ keytype: "normal", label: "у", labelShift: "У", shape: "normal", keycode: 18 },
{ keytype: "normal", label: "к", labelShift: "К", shape: "normal", keycode: 19 },
{ keytype: "normal", label: "е", labelShift: "Е", shape: "normal", keycode: 20 },
{ keytype: "normal", label: "н", labelShift: "Н", shape: "normal", keycode: 21 },
{ keytype: "normal", label: "г", labelShift: "Г", shape: "normal", keycode: 22 },
{ keytype: "normal", label: "ш", labelShift: "Ш", shape: "normal", keycode: 23 },
{ keytype: "normal", label: "щ", labelShift: "Щ", shape: "normal", keycode: 24 },
{ keytype: "normal", label: "з", labelShift: "З", shape: "normal", keycode: 25 },
{ keytype: "normal", label: "х", labelShift: "Х", shape: "normal", keycode: 26 },
{ keytype: "normal", label: "ъ", labelShift: "Ъ", shape: "normal", keycode: 27 },
{ keytype: "normal", label: "\\", labelShift: "/", shape: "expand", keycode: 43 }
],
[
{ keytype: "spacer", label: "", shape: "empty" },
{ keytype: "spacer", label: "", shape: "empty" },
{ keytype: "normal", label: "ф", labelShift: "Ф", shape: "normal", keycode: 30 },
{ keytype: "normal", label: "ы", labelShift: "Ы", shape: "normal", keycode: 31 },
{ keytype: "normal", label: "в", labelShift: "В", shape: "normal", keycode: 32 },
{ keytype: "normal", label: "а", labelShift: "А", shape: "normal", keycode: 33 },
{ keytype: "normal", label: "п", labelShift: "П", shape: "normal", keycode: 34 },
{ keytype: "normal", label: "р", labelShift: "Р", shape: "normal", keycode: 35 },
{ keytype: "normal", label: "о", labelShift: "О", shape: "normal", keycode: 36 },
{ keytype: "normal", label: "л", labelShift: "Л", shape: "normal", keycode: 37 },
{ keytype: "normal", label: "д", labelShift: "Д", shape: "normal", keycode: 38 },
{ keytype: "normal", label: "ж", labelShift: "Ж", shape: "normal", keycode: 39 },
{ keytype: "normal", label: "э", labelShift: "Э", shape: "normal", keycode: 40 },
{ keytype: "normal", label: "Enter", shape: "expand", keycode: 28 }
],
[
{ keytype: "modkey", label: "Shift", shape: "shift", keycode: 42 },
{ keytype: "normal", label: "я", labelShift: "Я", shape: "normal", keycode: 44 },
{ keytype: "normal", label: "ч", labelShift: "Ч", shape: "normal", keycode: 45 },
{ keytype: "normal", label: "с", labelShift: "С", shape: "normal", keycode: 46 },
{ keytype: "normal", label: "м", labelShift: "М", shape: "normal", keycode: 47 },
{ keytype: "normal", label: "и", labelShift: "И", shape: "normal", keycode: 48 },
{ keytype: "normal", label: "т", labelShift: "Т", shape: "normal", keycode: 49 },
{ keytype: "normal", label: "ь", labelShift: "Ь", shape: "normal", keycode: 50 },
{ keytype: "normal", label: "б", labelShift: "Б", shape: "normal", keycode: 51 },
{ keytype: "normal", label: "ю", labelShift: "Ю", shape: "normal", keycode: 52 },
{ keytype: "normal", label: ".", labelShift: ",", shape: "normal", keycode: 53 },
{ keytype: "modkey", label: "Shift", shape: "expand", keycode: 54 }
],
[
{ keytype: "modkey", label: "Ctrl", shape: "control", keycode: 29 },
{ keytype: "modkey", label: "Alt", shape: "normal", keycode: 56 },
{ keytype: "normal", label: "Space", shape: "space", keycode: 57 },
{ keytype: "modkey", label: "Alt", shape: "normal", keycode: 100 },
{ keytype: "normal", label: "Menu", shape: "normal", keycode: 139 },
{ keytype: "modkey", label: "Ctrl", shape: "control", keycode: 97 }
]
]
},
"Greek (polytonic)": {
name_short: "EL",
description: "Polytonic Greek - Full",
comment: "Greek keyboard layout with polytonic diacritical marks (XKB gr(polytonic))",
keys: [
[
{ keytype: "normal", label: "Esc", shape: "fn", keycode: 1 },
{ keytype: "normal", label: "F1", shape: "fn", keycode: 59 },
{ keytype: "normal", label: "F2", shape: "fn", keycode: 60 },
{ keytype: "normal", label: "F3", shape: "fn", keycode: 61 },
{ keytype: "normal", label: "F4", shape: "fn", keycode: 62 },
{ keytype: "normal", label: "F5", shape: "fn", keycode: 63 },
{ keytype: "normal", label: "F6", shape: "fn", keycode: 64 },
{ keytype: "normal", label: "F7", shape: "fn", keycode: 65 },
{ keytype: "normal", label: "F8", shape: "fn", keycode: 66 },
{ keytype: "normal", label: "F9", shape: "fn", keycode: 67 },
{ keytype: "normal", label: "F10", shape: "fn", keycode: 68 },
{ keytype: "normal", label: "F11", shape: "fn", keycode: 87 },
{ keytype: "normal", label: "F12", shape: "fn", keycode: 88 },
{ keytype: "normal", label: "PrtSc", shape: "fn", keycode: 99 },
{ keytype: "normal", label: "Del", shape: "fn", keycode: 111 }
],
[
{ keytype: "normal", label: "῀", labelShift: "῀", labelAlt: "`", shape: "normal", keycode: 41 }, // dead perispomeni / AltGr: grave
{ keytype: "normal", label: "1", labelShift: "!", shape: "normal", keycode: 2 },
{ keytype: "normal", label: "2", labelShift: "@", shape: "normal", keycode: 3 },
{ keytype: "normal", label: "3", labelShift: "#", shape: "normal", keycode: 4 },
{ keytype: "normal", label: "4", labelShift: "$", shape: "normal", keycode: 5 },
{ keytype: "normal", label: "5", labelShift: "%", shape: "normal", keycode: 6 },
{ keytype: "normal", label: "6", labelShift: "^", shape: "normal", keycode: 7 },
{ keytype: "normal", label: "7", labelShift: "&", shape: "normal", keycode: 8 },
{ keytype: "normal", label: "8", labelShift: "*", shape: "normal", keycode: 9 },
{ keytype: "normal", label: "9", labelShift: "(", shape: "normal", keycode: 10 },
{ keytype: "normal", label: "0", labelShift: ")", shape: "normal", keycode: 11 },
{ keytype: "normal", label: "-", labelShift: "_", shape: "normal", keycode: 12 },
{ keytype: "normal", label: "=", labelShift: "+", shape: "normal", keycode: 13 },
{ keytype: "normal", label: "Backspace", shape: "expand", keycode: 14 }
],
[
{ keytype: "normal", label: "Tab", shape: "tab", keycode: 15 },
{ keytype: "normal", label: "῀", labelShift: "῀", labelAlt: ";", shape: "normal", keycode: 16 }, // q → dead perispomeni / AltGr: ; (Greek question mark)
{ keytype: "normal", label: "ς", labelShift: "Σ", labelAlt: "¨", shape: "normal", keycode: 17 }, // w → final sigma / AltGr: dead diaeresis
{ keytype: "normal", label: "ε", labelShift: "Ε", shape: "normal", keycode: 18 },
{ keytype: "normal", label: "ρ", labelShift: "Ρ", shape: "normal", keycode: 19 },
{ keytype: "normal", label: "τ", labelShift: "Τ", shape: "normal", keycode: 20 },
{ keytype: "normal", label: "υ", labelShift: "Υ", shape: "normal", keycode: 21 },
{ keytype: "normal", label: "θ", labelShift: "Θ", shape: "normal", keycode: 22 },
{ keytype: "normal", label: "ι", labelShift: "Ι", shape: "normal", keycode: 23 },
{ keytype: "normal", label: "ο", labelShift: "Ο", shape: "normal", keycode: 24 },
{ keytype: "normal", label: "π", labelShift: "Π", shape: "normal", keycode: 25 },
{ keytype: "normal", label: "[", labelShift: "{", labelAlt: "«", shape: "normal", keycode: 26 },
{ keytype: "normal", label: "]", labelShift: "}", labelAlt: "»", shape: "normal", keycode: 27 },
{ keytype: "normal", label: "\\", labelShift: "|", shape: "expand", keycode: 43 }
],
[
{ keytype: "spacer", label: "", shape: "empty" },
{ keytype: "spacer", label: "", shape: "empty" },
{ keytype: "normal", label: "α", labelShift: "Α", shape: "normal", keycode: 30 },
{ keytype: "normal", label: "σ", labelShift: "Σ", shape: "normal", keycode: 31 },
{ keytype: "normal", label: "δ", labelShift: "Δ", shape: "normal", keycode: 32 },
{ keytype: "normal", label: "φ", labelShift: "Φ", shape: "normal", keycode: 33 },
{ keytype: "normal", label: "γ", labelShift: "Γ", shape: "normal", keycode: 34 },
{ keytype: "normal", label: "η", labelShift: "Η", shape: "normal", keycode: 35 },
{ keytype: "normal", label: "ξ", labelShift: "Ξ", shape: "normal", keycode: 36 },
{ keytype: "normal", label: "κ", labelShift: "Κ", shape: "normal", keycode: 37 },
{ keytype: "normal", label: "λ", labelShift: "Λ", shape: "normal", keycode: 38 },
{ keytype: "normal", label: "´", labelShift: "¨", labelAlt: "ͺ", shape: "normal", keycode: 39 }, // dead oxia / dead dialytika / AltGr: iota subscript
{ keytype: "normal", label: "`", labelShift: "`", labelAlt: "'", shape: "normal", keycode: 40 }, // dead varia / AltGr: apostrophe
{ keytype: "normal", label: "Enter", shape: "expand", keycode: 28 }
],
[
{ keytype: "modkey", label: "Shift", labelShift: "Shift", labelCaps: "Caps", shape: "shift", keycode: 42 },
{ keytype: "normal", label: "ζ", labelShift: "Ζ", shape: "normal", keycode: 44 },
{ keytype: "normal", label: "χ", labelShift: "Χ", shape: "normal", keycode: 45 },
{ keytype: "normal", label: "ψ", labelShift: "Ψ", shape: "normal", keycode: 46 },
{ keytype: "normal", label: "ω", labelShift: "Ω", shape: "normal", keycode: 47 },
{ keytype: "normal", label: "β", labelShift: "Β", shape: "normal", keycode: 48 },
{ keytype: "normal", label: "ν", labelShift: "Ν", shape: "normal", keycode: 49 },
{ keytype: "normal", label: "μ", labelShift: "Μ", shape: "normal", keycode: 50 },
{ keytype: "normal", label: ",", labelShift: "<", shape: "normal", keycode: 51 },
{ keytype: "normal", label: ".", labelShift: ">", shape: "normal", keycode: 52 },
{ keytype: "normal", label: "/", labelShift: "?", shape: "normal", keycode: 53 },
{ keytype: "modkey", label: "Shift", labelShift: "Shift", labelCaps: "Caps", shape: "expand", keycode: 54 }
],
[
{ keytype: "modkey", label: "Ctrl", shape: "control", keycode: 29 },
{ keytype: "modkey", label: "Alt", shape: "normal", keycode: 56 },
{ keytype: "normal", label: "Space", shape: "space", keycode: 57 },
{ keytype: "modkey", label: "Alt", shape: "normal", keycode: 100 },
{ keytype: "normal", label: "Menu", shape: "normal", keycode: 139 },
{ keytype: "modkey", label: "Ctrl", shape: "control", keycode: 97 }
]
]
},
"Arabic": {
name_short: "AR",
description: "Arabic Standard - Full",
comment: "Standard Arabic keyboard layout (XKB ara)",
keys: [
[
{ keytype: "normal", label: "Esc", shape: "fn", keycode: 1 },
{ keytype: "normal", label: "F1", shape: "fn", keycode: 59 },
{ keytype: "normal", label: "F2", shape: "fn", keycode: 60 },
{ keytype: "normal", label: "F3", shape: "fn", keycode: 61 },
{ keytype: "normal", label: "F4", shape: "fn", keycode: 62 },
{ keytype: "normal", label: "F5", shape: "fn", keycode: 63 },
{ keytype: "normal", label: "F6", shape: "fn", keycode: 64 },
{ keytype: "normal", label: "F7", shape: "fn", keycode: 65 },
{ keytype: "normal", label: "F8", shape: "fn", keycode: 66 },
{ keytype: "normal", label: "F9", shape: "fn", keycode: 67 },
{ keytype: "normal", label: "F10", shape: "fn", keycode: 68 },
{ keytype: "normal", label: "F11", shape: "fn", keycode: 87 },
{ keytype: "normal", label: "F12", shape: "fn", keycode: 88 },
{ keytype: "normal", label: "PrtSc", shape: "fn", keycode: 99 },
{ keytype: "normal", label: "Del", shape: "fn", keycode: 111 }
],
[
{ keytype: "normal", label: "ذ", labelShift: "ّ", shape: "normal", keycode: 41 }, // Thal / Shadda
{ keytype: "normal", label: "١", labelShift: "!", labelAlt: "1", shape: "normal", keycode: 2 },
{ keytype: "normal", label: "٢", labelShift: "@", labelAlt: "2", shape: "normal", keycode: 3 },
{ keytype: "normal", label: "٣", labelShift: "#", labelAlt: "3", shape: "normal", keycode: 4 },
{ keytype: "normal", label: "٤", labelShift: "$", labelAlt: "4", shape: "normal", keycode: 5 },
{ keytype: "normal", label: "٥", labelShift: "%", labelAlt: "5", shape: "normal", keycode: 6 },
{ keytype: "normal", label: "٦", labelShift: "^", labelAlt: "6", shape: "normal", keycode: 7 },
{ keytype: "normal", label: "٧", labelShift: "&", labelAlt: "7", shape: "normal", keycode: 8 },
{ keytype: "normal", label: "٨", labelShift: "*", labelAlt: "8", shape: "normal", keycode: 9 },
{ keytype: "normal", label: "٩", labelShift: "(", labelAlt: "9", shape: "normal", keycode: 10 },
{ keytype: "normal", label: "٠", labelShift: ")", labelAlt: "0", shape: "normal", keycode: 11 },
{ keytype: "normal", label: "-", labelShift: "_", shape: "normal", keycode: 12 },
{ keytype: "normal", label: "=", labelShift: "+", shape: "normal", keycode: 13 },
{ keytype: "normal", label: "Backspace", shape: "expand", keycode: 14 }
],
[
{ keytype: "normal", label: "Tab", shape: "tab", keycode: 15 },
{ keytype: "normal", label: "ض", labelShift: "َ", shape: "normal", keycode: 16 }, // Dad / Fatha
{ keytype: "normal", label: "ص", labelShift: "ً", shape: "normal", keycode: 17 }, // Sad / Fathatan
{ keytype: "normal", label: "ث", labelShift: "ُ", shape: "normal", keycode: 18 }, // Tha / Damma
{ keytype: "normal", label: "ق", labelShift: "ٌ", shape: "normal", keycode: 19 }, // Qaf / Dammatan
{ keytype: "normal", label: "ف", labelShift: "لإ", shape: "normal", keycode: 20 }, // Fa / Lam-Alef-Hamza-Below
{ keytype: "normal", label: "غ", labelShift: "إ", shape: "normal", keycode: 21 }, // Ghayn / Alef-Hamza-Below
{ keytype: "normal", label: "ع", labelShift: "`", shape: "normal", keycode: 22 }, // Ain / Grave
{ keytype: "normal", label: "ه", labelShift: "÷", shape: "normal", keycode: 23 }, // Ha / Division
{ keytype: "normal", label: "خ", labelShift: "×", shape: "normal", keycode: 24 }, // Kha / Multiplication
{ keytype: "normal", label: "ح", labelShift: "؛", shape: "normal", keycode: 25 }, // Hah / Arabic semicolon
{ keytype: "normal", label: "ج", labelShift: "<", shape: "normal", keycode: 26 }, // Jeem / <
{ keytype: "normal", label: "د", labelShift: ">", shape: "normal", keycode: 27 }, // Dal / >
{ keytype: "normal", label: "\\", labelShift: "|", shape: "expand", keycode: 43 }
],
[
{ keytype: "spacer", label: "", shape: "empty" },
{ keytype: "spacer", label: "", shape: "empty" },
{ keytype: "normal", label: "ش", labelShift: "ِ", shape: "normal", keycode: 30 }, // Sheen / Kasra
{ keytype: "normal", label: "س", labelShift: "ٍ", shape: "normal", keycode: 31 }, // Seen / Kasratan
{ keytype: "normal", label: "ي", labelShift: "]", shape: "normal", keycode: 32 }, // Ya / ]
{ keytype: "normal", label: "ب", labelShift: "[", shape: "normal", keycode: 33 }, // Ba / [
{ keytype: "normal", label: "ل", labelShift: "لأ", shape: "normal", keycode: 34 }, // Lam / Lam-Alef-Hamza-Above
{ keytype: "normal", label: "ا", labelShift: "أ", shape: "normal", keycode: 35 }, // Alef / Alef-Hamza-Above
{ keytype: "normal", label: "ت", labelShift: "ـ", shape: "normal", keycode: 36 }, // Ta / Tatweel
{ keytype: "normal", label: "ن", labelShift: "،", shape: "normal", keycode: 37 }, // Nun / Arabic comma
{ keytype: "normal", label: "م", labelShift: "/", shape: "normal", keycode: 38 }, // Meem / Slash
{ keytype: "normal", label: "ك", labelShift: ":", shape: "normal", keycode: 39 }, // Kaf / Colon
{ keytype: "normal", label: "ط", labelShift: '"', shape: "normal", keycode: 40 }, // Tah / Quote
{ keytype: "normal", label: "Enter", shape: "expand", keycode: 28 }
],
[
{ keytype: "modkey", label: "Shift", labelShift: "Shift", labelCaps: "Caps", shape: "shift", keycode: 42 },
{ keytype: "normal", label: "ئ", labelShift: "~", shape: "normal", keycode: 44 }, // Hamza-on-Ya / Tilde
{ keytype: "normal", label: "ء", labelShift: "ْ", shape: "normal", keycode: 45 }, // Hamza / Sukun
{ keytype: "normal", label: "ؤ", labelShift: "{", shape: "normal", keycode: 46 }, // Hamza-on-Waw / {
{ keytype: "normal", label: "ر", labelShift: "}", shape: "normal", keycode: 47 }, // Ra / }
{ keytype: "normal", label: "لا", labelShift: "لآ", shape: "normal", keycode: 48 }, // Lam-Alef / Lam-Alef-Madda
{ keytype: "normal", label: "ى", labelShift: "آ", shape: "normal", keycode: 49 }, // Alef Maksura / Alef-Madda
{ keytype: "normal", label: "ة", labelShift: "'", shape: "normal", keycode: 50 }, // Ta Marbuta / Apostrophe
{ keytype: "normal", label: "و", labelShift: ",", shape: "normal", keycode: 51 }, // Waw / Comma
{ keytype: "normal", label: "ز", labelShift: ".", shape: "normal", keycode: 52 }, // Zay / Period
{ keytype: "normal", label: "ظ", labelShift: "؟", shape: "normal", keycode: 53 }, // Zha / Arabic question mark
{ keytype: "modkey", label: "Shift", labelShift: "Shift", labelCaps: "Caps", shape: "expand", keycode: 54 }
],
[
{ keytype: "modkey", label: "Ctrl", shape: "control", keycode: 29 },
{ keytype: "modkey", label: "Alt", shape: "normal", keycode: 56 },
{ keytype: "normal", label: "Space", shape: "space", keycode: 57 },
{ keytype: "modkey", label: "Alt", shape: "normal", keycode: 100 },
{ keytype: "normal", label: "Menu", shape: "normal", keycode: 139 },
{ keytype: "modkey", label: "Ctrl", shape: "control", keycode: 97 }
]
]
},
"Persian": {
name_short: "FA",
description: "Persian ISIRI 9147 - Full",
comment: "Standard Persian keyboard layout (XKB ir(pes), ISIRI 9147)",
keys: [
[
{ keytype: "normal", label: "Esc", shape: "fn", keycode: 1 },
{ keytype: "normal", label: "F1", shape: "fn", keycode: 59 },
{ keytype: "normal", label: "F2", shape: "fn", keycode: 60 },
{ keytype: "normal", label: "F3", shape: "fn", keycode: 61 },
{ keytype: "normal", label: "F4", shape: "fn", keycode: 62 },
{ keytype: "normal", label: "F5", shape: "fn", keycode: 63 },
{ keytype: "normal", label: "F6", shape: "fn", keycode: 64 },
{ keytype: "normal", label: "F7", shape: "fn", keycode: 65 },
{ keytype: "normal", label: "F8", shape: "fn", keycode: 66 },
{ keytype: "normal", label: "F9", shape: "fn", keycode: 67 },
{ keytype: "normal", label: "F10", shape: "fn", keycode: 68 },
{ keytype: "normal", label: "F11", shape: "fn", keycode: 87 },
{ keytype: "normal", label: "F12", shape: "fn", keycode: 88 },
{ keytype: "normal", label: "PrtSc", shape: "fn", keycode: 99 },
{ keytype: "normal", label: "Del", shape: "fn", keycode: 111 }
],
[
{ keytype: "normal", label: "ZWJ", labelShift: "÷", labelAlt: "~", shape: "normal", keycode: 41 }, // Zero-Width Joiner / division
{ keytype: "normal", label: "۱", labelShift: "!", labelAlt: "1", shape: "normal", keycode: 2 },
{ keytype: "normal", label: "۲", labelShift: "٬", labelAlt: "2", shape: "normal", keycode: 3 }, // Arabic thousands separator
{ keytype: "normal", label: "۳", labelShift: "٫", labelAlt: "3", shape: "normal", keycode: 4 }, // Arabic decimal separator
{ keytype: "normal", label: "۴", labelShift: "﷼", labelAlt: "4", shape: "normal", keycode: 5 }, // Rial sign
{ keytype: "normal", label: "۵", labelShift: "٪", labelAlt: "5", shape: "normal", keycode: 6 }, // Arabic percent
{ keytype: "normal", label: "۶", labelShift: "×", labelAlt: "6", shape: "normal", keycode: 7 },
{ keytype: "normal", label: "۷", labelShift: "،", labelAlt: "7", shape: "normal", keycode: 8 }, // Arabic comma
{ keytype: "normal", label: "۸", labelShift: "*", labelAlt: "8", shape: "normal", keycode: 9 },
{ keytype: "normal", label: "۹", labelShift: ")", labelAlt: "9", shape: "normal", keycode: 10 }, // RTL-swapped parens
{ keytype: "normal", label: "۰", labelShift: "(", labelAlt: "0", shape: "normal", keycode: 11 }, // RTL-swapped parens
{ keytype: "normal", label: "-", labelShift: "ـ", shape: "normal", keycode: 12 }, // minus / tatweel
{ keytype: "normal", label: "=", labelShift: "+", shape: "normal", keycode: 13 },
{ keytype: "normal", label: "Backspace", shape: "expand", keycode: 14 }
],
[
{ keytype: "normal", label: "Tab", shape: "tab", keycode: 15 },
{ keytype: "normal", label: "ض", labelShift: "ْ", shape: "normal", keycode: 16 }, // Dad / Sukun
{ keytype: "normal", label: "ص", labelShift: "ٌ", shape: "normal", keycode: 17 }, // Sad / Dammatan
{ keytype: "normal", label: "ث", labelShift: "ٍ", labelAlt: "€", shape: "normal", keycode: 18 }, // Theh / Kasratan
{ keytype: "normal", label: "ق", labelShift: "ً", shape: "normal", keycode: 19 }, // Qaf / Fathatan
{ keytype: "normal", label: "ف", labelShift: "ُ", shape: "normal", keycode: 20 }, // Feh / Damma
{ keytype: "normal", label: "غ", labelShift: "ِ", shape: "normal", keycode: 21 }, // Ghain / Kasra
{ keytype: "normal", label: "ع", labelShift: "َ", shape: "normal", keycode: 22 }, // Ain / Fatha
{ keytype: "normal", label: "ه", labelShift: "ّ", shape: "normal", keycode: 23 }, // Heh / Shadda
{ keytype: "normal", label: "خ", labelShift: "]", shape: "normal", keycode: 24 }, // Khah / ]
{ keytype: "normal", label: "ح", labelShift: "[", shape: "normal", keycode: 25 }, // Hah / [
{ keytype: "normal", label: "ج", labelShift: "}", shape: "normal", keycode: 26 }, // Jeem / }
{ keytype: "normal", label: "چ", labelShift: "{", shape: "normal", keycode: 27 }, // Tcheh / {
{ keytype: "normal", label: "\\", labelShift: "|", shape: "expand", keycode: 43 }
],
[
{ keytype: "spacer", label: "", shape: "empty" },
{ keytype: "spacer", label: "", shape: "empty" },
{ keytype: "normal", label: "ش", labelShift: "ؤ", shape: "normal", keycode: 30 }, // Sheen / Hamza-on-Waw
{ keytype: "normal", label: "س", labelShift: "ئ", shape: "normal", keycode: 31 }, // Seen / Hamza-on-Yeh
{ keytype: "normal", label: "ی", labelShift: "ي", shape: "normal", keycode: 32 }, // Farsi Yeh / Arabic Yeh
{ keytype: "normal", label: "ب", labelShift: "إ", shape: "normal", keycode: 33 }, // Beh / Alef-Hamza-Below
{ keytype: "normal", label: "ل", labelShift: "أ", shape: "normal", keycode: 34 }, // Lam / Alef-Hamza-Above
{ keytype: "normal", label: "ا", labelShift: "آ", shape: "normal", keycode: 35 }, // Alef / Alef-Madda
{ keytype: "normal", label: "ت", labelShift: "ة", shape: "normal", keycode: 36 }, // Teh / Teh Marbuta
{ keytype: "normal", label: "ن", labelShift: "»", shape: "normal", keycode: 37 }, // Noon / Right guillemet
{ keytype: "normal", label: "م", labelShift: "«", shape: "normal", keycode: 38 }, // Meem / Left guillemet
{ keytype: "normal", label: "ک", labelShift: ":", shape: "normal", keycode: 39 }, // Keheh / Colon
{ keytype: "normal", label: "گ", labelShift: "؛", shape: "normal", keycode: 40 }, // Gaf / Arabic semicolon
{ keytype: "normal", label: "Enter", shape: "expand", keycode: 28 }
],
[
{ keytype: "modkey", label: "Shift", labelShift: "Shift", labelCaps: "Caps", shape: "shift", keycode: 42 },
{ keytype: "normal", label: "ظ", labelShift: "ك", shape: "normal", keycode: 44 }, // Zah / Arabic Kaf
{ keytype: "normal", label: "ط", labelShift: "ٓ", shape: "normal", keycode: 45 }, // Tah / Maddah above
{ keytype: "normal", label: "ز", labelShift: "ژ", shape: "normal", keycode: 46 }, // Zain / Jeh (Persian Zhe)
{ keytype: "normal", label: "ر", labelShift: "ٰ", shape: "normal", keycode: 47 }, // Ra / Superscript Alef
{ keytype: "normal", label: "ذ", labelShift: "ZWNJ", shape: "normal", keycode: 48 }, // Thal / Zero-Width Non-Joiner
{ keytype: "normal", label: "د", labelShift: "ٔ", shape: "normal", keycode: 49 }, // Dal / Hamza above
{ keytype: "normal", label: "پ", labelShift: "ء", shape: "normal", keycode: 50 }, // Peh / Hamza
{ keytype: "normal", label: "و", labelShift: ">", shape: "normal", keycode: 51 }, // Waw / >
{ keytype: "normal", label: ".", labelShift: "<", shape: "normal", keycode: 52 }, // Period / <
{ keytype: "normal", label: "/", labelShift: "؟", shape: "normal", keycode: 53 }, // Slash / Arabic question mark
{ keytype: "modkey", label: "Shift", labelShift: "Shift", labelCaps: "Caps", shape: "expand", keycode: 54 }
],
[
{ keytype: "modkey", label: "Ctrl", shape: "control", keycode: 29 },
{ keytype: "modkey", label: "Alt", shape: "normal", keycode: 56 },
{ keytype: "normal", label: "Space", shape: "space", keycode: 57 },
{ keytype: "modkey", label: "Alt", shape: "normal", keycode: 100 },
{ keytype: "normal", label: "Menu", shape: "normal", keycode: 139 },
{ keytype: "modkey", label: "Ctrl", shape: "control", keycode: 97 }
]
]
}
}

View file

@ -0,0 +1,94 @@
import qs
import qs.modules.common
import qs.modules.common.widgets
import qs.services
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import Quickshell.Hyprland
Scope {
id: root
property Component regionComponent: Component {
Region {}
}
Loader {
id: overlayLoader
active: GlobalStates.overlayOpen || OverlayContext.hasPinnedWidgets
sourceComponent: PanelWindow {
id: overlayWindow
exclusionMode: ExclusionMode.Ignore
WlrLayershell.namespace: "quickshell:overlay"
WlrLayershell.layer: WlrLayer.Overlay
// Use OnDemand for pinned widgets to allow focus switching with mouse clicks
WlrLayershell.keyboardFocus: GlobalStates.overlayOpen ? WlrKeyboardFocus.Exclusive : (OverlayContext.clickableWidgets.length > 0 ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None)
visible: true
color: "transparent"
mask: Region {
item: GlobalStates.overlayOpen ? overlayContent : null
regions: OverlayContext.clickableWidgets.map((widget) => regionComponent.createObject(this, {
item: widget
}));
}
anchors {
top: true
bottom: true
left: true
right: true
}
HyprlandFocusGrab {
id: grab
windows: [overlayWindow]
active: false
onCleared: () => {
if (!active) GlobalStates.overlayOpen = false;
}
}
Connections {
target: GlobalStates
function onOverlayOpenChanged() {
delayedGrabTimer.restart();
}
}
Timer {
id: delayedGrabTimer
interval: Appearance.animation.elementMoveFast.duration
onTriggered: {
grab.active = GlobalStates.overlayOpen;
}
}
OverlayContent {
id: overlayContent
anchors.fill: parent
}
}
}
IpcHandler {
target: "overlay"
function toggle(): void {
GlobalStates.overlayOpen = !GlobalStates.overlayOpen;
}
}
GlobalShortcut {
name: "overlayToggle"
description: "Toggles overlay on press"
onPressed: {
GlobalStates.overlayOpen = !GlobalStates.overlayOpen;
}
}
}

View file

@ -0,0 +1,8 @@
import QtQuick
import qs.modules.common
Rectangle {
id: contentItem
anchors.fill: parent
color: Appearance.m3colors.m3surfaceContainer
}

View file

@ -0,0 +1,66 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Widgets
import qs
import qs.services
import qs.modules.common
import qs.modules.common.widgets
import qs.modules.common.widgets.widgetCanvas
Item {
id: root
focus: true
readonly property bool usePasswordChars: !PolkitService.flow?.responseVisible ?? true
Keys.onPressed: (event) => { // Esc to close
if (event.key === Qt.Key_Escape) {
GlobalStates.overlayOpen = false;
}
}
property real initScale: Config.options.overlay.openingZoomAnimation ? 1.08 : 1.000001
scale: initScale
Component.onCompleted: {
scale = 1
}
Behavior on scale {
animation: Appearance.animation.elementMoveEnter.numberAnimation.createObject(this)
}
Rectangle {
id: bg
anchors.fill: parent
color: Appearance.colors.colScrim
visible: Config.options.overlay.darkenScreen && opacity > 0
opacity: (GlobalStates.overlayOpen && root.scale !== initScale) ? 1 : 0
Behavior on opacity {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
}
}
WidgetCanvas {
anchors.fill: parent
onClicked: GlobalStates.overlayOpen = false
OverlayTaskbar {
anchors {
horizontalCenter: parent.horizontalCenter
top: parent.top
topMargin: 50
}
}
Repeater {
model: ScriptModel {
values: Persistent.states.overlay.open.map(identifier => {
return OverlayContext.availableWidgets.find(w => w.identifier === identifier);
})
objectProp: "identifier"
}
delegate: OverlayWidgetDelegateChooser {
}
}
}
}

View file

@ -0,0 +1,44 @@
pragma Singleton
pragma ComponentBehavior: Bound
import Quickshell
Singleton {
id: root
signal requestCenter(string identifier)
readonly property list<var> availableWidgets: [
{ identifier: "crosshair", materialSymbol: "point_scan" },
{ identifier: "fpsLimiter", materialSymbol: "animation" },
{ identifier: "floatingImage", materialSymbol: "imagesmode" },
{ identifier: "recorder", materialSymbol: "screen_record" },
{ identifier: "resources", materialSymbol: "browse_activity" },
{ identifier: "notes", materialSymbol: "note_stack" },
{ identifier: "volumeMixer", materialSymbol: "volume_up" },
]
readonly property bool hasPinnedWidgets: root.pinnedWidgetIdentifiers.length > 0
property list<string> pinnedWidgetIdentifiers: []
property list<var> clickableWidgets: []
function pin(identifier: string, pin = true) {
if (pin) {
if (!root.pinnedWidgetIdentifiers.includes(identifier)) {
root.pinnedWidgetIdentifiers.push(identifier)
}
} else {
root.pinnedWidgetIdentifiers = root.pinnedWidgetIdentifiers.filter(id => id !== identifier)
}
}
function registerClickableWidget(widget: var, clickable = true) {
if (clickable) {
if (!root.clickableWidgets.includes(widget)) {
root.clickableWidgets.push(widget)
}
} else {
root.clickableWidgets = root.clickableWidgets.filter(w => w !== widget)
}
}
}

View file

@ -0,0 +1,151 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import Quickshell
import qs
import qs.services
import qs.modules.common
import qs.modules.common.functions
import qs.modules.common.widgets
import qs.modules.common.widgets.widgetCanvas
Rectangle {
id: root
property real padding: 8
opacity: GlobalStates.overlayOpen ? 1 : 0
implicitWidth: contentRow.implicitWidth + (padding * 2)
implicitHeight: contentRow.implicitHeight + (padding * 2)
color: Appearance.m3colors.m3surfaceContainer
radius: Appearance.rounding.large
border.color: Appearance.colors.colOutlineVariant
border.width: 1
Behavior on opacity {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
}
RowLayout {
id: contentRow
anchors {
fill: parent
margins: root.padding
}
spacing: 6
Row {
spacing: 4
Repeater {
model: ScriptModel {
values: OverlayContext.availableWidgets
}
delegate: WidgetButton {
required property var modelData
identifier: modelData.identifier
materialSymbol: modelData.materialSymbol
}
}
}
Separator {}
TimeWidget {}
Separator {
visible: Battery.available
}
BatteryWidget {
visible: Battery.available
}
}
component Separator: Rectangle {
implicitWidth: 1
color: Appearance.colors.colOutlineVariant
Layout.fillHeight: true
Layout.topMargin: 10
Layout.bottomMargin: 10
}
component TimeWidget: StyledText {
Layout.alignment: Qt.AlignVCenter
Layout.leftMargin: 8
Layout.rightMargin: 6
text: DateTime.time
color: Appearance.colors.colOnSurface
font {
family: Appearance.font.family.numbers
variableAxes: Appearance.font.variableAxes.numbers
pixelSize: 22
}
}
component BatteryWidget: Row {
id: batteryWidget
Layout.alignment: Qt.AlignVCenter
Layout.leftMargin: 6
Layout.rightMargin: 6
spacing: 2
property color colText: Battery.isLowAndNotCharging ? Appearance.colors.colError : Appearance.colors.colOnSurface
MaterialSymbol {
id: boltIcon
anchors.verticalCenter: parent.verticalCenter
fill: 1
text: Battery.isCharging ? "bolt" : "battery_android_full"
color: batteryWidget.colText
iconSize: 24
animateChange: true
}
StyledText {
id: batteryText
anchors.verticalCenter: parent.verticalCenter
text: Math.round(Battery.percentage * 100) + "%"
color: batteryWidget.colText
font {
family: Appearance.font.family.numbers
variableAxes: Appearance.font.variableAxes.numbers
pixelSize: 18
}
}
}
component WidgetButton: RippleButton {
id: widgetButton
required property string identifier
required property string materialSymbol
Layout.alignment: Qt.AlignVCenter
toggled: Persistent.states.overlay.open.includes(identifier)
altAction: () => OverlayContext.requestCenter(identifier)
onClicked: {
if (widgetButton.toggled) {
Persistent.states.overlay.open = Persistent.states.overlay.open.filter(type => type !== identifier);
} else {
Persistent.states.overlay.open.push(identifier);
}
}
implicitWidth: implicitHeight
colBackgroundToggled: Appearance.colors.colSecondaryContainer
colBackgroundToggledHover: Appearance.colors.colSecondaryContainerHover
colRippleToggled: Appearance.colors.colSecondaryContainerActive
buttonRadius: root.radius - (root.height - height) / 2
contentItem: Item {
anchors.centerIn: parent
implicitWidth: 32
implicitHeight: 32
MaterialSymbol {
id: iconWidget
anchors.centerIn: parent
iconSize: 24
text: widgetButton.materialSymbol
color: widgetButton.toggled ? Appearance.colors.colOnSecondaryContainer : Appearance.colors.colOnSurfaceVariant
}
}
}
}

View file

@ -0,0 +1,28 @@
pragma ComponentBehavior: Bound
import qs.services
import qs.modules.common
import qs.modules.common.widgets
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Bluetooth
import qs.modules.ii.overlay.crosshair
import qs.modules.ii.overlay.volumeMixer
import qs.modules.ii.overlay.floatingImage
import qs.modules.ii.overlay.fpsLimiter
import qs.modules.ii.overlay.recorder
import qs.modules.ii.overlay.resources
import qs.modules.ii.overlay.notes
DelegateChooser {
id: root
role: "identifier"
DelegateChoice { roleValue: "crosshair"; Crosshair {} }
DelegateChoice { roleValue: "floatingImage"; FloatingImage {} }
DelegateChoice { roleValue: "fpsLimiter"; FpsLimiter {} }
DelegateChoice { roleValue: "recorder"; Recorder {} }
DelegateChoice { roleValue: "resources"; Resources {} }
DelegateChoice { roleValue: "notes"; Notes {} }
DelegateChoice { roleValue: "volumeMixer"; VolumeMixer {} }
}

View file

@ -0,0 +1,336 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import Quickshell
import Qt5Compat.GraphicalEffects
import qs
import qs.modules.common
import qs.modules.common.functions
import qs.modules.common.widgets
import qs.modules.common.widgets.widgetCanvas
/*
* To make an overlay widget:
* 1. Create a modules/overlay/<yourWidget>/<YourWidget>.qml, using this as the base class and declare your widget content as contentItem
* 2. Add an entry to OverlayContext.availableWidgets with identifier=<yourWidgetIdentifier>
* 3. Add an entry in Persistent.states.overlay.<yourWidgetIdentifier> with x, y, width, height, pinned, clickthrough properties set to reasonable defaults
* 4. Add an entry in OverlayWidgetDelegateChooser with roleValue=<yourWidgetIdentifier> and Declare your widget in there
* Use existing entries as reference.
*/
AbstractOverlayWidget {
id: root
// To be defined by subclasses
required property Item contentItem
property bool fancyBorders: true
property bool showCenterButton: false
property bool showClickabilityButton: true
// Defaults n stuff
required property var modelData
readonly property string identifier: modelData.identifier
readonly property string materialSymbol: modelData.materialSymbol ?? "widgets"
property string title: identifier.replace(/([A-Z])/g, " $1").replace(/^./, function(str){ return str.toUpperCase(); })
property var persistentStateEntry: Persistent.states.overlay[identifier]
property real radius: Appearance.rounding.windowRounding
property real minimumWidth: contentItem.implicitWidth
property real minimumHeight: contentItem.implicitHeight
property real resizeMargin: 8
property real padding: 6
property real contentRadius: radius - padding
// Resizing
function getXResizeDirection(x) {
return (x < root.resizeMargin) ? -1 : (x > root.width - root.resizeMargin) ? 1 : 0
}
function getYResizeDirection(y) {
return (y < root.resizeMargin) ? -1 : (y > root.height - root.resizeMargin) ? 1 : 0
}
hoverEnabled: true
property bool resizable: true
property bool resizing: false
property int resizeXDirection: getXResizeDirection(mouseX)
property int resizeYDirection: getYResizeDirection(mouseY)
draggable: GlobalStates.overlayOpen
drag.target: undefined
animateXPos: !dragHandler.active
animateYPos: !dragHandler.active
z: dragHandler.active ? 2 : 1
cursorShape: {
if (dragHandler.active) return root.resizing ? cursorShape : Qt.ArrowCursor;
if (resizeMargin < mouseX && mouseX < width - resizeMargin &&
resizeMargin < mouseY && mouseY < height - resizeMargin) {
return Qt.ArrowCursor;
} else {
if (!root.resizable) return Qt.ArrowCursor;
const dragIsLeft = mouseX < width / 2
const dragIsTop = mouseY < height / 2
if ((dragIsLeft && dragIsTop) || (!dragIsLeft && !dragIsTop)) {
return Qt.SizeFDiagCursor
} else {
return Qt.SizeBDiagCursor
}
}
}
// Positioning & sizing
x: Math.round(persistentStateEntry.x) // Round or it'll be blurry
y: Math.round(persistentStateEntry.y) // Round or it'll be blurry
pinned: persistentStateEntry.pinned
clickthrough: persistentStateEntry.clickthrough
drag {
minimumX: 0
minimumY: 0
maximumX: root.parent?.width - root.width
maximumY: root.parent?.height - root.height
}
opacity: (GlobalStates.overlayOpen || !clickthrough) ? 1.0 : Config.options.overlay.clickthroughOpacity
// Guarded states & registration funcs
property bool open: Persistent.states.overlay.open
property bool actuallyPinned: pinned && open
property bool actuallyClickable: !clickthrough && actuallyPinned && open
onActuallyPinnedChanged: reportPinnedState();
onActuallyClickableChanged: reportClickableState();
function reportPinnedState() {
OverlayContext.pin(identifier, actuallyPinned);
}
function reportClickableState() {
OverlayContext.registerClickableWidget(contentItem, actuallyClickable);
}
// Self-registeration with OverlayContext
Component.onCompleted: {
reportPinnedState();
reportClickableState();
}
Connections {
target: OverlayContext
function onRequestCenter(identifier) {
if (identifier === root.identifier) {
root.center()
}
}
}
// Hooks
onPressed: (event) => {
// We're only interested in handling resize here
// Early returns
if (!root.resizable) return;
if (root.resizeMargin < event.x && event.x < root.width - root.resizeMargin &&
root.resizeMargin < event.y && event.y < root.height - root.resizeMargin) {
return;
}
// Resizing setup
root.resizing = true;
root.resizeXDirection = getXResizeDirection(event.x);
root.resizeYDirection = getYResizeDirection(event.y);
if (root.resizeYDirection !== 0 && root.resizeXDirection === 0) {
root.resizeXDirection = event.x < root.width / 2 ? -1 : 1;
} else if (root.resizeXDirection !== 0 && root.resizeYDirection === 0) {
root.resizeYDirection = event.y < root.height / 2 ? -1 : 1;
}
}
onPositionChanged: (event) => {
if (!resizing) return;
contentContainer.implicitWidth = Math.max(root.persistentStateEntry.width + dragHandler.xAxis.activeValue * root.resizeXDirection, root.minimumWidth);
contentContainer.implicitHeight = Math.max(root.persistentStateEntry.height + dragHandler.yAxis.activeValue * root.resizeYDirection, root.minimumHeight);
const negativeXDrag = root.resizeXDirection === -1;
const negativeYDrag = root.resizeYDirection === -1;
const wantedX = root.persistentStateEntry.x + (negativeXDrag ? dragHandler.xAxis.activeValue : 0)
const wantedY = root.persistentStateEntry.y + (negativeYDrag ? dragHandler.yAxis.activeValue : 0)
const negativeXDragLimit = root.persistentStateEntry.x + root.persistentStateEntry.width - contentContainer.implicitWidth;
const negativeYDragLimit = root.persistentStateEntry.y + root.persistentStateEntry.height - contentContainer.implicitHeight;
root.x = negativeXDrag ? Math.min(wantedX, negativeXDragLimit) : wantedX;
root.y = negativeYDrag ? Math.min(wantedY, negativeYDragLimit) : wantedY;
}
DragHandler {
id: dragHandler
acceptedButtons: Qt.LeftButton | Qt.RightButton
target: (root.draggable && !root.resizing) ? root : null
onActiveChanged: { // Handle drag release
if (!active) {
root.resizing = false;
root.savePosition();
}
}
xAxis.minimum: 0
xAxis.maximum: root.parent?.width - root.width
yAxis.minimum: 0
yAxis.maximum: root.parent?.height - root.height
}
function close() {
Persistent.states.overlay.open = Persistent.states.overlay.open.filter(type => type !== root.identifier);
}
function togglePinned() {
persistentStateEntry.pinned = !persistentStateEntry.pinned;
}
function toggleClickthrough() {
persistentStateEntry.clickthrough = !persistentStateEntry.clickthrough;
}
function savePosition(xPos = root.x, yPos = root.y, width = contentContainer.implicitWidth, height = contentContainer.implicitHeight) {
persistentStateEntry.x = Math.round(xPos);
persistentStateEntry.y = Math.round(yPos);
persistentStateEntry.width = Math.round(width);
persistentStateEntry.height = Math.round(height);
}
function center() {
const targetX = (root.parent.width - contentColumn.width) / 2 - root.resizeMargin
const targetY = (root.parent.height - contentContainer.height) / 2 - titleBar.implicitHeight + border.border.width - root.resizeMargin
root.x = targetX
root.y = targetY
root.savePosition(targetX, targetY)
}
visible: GlobalStates.overlayOpen || actuallyPinned
implicitWidth: contentColumn.implicitWidth + resizeMargin * 2
implicitHeight: contentColumn.implicitHeight + resizeMargin * 2
Rectangle {
id: border
anchors {
fill: parent
margins: root.resizeMargin
}
color: ColorUtils.transparentize(Appearance.colors.colLayer1Base, (root.fancyBorders && GlobalStates.overlayOpen) ? 0 : 1)
radius: root.radius
border.color: ColorUtils.transparentize(Appearance.colors.colOutlineVariant, GlobalStates.overlayOpen ? 0 : 1)
border.width: 1
layer.enabled: GlobalStates.overlayOpen
layer.effect: OpacityMask {
maskSource: Rectangle {
width: border.width
height: border.height
radius: root.radius
}
}
ColumnLayout {
id: contentColumn
z: root.fancyBorders ? 0 : -1
anchors.fill: parent
spacing: 0
// Title bar
Rectangle {
id: titleBar
opacity: GlobalStates.overlayOpen ? 1 : 0
Layout.fillWidth: true
implicitWidth: titleBarRow.implicitWidth + root.padding * 2
implicitHeight: titleBarRow.implicitHeight + root.padding * 2
color: root.fancyBorders ? "transparent" : Appearance.colors.colLayer1Base
// border.color: Appearance.colors.colOutlineVariant
// border.width: 1
RowLayout {
id: titleBarRow
anchors {
fill: parent
margins: root.padding
}
spacing: 2
MaterialSymbol {
text: root.materialSymbol
Layout.leftMargin: 6
iconSize: 20
Layout.alignment: Qt.AlignVCenter
Layout.rightMargin: 4
}
StyledText {
Layout.fillWidth: true
text: root.title
elide: Text.ElideRight
}
TitlebarButton {
visible: root.showCenterButton
materialSymbol: "recenter"
onClicked: root.center()
StyledToolTip {
text: "Center"
}
}
TitlebarButton {
visible: (root.pinned && root.showClickabilityButton)
materialSymbol: "mouse"
toggled: !root.clickthrough
onClicked: root.toggleClickthrough()
StyledToolTip {
text: "Clickable when pinned"
}
}
TitlebarButton {
materialSymbol: "keep"
toggled: root.pinned
onClicked: root.togglePinned()
StyledToolTip {
text: "Pin"
}
}
TitlebarButton {
materialSymbol: "close"
onClicked: root.close()
StyledToolTip {
text: "Close"
}
}
}
}
// Content
Item {
id: contentContainer
Layout.fillWidth: true
Layout.fillHeight: true
Layout.margins: root.fancyBorders ? root.padding : 0
Layout.topMargin: -border.border.width // Border of a rectangle is drawn inside its bounds, so we do this to make the gap not too big
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
implicitWidth: Math.max(root.persistentStateEntry.width, root.minimumWidth)
implicitHeight: Math.max(root.persistentStateEntry.height, root.minimumHeight)
children: [root.contentItem]
}
}
}
component TitlebarButton: RippleButton {
id: titlebarButton
required property string materialSymbol
buttonRadius: height / 2
implicitHeight: contentItem.implicitHeight
implicitWidth: implicitHeight
padding: 0
colBackgroundToggled: Appearance.colors.colSecondaryContainer
colBackgroundToggledHover: Appearance.colors.colSecondaryContainerHover
colRippleToggled: Appearance.colors.colSecondaryContainerActive
contentItem: Item {
anchors.centerIn: parent
implicitWidth: 30
implicitHeight: 30
MaterialSymbol {
id: iconWidget
anchors.centerIn: parent
iconSize: 20
text: titlebarButton.materialSymbol
fill: titlebarButton.toggled
color: titlebarButton.toggled ? Appearance.colors.colOnSecondaryContainer : Appearance.colors.colOnSurface
}
}
}
}

View file

@ -0,0 +1,19 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import qs.modules.common
import qs.modules.ii.overlay
StyledOverlayWidget {
id: root
fancyBorders: false // Crosshair should be see-through
showCenterButton: true
opacity: 1 // The crosshair itself already has transparency if configured
showClickabilityButton: false
clickthrough: true
resizable: false
contentItem: CrosshairContent {
anchors.centerIn: parent
}
}

View file

@ -0,0 +1,197 @@
pragma ComponentBehavior: Bound
import QtQuick
import qs.modules.common
import qs.modules.common.functions
Item {
id: root
// Keys to props
// f, 0f, 1f, m are irrelevant as they're firing error stuff
// 0 is irrelevant because it's some profile stuff
property var propertyMap: ({
"c": "color",
"u": "colorCode",
"h": "outline",
"o": "outlineOpacity",
"t": "outlineThickness",
"d": "centerDot",
"a": "centerDotOpacity",
"z": "centerDotSize",
"0a": "innerLineOpacity",
"0l": "innerLineLength",
"0v": "innerLineVerticalLength",
"0g": "innerLineUnbindAxesLengths",
"0t": "innerLineThickness",
"0o": "innerLineOffset",
"1b": "outerLines",
"1a": "outerLineOpacity",
"1l": "outerLineLength",
"1v": "outerLineVerticalLength",
"1g": "outerLineUnbindAxesLengths",
"1t": "outerLineThickness",
"1o": "outerLineOffset",
})
property var colorMap: ({
0: "#FFFFFF",
1: "#00FF00",
2: "#7FFF00",
3: "#DFFF00",
4: "#FFFF00",
5: "#00FFFF",
6: "#FF00FF",
7: "#FF0000"
})
// Raw props
property int color: 0
property string colorCode: "#FFFFFF"
property bool outline: true
property real outlineOpacity: 0.5
property int outlineThickness: 1
property bool centerDot: false
property real centerDotOpacity: 1
property int centerDotSize: 2
property bool innerLines: true
property real innerLineOpacity: 0.8
property int innerLineLength: 6
property int innerLineVerticalLength: innerLineLength
property bool innerLineUnbindAxesLengths: false
property int innerLineThickness: 2
property int innerLineOffset: 3
property bool outerLines: true
property real outerLineOpacity: 0.35
property int outerLineLength: 2
property int outerLineVerticalLength: outerLineLength
property bool outerLineUnbindAxesLengths: false
property int outerLineThickness: 2
property int outerLineOffset: 10
property string defaultCode: "c;0;u;FFFFFF;h;1;o;0.5;t;1;d;0;a;1;z;2;0a;0.8;0l;6;0v;6;0g;0;0t;2;0o;3;1b;1;1a;0.35;1l;2;1v;2;1g;0;1t;2;1o;10"
function loadFromCode(code: string): void {
let args = code.split(";");
for (let i = 0; i < args.length; i+= 2) {
let key = args[i];
let value = args[i+1];
let targetKey = root.propertyMap[key];
let targetType = typeof root[targetKey];
if (targetKey === undefined) continue;
if (targetType === "number") {
value = parseFloat(value);
} else if (targetType === "boolean") {
value = (value === "1");
}
if (targetKey === "colorCode") {
value = "#" + value.slice(0, 6);
}
root[targetKey] = value;
}
if (!root.innerLineUnbindAxesLengths) {
root.innerLineVerticalLength = root.innerLineLength;
}
if (!root.outerLineUnbindAxesLengths) {
root.outerLineVerticalLength = root.outerLineLength;
}
}
// Update values from code
property var code: Config.options.crosshair.code
Component.onCompleted: reloadFromCode();
onCodeChanged: reloadFromCode();
function reloadFromCode() {
root.loadFromCode(root.defaultCode);
root.loadFromCode(root.code);
}
// Aggregated props
property color crosshairColor: {
if (colorMap[color] !== undefined) return root.colorMap[color];
if (color === 8) return colorCode;
return "#FFFFFF";
}
property int borderWidth: outline ? outlineThickness : 0
property color borderColor: ColorUtils.transparentize("black", 1 - root.outlineOpacity)
property color innerLineColor: ColorUtils.transparentize(root.crosshairColor, 1 - root.innerLineOpacity)
property color outerLineColor: ColorUtils.transparentize(root.crosshairColor, 1 - root.outerLineOpacity)
property int innerLineTotalOffset: root.centerDotSize / 2 + 1 + root.innerLineOffset
property int outerLineTotalOffset: root.centerDotSize / 2 + 1 + root.outerLineOffset
property real centerDotTotalSize: root.centerDotSize + root.borderWidth * 2
property real innerLineTotalSize: (innerLineTotalOffset + root.innerLineLength + root.borderWidth) * 2
property real outerLineTotalSize: (outerLineTotalOffset + root.outerLineLength + root.borderWidth) * 2
implicitWidth: Math.max(centerDotTotalSize, innerLineTotalSize, outerLineTotalSize) + 2 // 2 for pixel correction
implicitHeight: implicitWidth
// width: implicitWidth
// height: implicitHeight
Rectangle {
id: centerDot
visible: root.centerDot
anchors.centerIn: parent
color: root.crosshairColor
opacity: root.centerDotOpacity
width: centerDotTotalSize
height: width
border.width: root.borderWidth
border.color: root.borderColor
}
Repeater {
id: innerLines
model: 4
Item {
id: innerHair
z: index % 2 // Vertical lines above horizontal lines
required property int index
property int pixelCorrection: (root.innerLineThickness % 2 === 1 && index > 1) ? 1 : 0
property int hairLength: (innerHair.index % 2 === 0 ? root.innerLineLength : root.innerLineVerticalLength)
visible: root.innerLines && hairLength > 0
anchors.fill: parent
rotation: index * 90
Rectangle {
x: parent.width / 2 + root.innerLineTotalOffset - root.borderWidth + innerHair.pixelCorrection
y: parent.height / 2 - height / 2
color: root.innerLineColor
width: innerHair.hairLength + root.borderWidth * 2
height: root.innerLineThickness + root.borderWidth * 2
border.width: root.borderWidth
border.color: root.borderColor
}
}
}
Repeater {
id: outerLines
model: 4
Item {
id: outerHair
z: index % 2 + 2 // Vertical lines above horizontal lines, above inner lines
required property int index
property int pixelCorrection: (root.outerLineThickness % 2 === 1 && index > 1) ? 1 : 0
property int hairLength: (outerHair.index % 2 === 0 ? root.outerLineLength : root.outerLineVerticalLength)
visible: root.outerLines && hairLength > 0
anchors.fill: parent
rotation: index * 90
Rectangle {
x: parent.width / 2 + root.outerLineTotalOffset - root.borderWidth + outerHair.pixelCorrection
y: parent.height / 2 - height / 2
color: root.outerLineColor
width: hairLength + root.borderWidth * 2
height: root.outerLineThickness + root.borderWidth * 2
border.width: root.borderWidth
border.color: root.borderColor
}
}
}
}

View file

@ -0,0 +1,98 @@
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Qt5Compat.GraphicalEffects
import qs.modules.common
import qs.modules.common.functions
import qs.modules.common.utils
import qs.modules.ii.overlay
StyledOverlayWidget {
id: root
showClickabilityButton: false
resizable: false
clickthrough: true
property string imageSource: Config.options.overlay.floatingImage.imageSource
property real scaleFactor: Config.options.overlay.floatingImage.scale
property int imageWidth: 0
property int imageHeight: 0
// Override to always save 0 size
function savePosition(xPos = root.x, yPos = root.y, width = 0, height = 0) {
root.persistentStateEntry.x = Math.round(xPos);
root.persistentStateEntry.y = Math.round(yPos);
root.persistentStateEntry.width = 0
root.persistentStateEntry.height = 0
}
onImageSourceChanged: {
imageDownloader.running = false;
imageDownloader.sourceUrl = root.imageSource;
imageDownloader.filePath = Qt.resolvedUrl(Directories.tempImages + "/" + Qt.md5(root.imageSource))
imageDownloader.running = true;
}
onScaleFactorChanged: {
setSize();
}
function setSize() {
bg.implicitWidth = root.imageWidth * root.scaleFactor;
bg.implicitHeight = root.imageHeight * root.scaleFactor;
}
contentItem: OverlayBackground {
id: bg
color: ColorUtils.transparentize(Appearance.m3colors.m3surfaceContainer, root.actuallyPinned ? 1 : 0)
radius: root.contentRadius
WheelHandler {
onWheel: (event) => {
if (event.angleDelta.y < 0) {
Config.options.overlay.floatingImage.scale = Math.max(0.1, Config.options.overlay.floatingImage.scale - 0.1);
}
else if (event.angleDelta.y > 0) {
Config.options.overlay.floatingImage.scale = Math.min(5.0, Config.options.overlay.floatingImage.scale + 0.1);
}
}
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
}
layer.enabled: true
layer.effect: OpacityMask {
maskSource: Rectangle {
width: bg.width
height: bg.height
radius: bg.radius
}
}
AnimatedImage {
id: animatedImage
anchors.centerIn: parent
width: root.imageWidth * root.scaleFactor
height: root.imageHeight * root.scaleFactor
sourceSize: {
const dpr = (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1;
return Qt.size(width * dpr, height * dpr);
}
playing: visible
asynchronous: true
source: ""
ImageDownloaderProcess {
id: imageDownloader
filePath: Qt.resolvedUrl(Directories.tempImages + "/" + Qt.md5(root.imageSource))
sourceUrl: root.imageSource
onDone: (path, width, height) => {
root.imageWidth = width;
root.imageHeight = height;
root.setSize();
animatedImage.source = path;
}
}
}
}
}

View file

@ -0,0 +1,14 @@
import QtQuick
import Quickshell
import qs.modules.common
import qs.modules.ii.overlay
StyledOverlayWidget {
id: root
title: "MangoHud FPS"
minimumWidth: 275
minimumHeight: 100
contentItem: FpsLimiterContent {
radius: root.contentRadius
}
}

View file

@ -0,0 +1,96 @@
import qs.services
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import Quickshell
import Quickshell.Io
import qs.modules.common
import qs.modules.common.widgets
import qs.modules.ii.overlay
OverlayBackground {
id: root
enum State { Normal, Success, Error }
property real padding: 16
property var currentState: FpsLimiterContent.State.Normal
implicitWidth: content.implicitWidth + (padding * 2)
implicitHeight: content.implicitHeight + (padding * 2)
Timer {
id: iconResetTimer
interval: 1000
onTriggered: {
root.currentState = FpsLimiterContent.State.Normal;
}
}
function applyLimit() {
var fpsValue = parseInt(fpsField.text);
if (isNaN(fpsValue) || fpsValue < 0) {
root.currentState = FpsLimiterContent.State.Error;
iconResetTimer.restart();
fpsField.text = "";
return;
}
var cfgPaths = [
"~/.config/MangoHud/MangoHud.conf",
]; // MangoHud config files
var updateCommands = cfgPaths.map(path => {
return "if grep -q '^fps_limit=' " + path + "; " +
"then sed -i 's/^fps_limit=.*/fps_limit=" + fpsValue + "/' " + path + "; " +
"else echo 'fps_limit=" + fpsValue + "' >> " + path + "; fi";
}).join("; ");
var cmd = updateCommands + "; pkill -SIGUSR2 mangohud";
fpsSetter.command = ["bash", "-c", cmd];
fpsSetter.startDetached();
root.currentState = FpsLimiterContent.State.Success;
iconResetTimer.restart();
// Clear the field after applying
fpsField.text = "";
}
Process {
id: fpsSetter
}
RowLayout {
id: content
anchors.centerIn: parent
spacing: 4
ToolbarTextField {
id: fpsField
Layout.fillWidth: true
Layout.preferredWidth: 200
placeholderText: root.currentState === FpsLimiterContent.State.Error ? Translation.tr("Enter a valid number") : Translation.tr("Set FPS limit")
inputMethodHints: Qt.ImhDigitsOnly
focus: true
onAccepted: {
root.applyLimit();
}
}
IconToolbarButton {
id: applyButton
text: switch (root.currentState) {
case FpsLimiterContent.State.Error: return "close";
case FpsLimiterContent.State.Success: return "check";
case FpsLimiterContent.State.Normal:
default: return "save";
}
enabled: root.currentState === FpsLimiterContent.State.Normal && fpsField.text.length > 0
onClicked: {
root.applyLimit();
}
}
}
}

View file

@ -0,0 +1,17 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import qs.services
import qs.modules.common
import qs.modules.ii.overlay
StyledOverlayWidget {
id: root
title: Translation.tr("Notes")
showCenterButton: true
contentItem: NotesContent {
radius: root.contentRadius
isClickthrough: root.clickthrough
}
}

View file

@ -0,0 +1,292 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import qs
import qs.services
import qs.modules.common
import qs.modules.common.widgets
import qs.modules.ii.overlay
OverlayBackground {
id: root
property alias content: textInput.text
property bool pendingReload: false
property var copyListEntries: []
property string lastParsedCopylistText: ""
property var parsedCopylistLines: []
property bool isClickthrough: false
property real maxCopyButtonSize: 20
Component.onCompleted: {
noteFile.reload();
updateCopyListEntries();
}
function saveContent() {
if (!textInput)
return;
noteFile.setText(root.content);
}
function focusAtEnd() {
if (!textInput)
return;
textInput.forceActiveFocus();
const endPos = root.content.length;
applySelection(endPos, endPos);
}
function applySelection(cursorPos, anchorPos) {
if (!textInput)
return;
const textLength = root.content.length;
const cursor = Math.max(0, Math.min(cursorPos, textLength));
const anchor = Math.max(0, Math.min(anchorPos, textLength));
textInput.select(anchor, cursor);
if (cursor === anchor)
textInput.deselect();
}
function scheduleCopylistUpdate(immediate = false) {
if (!textInput)
return;
if (immediate) {
copyListDebounce?.stop();
updateCopyListEntries();
} else {
copyListDebounce.restart();
}
}
function updateCopyListEntries() {
if (!textInput)
return;
const textValue = root.content;
if (!textValue || textValue.length === 0) {
lastParsedCopylistText = "";
parsedCopylistLines = [];
root.copyListEntries = [];
return;
}
if (textValue !== lastParsedCopylistText) {
const lineRegex = /(.*?)(\r?\n|$)/g;
let match = null;
const parsed = [];
while ((match = lineRegex.exec(textValue)) !== null) {
const lineText = match[1];
const newlineText = match[2];
const lineStart = match.index;
const lineEnd = lineStart + lineText.length;
const bulletMatch = lineText.match(/^\s*-\s+(.*\S)\s*$/);
if (bulletMatch) {
parsed.push({
content: bulletMatch[1].trim(),
start: lineStart,
end: lineEnd
});
}
if (newlineText === "")
break;
}
lastParsedCopylistText = textValue;
parsedCopylistLines = parsed;
if (parsed.length === 0) {
root.copyListEntries = [];
return;
}
}
updateCopylistPositions();
}
function updateCopylistPositions() {
if (!textInput || parsedCopylistLines.length === 0)
return;
const rawSelectionStart = textInput.selectionStart;
const rawSelectionEnd = textInput.selectionEnd;
const selectionStart = rawSelectionStart === -1 ? textInput.cursorPosition : rawSelectionStart;
const selectionEnd = rawSelectionEnd === -1 ? textInput.cursorPosition : rawSelectionEnd;
const rangeStart = Math.min(selectionStart, selectionEnd);
const rangeEnd = Math.max(selectionStart, selectionEnd);
const entries = parsedCopylistLines.map(line => {
// Don't show copy button if line is (partially) selected
const caretIntersects = rangeEnd > line.start && rangeStart <= line.end;
if (caretIntersects)
return null;
const startRect = textInput.positionToRectangle(line.start);
let endRect = textInput.positionToRectangle(line.end);
if (!isFinite(startRect.y))
return null;
if (!isFinite(endRect.y))
endRect = startRect;
const lineBottom = endRect.y + endRect.height;
const rectHeight = Math.max(lineBottom - startRect.y, textInput.font.pixelSize + 8);
return {
content: line.content,
y: startRect.y,
height: rectHeight
};
}).filter(entry => entry !== null);
root.copyListEntries = entries;
}
implicitWidth: 300
implicitHeight: 200
ColumnLayout {
id: contentItem
anchors.fill: parent
spacing: -16
ScrollView {
id: editorScrollView
Layout.fillWidth: true
Layout.fillHeight: true
clip: true
ScrollBar.vertical.policy: ScrollBar.AsNeeded
onWidthChanged: root.scheduleCopylistUpdate(true)
StyledTextArea { // This has to be a direct child of ScrollView for proper scrolling
id: textInput
anchors {
left: parent.left
right: parent.right
}
wrapMode: TextEdit.Wrap
placeholderText: Translation.tr("Write something here...\nUse '-' to create copyable bullet points, like this:\n\nSheep fricker\n- 4x Slab\n- 1x Boat\n- 4x Redstone Dust\n- 1x Sticky Piston\n- 1x End Rod\n- 4x Redstone Repeater\n- 1x Redstone Torch\n- 1x Sheep")
selectByMouse: true
persistentSelection: true
textFormat: TextEdit.PlainText
background: null
padding: 24
onTextChanged: {
if (textInput.activeFocus) {
saveDebounce.restart();
}
root.scheduleCopylistUpdate(true);
}
onHeightChanged: root.scheduleCopylistUpdate(true)
onContentHeightChanged: root.scheduleCopylistUpdate(true)
onCursorPositionChanged: root.scheduleCopylistUpdate()
onSelectionStartChanged: root.scheduleCopylistUpdate()
onSelectionEndChanged: root.scheduleCopylistUpdate()
}
Item {
anchors.fill: parent
visible: root.copyListEntries.length > 0
clip: true
Repeater {
model: ScriptModel {
values: root.copyListEntries
}
delegate: RippleButton {
id: copyButton
required property var modelData
readonly property real lineHeight: Math.min(Math.max(modelData.height, Appearance.font.pixelSize.normal + 6), root.maxCopyButtonSize)
readonly property real iconSizeLocal: Appearance.font.pixelSize.normal
readonly property real hitPadding: 6
property bool justCopied: false
implicitHeight: lineHeight
implicitWidth: lineHeight
buttonRadius: height / 2
y: modelData.y
anchors.right: parent.right
anchors.rightMargin: 10
z: 5
Timer {
id: resetState
interval: 700
onTriggered: {
copyButton.justCopied = false;
}
}
onClicked: {
Quickshell.clipboardText = copyButton.modelData.content;
justCopied = true;
resetState.start();
}
contentItem: Item {
anchors.centerIn: parent
MaterialSymbol {
id: iconItem
anchors.centerIn: parent
text: copyButton.justCopied ? "check" : "content_copy"
iconSize: copyButton.iconSizeLocal
color: Appearance.colors.colOnLayer1
}
}
}
}
}
}
StyledText {
id: statusLabel
Layout.fillWidth: true
Layout.margins: 16
horizontalAlignment: Text.AlignRight
text: saveDebounce.running ? Translation.tr("Saving...") : Translation.tr("Saved ")
color: Appearance.colors.colSubtext
}
}
Timer {
id: saveDebounce
interval: 500
repeat: false
onTriggered: saveContent()
}
Timer {
id: copyListDebounce
interval: 100
repeat: false
onTriggered: updateCopylistPositions()
}
FileView {
id: noteFile
path: Qt.resolvedUrl(Directories.notesPath)
onLoaded: {
root.content = noteFile.text();
if (root.content !== root.content) {
const previousCursor = textInput.cursorPosition;
const previousAnchor = textInput.selectionStart;
root.content = root.content;
applySelection(previousCursor, previousAnchor);
}
if (pendingReload) {
pendingReload = false;
Qt.callLater(root.focusAtEnd);
}
Qt.callLater(root.updateCopyListEntries);
}
onLoadFailed: error => {
if (error === FileViewError.FileNotFound) {
root.content = "";
noteFile.setText(root.content);
if (pendingReload) {
pendingReload = false;
Qt.callLater(root.focusAtEnd);
}
Qt.callLater(root.updateCopyListEntries);
} else {
console.log("[Overlay Notes] Error loading file: " + error);
}
}
}
}

View file

@ -0,0 +1,118 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import Quickshell
import qs
import qs.services
import qs.modules.common
import qs.modules.common.widgets
import qs.modules.ii.overlay
StyledOverlayWidget {
id: root
minimumWidth: 310
minimumHeight: 130
contentItem: OverlayBackground {
id: contentItem
radius: root.contentRadius
property real padding: 8
ColumnLayout {
id: contentColumn
anchors.centerIn: parent
spacing: 10
Row {
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
spacing: 10
BigRecorderButton {
materialSymbol: "screenshot_region"
name: "Screenshot region"
onClicked: {
GlobalStates.overlayOpen = false;
Quickshell.execDetached(["qs", "-p", Quickshell.shellPath(""), "ipc", "call", "region", "screenshot"]);
}
}
BigRecorderButton {
materialSymbol: "photo_camera"
name: "Screenshot"
onClicked: {
GlobalStates.overlayOpen = false;
Quickshell.execDetached(["bash", "-c", "grim - | wl-copy"]);
}
}
BigRecorderButton {
materialSymbol: "screen_record"
name: "Record region"
onClicked: {
GlobalStates.overlayOpen = false;
Quickshell.execDetached(["qs", "-p", Quickshell.shellPath(""), "ipc", "call", "region", "recordWithSound"]);
}
}
BigRecorderButton {
materialSymbol: "capture"
name: "Record screen"
onClicked: {
GlobalStates.overlayOpen = false;
Quickshell.execDetached([Directories.recordScriptPath, "--fullscreen", "--sound"]);
}
}
}
RippleButton {
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
Layout.fillWidth: false
buttonRadius: height / 2
colBackground: Appearance.colors.colLayer3
colBackgroundHover: Appearance.colors.colLayer3Hover
colRipple: Appearance.colors.colLayer3Active
onClicked: {
GlobalStates.overlayOpen = false;
Qt.openUrlExternally(`file://${Config.options.screenRecord.savePath}`);
}
contentItem: Row {
anchors.centerIn: parent
spacing: 6
MaterialSymbol {
anchors.verticalCenter: parent.verticalCenter
text: "animated_images"
iconSize: 20
}
StyledText {
anchors.verticalCenter: parent.verticalCenter
text: Translation.tr("Open recordings folder")
}
}
}
}
}
component BigRecorderButton: RippleButton {
id: bigButton
required property string materialSymbol
required property string name
implicitHeight: 66
implicitWidth: 66
buttonRadius: height / 2
colBackground: Appearance.colors.colLayer3
colBackgroundHover: Appearance.colors.colLayer3Hover
colRipple: Appearance.colors.colLayer3Active
contentItem: MaterialSymbol {
anchors.centerIn: parent
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
text: bigButton.materialSymbol
iconSize: 28
}
StyledToolTip {
text: bigButton.name
}
}
}

View file

@ -0,0 +1,131 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Hyprland
import Qt5Compat.GraphicalEffects
import qs
import qs.services
import qs.modules.common
import qs.modules.common.widgets
import qs.modules.ii.overlay
StyledOverlayWidget {
id: root
minimumWidth: 300
minimumHeight: 200
property list<var> resources: [
{
"icon": "planner_review",
"name": Translation.tr("CPU"),
"history": ResourceUsage.cpuUsageHistory,
"maxAvailableString": ResourceUsage.maxAvailableCpuString
},
{
"icon": "memory",
"name": Translation.tr("RAM"),
"history": ResourceUsage.memoryUsageHistory,
"maxAvailableString": ResourceUsage.maxAvailableMemoryString
},
{
"icon": "swap_horiz",
"name": Translation.tr("Swap"),
"history": ResourceUsage.swapUsageHistory,
"maxAvailableString": ResourceUsage.maxAvailableSwapString
},
]
contentItem: OverlayBackground {
id: contentItem
radius: root.contentRadius
property real padding: 4
ColumnLayout {
id: contentColumn
anchors {
fill: parent
margins: parent.padding
}
spacing: 8
SecondaryTabBar {
id: tabBar
currentIndex: Persistent.states.overlay.resources.tabIndex
onCurrentIndexChanged: {
Persistent.states.overlay.resources.tabIndex = tabBar.currentIndex;
}
Repeater {
model: root.resources.length
delegate: SecondaryTabButton {
required property int index
property var modelData: root.resources[index]
buttonIcon: modelData.icon
buttonText: modelData.name
}
}
}
ResourceSummary {
Layout.margins: 8
history: root.resources[tabBar.currentIndex]?.history ?? []
maxAvailableString: root.resources[tabBar.currentIndex]?.maxAvailableString ?? "--"
}
}
}
component ResourceSummary: RowLayout {
id: resourceSummary
required property list<real> history
required property string maxAvailableString
Layout.fillWidth: true
Layout.fillHeight: true
spacing: 12
ColumnLayout {
spacing: 2
StyledText {
text: (resourceSummary.history[resourceSummary.history.length - 1] * 100).toFixed(1) + "%"
font {
family: Appearance.font.family.numbers
variableAxes: Appearance.font.variableAxes.numbers
pixelSize: Appearance.font.pixelSize.huge
}
}
StyledText {
text: Translation.tr("of %1").arg(resourceSummary.maxAvailableString)
font {
// family: Appearance.font.family.numbers
// variableAxes: Appearance.font.variableAxes.numbers
pixelSize: Appearance.font.pixelSize.smallie
}
color: Appearance.colors.colSubtext
}
Item {
Layout.fillHeight: true
}
}
Rectangle {
id: graphBg
Layout.fillWidth: true
Layout.fillHeight: true
radius: Appearance.rounding.small
color: Appearance.colors.colSecondaryContainer
layer.enabled: true
layer.effect: OpacityMask {
maskSource: Rectangle {
width: graphBg.width
height: graphBg.height
radius: graphBg.radius
}
}
Graph {
anchors.fill: parent
values: root.resources[tabBar.currentIndex]?.history ?? []
points: ResourceUsage.historyLength
alignment: Graph.Alignment.Right
}
}
}
}

View file

@ -0,0 +1,80 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import qs.services
import qs.modules.common
import qs.modules.common.widgets
import qs.modules.ii.overlay
import qs.modules.ii.sidebarRight.volumeMixer
StyledOverlayWidget {
id: root
minimumWidth: 300
minimumHeight: 380
contentItem: OverlayBackground {
radius: root.contentRadius
property real padding: 6
ColumnLayout {
id: contentColumn
anchors {
fill: parent
margins: parent.padding
}
spacing: 8
SecondaryTabBar {
id: tabBar
currentIndex: Persistent.states.overlay.volumeMixer.tabIndex
onCurrentIndexChanged: {
Persistent.states.overlay.volumeMixer.tabIndex = tabBar.currentIndex;
}
SecondaryTabButton {
buttonIcon: "media_output"
buttonText: Translation.tr("Output")
}
SecondaryTabButton {
buttonIcon: "mic"
buttonText: Translation.tr("Input")
}
}
SwipeView {
id: swipeView
Layout.fillWidth: true
Layout.fillHeight: true
currentIndex: Persistent.states.overlay.volumeMixer.tabIndex
onCurrentIndexChanged: {
Persistent.states.overlay.volumeMixer.tabIndex = swipeView.currentIndex;
}
clip: true
PaddedVolumeDialogContent {
isSink: true
}
PaddedVolumeDialogContent {
isSink: false
}
}
}
}
component PaddedVolumeDialogContent: Item {
id: paddedVolumeDialogContent
property alias isSink: volDialogContent.isSink
property real padding: 12
implicitWidth: volDialogContent.implicitWidth + padding * 2
implicitHeight: volDialogContent.implicitHeight + padding * 2
VolumeDialogContent {
id: volDialogContent
anchors {
fill: parent
margins: paddedVolumeDialogContent.padding
}
}
}
}

View file

@ -0,0 +1,214 @@
import qs
import qs.services
import qs.modules.common
import qs.modules.common.widgets
import Qt.labs.synchronizer
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import Quickshell.Hyprland
Scope {
id: overviewScope
property bool dontAutoCancelSearch: false
PanelWindow {
id: panelWindow
property string searchingText: ""
readonly property HyprlandMonitor monitor: Hyprland.monitorFor(panelWindow.screen)
property bool monitorIsFocused: (Hyprland.focusedMonitor?.id == monitor?.id)
visible: GlobalStates.overviewOpen
WlrLayershell.namespace: "quickshell:overview"
WlrLayershell.layer: WlrLayer.Top
WlrLayershell.keyboardFocus: GlobalStates.overviewOpen ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None
color: "transparent"
mask: Region {
item: GlobalStates.overviewOpen ? columnLayout : null
}
anchors {
top: true
bottom: true
left: true
right: true
}
Connections {
target: GlobalStates
function onOverviewOpenChanged() {
if (!GlobalStates.overviewOpen) {
searchWidget.disableExpandAnimation();
overviewScope.dontAutoCancelSearch = false;
GlobalFocusGrab.dismiss();
} else {
if (!overviewScope.dontAutoCancelSearch) {
searchWidget.cancelSearch();
}
GlobalFocusGrab.addDismissable(panelWindow);
}
}
}
Connections {
target: GlobalFocusGrab
function onDismissed() {
GlobalStates.overviewOpen = false;
}
}
implicitWidth: columnLayout.implicitWidth
implicitHeight: columnLayout.implicitHeight
function setSearchingText(text) {
searchWidget.setSearchingText(text);
searchWidget.focusFirstItem();
}
Column {
id: columnLayout
visible: GlobalStates.overviewOpen
anchors {
horizontalCenter: parent.horizontalCenter
top: parent.top
}
spacing: -8
Keys.onPressed: event => {
if (event.key === Qt.Key_Escape) {
GlobalStates.overviewOpen = false;
}
}
SearchWidget {
id: searchWidget
anchors.horizontalCenter: parent.horizontalCenter
Synchronizer on searchingText {
property alias source: panelWindow.searchingText
}
}
Loader {
id: overviewLoader
anchors.horizontalCenter: parent.horizontalCenter
active: GlobalStates.overviewOpen && (Config?.options.overview.enable ?? true)
sourceComponent: OverviewWidget {
screen: panelWindow.screen
visible: (panelWindow.searchingText == "")
}
}
}
}
function toggleClipboard() {
if (GlobalStates.overviewOpen && overviewScope.dontAutoCancelSearch) {
GlobalStates.overviewOpen = false;
return;
}
overviewScope.dontAutoCancelSearch = true;
panelWindow.setSearchingText(Config.options.search.prefix.clipboard);
GlobalStates.overviewOpen = true;
}
function toggleEmojis() {
if (GlobalStates.overviewOpen && overviewScope.dontAutoCancelSearch) {
GlobalStates.overviewOpen = false;
return;
}
overviewScope.dontAutoCancelSearch = true;
panelWindow.setSearchingText(Config.options.search.prefix.emojis);
GlobalStates.overviewOpen = true;
}
IpcHandler {
target: "search"
function toggle() {
GlobalStates.overviewOpen = !GlobalStates.overviewOpen;
}
function workspacesToggle() {
GlobalStates.overviewOpen = !GlobalStates.overviewOpen;
}
function close() {
GlobalStates.overviewOpen = false;
}
function open() {
GlobalStates.overviewOpen = true;
}
function toggleReleaseInterrupt() {
GlobalStates.superReleaseMightTrigger = false;
}
function clipboardToggle() {
overviewScope.toggleClipboard();
}
}
GlobalShortcut {
name: "searchToggle"
description: "Toggles search on press"
onPressed: {
GlobalStates.overviewOpen = !GlobalStates.overviewOpen;
}
}
GlobalShortcut {
name: "overviewWorkspacesClose"
description: "Closes overview on press"
onPressed: {
GlobalStates.overviewOpen = false;
}
}
GlobalShortcut {
name: "overviewWorkspacesToggle"
description: "Toggles overview on press"
onPressed: {
GlobalStates.overviewOpen = !GlobalStates.overviewOpen;
}
}
GlobalShortcut {
name: "searchToggleRelease"
description: "Toggles search on release"
onPressed: {
GlobalStates.superReleaseMightTrigger = true;
}
onReleased: {
if (GlobalStates.shouldSuppressSuperReleaseSearch() || !GlobalStates.superReleaseMightTrigger) {
GlobalStates.superReleaseMightTrigger = true;
return;
}
GlobalStates.overviewOpen = !GlobalStates.overviewOpen;
}
}
GlobalShortcut {
name: "searchToggleReleaseInterrupt"
description: "Interrupts possibility of search being toggled on release. " + "This is necessary because GlobalShortcut.onReleased in quickshell triggers whether or not you press something else while holding the key. " + "To make sure this works consistently, use binditn = MODKEYS, catchall in an automatically triggered submap that includes everything."
onPressed: {
GlobalStates.superReleaseMightTrigger = false;
}
}
GlobalShortcut {
name: "overviewClipboardToggle"
description: "Toggle clipboard query on overview widget"
onPressed: {
overviewScope.toggleClipboard();
}
}
GlobalShortcut {
name: "overviewEmojiToggle"
description: "Toggle emoji query on overview widget"
onPressed: {
overviewScope.toggleEmojis();
}
}
}

Some files were not shown because too many files have changed in this diff Show more