Merge branch 'main' of https://github.com/end-4/dots-hyprland into sticky-merge-main

This commit is contained in:
end-4
2025-11-14 10:06:44 +01:00
350 changed files with 4312 additions and 1429 deletions
@@ -0,0 +1,298 @@
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
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
// 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;
}
property real wallpaperToScreenRatio: Math.min(wallpaperWidth / screen.width, wallpaperHeight / screen.height)
property real preferredWallpaperScale: Config.options.background.parallax.workspaceZoom
property real effectiveWallpaperScale: 1 // Some reasonable init value, to be updated
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 movableXSpace: ((wallpaperWidth / wallpaperToScreenRatio * effectiveWallpaperScale) - screen.width) / 2
property real movableYSpace: ((wallpaperHeight / wallpaperToScreenRatio * effectiveWallpaperScale) - screen.height) / 2
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;
if (width <= screenWidth || height <= screenHeight) {
// Undersized/perfectly sized wallpapers
bgRoot.effectiveWallpaperScale = Math.max(screenWidth / width, screenHeight / height);
} else {
// Oversized = can be zoomed for parallax, yay
bgRoot.effectiveWallpaperScale = Math.min(bgRoot.preferredWallpaperScale, width / screenWidth, height / screenHeight);
}
}
}
}
Item {
anchors.fill: parent
clip: true
// Wallpaper
StyledImage {
id: wallpaper
visible: opacity > 0 && !blurLoader.active
opacity: (status === Image.Ready && !bgRoot.wallpaperIsVideo) ? 1 : 0
cache: false
smooth: false
// Range = groups that workspaces span on
property int chunkSize: Config?.options.bar.workspaces.shown ?? 10
property int lower: Math.floor(bgRoot.firstWorkspaceId / chunkSize) * chunkSize
property int upper: Math.ceil(bgRoot.lastWorkspaceId / chunkSize) * chunkSize
property int range: upper - lower
property real valueX: {
let result = 0.5;
if (Config.options.background.parallax.enableWorkspace && !bgRoot.verticalParallax) {
result = ((bgRoot.monitor.activeWorkspace?.id - lower) / range);
}
if (Config.options.background.parallax.enableSidebar) {
result += (0.15 * GlobalStates.sidebarRightOpen - 0.15 * GlobalStates.sidebarLeftOpen);
}
return result;
}
property real valueY: {
let result = 0.5;
if (Config.options.background.parallax.enableWorkspace && bgRoot.verticalParallax) {
result = ((bgRoot.monitor.activeWorkspace?.id - lower) / range);
}
return result;
}
property real effectiveValueX: Math.max(0, Math.min(1, valueX))
property real effectiveValueY: Math.max(0, Math.min(1, valueY))
x: -(bgRoot.movableXSpace) - (effectiveValueX - 0.5) * 2 * bgRoot.movableXSpace
y: -(bgRoot.movableYSpace) - (effectiveValueY - 0.5) * 2 * bgRoot.movableYSpace
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
}
}
sourceSize {
width: bgRoot.screen.width * bgRoot.effectiveWallpaperScale * bgRoot.monitor.scale
height: bgRoot.screen.height * bgRoot.effectiveWallpaperScale * bgRoot.monitor.scale
}
width: bgRoot.wallpaperWidth / bgRoot.wallpaperToScreenRatio * bgRoot.effectiveWallpaperScale
height: bgRoot.wallpaperHeight / bgRoot.wallpaperToScreenRatio * bgRoot.effectiveWallpaperScale
}
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
anchors {
left: wallpaper.left
right: wallpaper.right
top: wallpaper.top
bottom: wallpaper.bottom
horizontalCenter: undefined
verticalCenter: undefined
readonly property real parallaxFactor: Config.options.background.parallax.widgetsFactor
leftMargin: {
const xOnWallpaper = bgRoot.movableXSpace;
const extraMove = (wallpaper.effectiveValueX * 2 * bgRoot.movableXSpace) * (parallaxFactor - 1);
return xOnWallpaper - extraMove;
}
topMargin: {
const yOnWallpaper = bgRoot.movableYSpace;
const extraMove = (wallpaper.effectiveValueY * 2 * bgRoot.movableYSpace) * (parallaxFactor - 1);
return yOnWallpaper - extraMove;
}
Behavior on leftMargin {
animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
}
Behavior on topMargin {
animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
}
}
width: wallpaper.width
height: wallpaper.height
states: State {
name: "centered"
when: GlobalStates.screenLocked || bgRoot.wallpaperSafetyTriggered
PropertyChanges {
target: widgetCanvas
width: parent.width
height: parent.height
}
AnchorChanges {
target: widgetCanvas
anchors {
left: undefined
right: undefined
top: undefined
bottom: undefined
horizontalCenter: parent.horizontalCenter
verticalCenter: parent.verticalCenter
}
}
}
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 / bgRoot.effectiveWallpaperScale
scaledScreenHeight: bgRoot.screen.height / bgRoot.effectiveWallpaperScale
wallpaperScale: bgRoot.effectiveWallpaperScale
}
}
FadeLoader {
shown: Config.options.background.widgets.clock.enable
sourceComponent: ClockWidget {
screenWidth: bgRoot.screen.width
screenHeight: bgRoot.screen.height
scaledScreenWidth: bgRoot.screen.width / bgRoot.effectiveWallpaperScale
scaledScreenHeight: bgRoot.screen.height / bgRoot.effectiveWallpaperScale
wallpaperScale: bgRoot.effectiveWallpaperScale
wallpaperSafetyTriggered: bgRoot.wallpaperSafetyTriggered
}
}
}
}
}
}
@@ -0,0 +1,101 @@
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 || (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;
}
}
}
}
@@ -0,0 +1,200 @@
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: 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 (root.forceCenter)
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)
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)
sourceComponent: ColumnLayout {
id: clockColumn
spacing: 6
ClockText {
font.pixelSize: 90
text: DateTime.time
}
ClockText {
Layout.topMargin: -5
text: DateTime.longDate
}
StyledText {
// Somehow gets fucked up if made a ClockText???
visible: Config.options.background.widgets.clock.quote.enable && Config.options.background.widgets.clock.quote.text.length > 0
Layout.fillWidth: true
horizontalAlignment: root.textHorizontalAlignment
font {
pixelSize: Appearance.font.pixelSize.normal
weight: 350
}
color: root.colText
style: Text.Raised
styleColor: Appearance.colors.colShadow
text: Config.options.background.widgets.clock.quote.text
}
}
}
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 ClockText: StyledText {
Layout.fillWidth: true
horizontalAlignment: root.textHorizontalAlignment
font {
family: Appearance.font.family.expressive
pixelSize: 20
weight: Font.DemiBold
}
color: root.colText
style: Text.Raised
styleColor: Appearance.colors.colShadow
animateChange: Config.options.background.widgets.clock.digital.animateChange
}
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
anchors.verticalCenter: statusTextRow.verticalCenter
font {
pixelSize: Appearance.font.pixelSize.large
weight: Font.Normal
}
style: Text.Raised
styleColor: Appearance.colors.colShadow
}
}
}
@@ -0,0 +1,218 @@
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")
}
}
Connections {
target: Config
function onReadyChanged() {
categoryFileView.path = Directories.generatedWallpaperCategoryPath
}
}
FileView {
id: categoryFileView
path: ""
watchChanges: true
onFileChanged: reload()
onLoaded: {
root.setClockPreset(categoryFileView.text().trim())
}
}
property bool useSineCookie: Config.options.background.widgets.clock.cookie.useSineCookie
StyledDropShadow {
target: 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: 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: !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
}
}
}
@@ -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
}
}
}
}
}
@@ -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)
}
}
}
@@ -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
}
}
}
}
}
@@ -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)
}
}
}
@@ -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)
}
}
}
}
@@ -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)
}
}
}
}
@@ -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
}
}
}
@@ -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
}
}
}
@@ -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
}
}
}
@@ -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)
}
}
}
@@ -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
}
}
}
}
}
}
@@ -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
}
}
}
}
@@ -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
}
}
}
}
@@ -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
}
}
}
@@ -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
}
}
}
}
@@ -0,0 +1,52 @@
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 string activeWindowAddress: `0x${activeWindow?.HyprlandToplevel?.address}`
property bool focusingThisMonitor: HyprlandData.activeWorkspace?.monitor == monitor?.name
property var biggestWindow: HyprlandData.biggestWindowForWorkspace(HyprlandData.monitors[root.monitor?.id]?.activeWorkspace.id)
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 && root.biggestWindow ?
root.activeWindow?.appId :
(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 && root.biggestWindow ?
root.activeWindow?.title :
(root.biggestWindow?.title) ?? `${Translation.tr("Workspace")} ${monitor?.activeWorkspace?.id ?? 1}`
}
}
}
@@ -0,0 +1,249 @@
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
exclusionMode: ExclusionMode.Ignore
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"
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
}
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;
}
}
}
@@ -0,0 +1,349 @@
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: root.brightnessMonitor.setBrightness(root.brightnessMonitor.brightness - 0.05)
onScrollUp: root.brightnessMonitor.setBrightness(root.brightnessMonitor.brightness + 0.05)
onMovedAway: GlobalStates.osdBrightnessOpen = false
onPressed: event => {
if (event.button === Qt.LeftButton)
GlobalStates.sidebarLeftOpen = !GlobalStates.sidebarLeftOpen;
}
// Visual content
ScrollHint {
reveal: barLeftSideMouseArea.hovered
icon: "light_mode"
tooltipText: Translation.tr("Scroll to change brightness")
side: "left"
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
}
RowLayout {
id: leftSectionRowLayout
anchors.fill: parent
spacing: 10
LeftSidebarButton { // Left sidebar button
Layout.alignment: Qt.AlignVCenter
Layout.leftMargin: Appearance.rounding.screenRounding
colBackground: barLeftSideMouseArea.hovered ? Appearance.colors.colLayer1Hover : ColorUtils.transparentize(Appearance.colors.colLayer1Hover, 1)
}
ActiveWindow {
visible: root.useShortenedForm === 0
Layout.rightMargin: Appearance.rounding.screenRounding
Layout.fillWidth: true
Layout.fillHeight: true
}
}
}
Row { // Middle section
id: middleSection
anchors {
top: parent.top
bottom: parent.bottom
horizontalCenter: parent.horizontalCenter
}
spacing: 4
BarGroup {
id: leftCenterGroup
anchors.verticalCenter: parent.verticalCenter
implicitWidth: root.centerSideModuleWidth
Resources {
alwaysShowAllResources: root.useShortenedForm === 2
Layout.fillWidth: root.useShortenedForm === 2
}
Media {
visible: root.useShortenedForm < 2
Layout.fillWidth: true
}
}
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: root.centerSideModuleWidth
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: {
const currentVolume = Audio.value;
const step = currentVolume < 0.1 ? 0.01 : 0.02 || 0.2;
Audio.sink.audio.volume -= step;
}
onScrollUp: {
const currentVolume = Audio.value;
const step = currentVolume < 0.1 ? 0.01 : 0.02 || 0.2;
Audio.sink.audio.volume = Math.min(1, Audio.sink.audio.volume + step);
}
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 {
Layout.rightMargin: indicatorsRowLayout.realSpacing
text: Network.materialSymbol
iconSize: Appearance.font.pixelSize.larger
color: rightSidebarButton.colText
}
MaterialSymbol {
visible: BluetoothStatus.available
text: BluetoothStatus.connected ? "bluetooth_connected" : BluetoothStatus.enabled ? "bluetooth" : "bluetooth_disabled"
iconSize: Appearance.font.pixelSize.larger
color: rightSidebarButton.colText
}
}
}
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 {}
}
}
}
}
}
@@ -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
}
}
@@ -0,0 +1,59 @@
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: true
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.centerIn: parent
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
}
}
@@ -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)}%`
}
}
}
@@ -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
}
@@ -0,0 +1,50 @@
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: true
acceptedButtons: Qt.NoButton
ClockWidgetPopup {
hoverTarget: mouseArea
}
}
}
@@ -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
}
}
}
}
@@ -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
}
}
}
@@ -0,0 +1,78 @@
import QtQuick
import qs
import qs.services
import qs.modules.common
import qs.modules.common.widgets
RippleButton {
id: root
property bool showPing: false
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)
}
}
}
}
@@ -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 : ''}`
}
}
}
@@ -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
}
}
}
@@ -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
}
}
}
@@ -0,0 +1,53 @@
import qs.modules.common
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: true
RowLayout {
id: rowLayout
spacing: 0
anchors.fill: parent
anchors.leftMargin: 4
anchors.rightMargin: 4
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
}
}
@@ -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)}%`
}
}
}
}
}
@@ -0,0 +1,60 @@
import qs.modules.common
import qs.modules.common.widgets
import QtQuick
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
}
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
}
}
}
}
@@ -0,0 +1,81 @@
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) return root.QsWindow?.mapFromItem(
root.hoverTarget,
(root.hoverTarget.width - popupBackground.implicitWidth) / 2, 0
).x;
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
}
}
}
@@ -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
}
}
@@ -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
}
}
@@ -0,0 +1,156 @@
import qs.modules.common
import qs.modules.common.widgets
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Hyprland
import Quickshell.Services.SystemTray
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 bool smartTray: Config.options.bar.tray.filterPassive
property list<var> itemsInUserList: SystemTray.items.values.filter(i => (Config.options.bar.tray.pinnedItems.includes(i.id) && (!smartTray || i.status !== Status.Passive)))
property list<var> itemsNotInUserList: SystemTray.items.values.filter(i => (!Config.options.bar.tray.pinnedItems.includes(i.id) && (!smartTray || i.status !== Status.Passive)))
property bool invertPins: Config.options.bar.tray.invertPinnedItems
property list<var> pinnedItems: invertPins ? itemsNotInUserList : itemsInUserList
property list<var> unpinnedItems: invertPins ? itemsInUserList : itemsNotInUserList
onUnpinnedItemsChanged: {
if (unpinnedItems.length == 0) root.closeOverflowMenu();
}
function grabFocus() {
focusGrab.active = true;
}
function setExtraWindowAndGrabFocus(window) {
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
}
}
}
@@ -0,0 +1,101 @@
import qs.modules.common
import qs.modules.common.widgets
import qs.modules.common.functions
import QtQuick
import Quickshell
import Quickshell.Services.SystemTray
import Quickshell.Widgets
import Qt5Compat.GraphicalEffects
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) menu.open();
break;
}
event.accepted = true;
}
onEntered: {
tooltip.text = item.tooltipTitle.length > 0 ? item.tooltipTitle
: (item.title.length > 0 ? item.title : item.id);
if (item.tooltipDescription.length > 0) tooltip.text += " • " + item.tooltipDescription;
if (Config.options.bar.tray.showItemId) tooltip.text += "\n[" + item.id + "]";
}
Loader {
id: menu
function open() {
menu.active = true;
}
active: false
sourceComponent: SysTrayMenu {
Component.onCompleted: this.open();
trayItemMenuHandle: root.item.menu
anchor {
window: root.QsWindow.window
rect.x: root.x + (Config.options.bar.vertical ? 0 : QsWindow.window?.width)
rect.y: root.y + (Config.options.bar.vertical ? QsWindow.window?.height : 0)
rect.height: root.height
rect.width: root.width
edges: Config.options.bar.bottom ? (Edges.Top | Edges.Left) : (Edges.Bottom | Edges.Right)
gravity: Config.options.bar.bottom ? (Edges.Top | Edges.Left) : (Edges.Bottom | Edges.Right)
}
onMenuOpened: (window) => root.menuOpened(window);
onMenuClosed: {
root.menuClosed();
menu.active = false;
}
}
}
IconImage {
id: trayIcon
visible: !Config.options.bar.tray.monochromeIcons
source: root.item.icon
anchors.centerIn: parent
width: parent.width
height: parent.height
}
Loader {
active: Config.options.bar.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
}
}
@@ -0,0 +1,217 @@
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 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")
}
}
}
}
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 {}
}
}
@@ -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
}
}
}
}
@@ -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) {
Hyprland.dispatch(`exec ${Directories.wallpaperSwitchScriptPath} --mode light --noswitch`);
} else {
Hyprland.dispatch(`exec ${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 "settings_slow_motion"
case PowerProfile.Performance: return "local_fire_department"
}
iconSize: Appearance.font.pixelSize.large
color: Appearance.colors.colOnLayer2
}
}
}
}
}
@@ -0,0 +1,319 @@
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 workspacesShown: Config.options.bar.workspaces.shown
readonly property int workspaceGroup: Math.floor((monitor?.activeWorkspace?.id - 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: (monitor?.activeWorkspace?.id - 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(`workspace r+1`);
else if (event.angleDelta.y > 0)
Hyprland.dispatch(`workspace r-1`);
}
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.BackButton
onPressed: (event) => {
if (event.button === Qt.BackButton) {
Hyprland.dispatch(`togglespecialworkspace`);
}
}
}
// 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 && monitor?.activeWorkspace?.id === index))
property var rightOccupied: (workspaceOccupied[index+1] && !(!activeWindow?.activated && monitor?.activeWorkspace?.id === 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 && monitor?.activeWorkspace?.id === 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(`workspace ${workspaceValue}`)
width: vertical ? undefined : workspaceButtonWidth
height: vertical ? 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: (monitor?.activeWorkspace?.id == 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: (monitor?.activeWorkspace?.id == 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)
}
}
}
}
}
}
}
}
}
@@ -0,0 +1,52 @@
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
hoverEnabled: true
onPressed: {
Weather.getData();
Quickshell.execDetached(["notify-send",
Translation.tr("Weather"),
Translation.tr("Refreshing (manually triggered)")
, "-a", "Shell"
])
}
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
}
}
@@ -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
}
}
}
@@ -0,0 +1,104 @@
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
}
}
}
}
@@ -0,0 +1,214 @@
import qs.services
import qs.modules.common
import qs.modules.common.widgets
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Qt5Compat.GraphicalEffects
import Qt.labs.synchronizer
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;
}
exclusiveZone: 0
implicitWidth: cheatsheetBackground.width + Appearance.sizes.elevationMargin * 2
implicitHeight: cheatsheetBackground.height + Appearance.sizes.elevationMargin * 2
WlrLayershell.namespace: "quickshell:cheatsheet"
// Hyprland 0.49: Focus is always exclusive and setting this breaks mouse focus grab
// WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
color: "transparent"
mask: Region {
item: cheatsheetBackground
}
HyprlandFocusGrab { // Click outside to close
id: grab
windows: [cheatsheetRoot]
active: cheatsheetRoot.visible
onCleared: () => {
if (!active)
cheatsheetRoot.hide();
}
}
// Background
StyledRectangularShadow {
target: cheatsheetBackground
}
Rectangle {
id: cheatsheetBackground
anchors.centerIn: parent
color: Appearance.colors.colLayer0
border.width: 1
border.color: Appearance.colors.colLayer0Border
radius: Appearance.rounding.windowRounding
property real padding: 20
implicitWidth: cheatsheetColumnLayout.implicitWidth + padding * 2
implicitHeight: cheatsheetColumnLayout.implicitHeight + padding * 2
Keys.onPressed: event => { // Esc to close
if (event.key === Qt.Key_Escape) {
cheatsheetRoot.hide();
}
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;
}
}
}
RippleButton { // Close button
id: closeButton
focus: cheatsheetRoot.visible
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
Toolbar {
Layout.alignment: Qt.AlignHCenter
enableShadow: false
ToolbarTabBar {
id: tabBar
tabButtonList: root.tabButtonList
currentIndex: swipeView.currentIndex
}
}
SwipeView { // Content pages
id: swipeView
Layout.topMargin: 5
Layout.fillWidth: true
Layout.fillHeight: true
currentIndex: tabBar.currentIndex
spacing: 10
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 {}
CheatsheetPeriodicTable {}
}
}
}
}
}
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;
}
}
}
@@ -0,0 +1,215 @@
pragma ComponentBehavior: Bound
import qs.services
import qs.modules.common
import qs.modules.common.widgets
import QtQuick
import QtQuick.Layouts
Item {
id: root
readonly property var keybinds: HyprlandKeybinds.keybinds
property real spacing: 20
property real titleSpacing: 7
property real padding: 4
implicitWidth: row.implicitWidth + padding * 2
implicitHeight: row.implicitHeight + padding * 2
// 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"]
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 : {},
)
Row { // Keybind columns
id: row
spacing: root.spacing
Repeater {
model: keybinds.children
delegate: Column { // Keybind sections
spacing: root.spacing
required property var modelData
anchors.top: row.top
Repeater {
model: modelData.children
delegate: Item { // Section with real keybinds
id: keybindSection
required property var modelData
implicitWidth: sectionColumn.implicitWidth
implicitHeight: sectionColumn.implicitHeight
Column {
id: sectionColumn
anchors.centerIn: parent
spacing: root.titleSpacing
StyledText {
id: sectionTitle
font {
family: Appearance.font.family.title
pixelSize: Appearance.font.pixelSize.title
variableAxes: Appearance.font.variableAxes.title
}
color: Appearance.colors.colOnLayer0
text: keybindSection.modelData.name
}
GridLayout {
id: keybindGrid
columns: 2
columnSpacing: 4
rowSpacing: 4
Repeater {
model: {
var result = [];
for (var i = 0; i < keybindSection.modelData.keybinds.length; i++) {
const keybind = keybindSection.modelData.keybinds[i];
if (!Config.options.cheatsheet.splitButtons) {
for (var j = 0; j < keybind.mods.length; j++) {
keybind.mods[j] = keySubstitutions[keybind.mods[j]] || keybind.mods[j];
}
keybind.mods = [keybind.mods.join(' ') ]
keybind.mods[0] += !keyBlacklist.includes(keybind.key) && keybind.mods[0].length ? ' ' : ''
keybind.mods[0] += !keyBlacklist.includes(keybind.key) ? (keySubstitutions[keybind.key] || keybind.key) : ''
}
result.push({
"type": "keys",
"mods": keybind.mods,
"key": keybind.key,
});
result.push({
"type": "comment",
"comment": keybind.comment,
});
}
return result;
}
delegate: Item {
required property var modelData
implicitWidth: keybindLoader.implicitWidth
implicitHeight: keybindLoader.implicitHeight
Loader {
id: keybindLoader
sourceComponent: (modelData.type === "keys") ? keysComponent : commentComponent
}
Component {
id: keysComponent
Row {
spacing: 4
Repeater {
model: modelData.mods
delegate: KeyboardKey {
required property var modelData
key: keySubstitutions[modelData] || modelData
pixelSize: Config.options.cheatsheet.fontSize.key
}
}
StyledText {
id: keybindPlus
visible: Config.options.cheatsheet.splitButtons && !keyBlacklist.includes(modelData.key) && modelData.mods.length > 0
text: "+"
}
KeyboardKey {
id: keybindKey
visible: Config.options.cheatsheet.splitButtons && !keyBlacklist.includes(modelData.key)
key: keySubstitutions[modelData.key] || modelData.key
pixelSize: Config.options.cheatsheet.fontSize.key
color: Appearance.colors.colOnLayer0
}
}
}
Component {
id: commentComponent
Item {
id: commentItem
implicitWidth: commentText.implicitWidth + 8 * 2
implicitHeight: commentText.implicitHeight
StyledText {
id: commentText
anchors.centerIn: parent
font.pixelSize: Config.options.cheatsheet.fontSize.comment || Appearance.font.pixelSize.smaller
text: modelData.comment
}
}
}
}
}
}
}
}
}
}
}
}
}
@@ -0,0 +1,63 @@
import "periodic_table.js" as PTable
import QtQuick
Item {
id: root
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
}
}
}
}
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
}
}
}
}
}
}
@@ -0,0 +1,78 @@
import qs.modules.common
import qs.modules.common.functions
import qs.modules.common.widgets
import QtQuick
RippleButton {
id: root
required property var element
opacity: element.type != "empty" ? 1 : 0
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
}
}
@@ -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"
}
@@ -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
}
}
}
}
}
}
}
}
}
@@ -0,0 +1,136 @@
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"
readonly property var desktopEntry: DesktopEntries.heuristicLookup(appToplevel.appId)
enabled: !isSeparator
implicitWidth: isSeparator ? 1 : implicitHeight - topInset - bottomInset
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: () => {
if (Config.options.dock.pinnedApps.indexOf(appToplevel.appId) !== -1) {
Config.options.dock.pinnedApps = Config.options.dock.pinnedApps.filter(id => id !== appToplevel.appId)
} else {
Config.options.dock.pinnedApps = Config.options.dock.pinnedApps.concat([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)
}
}
}
}
}
}
@@ -0,0 +1,229 @@
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
property bool buttonHovered: false
property bool requestDockShow: previewPopup.show
Layout.fillHeight: true
Layout.topMargin: Appearance.sizes.hyprlandGapsOut // why does this work
implicitWidth: listView.implicitWidth
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 allPreviewsReady: false
Connections {
target: root
function onLastHoveredButtonChanged() {
previewPopup.allPreviewsReady = false; // Reset readiness when the hovered button changes
}
}
function updatePreviewReadiness() {
for(var i = 0; i < previewRowLayout.children.length; i++) {
const view = previewRowLayout.children[i];
if (view.hasContent === false) {
allPreviewsReady = false;
return;
}
}
allPreviewsReady = true;
}
property bool shouldShow: {
const hoverConditions = (popupMouseArea.containsMouse || root.buttonHovered)
return hoverConditions && allPreviewsReady;
}
property bool show: false
onShouldShowChanged: {
if (shouldShow) {
// show = true;
updateTimer.restart();
} else {
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.visible
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: {
const itemCenter = root.QsWindow?.mapFromItem(root.lastHoveredButton, root.lastHoveredButton?.width / 2, 0);
return itemCenter.x - 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.colors.colSurfaceContainer
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
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
WrapperRectangle {
Layout.fillWidth: true
color: ColorUtils.transparentize(Appearance.colors.colSurfaceContainer)
radius: Appearance.rounding.small
margin: 5
StyledText {
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: windowControlsHeight
baseHeight: 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();
}
}
}
ScreencopyView {
id: screencopyView
captureSource: previewPopup ? windowButton.modelData : null
live: true
paintCursor: true
constraintSize: Qt.size(root.maxWindowPreviewWidth, root.maxWindowPreviewHeight)
onHasContentChanged: {
previewPopup.updatePreviewReadiness();
}
layer.enabled: true
layer.effect: OpacityMask {
maskSource: Rectangle {
width: screencopyView.width
height: screencopyView.height
radius: Appearance.rounding.small
}
}
}
}
}
}
}
}
}
}
}
@@ -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
}
@@ -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
}
@@ -0,0 +1,188 @@
pragma ComponentBehavior: Bound
import qs
import qs.services
import qs.modules.common
import qs.modules.common.functions
import QtQuick
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import Quickshell.Hyprland
Scope {
id: root
Process {
id: unlockKeyringProc
onExited: (exitCode, exitStatus) => {
KeyringStorage.fetchKeyringData();
}
}
function unlockKeyring() {
unlockKeyringProc.exec({
environment: ({
"UNLOCK_PASSWORD": lockContext.currentText
}),
command: ["bash", "-c", Quickshell.shellPath("scripts/keyring/unlock.sh")]
})
}
property var windowData: []
function saveWindowPositionAndTile() {
Quickshell.execDetached(["hyprctl", "keyword", "dwindle:pseudotile", "true"])
root.windowData = HyprlandData.windowList.filter(w => (w.floating && w.workspace.id === HyprlandData.activeWorkspace.id))
root.windowData.forEach(w => {
Hyprland.dispatch(`pseudo address:${w.address}`)
Hyprland.dispatch(`settiled address:${w.address}`)
Hyprland.dispatch(`movetoworkspacesilent ${w.workspace.id},address:${w.address}`)
})
}
function restoreWindowPositionAndTile() {
root.windowData.forEach(w => {
Hyprland.dispatch(`setfloating address:${w.address}`)
Hyprland.dispatch(`movewindowpixel exact ${w.at[0]} ${w.at[1]}, address:${w.address}`)
Hyprland.dispatch(`pseudo address:${w.address}`)
})
Quickshell.execDetached(["hyprctl", "keyword", "dwindle:pseudotile", "false"])
}
// This stores all the information shared between the lock surfaces on each screen.
// https://github.com/quickshell-mirror/quickshell-examples/tree/master/lockscreen
LockContext {
id: lockContext
Connections {
target: GlobalStates
function onScreenLockedChanged() {
if (GlobalStates.screenLocked) {
lockContext.reset();
lockContext.tryFingerUnlock();
}
}
}
onUnlocked: (targetAction) => {
// Perform the target action if it's not just unlocking
if (targetAction == LockContext.ActionEnum.Poweroff) {
Session.poweroff();
return;
} else if (targetAction == LockContext.ActionEnum.Reboot) {
Session.reboot();
return;
}
// Unlock the keyring if configured to do so
if (Config.options.lock.security.unlockKeyring) root.unlockKeyring();
// Unlock the screen before exiting, or the compositor will display a
// fallback lock you can't interact with.
GlobalStates.screenLocked = false;
// Refocus last focused window on unlock (hack)
Quickshell.execDetached(["bash", "-c", `sleep 0.2; hyprctl --batch "dispatch togglespecialworkspace; dispatch togglespecialworkspace"`])
// Reset
lockContext.reset();
}
}
WlSessionLock {
id: lock
locked: GlobalStates.screenLocked
WlSessionLockSurface {
color: "transparent"
Loader {
active: GlobalStates.screenLocked
anchors.fill: parent
opacity: active ? 1 : 0
Behavior on opacity {
animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this)
}
sourceComponent: LockSurface {
context: lockContext
}
}
}
}
// Blur layer hack
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
property int horizontalSqueeze: modelData.width * 0.2
onShouldPushChanged: {
if (shouldPush) {
root.saveWindowPositionAndTile();
Quickshell.execDetached(["bash", "-c", `hyprctl keyword monitor ${targetMonitorName}, addreserved, ${verticalMovementDistance}, ${-verticalMovementDistance}, ${horizontalSqueeze}, ${horizontalSqueeze}`])
} else {
Quickshell.execDetached(["bash", "-c", `hyprctl keyword monitor ${targetMonitorName}, addreserved, 0, 0, 0, 0`])
root.restoreWindowPositionAndTile();
}
}
}
}
function lock() {
if (Config.options.lock.useHyprlock) {
Quickshell.execDetached(["bash", "-c", "pidof hyprlock || hyprlock"]);
return;
}
GlobalStates.screenLocked = true;
}
IpcHandler {
target: "lock"
function activate(): void {
root.lock();
}
function focus(): void {
lockContext.shouldReFocus();
}
}
GlobalShortcut {
name: "lock"
description: "Locks the screen"
onPressed: {
root.lock()
}
}
GlobalShortcut {
name: "lockFocus"
description: "Re-focuses the lock screen. This is because Hyprland after waking up for whatever reason"
+ "decides to keyboard-unfocus the lock screen"
onPressed: {
lockContext.shouldReFocus();
}
}
function initIfReady() {
if (!Config.ready || !Persistent.ready) return;
if (Config.options.lock.launchOnStartup && Persistent.isNewHyprlandInstance) {
root.lock();
} else {
KeyringStorage.fetchKeyringData();
}
}
Connections {
target: Config
function onReadyChanged() {
root.initIfReady();
}
}
Connections {
target: Persistent
function onReadyChanged() {
root.initIfReady();
}
}
}
@@ -0,0 +1,135 @@
import qs
import qs.modules.common
import QtQuick
import Quickshell
import Quickshell.Io
import Quickshell.Services.Pam
Scope {
id: root
enum ActionEnum { Unlock, Poweroff, Reboot }
signal shouldReFocus()
signal unlocked(targetAction: var)
signal failed()
// These properties are in the context and not individual lock surfaces
// so all surfaces can share the same state.
property string currentText: ""
property bool unlockInProgress: false
property bool showFailure: false
property bool fingerprintsConfigured: false
property var targetAction: LockContext.ActionEnum.Unlock
function resetTargetAction() {
root.targetAction = LockContext.ActionEnum.Unlock;
}
function clearText() {
root.currentText = "";
}
function resetClearTimer() {
passwordClearTimer.restart();
}
function reset() {
root.resetTargetAction();
root.clearText();
root.unlockInProgress = false;
stopFingerPam();
}
Timer {
id: passwordClearTimer
interval: 10000
onTriggered: {
root.reset();
}
}
onCurrentTextChanged: {
if (currentText.length > 0) {
showFailure = false;
GlobalStates.screenUnlockFailed = false;
}
GlobalStates.screenLockContainsCharacters = currentText.length > 0;
passwordClearTimer.restart();
}
function tryUnlock() {
root.unlockInProgress = true;
pam.start();
}
function tryFingerUnlock() {
if (root.fingerprintsConfigured) {
fingerPam.start();
}
}
function stopFingerPam() {
if (fingerPam.running) {
fingerPam.abort();
}
}
Process {
id: fingerprintCheckProc
running: true
command: ["bash", "-c", "fprintd-list $(whoami)"]
stdout: StdioCollector {
id: fingerprintOutputCollector
onStreamFinished: {
root.fingerprintsConfigured = fingerprintOutputCollector.text.includes("Fingerprints for user");
}
}
onExited: (exitCode, exitStatus) => {
if (exitCode !== 0) {
// console.warn("[LockContext] fprintd-list command exited with error:", exitCode, exitStatus);
root.fingerprintsConfigured = false;
}
}
}
PamContext {
id: pam
// pam_unix will ask for a response for the password prompt
onPamMessage: {
if (this.responseRequired) {
this.respond(root.currentText);
}
}
// pam_unix won't send any important messages so all we need is the completion status.
onCompleted: result => {
if (result == PamResult.Success) {
root.unlocked(root.targetAction);
stopFingerPam();
} else {
root.clearText();
root.unlockInProgress = false;
GlobalStates.screenUnlockFailed = true;
root.showFailure = true;
}
}
}
PamContext {
id: fingerPam
configDirectory: "pam"
config: "fprintd.conf"
onCompleted: result => {
if (result == PamResult.Success) {
root.unlocked(root.targetAction);
stopFingerPam();
} else if (result == PamResult.Error) { // if timeout or etc..
tryFingerUnlock()
}
}
}
}
@@ -0,0 +1,362 @@
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.ii.bar as Bar
import Quickshell
import Quickshell.Services.SystemTray
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
// 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
Keys.onPressed: event => {
root.context.resetClearTimer();
if (event.key === Qt.Key_Escape) { // Esc to clear
root.context.currentText = "";
}
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 ]]"
// }
// }
// 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
// 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()
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
SequentialAnimation {
id: wrongPasswordShakeAnim
NumberAnimation { target: passwordBox; property: "Layout.leftMargin"; to: -30; duration: 50 }
NumberAnimation { target: passwordBox; property: "Layout.leftMargin"; to: 30; duration: 50 }
NumberAnimation { target: passwordBox; property: "Layout.leftMargin"; to: -15; duration: 40 }
NumberAnimation { target: passwordBox; property: "Layout.leftMargin"; to: 15; duration: 40 }
NumberAnimation { target: passwordBox; property: "Layout.leftMargin"; to: 0; duration: 30 }
}
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
}
}
}
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 "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
}
}
}
@@ -0,0 +1,95 @@
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
contentWidth: dotsRow.implicitWidth
contentX: (Math.max(contentWidth - width, 0))
Behavior on contentX {
animation: Appearance.animation.elementMoveEnter.numberAnimation.createObject(this)
}
Row {
id: dotsRow
anchors {
left: parent.left
verticalCenter: parent.verticalCenter
leftMargin: 4
}
spacing: 10
Repeater {
model: ScriptModel {
values: Array(root.length)
}
delegate: Item {
id: charItem
required property int index
implicitWidth: 10
implicitHeight: 10
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: Appearance.colors.colPrimary
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
}
}
}
}
}
}
}
@@ -0,0 +1 @@
auth sufficient pam_fprintd.so
@@ -0,0 +1,249 @@
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: Mpris.players.values.filter(player => isRealPlayer(player))
readonly property var meaningfulPlayers: filterDuplicatePlayers(realPlayers)
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: []
property bool hasPlasmaIntegration: false
Process {
id: plasmaIntegrationAvailabilityCheckProc
running: true
command: ["bash", "-c", "command -v plasma-browser-integration-host"]
onExited: (exitCode, exitStatus) => {
root.hasPlasmaIntegration = (exitCode === 0);
}
}
function isRealPlayer(player) {
if (!Config.options.media.filterDuplicatePlayers) {
return true;
}
return (
// Remove unecessary native buses from browsers if there's plasma integration
!(hasPlasmaIntegration && player.dbusName.startsWith('org.mpris.MediaPlayer2.firefox')) && !(hasPlasmaIntegration && player.dbusName.startsWith('org.mpris.MediaPlayer2.chromium')) &&
// playerctld just copies other buses and we don't need duplicates
!player.dbusName?.startsWith('org.mpris.MediaPlayer2.playerctld') &&
// Non-instance mpd bus
!(player.dbusName?.endsWith('.mpd') && !player.dbusName.endsWith('MediaPlayer2.mpd')));
}
function filterDuplicatePlayers(players) {
let filtered = [];
let used = new Set();
for (let i = 0; i < players.length; ++i) {
if (used.has(i))
continue;
let p1 = players[i];
let group = [i];
// Find duplicates by trackTitle prefix
for (let j = i + 1; j < players.length; ++j) {
let p2 = players[j];
if (p1.trackTitle && p2.trackTitle && (p1.trackTitle.includes(p2.trackTitle) || p2.trackTitle.includes(p1.trackTitle)) || (p1.position - p2.position <= 2 && p1.length - p2.length <= 2)) {
group.push(j);
}
}
// Pick the one with non-empty trackArtUrl, or fallback to the first
let chosenIdx = group.find(idx => players[idx].trackArtUrl && players[idx].trackArtUrl.length > 0);
if (chosenIdx === undefined)
chosenIdx = group[0];
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 && Mpris.players.values.filter(player => isRealPlayer(player)).length === 0) {
GlobalStates.mediaControlsOpen = false;
}
}
sourceComponent: PanelWindow {
id: mediaControlsRoot
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 ? ((mediaControlsRoot.screen.height / 2) - widgetHeight * 1.5) : Appearance.sizes.barHeight
bottom: Appearance.sizes.barHeight
left: Config.options.bar.vertical ? Appearance.sizes.barHeight : ((mediaControlsRoot.screen.width / 2) - (osdWidth / 2) - widgetWidth)
right: Appearance.sizes.barHeight
}
mask: Region {
item: playerColumnLayout
}
HyprlandFocusGrab {
windows: [mediaControlsRoot]
active: mediaControlsLoader.active
onCleared: () => {
if (!active) {
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 (mediaControlsRoot.anchors.left) return Qt.AlignLeft;
if (mediaControlsRoot.anchors.right) return Qt.AlignRight;
return Qt.AlignHCenter;
}
Layout.leftMargin: Appearance.sizes.hyprlandGapsOut
Layout.rightMargin: Appearance.sizes.hyprlandGapsOut
visible: root.meaningfulPlayers.length === 0
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;
}
}
}
@@ -0,0 +1,315 @@
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 var artUrl: player?.trackArtUrl
property string artDownloadLocation: Directories.coverArt
property string artFileName: Qt.md5(artUrl)
property string artFilePath: `${artDownloadLocation}/${artFileName}`
property color artDominantColor: ColorUtils.mix((colorQuantizer?.colors[0] ?? Appearance.colors.colPrimary), Appearance.colors.colPrimaryContainer, 0.8) || Appearance.m3colors.m3secondaryContainer
property bool downloaded: false
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
property string displayedArtFilePath: root.downloaded ? Qt.resolvedUrl(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()
}
}
onArtFilePathChanged: {
if (root.artUrl.length == 0) {
root.artDominantColor = Appearance.m3colors.m3secondaryContainer
return;
}
// Binding does not work in Process
coverArtDownloader.targetFile = root.artUrl
coverArtDownloader.artFilePath = root.artFilePath
// Download
root.downloaded = false
coverArtDownloader.running = true
}
Process { // Cover art downloader
id: coverArtDownloader
property string targetFile: root.artUrl
property string artFilePath: root.artFilePath
command: [ "bash", "-c", `[ -f ${artFilePath} ] || curl -sSL '${targetFile}' -o '${artFilePath}'` ]
onExited: (exitCode, exitStatus) => {
root.downloaded = true
}
}
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
}
}
Image {
id: blurredArt
anchors.fill: parent
source: root.displayedArtFilePath
sourceSize.width: background.width
sourceSize.height: background.height
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
sourceSize.width: size
sourceSize.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.player?.trackTitle) || "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.player?.trackArtist
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)
}
}
}
}
}
}
}
}
@@ -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 => 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
}
}
}
@@ -0,0 +1,224 @@
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"
},
]
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 {
// 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;
}
}
}
@@ -0,0 +1,104 @@
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 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
}
}
}
}
}
@@ -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.active ? "routine" : "light_mode"
rotateIcon: true
scaleIcon: true
name: Translation.tr("Brightness")
value: root.brightnessMonitor?.brightness ?? 50
}
@@ -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")
}
@@ -0,0 +1,166 @@
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: {
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
}
// 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
}
}
}
@@ -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
}
}
}
}
}
}
@@ -0,0 +1,121 @@
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;
}
}
}
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
}
}
@@ -0,0 +1,312 @@
// 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: "AltGr", 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 }
]
]
}
}
@@ -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;
}
}
}
@@ -0,0 +1,8 @@
import QtQuick
import qs.modules.common
Rectangle {
id: contentItem
anchors.fill: parent
color: Appearance.colors.colSurfaceContainer
}
@@ -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 {
}
}
}
}
@@ -0,0 +1,42 @@
pragma Singleton
pragma ComponentBehavior: Bound
import Quickshell
Singleton {
id: root
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: "stickypad", 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)
}
}
}
@@ -0,0 +1,150 @@
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)
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
}
}
}
}
@@ -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.stickypad
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: "stickypad"; Stickypad {} }
DelegateChoice { roleValue: "volumeMixer"; VolumeMixer {} }
}
@@ -0,0 +1,327 @@
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();
}
// 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.colLayer1, (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.colLayer1
// 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
}
}
}
}
@@ -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
}
}
@@ -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
}
}
}
}
@@ -0,0 +1,95 @@
pragma ComponentBehavior: Bound
import QtQuick
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.width: width
sourceSize.height: height
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;
}
}
}
}
}
@@ -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
}
}
@@ -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();
}
}
}
}
@@ -0,0 +1,117 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import Quickshell
import qs
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: qsTr("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
}
}
}
@@ -0,0 +1,132 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Hyprland
import Qt5Compat.GraphicalEffects
import Qt.labs.synchronizer
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
}
}
}
}
@@ -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
}
}
}
}
@@ -0,0 +1,241 @@
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
Variants {
id: overviewVariants
model: Quickshell.screens
PanelWindow {
id: root
required property var modelData
property string searchingText: ""
readonly property HyprlandMonitor monitor: Hyprland.monitorFor(root.screen)
property bool monitorIsFocused: (Hyprland.focusedMonitor?.id == monitor?.id)
screen: modelData
visible: GlobalStates.overviewOpen
WlrLayershell.namespace: "quickshell:overview"
WlrLayershell.layer: WlrLayer.Overlay
// 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
}
HyprlandFocusGrab {
id: grab
windows: [root]
property bool canBeActive: root.monitorIsFocused
active: false
onCleared: () => {
if (!active)
GlobalStates.overviewOpen = false;
}
}
Connections {
target: GlobalStates
function onOverviewOpenChanged() {
if (!GlobalStates.overviewOpen) {
searchWidget.disableExpandAnimation();
overviewScope.dontAutoCancelSearch = false;
} else {
if (!overviewScope.dontAutoCancelSearch) {
searchWidget.cancelSearch();
}
delayedGrabTimer.start();
}
}
}
Timer {
id: delayedGrabTimer
interval: Config.options.hacks.arbitraryRaceConditionDelay
repeat: false
onTriggered: {
if (!grab.canBeActive)
return;
grab.active = GlobalStates.overviewOpen;
}
}
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;
} else if (event.key === Qt.Key_Left) {
if (!root.searchingText)
Hyprland.dispatch("workspace r-1");
} else if (event.key === Qt.Key_Right) {
if (!root.searchingText)
Hyprland.dispatch("workspace r+1");
}
}
SearchWidget {
id: searchWidget
anchors.horizontalCenter: parent.horizontalCenter
Synchronizer on searchingText {
property alias source: root.searchingText
}
}
Loader {
id: overviewLoader
anchors.horizontalCenter: parent.horizontalCenter
active: GlobalStates.overviewOpen && (Config?.options.overview.enable ?? true)
sourceComponent: OverviewWidget {
panelWindow: root
visible: (root.searchingText == "")
}
}
}
}
}
function toggleClipboard() {
if (GlobalStates.overviewOpen && overviewScope.dontAutoCancelSearch) {
GlobalStates.overviewOpen = false;
return;
}
for (let i = 0; i < overviewVariants.instances.length; i++) {
let panelWindow = overviewVariants.instances[i];
if (panelWindow.modelData.name == Hyprland.focusedMonitor.name) {
overviewScope.dontAutoCancelSearch = true;
panelWindow.setSearchingText(Config.options.search.prefix.clipboard);
GlobalStates.overviewOpen = true;
return;
}
}
}
function toggleEmojis() {
if (GlobalStates.overviewOpen && overviewScope.dontAutoCancelSearch) {
GlobalStates.overviewOpen = false;
return;
}
for (let i = 0; i < overviewVariants.instances.length; i++) {
let panelWindow = overviewVariants.instances[i];
if (panelWindow.modelData.name == Hyprland.focusedMonitor.name) {
overviewScope.dontAutoCancelSearch = true;
panelWindow.setSearchingText(Config.options.search.prefix.emojis);
GlobalStates.overviewOpen = true;
return;
}
}
}
IpcHandler {
target: "overview"
function toggle() {
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: "overviewToggle"
description: "Toggles overview on press"
onPressed: {
GlobalStates.overviewOpen = !GlobalStates.overviewOpen;
}
}
GlobalShortcut {
name: "overviewClose"
description: "Closes overview"
onPressed: {
GlobalStates.overviewOpen = false;
}
}
GlobalShortcut {
name: "overviewToggleRelease"
description: "Toggles overview on release"
onPressed: {
GlobalStates.superReleaseMightTrigger = true;
}
onReleased: {
if (!GlobalStates.superReleaseMightTrigger) {
GlobalStates.superReleaseMightTrigger = true;
return;
}
GlobalStates.overviewOpen = !GlobalStates.overviewOpen;
}
}
GlobalShortcut {
name: "overviewToggleReleaseInterrupt"
description: "Interrupts possibility of overview 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();
}
}
}
@@ -0,0 +1,329 @@
pragma ComponentBehavior: Bound
import qs
import qs.services
import qs.modules.common
import qs.modules.common.widgets
import qs.modules.common.functions
import Qt5Compat.GraphicalEffects
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Wayland
import Quickshell.Hyprland
Item {
id: root
required property var panelWindow
readonly property HyprlandMonitor monitor: Hyprland.monitorFor(panelWindow.screen)
readonly property var toplevels: ToplevelManager.toplevels
readonly property int workspacesShown: Config.options.overview.rows * Config.options.overview.columns
readonly property int workspaceGroup: Math.floor((monitor.activeWorkspace?.id - 1) / workspacesShown)
property bool monitorIsFocused: (Hyprland.focusedMonitor?.name == monitor.name)
property var windows: HyprlandData.windowList
property var windowByAddress: HyprlandData.windowByAddress
property var windowAddresses: HyprlandData.addresses
property var monitorData: HyprlandData.monitors.find(m => m.id === root.monitor?.id)
property real scale: Config.options.overview.scale
property color activeBorderColor: Appearance.colors.colSecondary
property real workspaceImplicitWidth: (monitorData?.transform % 2 === 1) ?
((monitor.height - monitorData?.reserved[0] - monitorData?.reserved[2]) * root.scale / monitor.scale) :
((monitor.width - monitorData?.reserved[0] - monitorData?.reserved[2]) * root.scale / monitor.scale)
property real workspaceImplicitHeight: (monitorData?.transform % 2 === 1) ?
((monitor.width - monitorData?.reserved[1] - monitorData?.reserved[3]) * root.scale / monitor.scale) :
((monitor.height - monitorData?.reserved[1] - monitorData?.reserved[3]) * root.scale / monitor.scale)
property real largeWorkspaceRadius: Appearance.rounding.large
property real smallWorkspaceRadius: Appearance.rounding.verysmall
property real workspaceNumberMargin: 80
property real workspaceNumberSize: 250 * monitor.scale
property int workspaceZ: 0
property int windowZ: 1
property int windowDraggingZ: 99999
property real workspaceSpacing: 5
property int draggingFromWorkspace: -1
property int draggingTargetWorkspace: -1
implicitWidth: overviewBackground.implicitWidth + Appearance.sizes.elevationMargin * 2
implicitHeight: overviewBackground.implicitHeight + Appearance.sizes.elevationMargin * 2
property Component windowComponent: OverviewWindow {}
property list<OverviewWindow> windowWidgets: []
StyledRectangularShadow {
target: overviewBackground
}
Rectangle { // Background
id: overviewBackground
property real padding: 10
anchors.fill: parent
anchors.margins: Appearance.sizes.elevationMargin
implicitWidth: workspaceColumnLayout.implicitWidth + padding * 2
implicitHeight: workspaceColumnLayout.implicitHeight + padding * 2
radius: root.largeWorkspaceRadius + padding
color: Appearance.colors.colBackgroundSurfaceContainer
Column { // Workspaces
id: workspaceColumnLayout
z: root.workspaceZ
anchors.centerIn: parent
spacing: workspaceSpacing
Repeater {
model: Config.options.overview.rows
delegate: Row {
id: row
required property int index
spacing: workspaceSpacing
Repeater { // Workspace repeater
model: Config.options.overview.columns
Rectangle { // Workspace
id: workspace
required property int index
property int colIndex: index
property int workspaceValue: root.workspaceGroup * root.workspacesShown + row.index * Config.options.overview.columns + colIndex + 1
property color defaultWorkspaceColor: ColorUtils.mix(Appearance.colors.colBackgroundSurfaceContainer, Appearance.colors.colSurfaceContainerHigh, 0.8)
property color hoveredWorkspaceColor: ColorUtils.mix(defaultWorkspaceColor, Appearance.colors.colLayer1Hover, 0.1)
property color hoveredBorderColor: Appearance.colors.colLayer2Hover
property bool hoveredWhileDragging: false
implicitWidth: root.workspaceImplicitWidth
implicitHeight: root.workspaceImplicitHeight
color: hoveredWhileDragging ? hoveredWorkspaceColor : defaultWorkspaceColor
property bool workspaceAtLeft: colIndex === 0
property bool workspaceAtRight: colIndex === Config.options.overview.columns - 1
property bool workspaceAtTop: row.index === 0
property bool workspaceAtBottom: row.index === Config.options.overview.rows - 1
topLeftRadius: (workspaceAtLeft && workspaceAtTop) ? root.largeWorkspaceRadius : root.smallWorkspaceRadius
topRightRadius: (workspaceAtRight && workspaceAtTop) ? root.largeWorkspaceRadius : root.smallWorkspaceRadius
bottomLeftRadius: (workspaceAtLeft && workspaceAtBottom) ? root.largeWorkspaceRadius : root.smallWorkspaceRadius
bottomRightRadius: (workspaceAtRight && workspaceAtBottom) ? root.largeWorkspaceRadius : root.smallWorkspaceRadius
border.width: 2
border.color: hoveredWhileDragging ? hoveredBorderColor : "transparent"
StyledText {
anchors.centerIn: parent
text: workspace.workspaceValue
font {
pixelSize: root.workspaceNumberSize * root.scale
weight: Font.DemiBold
family: Appearance.font.family.expressive
}
color: ColorUtils.transparentize(Appearance.colors.colOnLayer1, 0.8)
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
MouseArea {
id: workspaceArea
anchors.fill: parent
acceptedButtons: Qt.LeftButton
onPressed: {
if (root.draggingTargetWorkspace === -1) {
GlobalStates.overviewOpen = false
Hyprland.dispatch(`workspace ${workspace.workspaceValue}`)
}
}
}
DropArea {
anchors.fill: parent
onEntered: {
root.draggingTargetWorkspace = workspace.workspaceValue
if (root.draggingFromWorkspace == root.draggingTargetWorkspace) return;
hoveredWhileDragging = true
}
onExited: {
hoveredWhileDragging = false
if (root.draggingTargetWorkspace == workspace.workspaceValue) root.draggingTargetWorkspace = -1
}
}
}
}
}
}
}
Item { // Windows & focused workspace indicator
id: windowSpace
anchors.centerIn: parent
implicitWidth: workspaceColumnLayout.implicitWidth
implicitHeight: workspaceColumnLayout.implicitHeight
Repeater { // Window repeater
model: ScriptModel {
values: {
// console.log(JSON.stringify(ToplevelManager.toplevels.values.map(t => t), null, 2))
return [...ToplevelManager.toplevels.values.filter((toplevel) => {
const address = `0x${toplevel.HyprlandToplevel?.address}`
var win = windowByAddress[address]
const inWorkspaceGroup = (root.workspaceGroup * root.workspacesShown < win?.workspace?.id && win?.workspace?.id <= (root.workspaceGroup + 1) * root.workspacesShown)
return inWorkspaceGroup;
})].reverse()
}
}
delegate: OverviewWindow {
id: window
required property var modelData
property int monitorId: windowData?.monitor
property var monitor: HyprlandData.monitors.find(m => m.id == monitorId)
property var address: `0x${modelData.HyprlandToplevel.address}`
toplevel: modelData
monitorData: this.monitor
scale: root.scale
widgetMonitor: HyprlandData.monitors.find(m => m.id == root.monitor.id)
windowData: windowByAddress[address]
property bool atInitPosition: (initX == x && initY == y)
// Offset on the canvas
property int workspaceColIndex: (windowData?.workspace.id - 1) % Config.options.overview.columns
property int workspaceRowIndex: Math.floor((windowData?.workspace.id - 1) % root.workspacesShown / Config.options.overview.columns)
xOffset: (root.workspaceImplicitWidth + workspaceSpacing) * workspaceColIndex
yOffset: (root.workspaceImplicitHeight + workspaceSpacing) * workspaceRowIndex
property real xWithinWorkspaceWidget: Math.max((windowData?.at[0] - (monitor?.x ?? 0) - monitorData?.reserved[0]) * root.scale, 0)
property real yWithinWorkspaceWidget: Math.max((windowData?.at[1] - (monitor?.y ?? 0) - monitorData?.reserved[1]) * root.scale, 0)
// Radius
property real minRadius: Appearance.rounding.small
property bool workspaceAtLeft: workspaceColIndex === 0
property bool workspaceAtRight: workspaceColIndex === Config.options.overview.columns - 1
property bool workspaceAtTop: workspaceRowIndex === 0
property bool workspaceAtBottom: workspaceRowIndex === Config.options.overview.rows - 1
property bool workspaceAtTopLeft: (workspaceAtLeft && workspaceAtTop)
property bool workspaceAtTopRight: (workspaceAtRight && workspaceAtTop)
property bool workspaceAtBottomLeft: (workspaceAtLeft && workspaceAtBottom)
property bool workspaceAtBottomRight: (workspaceAtRight && workspaceAtBottom)
property real distanceFromLeftEdge: xWithinWorkspaceWidget
property real distanceFromRightEdge: root.workspaceImplicitWidth - (xWithinWorkspaceWidget + targetWindowWidth)
property real distanceFromTopEdge: yWithinWorkspaceWidget
property real distanceFromBottomEdge: root.workspaceImplicitHeight - (yWithinWorkspaceWidget + targetWindowHeight)
property real distanceFromTopLeftCorner: Math.max(distanceFromLeftEdge, distanceFromTopEdge)
property real distanceFromTopRightCorner: Math.max(distanceFromRightEdge, distanceFromTopEdge)
property real distanceFromBottomLeftCorner: Math.max(distanceFromLeftEdge, distanceFromBottomEdge)
property real distanceFromBottomRightCorner: Math.max(distanceFromRightEdge, distanceFromBottomEdge)
topLeftRadius: Math.max((workspaceAtTopLeft ? root.largeWorkspaceRadius : root.smallWorkspaceRadius) - distanceFromTopLeftCorner, minRadius)
topRightRadius: Math.max((workspaceAtTopRight ? root.largeWorkspaceRadius : root.smallWorkspaceRadius) - distanceFromTopRightCorner, minRadius)
bottomLeftRadius: Math.max((workspaceAtBottomLeft ? root.largeWorkspaceRadius : root.smallWorkspaceRadius) - distanceFromBottomLeftCorner, minRadius)
bottomRightRadius: Math.max((workspaceAtBottomRight ? root.largeWorkspaceRadius : root.smallWorkspaceRadius) - distanceFromBottomRightCorner, minRadius)
Timer {
id: updateWindowPosition
interval: Config.options.hacks.arbitraryRaceConditionDelay
repeat: false
running: false
onTriggered: {
window.x = Math.round(xWithinWorkspaceWidget + xOffset)
window.y = Math.round(yWithinWorkspaceWidget + yOffset)
}
}
z: Drag.active ? root.windowDraggingZ : (root.windowZ + windowData?.floating)
Drag.hotSpot.x: width / 2
Drag.hotSpot.y: height / 2
MouseArea {
id: dragArea
anchors.fill: parent
hoverEnabled: true
onEntered: hovered = true // For hover color change
onExited: hovered = false // For hover color change
acceptedButtons: Qt.LeftButton | Qt.MiddleButton
drag.target: parent
onPressed: (mouse) => {
root.draggingFromWorkspace = windowData?.workspace.id
window.pressed = true
window.Drag.active = true
window.Drag.source = window
window.Drag.hotSpot.x = mouse.x
window.Drag.hotSpot.y = mouse.y
// console.log(`[OverviewWindow] Dragging window ${windowData?.address} from position (${window.x}, ${window.y})`)
}
onReleased: {
const targetWorkspace = root.draggingTargetWorkspace
window.pressed = false
window.Drag.active = false
root.draggingFromWorkspace = -1
if (targetWorkspace !== -1 && targetWorkspace !== windowData?.workspace.id) {
Hyprland.dispatch(`movetoworkspacesilent ${targetWorkspace}, address:${window.windowData?.address}`)
updateWindowPosition.restart()
}
else {
if (!window.windowData.floating) {
updateWindowPosition.restart()
return
}
const percentageX = Math.round((window.x - xOffset) / root.workspaceImplicitWidth * 100)
const percentageY = Math.round((window.y - yOffset) / root.workspaceImplicitHeight * 100)
Hyprland.dispatch(`movewindowpixel exact ${percentageX}% ${percentageY}%, address:${window.windowData?.address}`)
}
}
onClicked: (event) => {
if (!windowData) return;
if (event.button === Qt.LeftButton) {
GlobalStates.overviewOpen = false
Hyprland.dispatch(`focuswindow address:${windowData.address}`)
event.accepted = true
} else if (event.button === Qt.MiddleButton) {
Hyprland.dispatch(`closewindow address:${windowData.address}`)
event.accepted = true
}
}
StyledToolTip {
extraVisibleCondition: false
alternativeVisibleCondition: dragArea.containsMouse && !window.Drag.active
text: `${windowData.title}\n[${windowData.class}] ${windowData.xwayland ? "[XWayland] " : ""}`
}
}
}
}
Rectangle { // Focused workspace indicator
id: focusedWorkspaceIndicator
property int activeWorkspaceInGroup: monitor.activeWorkspace?.id - (root.workspaceGroup * root.workspacesShown)
property int rowIndex: Math.floor((activeWorkspaceInGroup - 1) / Config.options.overview.columns)
property int colIndex: (activeWorkspaceInGroup - 1) % Config.options.overview.columns
x: (root.workspaceImplicitWidth + workspaceSpacing) * colIndex
y: (root.workspaceImplicitHeight + workspaceSpacing) * rowIndex
z: root.windowZ
width: root.workspaceImplicitWidth
height: root.workspaceImplicitHeight
color: "transparent"
property bool workspaceAtLeft: colIndex === 0
property bool workspaceAtRight: colIndex === Config.options.overview.columns - 1
property bool workspaceAtTop: rowIndex === 0
property bool workspaceAtBottom: rowIndex === Config.options.overview.rows - 1
topLeftRadius: (workspaceAtLeft && workspaceAtTop) ? root.largeWorkspaceRadius : root.smallWorkspaceRadius
topRightRadius: (workspaceAtRight && workspaceAtTop) ? root.largeWorkspaceRadius : root.smallWorkspaceRadius
bottomLeftRadius: (workspaceAtLeft && workspaceAtBottom) ? root.largeWorkspaceRadius : root.smallWorkspaceRadius
bottomRightRadius: (workspaceAtRight && workspaceAtBottom) ? root.largeWorkspaceRadius : root.smallWorkspaceRadius
border.width: 2
border.color: root.activeBorderColor
Behavior on x {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
}
Behavior on y {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
}
Behavior on topLeftRadius {
animation: Appearance.animation.elementMoveEnter.numberAnimation.createObject(this)
}
Behavior on topRightRadius {
animation: Appearance.animation.elementMoveEnter.numberAnimation.createObject(this)
}
Behavior on bottomLeftRadius {
animation: Appearance.animation.elementMoveEnter.numberAnimation.createObject(this)
}
Behavior on bottomRightRadius {
animation: Appearance.animation.elementMoveEnter.numberAnimation.createObject(this)
}
}
}
}
}
@@ -0,0 +1,144 @@
pragma ComponentBehavior: Bound
import qs
import qs.services
import qs.modules.common
import qs.modules.common.functions
import Qt5Compat.GraphicalEffects
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Wayland
Item { // Window
id: root
property var toplevel
property var windowData
property var monitorData
property var scale
property bool restrictToWorkspace: true
property real widthRatio: {
const widgetWidth = widgetMonitor.transform & 1 ? widgetMonitor.height : widgetMonitor.width;
const monitorWidth = monitorData.transform & 1 ? monitorData.height : monitorData.width;
return (widgetWidth * monitorData.scale) / (monitorWidth * widgetMonitor.scale);
}
property real heightRatio: {
const widgetHeight = widgetMonitor.transform & 1 ? widgetMonitor.width : widgetMonitor.height;
const monitorHeight = monitorData.transform & 1 ? monitorData.width : monitorData.height;
return (widgetHeight * monitorData.scale) / (monitorHeight * widgetMonitor.scale);
}
property real initX: {
return Math.max((windowData?.at[0] - (monitorData?.x ?? 0) - monitorData?.reserved[0]) * widthRatio * root.scale, 0) + xOffset;
}
property real initY: {
return Math.max((windowData?.at[1] - (monitorData?.y ?? 0) - monitorData?.reserved[1]) * heightRatio * root.scale, 0) + yOffset;
}
property real xOffset: 0
property real yOffset: 0
property var widgetMonitor
property int widgetMonitorId: widgetMonitor.id
property var targetWindowWidth: windowData?.size[0] * scale * widthRatio
property var targetWindowHeight: windowData?.size[1] * scale * heightRatio
property bool hovered: false
property bool pressed: false
property bool centerIcons: Config.options.overview.centerIcons
property real iconGapRatio: 0.06
property real iconToWindowRatio: centerIcons ? 0.35 : 0.15
property real xwaylandIndicatorToIconRatio: 0.35
property real iconToWindowRatioCompact: 0.6
property string iconPath: Quickshell.iconPath(AppSearch.guessIcon(windowData?.class), "image-missing")
property bool compactMode: Appearance.font.pixelSize.smaller * 4 > targetWindowHeight || Appearance.font.pixelSize.smaller * 4 > targetWindowWidth
property bool indicateXWayland: windowData?.xwayland ?? false
x: initX
y: initY
width: targetWindowWidth
height: targetWindowHeight
opacity: windowData.monitor == widgetMonitorId ? 1 : 0.4
property real topLeftRadius
property real topRightRadius
property real bottomLeftRadius
property real bottomRightRadius
layer.enabled: true
layer.effect: OpacityMask {
maskSource: Rectangle {
width: root.width
height: root.height
topLeftRadius: root.topLeftRadius
topRightRadius: root.topRightRadius
bottomRightRadius: root.bottomRightRadius
bottomLeftRadius: root.bottomLeftRadius
}
}
Behavior on x {
animation: Appearance.animation.elementMoveEnter.numberAnimation.createObject(this)
}
Behavior on y {
animation: Appearance.animation.elementMoveEnter.numberAnimation.createObject(this)
}
Behavior on width {
animation: Appearance.animation.elementMoveEnter.numberAnimation.createObject(this)
}
Behavior on height {
animation: Appearance.animation.elementMoveEnter.numberAnimation.createObject(this)
}
ScreencopyView {
id: windowPreview
anchors.fill: parent
captureSource: GlobalStates.overviewOpen ? root.toplevel : null
live: true
// Color overlay for interactions
Rectangle {
anchors.fill: parent
topLeftRadius: root.topLeftRadius
topRightRadius: root.topRightRadius
bottomRightRadius: root.bottomRightRadius
bottomLeftRadius: root.bottomLeftRadius
color: pressed ? ColorUtils.transparentize(Appearance.colors.colLayer2Active, 0.5) :
hovered ? ColorUtils.transparentize(Appearance.colors.colLayer2Hover, 0.7) :
ColorUtils.transparentize(Appearance.colors.colLayer2)
border.color : ColorUtils.transparentize(Appearance.m3colors.m3outline, 0.88)
border.width : 1
}
Image {
id: windowIcon
property real baseSize: Math.min(root.targetWindowWidth, root.targetWindowHeight)
anchors {
top: root.centerIcons ? undefined : parent.top
left: root.centerIcons ? undefined : parent.left
centerIn: root.centerIcons ? parent : undefined
margins: baseSize * root.iconGapRatio
}
property var iconSize: {
// console.log("-=-=-", root.toplevel.title, "-=-=-")
// console.log("Target window size:", targetWindowWidth, targetWindowHeight)
// console.log("Icon ratio:", root.compactMode ? root.iconToWindowRatioCompact : root.iconToWindowRatio)
// console.log("Scale:", root.monitorData.scale)
// console.log("Final:", Math.min(targetWindowWidth, targetWindowHeight) * (root.compactMode ? root.iconToWindowRatioCompact : root.iconToWindowRatio) / root.monitorData.scale)
return baseSize * (root.compactMode ? root.iconToWindowRatioCompact : root.iconToWindowRatio);
}
// mipmap: true
Layout.alignment: Qt.AlignHCenter
source: root.iconPath
width: iconSize
height: iconSize
sourceSize: Qt.size(iconSize, iconSize)
Behavior on width {
animation: Appearance.animation.elementMoveEnter.numberAnimation.createObject(this)
}
Behavior on height {
animation: Appearance.animation.elementMoveEnter.numberAnimation.createObject(this)
}
}
}
}
@@ -0,0 +1,152 @@
import qs
import qs.services
import qs.modules.common
import qs.modules.common.widgets
import qs.modules.common.functions
import Qt5Compat.GraphicalEffects
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import Quickshell.Hyprland
RowLayout {
id: root
spacing: 6
property bool animateWidth: false
property alias searchInput: searchInput
property string searchingText
function forceFocus() {
searchInput.forceActiveFocus();
}
enum SearchPrefixType { Action, App, Clipboard, Emojis, Math, ShellCommand, WebSearch, DefaultSearch }
property var searchPrefixType: {
if (root.searchingText.startsWith(Config.options.search.prefix.action)) return SearchBar.SearchPrefixType.Action;
if (root.searchingText.startsWith(Config.options.search.prefix.app)) return SearchBar.SearchPrefixType.App;
if (root.searchingText.startsWith(Config.options.search.prefix.clipboard)) return SearchBar.SearchPrefixType.Clipboard;
if (root.searchingText.startsWith(Config.options.search.prefix.emojis)) return SearchBar.SearchPrefixType.Emojis;
if (root.searchingText.startsWith(Config.options.search.prefix.math)) return SearchBar.SearchPrefixType.Math;
if (root.searchingText.startsWith(Config.options.search.prefix.shellCommand)) return SearchBar.SearchPrefixType.ShellCommand;
if (root.searchingText.startsWith(Config.options.search.prefix.webSearch)) return SearchBar.SearchPrefixType.WebSearch;
return SearchBar.SearchPrefixType.DefaultSearch;
}
MaterialShapeWrappedMaterialSymbol {
id: searchIcon
Layout.alignment: Qt.AlignVCenter
iconSize: Appearance.font.pixelSize.huge
shape: switch(root.searchPrefixType) {
case SearchBar.SearchPrefixType.Action: return MaterialShape.Shape.Pill;
case SearchBar.SearchPrefixType.App: return MaterialShape.Shape.Clover4Leaf;
case SearchBar.SearchPrefixType.Clipboard: return MaterialShape.Shape.Gem;
case SearchBar.SearchPrefixType.Emojis: return MaterialShape.Shape.Sunny;
case SearchBar.SearchPrefixType.Math: return MaterialShape.Shape.PuffyDiamond;
case SearchBar.SearchPrefixType.ShellCommand: return MaterialShape.Shape.PixelCircle;
case SearchBar.SearchPrefixType.WebSearch: return MaterialShape.Shape.SoftBurst;
default: return MaterialShape.Shape.Cookie7Sided;
}
text: switch (root.searchPrefixType) {
case SearchBar.SearchPrefixType.Action: return "settings_suggest";
case SearchBar.SearchPrefixType.App: return "apps";
case SearchBar.SearchPrefixType.Clipboard: return "content_paste_search";
case SearchBar.SearchPrefixType.Emojis: return "add_reaction";
case SearchBar.SearchPrefixType.Math: return "calculate";
case SearchBar.SearchPrefixType.ShellCommand: return "terminal";
case SearchBar.SearchPrefixType.WebSearch: return "travel_explore";
case SearchBar.SearchPrefixType.DefaultSearch: return "search";
default: return "search";
}
}
ToolbarTextField { // Search box
id: searchInput
Layout.topMargin: 4
Layout.bottomMargin: 4
implicitHeight: 40
focus: GlobalStates.overviewOpen
font.pixelSize: Appearance.font.pixelSize.small
placeholderText: Translation.tr("Search, calculate or run")
implicitWidth: root.searchingText == "" ? Appearance.sizes.searchWidthCollapsed : Appearance.sizes.searchWidth
Behavior on implicitWidth {
id: searchWidthBehavior
enabled: root.animateWidth
NumberAnimation {
duration: 300
easing.type: Appearance.animation.elementMove.type
easing.bezierCurve: Appearance.animation.elementMove.bezierCurve
}
}
onTextChanged: root.searchingText = text
onAccepted: {
if (appResults.count > 0) {
// Get the first visible delegate and trigger its click
let firstItem = appResults.itemAtIndex(0);
if (firstItem && firstItem.clicked) {
firstItem.clicked();
}
}
}
}
IconToolbarButton {
Layout.topMargin: 4
Layout.bottomMargin: 4
onClicked: {
GlobalStates.overviewOpen = false;
Quickshell.execDetached(["qs", "-p", Quickshell.shellPath(""), "ipc", "call", "region", "search"]);
}
text: "image_search"
StyledToolTip {
text: Translation.tr("Google Lens")
}
}
IconToolbarButton {
id: songRecButton
Layout.topMargin: 4
Layout.bottomMargin: 4
Layout.rightMargin: 4
toggled: SongRec.running
onClicked: SongRec.toggleRunning()
text: "music_cast"
StyledToolTip {
text: Translation.tr("Recognize music")
}
colText: toggled ? Appearance.colors.colOnPrimary : Appearance.colors.colOnSurfaceVariant
background: MaterialShape {
RotationAnimation on rotation {
running: songRecButton.toggled
duration: 12000
easing.type: Easing.Linear
loops: Animation.Infinite
from: 0
to: 360
}
shape: {
if (songRecButton.down) {
return songRecButton.toggled ? MaterialShape.Shape.Circle : MaterialShape.Shape.Square
} else {
return songRecButton.toggled ? MaterialShape.Shape.SoftBurst : MaterialShape.Shape.Circle
}
}
color: {
if (songRecButton.toggled) {
return songRecButton.hovered ? Appearance.colors.colPrimaryHover : Appearance.colors.colPrimary
} else {
return songRecButton.hovered ? Appearance.colors.colSurfaceContainerHigh : ColorUtils.transparentize(Appearance.colors.colSurfaceContainerHigh)
}
}
Behavior on color {
animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this)
}
}
}
}
@@ -0,0 +1,286 @@
// pragma NativeMethodBehavior: AcceptThisObject
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.Widgets
import Quickshell.Hyprland
RippleButton {
id: root
property var entry
property string query
property bool entryShown: entry?.shown ?? true
property string itemType: entry?.type ?? Translation.tr("App")
property string itemName: entry?.name ?? ""
property string itemIcon: entry?.icon ?? ""
property var itemExecute: entry?.execute
property string fontType: entry?.fontType ?? "main"
property string itemClickActionName: entry?.clickActionName ?? "Open"
property string bigText: entry?.bigText ?? ""
property string materialSymbol: entry?.materialSymbol ?? ""
property string cliphistRawString: entry?.cliphistRawString ?? ""
property bool blurImage: entry?.blurImage ?? false
property string blurImageText: entry?.blurImageText ?? "Image hidden"
visible: root.entryShown
property int horizontalMargin: 10
property int buttonHorizontalPadding: 10
property int buttonVerticalPadding: 6
property bool keyboardDown: false
implicitHeight: rowLayout.implicitHeight + root.buttonVerticalPadding * 2
implicitWidth: rowLayout.implicitWidth + root.buttonHorizontalPadding * 2
buttonRadius: Appearance.rounding.normal
colBackground: (root.down || root.keyboardDown) ? Appearance.colors.colPrimaryContainerActive :
((root.hovered || root.focus) ? Appearance.colors.colPrimaryContainer :
ColorUtils.transparentize(Appearance.colors.colPrimaryContainer, 1))
colBackgroundHover: Appearance.colors.colPrimaryContainer
colRipple: Appearance.colors.colPrimaryContainerActive
property string highlightPrefix: `<u><font color="${Appearance.colors.colPrimary}">`
property string highlightSuffix: `</font></u>`
function highlightContent(content, query) {
if (!query || query.length === 0 || content == query || fontType === "monospace")
return StringUtils.escapeHtml(content);
let contentLower = content.toLowerCase();
let queryLower = query.toLowerCase();
let result = "";
let lastIndex = 0;
let qIndex = 0;
for (let i = 0; i < content.length && qIndex < query.length; i++) {
if (contentLower[i] === queryLower[qIndex]) {
// Add non-highlighted part (escaped)
if (i > lastIndex)
result += StringUtils.escapeHtml(content.slice(lastIndex, i));
// Add highlighted character (escaped)
result += root.highlightPrefix + StringUtils.escapeHtml(content[i]) + root.highlightSuffix;
lastIndex = i + 1;
qIndex++;
}
}
// Add the rest of the string (escaped)
if (lastIndex < content.length)
result += StringUtils.escapeHtml(content.slice(lastIndex));
return result;
}
property string displayContent: highlightContent(root.itemName, root.query)
property list<string> urls: {
if (!root.itemName) return [];
// Regular expression to match URLs
const urlRegex = /https?:\/\/[^\s<>"{}|\\^`[\]]+/gi;
const matches = root.itemName?.match(urlRegex)
?.filter(url => !url.includes("…")) // Elided = invalid
return matches ? matches : [];
}
PointingHandInteraction {}
background {
anchors.fill: root
anchors.leftMargin: root.horizontalMargin
anchors.rightMargin: root.horizontalMargin
}
onClicked: {
GlobalStates.overviewOpen = false
root.itemExecute()
}
Keys.onPressed: (event) => {
if (event.key === Qt.Key_Delete && event.modifiers === Qt.ShiftModifier) {
const deleteAction = root.entry.actions.find(action => action.name == "Delete");
if (deleteAction) {
deleteAction.execute()
}
} else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
root.keyboardDown = true
root.clicked()
event.accepted = true;
}
}
Keys.onReleased: (event) => {
if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
root.keyboardDown = false
event.accepted = true;
}
}
RowLayout {
id: rowLayout
spacing: iconLoader.sourceComponent === null ? 0 : 10
anchors.fill: parent
anchors.leftMargin: root.horizontalMargin + root.buttonHorizontalPadding
anchors.rightMargin: root.horizontalMargin + root.buttonHorizontalPadding
// Icon
Loader {
id: iconLoader
active: true
sourceComponent: root.materialSymbol !== "" ? materialSymbolComponent :
root.bigText ? bigTextComponent :
root.itemIcon !== "" ? iconImageComponent :
null
}
Component {
id: iconImageComponent
IconImage {
source: Quickshell.iconPath(root.itemIcon, "image-missing")
width: 35
height: 35
}
}
Component {
id: materialSymbolComponent
MaterialSymbol {
text: root.materialSymbol
iconSize: 30
color: Appearance.m3colors.m3onSurface
}
}
Component {
id: bigTextComponent
StyledText {
text: root.bigText
font.pixelSize: Appearance.font.pixelSize.larger
color: Appearance.m3colors.m3onSurface
}
}
// Main text
ColumnLayout {
id: contentColumn
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
spacing: 0
StyledText {
font.pixelSize: Appearance.font.pixelSize.smaller
color: Appearance.colors.colSubtext
visible: root.itemType && root.itemType != Translation.tr("App")
text: root.itemType
}
RowLayout {
Loader { // Checkmark for copied clipboard entry
visible: itemName == Quickshell.clipboardText && root.cliphistRawString
active: itemName == Quickshell.clipboardText && root.cliphistRawString
sourceComponent: Rectangle {
implicitWidth: activeText.implicitHeight
implicitHeight: activeText.implicitHeight
radius: Appearance.rounding.full
color: Appearance.colors.colPrimary
MaterialSymbol {
id: activeText
anchors.centerIn: parent
text: "check"
font.pixelSize: Appearance.font.pixelSize.normal
color: Appearance.m3colors.m3onPrimary
}
}
}
Repeater { // Favicons for links
model: root.query == root.itemName ? [] : root.urls
Favicon {
required property var modelData
size: parent.height
url: modelData
}
}
StyledText { // Item name/content
Layout.fillWidth: true
id: nameText
textFormat: Text.StyledText // RichText also works, but StyledText ensures elide work
font.pixelSize: Appearance.font.pixelSize.small
font.family: Appearance.font.family[root.fontType]
color: Appearance.m3colors.m3onSurface
horizontalAlignment: Text.AlignLeft
elide: Text.ElideRight
text: `${root.displayContent}`
}
}
Loader { // Clipboard image preview
active: root.cliphistRawString && Cliphist.entryIsImage(root.cliphistRawString)
sourceComponent: CliphistImage {
Layout.fillWidth: true
entry: root.cliphistRawString
maxWidth: contentColumn.width
maxHeight: 140
blur: root.blurImage
blurText: root.blurImageText
}
}
}
// Action text
StyledText {
Layout.fillWidth: false
visible: (root.hovered || root.focus)
id: clickAction
font.pixelSize: Appearance.font.pixelSize.normal
color: Appearance.colors.colOnPrimaryContainer
horizontalAlignment: Text.AlignRight
text: root.itemClickActionName
}
RowLayout {
Layout.alignment: Qt.AlignTop
Layout.topMargin: root.buttonVerticalPadding
Layout.bottomMargin: -root.buttonVerticalPadding // Why is this necessary? Good question.
spacing: 4
Repeater {
model: (root.entry.actions ?? []).slice(0, 4)
delegate: RippleButton {
id: actionButton
required property var modelData
property string iconName: modelData.icon ?? ""
property string materialIconName: modelData.materialIcon ?? ""
implicitHeight: 34
implicitWidth: 34
colBackgroundHover: Appearance.colors.colSecondaryContainerHover
colRipple: Appearance.colors.colSecondaryContainerActive
contentItem: Item {
id: actionContentItem
anchors.centerIn: parent
Loader {
anchors.centerIn: parent
active: !(actionButton.iconName !== "") || actionButton.materialIconName
sourceComponent: MaterialSymbol {
text: actionButton.materialIconName || "video_settings"
font.pixelSize: Appearance.font.pixelSize.hugeass
color: Appearance.m3colors.m3onSurface
}
}
Loader {
anchors.centerIn: parent
active: actionButton.materialIconName.length == 0 && actionButton.iconName && actionButton.iconName !== ""
sourceComponent: IconImage {
source: Quickshell.iconPath(actionButton.iconName)
implicitSize: 20
}
}
}
onClicked: modelData.execute()
StyledToolTip {
text: modelData.name
}
}
}
}
}
}
@@ -0,0 +1,471 @@
import qs
import qs.services
import qs.modules.common
import qs.modules.common.widgets
import qs.modules.common.functions
import Qt.labs.synchronizer
import Qt5Compat.GraphicalEffects
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Io
Item { // Wrapper
id: root
readonly property string xdgConfigHome: Directories.config
property string searchingText: ""
property bool showResults: searchingText != ""
implicitWidth: searchWidgetContent.implicitWidth + Appearance.sizes.elevationMargin * 2
implicitHeight: searchBar.implicitHeight + searchBar.verticalPadding * 2 + Appearance.sizes.elevationMargin * 2
property string mathResult: ""
property bool clipboardWorkSafetyActive: {
const enabled = Config.options.workSafety.enable.clipboard;
const sensitiveNetwork = (StringUtils.stringListContainsSubstring(Network.networkName.toLowerCase(), Config.options.workSafety.triggerCondition.networkNameKeywords))
return enabled && sensitiveNetwork;
}
property var searchActions: [
{
action: "accentcolor",
execute: args => {
Quickshell.execDetached([Directories.wallpaperSwitchScriptPath, "--noswitch", "--color", ...(args != '' ? [`${args}`] : [])]);
}
},
{
action: "dark",
execute: () => {
Quickshell.execDetached([Directories.wallpaperSwitchScriptPath, "--mode", "dark", "--noswitch"]);
}
},
{
action: "konachanwallpaper",
execute: () => {
Quickshell.execDetached([Quickshell.shellPath("scripts/colors/random/random_konachan_wall.sh")]);
}
},
{
action: "light",
execute: () => {
Quickshell.execDetached([Directories.wallpaperSwitchScriptPath, "--mode", "light", "--noswitch"]);
}
},
{
action: "superpaste",
execute: args => {
if (!/^(\d+)/.test(args.trim())) { // Invalid if doesn't start with numbers
Quickshell.execDetached([
"notify-send",
Translation.tr("Superpaste"),
Translation.tr("Usage: <tt>%1superpaste NUM_OF_ENTRIES[i]</tt>\nSupply <tt>i</tt> when you want images\nExamples:\n<tt>%1superpaste 4i</tt> for the last 4 images\n<tt>%1superpaste 7</tt> for the last 7 entries").arg(Config.options.search.prefix.action),
"-a", "Shell"
]);
return;
}
const syntaxMatch = /^(?:(\d+)(i)?)/.exec(args.trim());
const count = syntaxMatch[1] ? parseInt(syntaxMatch[1]) : 1;
const isImage = !!syntaxMatch[2];
Cliphist.superpaste(count, isImage);
}
},
{
action: "todo",
execute: args => {
Todo.addTask(args);
}
},
{
action: "wallpaper",
execute: () => {
GlobalStates.wallpaperSelectorOpen = true;
}
},
{
action: "wipeclipboard",
execute: () => {
Cliphist.wipe();
}
},
]
function focusFirstItem() {
appResults.currentIndex = 0;
}
function focusSearchInput() {
searchBar.forceFocus();
}
function disableExpandAnimation() {
searchBar.animateWidth = false;
}
function cancelSearch() {
searchBar.searchInput.selectAll();
root.searchingText = "";
searchBar.animateWidth = true;
}
function setSearchingText(text) {
searchBar.searchInput.text = text;
root.searchingText = text;
}
function containsUnsafeLink(entry) {
if (entry == undefined) return false;
const unsafeKeywords = Config.options.workSafety.triggerCondition.linkKeywords;
return StringUtils.stringListContainsSubstring(entry.toLowerCase(), unsafeKeywords);
}
Timer {
id: nonAppResultsTimer
interval: Config.options.search.nonAppResultDelay
onTriggered: {
let expr = root.searchingText;
if (expr.startsWith(Config.options.search.prefix.math)) {
expr = expr.slice(Config.options.search.prefix.math.length);
}
mathProcess.calculateExpression(expr);
}
}
Process {
id: mathProcess
property list<string> baseCommand: ["qalc", "-t"]
function calculateExpression(expression) {
mathProcess.running = false;
mathProcess.command = baseCommand.concat(expression);
mathProcess.running = true;
}
stdout: SplitParser {
onRead: data => {
root.mathResult = data;
root.focusFirstItem();
}
}
}
Keys.onPressed: event => {
// Prevent Esc and Backspace from registering
if (event.key === Qt.Key_Escape)
return;
// Handle Backspace: focus and delete character if not focused
if (event.key === Qt.Key_Backspace) {
if (!searchBar.searchInput.activeFocus) {
root.focusSearchInput();
if (event.modifiers & Qt.ControlModifier) {
// Delete word before cursor
let text = searchBar.searchInput.text;
let pos = searchBar.searchInput.cursorPosition;
if (pos > 0) {
// Find the start of the previous word
let left = text.slice(0, pos);
let match = left.match(/(\s*\S+)\s*$/);
let deleteLen = match ? match[0].length : 1;
searchBar.searchInput.text = text.slice(0, pos - deleteLen) + text.slice(pos);
searchBar.searchInput.cursorPosition = pos - deleteLen;
}
} else {
// Delete character before cursor if any
if (searchBar.searchInput.cursorPosition > 0) {
searchBar.searchInput.text = searchBar.searchInput.text.slice(0, searchBar.searchInput.cursorPosition - 1) + searchBar.searchInput.text.slice(searchBar.searchInput.cursorPosition);
searchBar.searchInput.cursorPosition -= 1;
}
}
// Always move cursor to end after programmatic edit
searchBar.searchInput.cursorPosition = searchBar.searchInput.text.length;
event.accepted = true;
}
// If already focused, let TextField handle it
return;
}
// Only handle visible printable characters (ignore control chars, arrows, etc.)
if (event.text && event.text.length === 1 && event.key !== Qt.Key_Enter && event.key !== Qt.Key_Return && event.key !== Qt.Key_Delete && event.text.charCodeAt(0) >= 0x20) // ignore control chars like Backspace, Tab, etc.
{
if (!searchBar.searchInput.activeFocus) {
root.focusSearchInput();
// Insert the character at the cursor position
searchBar.searchInput.text = searchBar.searchInput.text.slice(0, searchBar.searchInput.cursorPosition) + event.text + searchBar.searchInput.text.slice(searchBar.searchInput.cursorPosition);
searchBar.searchInput.cursorPosition += 1;
event.accepted = true;
root.focusFirstItem();
}
}
}
StyledRectangularShadow {
target: searchWidgetContent
}
Rectangle { // Background
id: searchWidgetContent
anchors {
top: parent.top
horizontalCenter: parent.horizontalCenter
topMargin: Appearance.sizes.elevationMargin
}
clip: true
implicitWidth: columnLayout.implicitWidth
implicitHeight: columnLayout.implicitHeight
radius: searchBar.height / 2 + searchBar.verticalPadding
color: Appearance.colors.colBackgroundSurfaceContainer
Behavior on implicitHeight {
id: searchHeightBehavior
enabled: GlobalStates.overviewOpen && root.showResults
animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
}
ColumnLayout {
id: columnLayout
anchors {
top: parent.top
horizontalCenter: parent.horizontalCenter
}
spacing: 0
// clip: true
layer.enabled: true
layer.effect: OpacityMask {
maskSource: Rectangle {
width: searchWidgetContent.width
height: searchWidgetContent.width
radius: searchWidgetContent.radius
}
}
SearchBar {
id: searchBar
property real verticalPadding: 4
Layout.fillWidth: true
Layout.leftMargin: 10
Layout.rightMargin: 4
Layout.topMargin: verticalPadding
Layout.bottomMargin: verticalPadding
Synchronizer on searchingText {
property alias source: root.searchingText
}
}
Rectangle {
// Separator
visible: root.showResults
Layout.fillWidth: true
height: 1
color: Appearance.colors.colOutlineVariant
}
ListView { // App results
id: appResults
visible: root.showResults
Layout.fillWidth: true
implicitHeight: Math.min(600, appResults.contentHeight + topMargin + bottomMargin)
clip: true
topMargin: 10
bottomMargin: 10
spacing: 2
KeyNavigation.up: searchBar
highlightMoveDuration: 100
onFocusChanged: {
if (focus)
appResults.currentIndex = 1;
}
Connections {
target: root
function onSearchingTextChanged() {
if (appResults.count > 0)
appResults.currentIndex = 0;
}
}
model: ScriptModel {
id: model
objectProp: "key"
values: {
// Search results are handled here
////////////////// Skip? //////////////////
if (root.searchingText == "")
return [];
///////////// Special cases ///////////////
if (root.searchingText.startsWith(Config.options.search.prefix.clipboard)) {
// Clipboard
const searchString = StringUtils.cleanPrefix(root.searchingText, Config.options.search.prefix.clipboard);
return Cliphist.fuzzyQuery(searchString).map((entry, index, array) => {
const mightBlurImage = Cliphist.entryIsImage(entry) && root.clipboardWorkSafetyActive;
let shouldBlurImage = mightBlurImage;
if (mightBlurImage) {
shouldBlurImage = shouldBlurImage && (containsUnsafeLink(array[index - 1]) || containsUnsafeLink(array[index + 1]));
}
const type = `#${entry.match(/^\s*(\S+)/)?.[1] || ""}`
return {
key: type,
cliphistRawString: entry,
name: StringUtils.cleanCliphistEntry(entry),
clickActionName: "",
type: type,
execute: () => {
Cliphist.copy(entry)
},
actions: [
{
name: "Copy",
materialIcon: "content_copy",
execute: () => {
Cliphist.copy(entry);
}
},
{
name: "Delete",
materialIcon: "delete",
execute: () => {
Cliphist.deleteEntry(entry);
}
}
],
blurImage: shouldBlurImage,
blurImageText: Translation.tr("Work safety")
};
}).filter(Boolean);
}
else if (root.searchingText.startsWith(Config.options.search.prefix.emojis)) {
// Clipboard
const searchString = StringUtils.cleanPrefix(root.searchingText, Config.options.search.prefix.emojis);
return Emojis.fuzzyQuery(searchString).map(entry => {
const emoji = entry.match(/^\s*(\S+)/)?.[1] || ""
return {
key: emoji,
cliphistRawString: entry,
bigText: emoji,
name: entry.replace(/^\s*\S+\s+/, ""),
clickActionName: "",
type: "Emoji",
execute: () => {
Quickshell.clipboardText = entry.match(/^\s*(\S+)/)?.[1];
}
};
}).filter(Boolean);
}
////////////////// Init ///////////////////
nonAppResultsTimer.restart();
const mathResultObject = {
key: `Math result: ${root.mathResult}`,
name: root.mathResult,
clickActionName: Translation.tr("Copy"),
type: Translation.tr("Math result"),
fontType: "monospace",
materialSymbol: 'calculate',
execute: () => {
Quickshell.clipboardText = root.mathResult;
}
};
const appResultObjects = AppSearch.fuzzyQuery(StringUtils.cleanPrefix(root.searchingText, Config.options.search.prefix.app)).map(entry => {
entry.clickActionName = Translation.tr("Launch");
entry.type = Translation.tr("App");
entry.key = entry.execute
return entry;
})
const commandResultObject = {
key: `cmd ${root.searchingText}`,
name: StringUtils.cleanPrefix(root.searchingText, Config.options.search.prefix.shellCommand).replace("file://", ""),
clickActionName: Translation.tr("Run"),
type: Translation.tr("Run command"),
fontType: "monospace",
materialSymbol: 'terminal',
execute: () => {
let cleanedCommand = root.searchingText.replace("file://", "");
cleanedCommand = StringUtils.cleanPrefix(cleanedCommand, Config.options.search.prefix.shellCommand);
if (cleanedCommand.startsWith(Config.options.search.prefix.shellCommand)) {
cleanedCommand = cleanedCommand.slice(Config.options.search.prefix.shellCommand.length);
}
Quickshell.execDetached(["bash", "-c", searchingText.startsWith('sudo') ? `${Config.options.apps.terminal} fish -C '${cleanedCommand}'` : cleanedCommand]);
}
};
const webSearchResultObject = {
key: `website ${root.searchingText}`,
name: StringUtils.cleanPrefix(root.searchingText, Config.options.search.prefix.webSearch),
clickActionName: Translation.tr("Search"),
type: Translation.tr("Search the web"),
materialSymbol: 'travel_explore',
execute: () => {
let query = StringUtils.cleanPrefix(root.searchingText, Config.options.search.prefix.webSearch);
let url = Config.options.search.engineBaseUrl + query;
for (let site of Config.options.search.excludedSites) {
url += ` -site:${site}`;
}
Qt.openUrlExternally(url);
}
}
const launcherActionObjects = root.searchActions.map(action => {
const actionString = `${Config.options.search.prefix.action}${action.action}`;
if (actionString.startsWith(root.searchingText) || root.searchingText.startsWith(actionString)) {
return {
key: `Action ${actionString}`,
name: root.searchingText.startsWith(actionString) ? root.searchingText : actionString,
clickActionName: Translation.tr("Run"),
type: Translation.tr("Action"),
materialSymbol: 'settings_suggest',
execute: () => {
action.execute(root.searchingText.split(" ").slice(1).join(" "));
}
};
}
return null;
}).filter(Boolean);
//////// Prioritized by prefix /////////
let result = [];
const startsWithNumber = /^\d/.test(root.searchingText);
const startsWithMathPrefix = root.searchingText.startsWith(Config.options.search.prefix.math);
const startsWithShellCommandPrefix = root.searchingText.startsWith(Config.options.search.prefix.shellCommand);
const startsWithWebSearchPrefix = root.searchingText.startsWith(Config.options.search.prefix.webSearch);
if (startsWithNumber || startsWithMathPrefix) {
result.push(mathResultObject);
} else if (startsWithShellCommandPrefix) {
result.push(commandResultObject);
} else if (startsWithWebSearchPrefix) {
result.push(webSearchResultObject);
}
//////////////// Apps //////////////////
result = result.concat(appResultObjects);
////////// Launcher actions ////////////
result = result.concat(launcherActionObjects);
/// Math result, command, web search ///
if (Config.options.search.prefix.showDefaultActionsWithoutPrefix) {
if (!startsWithShellCommandPrefix) result.push(commandResultObject);
if (!startsWithNumber && !startsWithMathPrefix) result.push(mathResultObject);
if (!startsWithWebSearchPrefix) result.push(webSearchResultObject);
}
return result;
}
}
delegate: SearchItem {
// The selectable item for each search result
required property var modelData
anchors.left: parent?.left
anchors.right: parent?.right
entry: modelData
query: StringUtils.cleanOnePrefix(root.searchingText, [
Config.options.search.prefix.action,
Config.options.search.prefix.app,
Config.options.search.prefix.clipboard,
Config.options.search.prefix.emojis,
Config.options.search.prefix.math,
Config.options.search.prefix.shellCommand,
Config.options.search.prefix.webSearch
])
}
}
}
}
}
@@ -0,0 +1,42 @@
import qs
import qs.services
import qs.modules.common
import qs.modules.common.widgets
import qs.modules.common.functions
import QtQuick
import Quickshell
import Quickshell.Wayland
import Quickshell.Hyprland
Scope {
id: root
Loader {
active: PolkitService.active
sourceComponent: Variants {
model: Quickshell.screens
delegate: PanelWindow {
id: panelWindow
required property var modelData
screen: modelData
anchors {
top: true
left: true
right: true
bottom: true
}
color: "transparent"
WlrLayershell.namespace: "quickshell:polkit"
WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand
WlrLayershell.layer: WlrLayer.Overlay
exclusionMode: ExclusionMode.Ignore
PolkitContent {
anchors.fill: parent
}
}
}
}
}
@@ -0,0 +1,113 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Widgets
import qs.services
import qs.modules.common
import qs.modules.common.widgets
Item {
id: root
readonly property bool usePasswordChars: !PolkitService.flow?.responseVisible ?? true
Keys.onPressed: event => { // Esc to close
if (event.key === Qt.Key_Escape) {
PolkitService.cancel();
}
}
function submit() {
PolkitService.submit(inputField.text);
}
Connections {
target: PolkitService
function onInteractionAvailableChanged() {
if (!PolkitService.interactionAvailable) return;
inputField.text = "";
inputField.forceActiveFocus();
}
}
Rectangle {
id: bg
anchors.fill: parent
color: Appearance.colors.colScrim
opacity: 0
Component.onCompleted: {
opacity = 1
}
Behavior on opacity {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
}
}
WindowDialog {
anchors.centerIn: parent
backgroundWidth: 450
show: false
Component.onCompleted: {
show = true
}
MaterialSymbol {
Layout.alignment: Qt.AlignHCenter
iconSize: 26
text: "security"
color: Appearance.colors.colSecondary
}
WindowDialogTitle {
id: titleText
Layout.fillWidth: true
horizontalAlignment: Text.AlignHCenter
text: Translation.tr("Authentication")
}
WindowDialogParagraph {
Layout.fillWidth: true
horizontalAlignment: Text.AlignLeft
text: {
if (!PolkitService.flow) return;
return PolkitService.flow.message.endsWith(".")
? PolkitService.flow.message.slice(0, -1)
: PolkitService.flow.message
}
}
MaterialTextField {
id: inputField
Layout.fillWidth: true
focus: true
enabled: PolkitService.interactionAvailable
placeholderText: {
const inputPrompt = PolkitService.flow?.inputPrompt.trim() ?? "";
const cleanedInputPrompt = inputPrompt.endsWith(":") ? inputPrompt.slice(0, -1) : inputPrompt;
return cleanedInputPrompt || (root.usePasswordChars ? Translation.tr("Password") : Translation.tr("Input"))
}
echoMode: root.usePasswordChars ? TextInput.Password : TextInput.Normal
onAccepted: root.submit();
Keys.onPressed: event => { // Esc to close
if (event.key === Qt.Key_Escape) {
PolkitService.cancel();
}
}
}
WindowDialogButtonRow {
Item {
Layout.fillWidth: true
}
DialogButton {
buttonText: Translation.tr("Cancel")
onClicked: PolkitService.cancel();
}
DialogButton {
enabled: PolkitService.interactionAvailable
buttonText: Translation.tr("OK")
onClicked: root.submit();
}
}
}
}
@@ -0,0 +1,49 @@
import qs.modules.common
import qs.modules.common.widgets
import QtQuick
import QtQuick.Shapes
import Quickshell
Item {
id: root
required property color color
required property color overlayColor
required property list<point> points
property int strokeWidth: Config.options.regionSelector.circle.strokeWidth
function updatePoints() {
if (!root.dragging) return;
root.points.push({ x: root.mouseX, y: root.mouseY });
}
Rectangle {
id: darkenOverlay
z: 1
anchors.fill: parent
color: root.overlayColor
}
Shape {
id: shape
z: 2
anchors.fill: parent
layer.enabled: true
layer.smooth: true
preferredRendererType: Shape.CurveRenderer
ShapePath {
id: shapePath
strokeWidth: root.strokeWidth
pathHints: ShapePath.PathLinear
fillColor: "transparent"
strokeColor: root.color
capStyle: ShapePath.RoundCap
joinStyle: ShapePath.RoundJoin
PathPolyline {
path: root.points
}
}
}
}
@@ -0,0 +1,80 @@
pragma ComponentBehavior: Bound
import qs
import qs.modules.common
import qs.modules.common.functions
import qs.modules.common.widgets
import qs.services
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Qt5Compat.GraphicalEffects
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import Quickshell.Hyprland
// Options toolbar
Toolbar {
id: root
// Use a synchronizer on these
property var action
property var selectionMode
// Signals
signal dismiss()
MaterialShape {
Layout.fillHeight: true
Layout.leftMargin: 2
Layout.rightMargin: 2
implicitSize: 36 // Intentionally smaller because this one is brighter than others
shape: switch (root.action) {
case RegionSelection.SnipAction.Copy:
case RegionSelection.SnipAction.Edit:
return MaterialShape.Shape.Cookie4Sided;
case RegionSelection.SnipAction.Search:
return MaterialShape.Shape.Pentagon;
case RegionSelection.SnipAction.CharRecognition:
return MaterialShape.Shape.Sunny;
case RegionSelection.SnipAction.Record:
case RegionSelection.SnipAction.RecordWithSound:
return MaterialShape.Shape.Gem;
default:
return MaterialShape.Shape.Cookie12Sided;
}
color: Appearance.colors.colPrimary
MaterialSymbol {
anchors.centerIn: parent
iconSize: 22
color: Appearance.colors.colOnPrimary
animateChange: true
text: switch (root.action) {
case RegionSelection.SnipAction.Copy:
case RegionSelection.SnipAction.Edit:
return "content_cut";
case RegionSelection.SnipAction.Search:
return "image_search";
case RegionSelection.SnipAction.CharRecognition:
return "document_scanner";
case RegionSelection.SnipAction.Record:
case RegionSelection.SnipAction.RecordWithSound:
return "videocam";
default:
return "";
}
}
}
ToolbarTabBar {
id: tabBar
tabButtonList: [
{"icon": "activity_zone", "name": Translation.tr("Rect")},
{"icon": "gesture", "name": Translation.tr("Circle")}
]
currentIndex: root.selectionMode === RegionSelection.SelectionMode.RectCorners ? 0 : 1
onCurrentIndexChanged: {
root.selectionMode = currentIndex === 0 ? RegionSelection.SelectionMode.RectCorners : RegionSelection.SelectionMode.Circle;
}
}
}
@@ -0,0 +1,90 @@
import qs.modules.common
import qs.modules.common.widgets
import QtQuick
Item {
id: root
required property real regionX
required property real regionY
required property real regionWidth
required property real regionHeight
required property real mouseX
required property real mouseY
required property color color
required property color overlayColor
property bool showAimLines: Config.options.regionSelector.rect.showAimLines
// Overlay to darken screen
// Base dark overlay around region
Rectangle {
id: darkenOverlay
z: 1
anchors {
left: parent.left
top: parent.top
leftMargin: root.regionX - darkenOverlay.border.width
topMargin: root.regionY - darkenOverlay.border.width
}
width: root.regionWidth + darkenOverlay.border.width * 2
height: root.regionHeight + darkenOverlay.border.width * 2
color: "transparent"
border.color: root.overlayColor
border.width: Math.max(root.width, root.height)
}
// Selection border
Rectangle {
id: selectionBorder
z: 1
anchors {
left: parent.left
top: parent.top
leftMargin: root.regionX
topMargin: root.regionY
}
width: root.regionWidth
height: root.regionHeight
color: "transparent"
border.color: root.color
border.width: 2
// radius: root.standardRounding
radius: 0 // TODO: figure out how to make the overlay thing work with rounding
}
StyledText {
z: 2
anchors {
top: selectionBorder.bottom
right: selectionBorder.right
margins: 8
}
color: root.color
text: `${Math.round(root.regionWidth)} x ${Math.round(root.regionHeight)}`
}
// Coord lines
Rectangle { // Vertical
visible: root.showAimLines
opacity: 0.2
z: 2
x: root.mouseX
anchors {
top: parent.top
bottom: parent.bottom
}
width: 1
color: root.color
}
Rectangle { // Horizontal
visible: root.showAimLines
opacity: 0.2
z: 2
y: root.mouseY
anchors {
left: parent.left
right: parent.right
}
height: 1
color: root.color
}
}

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