Watch
1
0
Fork
You've already forked souveraine
0

publish: the public projection begins here

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

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

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

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

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

View file

@ -0,0 +1,418 @@
import QtQuick
import Quickshell
import qs.modules.common.functions
pragma Singleton
pragma ComponentBehavior: Bound
Singleton {
id: root
property QtObject m3colors
property QtObject animation
property QtObject animationCurves
property QtObject colors
property QtObject rounding
property QtObject font
property QtObject sizes
property string syntaxHighlightingTheme
// Transparency. The quadratic functions were derived from analysis of hand-picked transparency values.
ColorQuantizer {
id: wallColorQuant
property string wallpaperPath: Config.options.background.wallpaperPath
property bool wallpaperIsVideo: wallpaperPath.endsWith(".mp4") || wallpaperPath.endsWith(".webm") || wallpaperPath.endsWith(".mkv") || wallpaperPath.endsWith(".avi") || wallpaperPath.endsWith(".mov")
source: Qt.resolvedUrl(wallpaperIsVideo ? Config.options.background.thumbnailPath : Config.options.background.wallpaperPath)
depth: 0 // 2^0 = 1 color
rescaleSize: 10
}
property real wallpaperVibrancy: (wallColorQuant.colors[0]?.hslSaturation + wallColorQuant.colors[0]?.hslLightness) / 2
property real autoBackgroundTransparency: { // y = 0.5768x^2 - 0.759x + 0.2896
let x = wallpaperVibrancy
let y = 0.5768 * (x * x) - 0.759 * (x) + 0.2896
return Math.max(0, Math.min(0.22, y)) - 0.12 * (m3colors.darkmode ? 0 : 1)
}
property real autoContentTransparency: 0.9
property real backgroundTransparency: Config?.options.appearance.transparency.enable ? Config?.options.appearance.transparency.automatic ? autoBackgroundTransparency : Config?.options.appearance.transparency.backgroundTransparency : 0
property real contentTransparency: Config?.options.appearance.transparency.automatic ? autoContentTransparency : Config?.options.appearance.transparency.contentTransparency
m3colors: QtObject {
property bool darkmode: true
property bool transparent: false
property color m3background: "#141313"
property color m3onBackground: "#e6e1e1"
property color m3surface: "#141313"
property color m3surfaceDim: "#141313"
property color m3surfaceBright: "#3a3939"
property color m3surfaceContainerLowest: "#0f0e0e"
property color m3surfaceContainerLow: "#1c1b1c"
property color m3surfaceContainer: "#201f20"
property color m3surfaceContainerHigh: "#2b2a2a"
property color m3surfaceContainerHighest: "#363435"
property color m3onSurface: "#e6e1e1"
property color m3surfaceVariant: "#49464a"
property color m3onSurfaceVariant: "#cbc5ca"
property color m3inverseSurface: "#e6e1e1"
property color m3inverseOnSurface: "#313030"
property color m3outline: "#948f94"
property color m3outlineVariant: "#49464a"
property color m3shadow: "#000000"
property color m3scrim: "#000000"
property color m3surfaceTint: "#cbc4cb"
property color m3primary: "#cbc4cb"
property color m3onPrimary: "#322f34"
property color m3primaryContainer: "#2d2a2f"
property color m3onPrimaryContainer: "#bcb6bc"
property color m3inversePrimary: "#615d63"
property color m3secondary: "#cac5c8"
property color m3onSecondary: "#323032"
property color m3secondaryContainer: "#4d4b4d"
property color m3onSecondaryContainer: "#ece6e9"
property color m3tertiary: "#d1c3c6"
property color m3onTertiary: "#372e30"
property color m3tertiaryContainer: "#31292b"
property color m3onTertiaryContainer: "#c1b4b7"
property color m3error: "#ffb4ab"
property color m3onError: "#690005"
property color m3errorContainer: "#93000a"
property color m3onErrorContainer: "#ffdad6"
property color m3primaryFixed: "#e7e0e7"
property color m3primaryFixedDim: "#cbc4cb"
property color m3onPrimaryFixed: "#1d1b1f"
property color m3onPrimaryFixedVariant: "#49454b"
property color m3secondaryFixed: "#e6e1e4"
property color m3secondaryFixedDim: "#cac5c8"
property color m3onSecondaryFixed: "#1d1b1d"
property color m3onSecondaryFixedVariant: "#484648"
property color m3tertiaryFixed: "#eddfe1"
property color m3tertiaryFixedDim: "#d1c3c6"
property color m3onTertiaryFixed: "#211a1c"
property color m3onTertiaryFixedVariant: "#4e4447"
property color m3success: "#B5CCBA"
property color m3onSuccess: "#213528"
property color m3successContainer: "#374B3E"
property color m3onSuccessContainer: "#D1E9D6"
property color term0: "#EDE4E4"
property color term1: "#B52755"
property color term2: "#A97363"
property color term3: "#AF535D"
property color term4: "#A67F7C"
property color term5: "#B2416B"
property color term6: "#8D76AD"
property color term7: "#272022"
property color term8: "#0E0D0D"
property color term9: "#B52755"
property color term10: "#A97363"
property color term11: "#AF535D"
property color term12: "#A67F7C"
property color term13: "#B2416B"
property color term14: "#8D76AD"
property color term15: "#221A1A"
}
colors: QtObject {
property color colSubtext: m3colors.m3outline
// Layer 0
property color colLayer0Base: ColorUtils.mix(m3colors.m3background, m3colors.m3primary, Config.options.appearance.extraBackgroundTint ? 0.99 : 1)
property color colLayer0: ColorUtils.transparentize(colLayer0Base, root.backgroundTransparency)
property color colOnLayer0: m3colors.m3onBackground
property color colLayer0Hover: ColorUtils.transparentize(ColorUtils.mix(colLayer0, colOnLayer0, 0.9, root.contentTransparency))
property color colLayer0Active: ColorUtils.transparentize(ColorUtils.mix(colLayer0, colOnLayer0, 0.8, root.contentTransparency))
property color colLayer0Border: ColorUtils.mix(root.m3colors.m3outlineVariant, colLayer0, 0.4)
// Layer 1
property color colLayer1Base: m3colors.m3surfaceContainerLow
property color colLayer1: ColorUtils.solveOverlayColor(colLayer0Base, colLayer1Base, 1 - root.contentTransparency);
property color colOnLayer1: m3colors.m3onSurfaceVariant;
property color colOnLayer1Inactive: ColorUtils.mix(colOnLayer1, colLayer1, 0.45);
property color colLayer1Hover: ColorUtils.transparentize(ColorUtils.mix(colLayer1, colOnLayer1, 0.92), root.contentTransparency)
property color colLayer1Active: ColorUtils.transparentize(ColorUtils.mix(colLayer1, colOnLayer1, 0.85), root.contentTransparency);
// Layer 2
property color colLayer2Base: m3colors.m3surfaceContainer
property color colLayer2: ColorUtils.solveOverlayColor(colLayer1Base, colLayer2Base, 1 - root.contentTransparency)
property color colLayer2Hover: ColorUtils.solveOverlayColor(colLayer1Base, ColorUtils.mix(colLayer2Base, colOnLayer2, 0.90), 1 - root.contentTransparency)
property color colLayer2Active: ColorUtils.solveOverlayColor(colLayer1Base, ColorUtils.mix(colLayer2Base, colOnLayer2, 0.80), 1 - root.contentTransparency);
property color colLayer2Disabled: ColorUtils.solveOverlayColor(colLayer1Base, ColorUtils.mix(colLayer2Base, m3colors.m3background, 0.8), 1 - root.contentTransparency);
property color colOnLayer2: m3colors.m3onSurface;
property color colOnLayer2Disabled: ColorUtils.mix(colOnLayer2, m3colors.m3background, 0.4);
// Layer 3
property color colLayer3Base: m3colors.m3surfaceContainerHigh
property color colLayer3: ColorUtils.solveOverlayColor(colLayer2Base, colLayer3Base, 1 - root.contentTransparency)
property color colLayer3Hover: ColorUtils.solveOverlayColor(colLayer2Base, ColorUtils.mix(colLayer3Base, colOnLayer3, 0.90), 1 - root.contentTransparency)
property color colLayer3Active: ColorUtils.solveOverlayColor(colLayer2Base, ColorUtils.mix(colLayer3Base, colOnLayer3, 0.80), 1 - root.contentTransparency);
property color colOnLayer3: m3colors.m3onSurface;
// Layer 4
property color colLayer4Base: m3colors.m3surfaceContainerHighest
property color colLayer4: ColorUtils.solveOverlayColor(colLayer3Base, colLayer4Base, 1 - root.contentTransparency)
property color colLayer4Hover: ColorUtils.solveOverlayColor(colLayer3Base, ColorUtils.mix(colLayer4Base, colOnLayer4, 0.90), 1 - root.contentTransparency)
property color colLayer4Active: ColorUtils.solveOverlayColor(colLayer3Base, ColorUtils.mix(colLayer4Base, colOnLayer4, 0.80), 1 - root.contentTransparency);
property color colOnLayer4: m3colors.m3onSurface;
// Primary
property color colPrimary: m3colors.m3primary
property color colOnPrimary: m3colors.m3onPrimary
property color colPrimaryHover: ColorUtils.mix(colors.colPrimary, colLayer1Hover, 0.87)
property color colPrimaryActive: ColorUtils.mix(colors.colPrimary, colLayer1Active, 0.7)
property color colPrimaryContainer: m3colors.m3primaryContainer
property color colPrimaryContainerHover: ColorUtils.mix(colors.colPrimaryContainer, colors.colOnPrimaryContainer, 0.9)
property color colPrimaryContainerActive: ColorUtils.mix(colors.colPrimaryContainer, colors.colOnPrimaryContainer, 0.8)
property color colOnPrimaryContainer: m3colors.m3onPrimaryContainer
// Secondary
property color colSecondary: m3colors.m3secondary
property color colSecondaryHover: ColorUtils.mix(m3colors.m3secondary, colLayer1Hover, 0.85)
property color colSecondaryActive: ColorUtils.mix(m3colors.m3secondary, colLayer1Active, 0.4)
property color colOnSecondary: m3colors.m3onSecondary
property color colSecondaryContainer: m3colors.m3secondaryContainer
property color colSecondaryContainerHover: ColorUtils.mix(m3colors.m3secondaryContainer, m3colors.m3onSecondaryContainer, 0.90)
property color colSecondaryContainerActive: ColorUtils.mix(m3colors.m3secondaryContainer, m3colors.m3onSecondaryContainer, 0.54)
property color colOnSecondaryContainer: m3colors.m3onSecondaryContainer
// Tertiary
property color colTertiary: m3colors.m3tertiary
property color colTertiaryHover: ColorUtils.mix(m3colors.m3tertiary, colLayer1Hover, 0.85)
property color colTertiaryActive: ColorUtils.mix(m3colors.m3tertiary, colLayer1Active, 0.4)
property color colTertiaryContainer: m3colors.m3tertiaryContainer
property color colTertiaryContainerHover: ColorUtils.mix(m3colors.m3tertiaryContainer, m3colors.m3onTertiaryContainer, 0.90)
property color colTertiaryContainerActive: ColorUtils.mix(m3colors.m3tertiaryContainer, colLayer1Active, 0.54)
property color colOnTertiary: m3colors.m3onTertiary
property color colOnTertiaryContainer: m3colors.m3onTertiaryContainer
// Surface
property color colBackgroundSurfaceContainer: ColorUtils.transparentize(m3colors.m3surfaceContainer, root.backgroundTransparency)
property color colSurfaceContainerLow: ColorUtils.solveOverlayColor(m3colors.m3background, m3colors.m3surfaceContainerLow, 1 - root.contentTransparency)
property color colSurfaceContainer: ColorUtils.solveOverlayColor(m3colors.m3surfaceContainerLow, m3colors.m3surfaceContainer, 1 - root.contentTransparency)
property color colSurfaceContainerHigh: ColorUtils.solveOverlayColor(m3colors.m3surfaceContainer, m3colors.m3surfaceContainerHigh, 1 - root.contentTransparency)
property color colSurfaceContainerHighest: ColorUtils.solveOverlayColor(m3colors.m3surfaceContainerHigh, m3colors.m3surfaceContainerHighest, 1 - root.contentTransparency)
property color colSurfaceContainerHighestHover: ColorUtils.mix(m3colors.m3surfaceContainerHighest, m3colors.m3onSurface, 0.95)
property color colSurfaceContainerHighestActive: ColorUtils.mix(m3colors.m3surfaceContainerHighest, m3colors.m3onSurface, 0.85)
property color colOnSurface: m3colors.m3onSurface
property color colOnSurfaceVariant: m3colors.m3onSurfaceVariant
// Misc
property color colTooltip: m3colors.m3inverseSurface
property color colOnTooltip: m3colors.m3inverseOnSurface
property color colScrim: ColorUtils.transparentize(m3colors.m3scrim, 0.5)
property color colShadow: ColorUtils.transparentize(m3colors.m3shadow, 0.7)
property color colOutline: m3colors.m3outline
property color colOutlineVariant: m3colors.m3outlineVariant
property color colError: m3colors.m3error
property color colErrorHover: ColorUtils.mix(m3colors.m3error, colLayer1Hover, 0.85)
property color colErrorActive: ColorUtils.mix(m3colors.m3error, colLayer1Active, 0.7)
property color colOnError: m3colors.m3onError
property color colErrorContainer: m3colors.m3errorContainer
property color colErrorContainerHover: ColorUtils.mix(m3colors.m3errorContainer, m3colors.m3onErrorContainer, 0.90)
property color colErrorContainerActive: ColorUtils.mix(m3colors.m3errorContainer, m3colors.m3onErrorContainer, 0.70)
property color colOnErrorContainer: m3colors.m3onErrorContainer
}
rounding: QtObject {
property int unsharpen: 2
property int unsharpenmore: 6
property int verysmall: 8
property int small: 12
property int normal: 17
property int large: 23
property int verylarge: 30
property int full: 9999
property int screenRounding: large
property int windowRounding: 18
}
font: QtObject {
property QtObject family: QtObject {
property string main: Config.options.appearance.fonts.main
property string numbers: Config.options.appearance.fonts.numbers
property string title: Config.options.appearance.fonts.title
property string iconMaterial: "Material Symbols Rounded"
property string iconNerd: Config.options.appearance.fonts.iconNerd
property string monospace: Config.options.appearance.fonts.monospace
property string reading: Config.options.appearance.fonts.reading
property string expressive: Config.options.appearance.fonts.expressive
}
property QtObject variableAxes: QtObject {
property var main: ({
"wght": 450,
"wdth": 100,
})
property var numbers: ({
"wght": 450,
})
property var title: ({ // Slightly bold weight for title
"wght": 550, // Weight (Lowered to compensate for increased grade)
})
}
property QtObject pixelSize: QtObject {
property int smallest: 10
property int smaller: 12
property int smallie: 13
property int small: 15
property int normal: 16
property int large: 17
property int larger: 19
property int huge: 22
property int hugeass: 23
property int title: huge
}
}
animationCurves: QtObject {
readonly property list<real> expressiveFastSpatial: [0.42, 1.67, 0.21, 0.90, 1, 1] // Default, 350ms
readonly property list<real> expressiveDefaultSpatial: [0.38, 1.21, 0.22, 1.00, 1, 1] // Default, 500ms
readonly property list<real> expressiveSlowSpatial: [0.39, 1.29, 0.35, 0.98, 1, 1] // Default, 650ms
readonly property list<real> expressiveEffects: [0.34, 0.80, 0.34, 1.00, 1, 1] // Default, 200ms
readonly property list<real> emphasized: [0.05, 0, 2 / 15, 0.06, 1 / 6, 0.4, 5 / 24, 0.82, 0.25, 1, 1, 1]
readonly property list<real> emphasizedFirstHalf: [0.05, 0, 2 / 15, 0.06, 1 / 6, 0.4, 5 / 24, 0.82]
readonly property list<real> emphasizedLastHalf: [5 / 24, 0.82, 0.25, 1, 1, 1]
readonly property list<real> emphasizedAccel: [0.3, 0, 0.8, 0.15, 1, 1]
readonly property list<real> emphasizedDecel: [0.05, 0.7, 0.1, 1, 1, 1]
readonly property list<real> standard: [0.2, 0, 0, 1, 1, 1]
readonly property list<real> standardAccel: [0.3, 0, 1, 1, 1, 1]
readonly property list<real> standardDecel: [0, 0, 0, 1, 1, 1]
readonly property real expressiveFastSpatialDuration: 350
readonly property real expressiveDefaultSpatialDuration: 500
readonly property real expressiveSlowSpatialDuration: 650
readonly property real expressiveEffectsDuration: 200
}
animation: QtObject {
property QtObject elementMove: QtObject {
property int duration: animationCurves.expressiveDefaultSpatialDuration
property int type: Easing.BezierSpline
property list<real> bezierCurve: animationCurves.expressiveDefaultSpatial
property int velocity: 650
property Component numberAnimation: Component {
NumberAnimation {
duration: root.animation.elementMove.duration
easing.type: root.animation.elementMove.type
easing.bezierCurve: root.animation.elementMove.bezierCurve
}
}
}
property QtObject elementMoveSmall: QtObject {
property int duration: animationCurves.expressiveFastSpatialDuration
property int type: Easing.BezierSpline
property list<real> bezierCurve: animationCurves.expressiveFastSpatial
property int velocity: 650
property Component numberAnimation: Component {
NumberAnimation {
duration: root.animation.elementMoveSmall.duration
easing.type: root.animation.elementMoveSmall.type
easing.bezierCurve: root.animation.elementMoveSmall.bezierCurve
}
}
}
property QtObject elementMoveEnter: QtObject {
property int duration: 400
property int type: Easing.BezierSpline
property list<real> bezierCurve: animationCurves.emphasizedDecel
property int velocity: 650
property Component numberAnimation: Component {
NumberAnimation {
alwaysRunToEnd: true
duration: root.animation.elementMoveEnter.duration
easing.type: root.animation.elementMoveEnter.type
easing.bezierCurve: root.animation.elementMoveEnter.bezierCurve
}
}
}
property QtObject elementMoveExit: QtObject {
property int duration: 200
property int type: Easing.BezierSpline
property list<real> bezierCurve: animationCurves.emphasizedAccel
property int velocity: 650
property Component numberAnimation: Component {
NumberAnimation {
alwaysRunToEnd: true
duration: root.animation.elementMoveExit.duration
easing.type: root.animation.elementMoveExit.type
easing.bezierCurve: root.animation.elementMoveExit.bezierCurve
}
}
}
property QtObject elementMoveFast: QtObject {
property int duration: animationCurves.expressiveEffectsDuration
property int type: Easing.BezierSpline
property list<real> bezierCurve: animationCurves.expressiveEffects
property int velocity: 850
property Component colorAnimation: Component { ColorAnimation {
duration: root.animation.elementMoveFast.duration
easing.type: root.animation.elementMoveFast.type
easing.bezierCurve: root.animation.elementMoveFast.bezierCurve
}}
property Component numberAnimation: Component { NumberAnimation {
alwaysRunToEnd: true
duration: root.animation.elementMoveFast.duration
easing.type: root.animation.elementMoveFast.type
easing.bezierCurve: root.animation.elementMoveFast.bezierCurve
}}
}
property QtObject elementResize: QtObject {
property int duration: 300
property int type: Easing.BezierSpline
property list<real> bezierCurve: animationCurves.emphasized
property int velocity: 650
property Component numberAnimation: Component {
NumberAnimation {
alwaysRunToEnd: true
duration: root.animation.elementResize.duration
easing.type: root.animation.elementResize.type
easing.bezierCurve: root.animation.elementResize.bezierCurve
}
}
}
property QtObject clickBounce: QtObject {
property int duration: 400
property int type: Easing.BezierSpline
property list<real> bezierCurve: animationCurves.expressiveDefaultSpatial
property int velocity: 850
property Component numberAnimation: Component { NumberAnimation {
alwaysRunToEnd: true
duration: root.animation.clickBounce.duration
easing.type: root.animation.clickBounce.type
easing.bezierCurve: root.animation.clickBounce.bezierCurve
}}
}
property QtObject scroll: QtObject {
property int duration: 200
property int type: Easing.BezierSpline
property list<real> bezierCurve: root.animationCurves.standardDecel
}
property QtObject menuDecel: QtObject {
property int duration: 350
property int type: Easing.OutExpo
}
}
sizes: QtObject {
property real baseBarHeight: 40
property real barHeight: Config.options.bar.cornerStyle === 1 ?
(baseBarHeight + root.sizes.hyprlandGapsOut * 2) : baseBarHeight
property real barCenterSideModuleWidth: Config.options?.bar.verbose ? 360 : 140
property real barCenterSideModuleWidthShortened: 280
property real barCenterSideModuleWidthHellaShortened: 190
property real barShortenScreenWidthThreshold: 1200 // Shorten if screen width is at most this value
property real barHellaShortenScreenWidthThreshold: 1000 // Shorten even more...
property real elevationMargin: 10
property real fabShadowRadius: 5
property real fabHoveredShadowRadius: 7
property real hyprlandGapsOut: 5
property real mediaControlsWidth: 440
property real mediaControlsHeight: 160
property real notificationPopupWidth: 410
property real osdWidth: 180
property real searchWidthCollapsed: 210
property real searchWidth: 360
property real sidebarWidth: Config.options.sidebar.width
property real sidebarWidthExtended: Config.options.sidebar.widthExtended
property real baseVerticalBarWidth: 46
property real verticalBarWidth: Config.options.bar.cornerStyle === 1 ?
(baseVerticalBarWidth + root.sizes.hyprlandGapsOut * 2) : baseVerticalBarWidth
property real wallpaperSelectorWidth: 1200
property real wallpaperSelectorHeight: 690
property real wallpaperSelectorItemMargins: 8
property real wallpaperSelectorItemPadding: 6
}
syntaxHighlightingTheme: root.m3colors.darkmode ? "Monokai" : "ayu Light"
}

View file

@ -0,0 +1,686 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Io
import qs.modules.common.functions
Singleton {
id: root
property string filePath: Directories.shellConfigPath
property alias options: configOptionsJsonAdapter
property bool ready: false
property int readWriteDelay: 50 // milliseconds
property bool blockWrites: false
function setNestedValue(nestedKey, value) {
let keys = nestedKey.split(".");
let obj = root.options;
let parents = [obj];
// Traverse and collect parent objects
for (let i = 0; i < keys.length - 1; ++i) {
if (!obj[keys[i]] || typeof obj[keys[i]] !== "object") {
obj[keys[i]] = {};
}
obj = obj[keys[i]];
parents.push(obj);
}
// Convert value to correct type using JSON.parse when safe
let convertedValue = value;
if (typeof value === "string") {
let trimmed = value.trim();
if (trimmed === "true" || trimmed === "false" || !isNaN(Number(trimmed))) {
try {
convertedValue = JSON.parse(trimmed);
} catch (e) {
convertedValue = value;
}
}
}
obj[keys[keys.length - 1]] = convertedValue;
}
Timer {
id: fileReloadTimer
interval: root.readWriteDelay
repeat: false
onTriggered: {
configFileView.reload()
}
}
Timer {
id: fileWriteTimer
interval: root.readWriteDelay
repeat: false
onTriggered: {
configFileView.writeAdapter()
}
}
FileView {
id: configFileView
path: root.filePath
watchChanges: true
blockWrites: root.blockWrites
onFileChanged: fileReloadTimer.restart()
onAdapterUpdated: fileWriteTimer.restart()
onLoaded: root.ready = true
onLoadFailed: error => {
if (error == FileViewError.FileNotFound) {
writeAdapter();
}
}
JsonAdapter {
id: configOptionsJsonAdapter
property string panelFamily: "ii" // "ii", "waffle"
property JsonObject policies: JsonObject {
property int ai: 1 // 0: No | 1: Yes | 2: Local
property int weeb: 1 // 0: No | 1: Open | 2: Closet
}
property JsonObject ai: JsonObject {
property string systemPrompt: "## Style\n- Use casual tone, don't be formal!\n- Always be brief and to the point, unless asked otherwise\n- Don't repeat the user's question\n- Be approachable: Avoid using overly complicated, domain-specific terms and provide analogies when asked to explain a concept\n\n## Context (ignore when irrelevant)\n- You are a helpful and inspiring sidebar assistant on a {DISTRO} Linux system\n- Desktop environment: {DE}\n- Current date & time: {DATETIME}\n- Focused app: {WINDOWCLASS}\n\n## Presentation\n- Use Markdown features in your response: \n - **Bold** text to **highlight keywords** in your response\n - **Split long information into small sections** with h2 headers and a relevant emoji at the start of it (for example `## 🐧 Linux`). Bullet points are preferred over long paragraphs, unless you're offering writing support or instructed otherwise by the user.\n- Asked to compare different options? You should firstly use a table to compare the main aspects, then elaborate or include relevant comments from online forums *after* the table. Make sure to provide a final recommendation for the user's use case!\n- Use LaTeX formatting for mathematical and scientific notations whenever appropriate. Enclose all LaTeX '$$' delimiters. NEVER generate LaTeX code in a latex block unless the user explicitly asks for it. DO NOT use LaTeX for regular documents (resumes, letters, essays, CVs, etc.).\n\nThanks!\n"
property string tool: "functions" // search, functions, or none
property list<var> extraModels: [
{
"api_format": "openai", // Most of the time you want "openai". Use "gemini" for Google's models
"description": "This is a custom model. Edit the config to add more! | Anyway, this is DeepSeek R1 Distill LLaMA 70B",
"endpoint": "https://openrouter.ai/api/v1/chat/completions",
"homepage": "https://openrouter.ai/deepseek/deepseek-r1-distill-llama-70b:free", // Not mandatory
"icon": "spark-symbolic", // Not mandatory
"key_get_link": "https://openrouter.ai/settings/keys", // Not mandatory
"key_id": "openrouter",
"model": "deepseek/deepseek-r1-distill-llama-70b:free",
"name": "Custom: DS R1 Dstl. LLaMA 70B",
"requires_key": true
}
]
}
property JsonObject appearance: JsonObject {
property bool extraBackgroundTint: true
property int fakeScreenRounding: 2 // 0: None | 1: Always | 2: When not fullscreen
property JsonObject fonts: JsonObject {
property string main: "Google Sans Flex"
property string numbers: "Google Sans Flex"
property string title: "Google Sans Flex"
property string iconNerd: "JetBrains Mono NF"
property string monospace: "JetBrains Mono NF"
property string reading: "Readex Pro"
property string expressive: "Space Grotesk"
}
property JsonObject transparency: JsonObject {
property bool enable: false
property bool automatic: true
property real backgroundTransparency: 0.11
property real contentTransparency: 0.57
}
property JsonObject wallpaperTheming: JsonObject {
property bool enableAppsAndShell: true
property bool enableQtApps: true
property bool enableTerminal: true
property JsonObject terminalGenerationProps: JsonObject {
property real harmony: 0.6
property real harmonizeThreshold: 100
property real termFgBoost: 0.35
property real termBgTone: 0.4
property bool forceDarkMode: false
}
}
property JsonObject palette: JsonObject {
property string type: "auto" // Allowed: auto, scheme-content, scheme-expressive, scheme-fidelity, scheme-fruit-salad, scheme-monochrome, scheme-neutral, scheme-rainbow, scheme-tonal-spot
property string accentColor: ""
}
}
property JsonObject audio: JsonObject {
// Values in %
property JsonObject protection: JsonObject {
// Prevent sudden bangs
property bool enable: false
property real maxAllowedIncrease: 10
property real maxAllowed: 99
}
}
property JsonObject apps: JsonObject {
property string bluetooth: "kcmshell6 kcm_bluetooth"
property string changePassword: "kitty -1 --hold=yes fish -i -c 'passwd'"
property string network: "kcmshell6 kcm_networkmanagement"
property string manageUser: "kcmshell6 kcm_users"
property string networkEthernet: "kcmshell6 kcm_networkmanagement"
property string taskManager: "plasma-systemmonitor --page-name Processes"
property string terminal: "kitty -1" // This is only for shell actions
property string update: "kitty -1 --hold=yes fish -i -c 'pkexec pacman -Syu'"
property string volumeMixer: `~/.config/hypr/hyprland/scripts/launch_first_available.sh "pavucontrol-qt" "pavucontrol"`
}
property JsonObject background: JsonObject {
property JsonObject widgets: JsonObject {
property JsonObject clock: JsonObject {
property bool enable: true
property bool showOnlyWhenLocked: false
property string placementStrategy: "leastBusy" // "free", "leastBusy", "mostBusy"
property real x: 100
property real y: 100
property string style: "cookie" // Options: "cookie", "digital"
property string styleLocked: "cookie" // Options: "cookie", "digital"
property JsonObject cookie: JsonObject {
property bool aiStyling: false
property int sides: 14
property string dialNumberStyle: "full" // Options: "dots" , "numbers", "full" , "none"
property string hourHandStyle: "fill" // Options: "classic", "fill", "hollow", "hide"
property string minuteHandStyle: "medium" // Options "classic", "thin", "medium", "bold", "hide"
property string secondHandStyle: "dot" // Options: "dot", "line", "classic", "hide"
property string dateStyle: "bubble" // Options: "border", "rect", "bubble" , "hide"
property bool timeIndicators: true
property bool hourMarks: false
property bool dateInClock: true
property bool constantlyRotate: false
property bool useSineCookie: false
}
property JsonObject digital: JsonObject {
property bool adaptiveAlignment: true
property bool showDate: true
property bool animateChange: true
property bool vertical: false
property JsonObject font: JsonObject {
property string family: "Google Sans Flex"
property real weight: 350
property real width: 100
property real size: 90
property real roundness: 0
}
}
property JsonObject quote: JsonObject {
property bool enable: false
property string text: ""
}
}
property JsonObject weather: JsonObject {
property bool enable: false
property string placementStrategy: "free" // "free", "leastBusy", "mostBusy"
property real x: 400
property real y: 100
}
property JsonObject visualizer: JsonObject {
property bool enable: false
property string placementStrategy: "free"
property real x: 500
property real y: 500
property int bars: 30
property string style: "bars"
property bool vertical: false
}
property JsonObject stats: JsonObject {
property bool enable: false
property string placementStrategy: "free"
property real x: 150
property real y: 300
property string githubUsername: ""
property string codeforcesUsername: ""
property bool showGraphs: false
}
property JsonObject systemResources: JsonObject {
property bool enable: false
property string placementStrategy: "free"
property real x: 150
property real y: 600
property bool showGraphs: false
}
}
property string wallpaperPath: ""
property string thumbnailPath: ""
property bool hideWhenFullscreen: true
property JsonObject parallax: JsonObject {
property bool vertical: false
property bool autoVertical: false
property bool enableWorkspace: false
property real workspaceZoom: 1.07 // Relative to wallpaper size
property bool enableSidebar: false
property real widgetsFactor: 1.2
}
}
property JsonObject bar: JsonObject {
property JsonObject autoHide: JsonObject {
property bool enable: false
property int hoverRegionWidth: 2
property bool pushWindows: false
property JsonObject showWhenPressingSuper: JsonObject {
property bool enable: true
property int delay: 140
property bool suppressSearchOnHold: true
property int suppressSearchDelay: 200
}
}
property bool bottom: false // Instead of top
property int cornerStyle: 0 // 0: Hug | 1: Float | 2: Plain rectangle
property bool floatStyleShadow: true // Show shadow behind bar when cornerStyle == 1 (Float)
property bool borderless: false // true for no grouping of items
property string topLeftIcon: "spark" // Options: "distro" or any icon name in ~/.config/quickshell/ii/assets/icons
property bool showBackground: true
property bool verbose: true
property bool vertical: false
property JsonObject resources: JsonObject {
property bool alwaysShowSwap: true
property bool alwaysShowCpu: true
property int memoryWarningThreshold: 95
property int swapWarningThreshold: 85
property int cpuWarningThreshold: 90
}
property list<string> screenList: [] // List of names, like "eDP-1", find out with 'hyprctl monitors' command
property JsonObject utilButtons: JsonObject {
property bool showScreenSnip: true
property bool showColorPicker: false
property bool showMicToggle: false
property bool showKeyboardToggle: true
property bool showDarkModeToggle: true
property bool showPerformanceProfileToggle: false
property bool showScreenRecord: false
}
property JsonObject workspaces: JsonObject {
property bool monochromeIcons: true
property int shown: 10
property bool showAppIcons: true
property bool alwaysShowNumbers: false
property int showNumberDelay: 300 // milliseconds
property list<string> numberMap: ["1", "2"] // Characters to show instead of numbers on workspace indicator
property bool useNerdFont: false
}
property JsonObject weather: JsonObject {
property bool enable: false
property bool enableGPS: true // gps based location
property string city: "" // When 'enableGPS' is false
property bool useUSCS: false // Instead of metric (SI) units
property int fetchInterval: 10 // minutes
}
property JsonObject claudeUsage: JsonObject {
property bool enable: false // Show a Claude (Pro/Max) subscription usage gauge in the bar
property bool defaultWeekly: false // Start on the 7-day window; click the gauge to switch session <-> week
property int warningThreshold: 90 // Turn the gauge red at/above this utilization (%)
property int fetchInterval: 5 // minutes
}
property JsonObject indicators: JsonObject {
property JsonObject notifications: JsonObject {
property bool showUnreadCount: false
}
}
property JsonObject tooltips: JsonObject {
property bool clickToShow: false
}
}
property JsonObject battery: JsonObject {
property int low: 20
property int critical: 5
property int full: 101
property bool automaticSuspend: true
property int suspend: 3
}
property JsonObject calendar: JsonObject {
property string locale: "en-GB"
}
property JsonObject cheatsheet: JsonObject {
// Use a nerdfont to see the icons
// 0: 󰖳 | 1: 󰌽 | 2: 󰘳 | 3: | 4: 󰨡
// 5: | 6: | 7: 󰣇 | 8: | 9:
// 10: | 11: | 12: | 13: | 14: 󱄛
property string superKey: ""
property bool useMacSymbol: false
property bool splitButtons: false
property bool useMouseSymbol: false
property bool useFnSymbol: false
property JsonObject fontSize: JsonObject {
property int key: Appearance.font.pixelSize.smaller
property int comment: Appearance.font.pixelSize.smaller
}
}
property JsonObject conflictKiller: JsonObject {
property bool autoKillNotificationDaemons: false
property bool autoKillTrays: false
}
property JsonObject crosshair: JsonObject {
// Valorant crosshair format. Use https://www.vcrdb.net/builder
property string code: "0;P;d;1;0l;10;0o;2;1b;0"
}
property JsonObject dock: JsonObject {
property bool enable: false
property bool monochromeIcons: true
property real height: 60
property real hoverRegionHeight: 2
property bool pinnedOnStartup: false
property bool hoverToReveal: true // When false, only reveals on empty workspace
property list<string> pinnedApps: [ // IDs of pinned entries
"org.kde.dolphin", "kitty",]
property list<string> ignoredAppRegexes: []
}
property JsonObject interactions: JsonObject {
property JsonObject scrolling: JsonObject {
property bool fasterTouchpadScroll: false // Enable faster scrolling with touchpad
property int mouseScrollDeltaThreshold: 120 // delta >= this then it gets detected as mouse scroll rather than touchpad
property int mouseScrollFactor: 120
property int touchpadScrollFactor: 450
}
property JsonObject deadPixelWorkaround: JsonObject { // Hyprland leaves out 1 pixel on the right for interactions
property bool enable: false
}
}
property JsonObject language: JsonObject {
property string ui: "auto" // UI language. "auto" for system locale, or specific language code like "zh_CN", "en_US"
property JsonObject translator: JsonObject {
property string engine: "auto" // Run `trans -list-engines` for available engines. auto should use google
property string targetLanguage: "auto" // Run `trans -list-all` for available languages
property string sourceLanguage: "auto"
property list<string> history: []
}
}
property JsonObject launcher: JsonObject {
property list<string> pinnedApps: [ "org.kde.dolphin", "kitty", "cmake-gui"]
}
property JsonObject light: JsonObject {
property JsonObject night: JsonObject {
property bool automatic: true
property string from: "19:00" // Format: "HH:mm", 24-hour time
property string to: "06:30" // Format: "HH:mm", 24-hour time
property int colorTemperature: 5000
}
property JsonObject antiFlashbang: JsonObject {
property bool enable: false
}
}
property JsonObject lock: JsonObject {
property bool useHyprlock: false
property bool launchOnStartup: false
property int dpmsTimeout: 3
property string unlockHook: ""
property JsonObject blur: JsonObject {
property bool enable: true
property real radius: 100
property real extraZoom: 1.1
}
property bool centerClock: true
property bool showLockedText: true
property JsonObject security: JsonObject {
property bool unlockKeyring: true
property bool requirePasswordToPower: false
}
property bool materialShapeChars: true
}
property JsonObject media: JsonObject {
// Attempt to remove dupes (the aggregator playerctl one and browsers' native ones when there's plasma browser integration)
property bool filterDuplicatePlayers: true
}
property JsonObject networking: JsonObject {
property string userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36"
}
property JsonObject notifications: JsonObject {
property int timeout: 7000
property JsonObject monitor: JsonObject {
property bool enable: false
property string name: "" // Name of the monitor to show notifications on, like "eDP-1". Find out with 'hyprctl monitors' command
}
}
property JsonObject osd: JsonObject {
property int timeout: 1000
}
property JsonObject osk: JsonObject {
property string layout: "qwerty_full"
property bool pinnedOnStartup: false
}
property JsonObject overlay: JsonObject {
property bool openingZoomAnimation: true
property bool darkenScreen: true
property real clickthroughOpacity: 0.8
property JsonObject floatingImage: JsonObject {
property string imageSource: "https://media.tenor.com/H5U5bJzj3oAAAAAi/kukuru.gif"
property real scale: 0.5
}
}
property JsonObject overview: JsonObject {
property bool enable: true
property real scale: 0.18 // Relative to screen size
property real rows: 2
property real columns: 5
property bool orderRightLeft: false
property bool orderBottomUp: false
property bool centerIcons: true
}
property JsonObject regionSelector: JsonObject {
property JsonObject targetRegions: JsonObject {
property bool windows: true
property bool layers: false
property bool content: true
property bool showLabel: false
property real opacity: 0.3
property real contentRegionOpacity: 0.8
property int selectionPadding: 5
}
property JsonObject rect: JsonObject {
property bool showAimLines: true
}
property JsonObject circle: JsonObject {
property int strokeWidth: 6
property int padding: 10
}
property JsonObject annotation: JsonObject {
property bool useSatty: false
}
}
property JsonObject resources: JsonObject {
property int updateInterval: 3000
property int historyLength: 60
}
property JsonObject tray: JsonObject {
property bool monochromeIcons: true
property bool showItemId: false
property bool invertPinnedItems: true // Makes the below a whitelist for the tray and blacklist for the pinned area
property list<var> pinnedItems: [ "Fcitx" ]
property bool filterPassive: true
}
property JsonObject musicRecognition: JsonObject {
property int timeout: 16
property int interval: 4
}
property bool autoIdleInhibit: false
property JsonObject search: JsonObject {
property int nonAppResultDelay: 30 // This prevents lagging when typing
property string engineBaseUrl: "https://www.google.com/search?q="
property list<string> excludedSites: ["quora.com", "facebook.com"]
property bool sloppy: false // Uses levenshtein distance based scoring instead of fuzzy sort. Very weird.
property JsonObject prefix: JsonObject {
property bool showDefaultActionsWithoutPrefix: true
property string action: "/"
property string app: ">"
property string clipboard: ";"
property string emojis: ":"
property string math: "="
property string shellCommand: "$"
property string webSearch: "?"
}
property JsonObject imageSearch: JsonObject {
property string imageSearchEngineBaseUrl: "https://lens.google.com/uploadbyurl?url="
property bool useCircleSelection: false
}
property JsonObject fileSearch: JsonObject {
property bool enable: false
property list<string> paths: [Directories.home]
property list<string> exclude: [".git", "node_modules", "target", "build"]
property bool excludeHiddenDirs: true
property int maxResults: 30
}
}
property JsonObject sidebar: JsonObject {
property bool keepRightSidebarLoaded: true
property int width: 460 // Collapsed sidebar width in px
property int widthExtended: 750 // Extended (Ctrl+O) sidebar width in px
property JsonObject translator: JsonObject {
property bool enable: false
property int delay: 300 // Delay before sending request. Reduces (potential) rate limits and lag.
}
property JsonObject ai: JsonObject {
property bool textFadeIn: false
// Chat message text size. Defaults to the theme value (live
// binding, like cheatsheet.fontSize) so a theme change tracks
// automatically instead of pinning a literal.
property int fontSize: Appearance.font.pixelSize.small
}
property JsonObject booru: JsonObject {
property bool allowNsfw: false
property string defaultProvider: "yandere"
property int limit: 20
property JsonObject zerochan: JsonObject {
property string username: "[unset]"
}
}
property JsonObject cornerOpen: JsonObject {
property bool enable: true
property bool bottom: false
property bool valueScroll: true
property bool clickless: false
property int cornerRegionWidth: 250
property int cornerRegionHeight: 5
property bool visualize: false
property bool clicklessCornerEnd: true
property int clicklessCornerVerticalOffset: 1
}
property JsonObject quickToggles: JsonObject {
property string style: "android" // Options: classic, android
property JsonObject android: JsonObject {
property int columns: 5
property list<var> toggles: [
{ "size": 2, "type": "network" },
{ "size": 2, "type": "bluetooth" },
{ "size": 1, "type": "idleInhibitor" },
{ "size": 1, "type": "mic" },
{ "size": 2, "type": "audio" },
{ "size": 2, "type": "nightLight" }
]
}
}
property JsonObject quickSliders: JsonObject {
property bool enable: false
property bool showMic: false
property bool showVolume: true
property bool showBrightness: true
}
}
property JsonObject screenRecord: JsonObject {
property string savePath: Directories.videos.replace("file://","") // strip "file://"
}
property JsonObject screenSnip: JsonObject {
property string savePath: "" // only copy to clipboard when empty
}
property JsonObject sounds: JsonObject {
property bool battery: false
property bool pomodoro: false
property string theme: "freedesktop"
}
property JsonObject time: JsonObject {
// https://doc.qt.io/qt-6/qtime.html#toString
property string format: "hh:mm"
property string shortDateFormat: "dd/MM"
property string dateWithYearFormat: "dd/MM/yyyy"
property string dateFormat: "ddd, dd/MM"
property JsonObject pomodoro: JsonObject {
property int breakTime: 300
property int cyclesBeforeLongBreak: 4
property int focus: 1500
property int longBreak: 900
property bool notifications: true
}
property bool secondPrecision: false
}
property JsonObject updates: JsonObject {
property bool enableCheck: true
property int checkInterval: 120 // minutes
property int adviseUpdateThreshold: 75 // packages
property int stronglyAdviseUpdateThreshold: 200 // packages
}
property JsonObject wallpaperSelector: JsonObject {
property bool useSystemFileDialog: false
}
property JsonObject windows: JsonObject {
property bool showTitlebar: true // Client-side decoration for shell apps
property bool centerTitle: true
}
property JsonObject hacks: JsonObject {
property int arbitraryRaceConditionDelay: 20 // milliseconds
}
property JsonObject workSafety: JsonObject {
property JsonObject enable: JsonObject {
property bool wallpaper: false
property bool clipboard: false
}
property JsonObject triggerCondition: JsonObject {
property list<string> networkNameKeywords: ["airport", "cafe", "college", "company", "eduroam", "free", "guest", "public", "school", "university"]
property list<string> fileKeywords: ["anime", "booru", "ecchi", "hentai", "yande.re", "konachan", "breast", "nipples", "pussy", "nsfw", "spoiler", "girl"]
property list<string> linkKeywords: ["hentai", "porn", "sukebei", "hitomi.la", "rule34", "gelbooru", "fanbox", "dlsite"]
}
}
property JsonObject waffles: JsonObject {
// Some spots are kinda janky/awkward. Setting the following to
// false will make (some) stuff also be like that for accuracy.
// Example: the right-click menu of the Start button
property JsonObject tweaks: JsonObject {
property bool switchHandlePositionFix: true
property bool smootherMenuAnimations: true
property bool smootherSearchBar: true
}
property JsonObject bar: JsonObject {
property bool bottom: true
property bool leftAlignApps: false
}
property JsonObject actionCenter: JsonObject {
property list<string> toggles: [ "network", "bluetooth", "easyEffects", "powerProfile", "idleInhibitor", "nightLight", "darkMode", "antiFlashbang", "cloudflareWarp", "mic", "musicRecognition", "notifications", "onScreenKeyboard", "gameMode", "screenSnip", "colorPicker" ]
}
property JsonObject calendar: JsonObject {
property bool force2CharDayOfWeek: true
}
}
}
}
}

View file

@ -0,0 +1,66 @@
pragma Singleton
pragma ComponentBehavior: Bound
import qs.services
import qs.modules.common.functions
import QtCore
import QtQuick
import Quickshell
Singleton {
// XDG Dirs, with "file://"
readonly property string home: StandardPaths.standardLocations(StandardPaths.HomeLocation)[0]
readonly property string config: StandardPaths.standardLocations(StandardPaths.ConfigLocation)[0]
readonly property string state: StandardPaths.standardLocations(StandardPaths.StateLocation)[0]
readonly property string cache: StandardPaths.standardLocations(StandardPaths.CacheLocation)[0]
readonly property string genericCache: StandardPaths.standardLocations(StandardPaths.GenericCacheLocation)[0]
readonly property string documents: StandardPaths.standardLocations(StandardPaths.DocumentsLocation)[0]
readonly property string downloads: StandardPaths.standardLocations(StandardPaths.DownloadLocation)[0]
readonly property string pictures: StandardPaths.standardLocations(StandardPaths.PicturesLocation)[0]
readonly property string music: StandardPaths.standardLocations(StandardPaths.MusicLocation)[0]
readonly property string videos: StandardPaths.standardLocations(StandardPaths.MoviesLocation)[0]
// Other dirs used by the shell, without "file://"
property string assetsPath: Quickshell.shellPath("assets")
property string scriptPath: Quickshell.shellPath("scripts")
property string favicons: FileUtils.trimFileProtocol(`${Directories.cache}/media/favicons`)
property string coverArt: FileUtils.trimFileProtocol(`${Directories.cache}/media/coverart`)
property string tempImages: "/tmp/quickshell/media/images"
property string booruPreviews: FileUtils.trimFileProtocol(`${Directories.cache}/media/boorus`)
property string booruDownloads: FileUtils.trimFileProtocol(Directories.pictures + "/homework")
property string booruDownloadsNsfw: FileUtils.trimFileProtocol(Directories.pictures + "/homework/🌶️")
property string latexOutput: FileUtils.trimFileProtocol(`${Directories.cache}/media/latex`)
property string shellConfig: FileUtils.trimFileProtocol(`${Directories.config}/illogical-impulse`)
property string shellConfigName: "config.json"
property string shellConfigPath: `${Directories.shellConfig}/${Directories.shellConfigName}`
property string todoPath: FileUtils.trimFileProtocol(`${Directories.state}/user/todo.json`)
property string notesPath: FileUtils.trimFileProtocol(`${Directories.state}/user/notes.txt`)
property string conflictCachePath: FileUtils.trimFileProtocol(`${Directories.cache}/conflict-killer`)
property string notificationsPath: FileUtils.trimFileProtocol(`${Directories.cache}/notifications/notifications.json`)
property string generatedMaterialThemePath: FileUtils.trimFileProtocol(`${Directories.state}/user/generated/colors.json`)
property string generatedWallpaperCategoryPath: FileUtils.trimFileProtocol(`${Directories.state}/user/generated/wallpaper/category.txt`)
property string cliphistDecode: FileUtils.trimFileProtocol(`/tmp/quickshell/media/cliphist`)
property string screenshotTemp: "/tmp/quickshell/media/screenshot"
property string wallpaperSwitchScriptPath: FileUtils.trimFileProtocol(`${Directories.scriptPath}/colors/switchwall.sh`)
property string defaultAiPrompts: Quickshell.shellPath("defaults/ai/prompts")
property string userAiPrompts: FileUtils.trimFileProtocol(`${Directories.shellConfig}/ai/prompts`)
property string userActions: FileUtils.trimFileProtocol(`${Directories.shellConfig}/actions`)
property string aiChats: FileUtils.trimFileProtocol(`${Directories.state}/user/ai/chats`)
property string aiTranslationScriptPath: FileUtils.trimFileProtocol(`${Directories.scriptPath}/ai/gemini-translate.sh`)
property string recordScriptPath: FileUtils.trimFileProtocol(`${Directories.scriptPath}/videos/record.sh`)
property string userAvatarPathAccountsService: FileUtils.trimFileProtocol(`/var/lib/AccountsService/icons/${SystemInfo.username}`)
property string userAvatarPathRicersAndWeirdSystems: FileUtils.trimFileProtocol(`${Directories.home}.face`)
property string userAvatarPathRicersAndWeirdSystems2: FileUtils.trimFileProtocol(`${Directories.home}.face.icon`)
// Cleanup on init
Component.onCompleted: {
Quickshell.execDetached(["mkdir", "-p", `${shellConfig}`])
Quickshell.execDetached(["mkdir", "-p", `${favicons}`])
Quickshell.execDetached(["bash", "-c", `rm -rf '${coverArt}'; mkdir -p '${coverArt}'`])
Quickshell.execDetached(["bash", "-c", `rm -rf '${booruPreviews}'; mkdir -p '${booruPreviews}'`])
Quickshell.execDetached(["bash", "-c", `rm -rf '${latexOutput}'; mkdir -p '${latexOutput}'`])
Quickshell.execDetached(["bash", "-c", `rm -rf '${cliphistDecode}'; mkdir -p '${cliphistDecode}'`])
Quickshell.execDetached(["mkdir", "-p", `${aiChats}`])
Quickshell.execDetached(["mkdir", "-p", `${userActions}`])
Quickshell.execDetached(["rm", "-rf", `${tempImages}`])
}
}

View file

@ -0,0 +1,112 @@
pragma Singleton
// From https://github.com/caelestia-dots/shell (GPLv3)
import Quickshell
import qs.services
Singleton {
id: root
function getBatteryIcon(percentage: int): string {
if (percentage >= 93) return "battery_android_full";
if (percentage >= 78) return "battery_android_6";
if (percentage >= 64) return "battery_android_5";
if (percentage >= 50) return "battery_android_4";
if (percentage >= 35) return "battery_android_3";
if (percentage >= 21) return "battery_android_2";
if (percentage >= 7) return "battery_android_1";
return "battery_android_0";
}
function getBluetoothDeviceMaterialSymbol(systemIconName: string): string {
if (systemIconName.includes("headset") || systemIconName.includes("headphones"))
return "headphones";
if (systemIconName.includes("audio"))
return "speaker";
if (systemIconName.includes("phone"))
return "smartphone";
if (systemIconName.includes("mouse"))
return "mouse";
if (systemIconName.includes("keyboard"))
return "keyboard";
return "bluetooth";
}
function getNetworkMaterialSymbol() {
if (Network.ethernet) return "lan";
if (Network.wifiEnabled && Network.wifiStatus === "connected") {
const strength = Network.active?.strength ?? 0
if (strength > 83) return "signal_wifi_4_bar";
if (strength > 67) return "network_wifi";
if (strength > 50) return "network_wifi_3_bar";
if (strength > 33) return "network_wifi_2_bar";
if (strength > 17) return "network_wifi_1_bar";
return "signal_wifi_0_bar"
} else {
if (Network.wifiStatus === "connecting") return "signal_wifi_statusbar_not_connected";
if (Network.wifiStatus === "disconnected") return "wifi_find";
if (Network.wifiStatus === "disabled") return "signal_wifi_off";
return "signal_wifi_bad";
}
}
readonly property var weatherIconMap: ({
"113": "clear_day",
"116": "partly_cloudy_day",
"119": "cloud",
"122": "cloud",
"143": "foggy",
"176": "rainy",
"179": "rainy",
"182": "rainy",
"185": "rainy",
"200": "thunderstorm",
"227": "cloudy_snowing",
"230": "snowing_heavy",
"248": "foggy",
"260": "foggy",
"263": "rainy",
"266": "rainy",
"281": "rainy",
"284": "rainy",
"293": "rainy",
"296": "rainy",
"299": "rainy",
"302": "weather_hail",
"305": "rainy",
"308": "weather_hail",
"311": "rainy",
"314": "rainy",
"317": "rainy",
"320": "cloudy_snowing",
"323": "cloudy_snowing",
"326": "cloudy_snowing",
"329": "snowing_heavy",
"332": "snowing_heavy",
"335": "snowing",
"338": "snowing_heavy",
"350": "rainy",
"353": "rainy",
"356": "rainy",
"359": "weather_hail",
"362": "rainy",
"365": "rainy",
"368": "cloudy_snowing",
"371": "snowing",
"374": "rainy",
"377": "rainy",
"386": "thunderstorm",
"389": "thunderstorm",
"392": "thunderstorm",
"395": "snowing"
})
function getWeatherIcon(code) {
const key = String(code)
if (weatherIconMap.hasOwnProperty(key)) {
return weatherIconMap[key]
}
}
}

View file

@ -0,0 +1,31 @@
pragma Singleton
import Quickshell
Singleton {
// Formats
readonly property list<string> validImageTypes: ["jpeg", "png", "webp", "tiff", "svg"]
readonly property list<string> validImageExtensions: ["jpg", "jpeg", "png", "webp", "tif", "tiff", "svg"]
function isValidImageByName(name: string): bool {
return validImageExtensions.some(t => name.endsWith(`.${t}`));
}
// Thumbnails
// https://specifications.freedesktop.org/thumbnail-spec/latest/directory.html
readonly property var thumbnailSizes: ({
"normal": 128,
"large": 256,
"x-large": 512,
"xx-large": 1024
})
function thumbnailSizeNameForDimensions(width: int, height: int): string {
const sizeNames = Object.keys(thumbnailSizes);
for(let i = 0; i < sizeNames.length; i++) {
const sizeName = sizeNames[i];
const maxSize = thumbnailSizes[sizeName];
if (width <= maxSize && height <= maxSize) return sizeName;
}
return "xx-large";
}
}

View file

@ -0,0 +1,163 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Io
Singleton {
id: root
property alias states: persistentStatesJsonAdapter
property string fileDir: Directories.state
property string fileName: "states.json"
property string filePath: `${root.fileDir}/${root.fileName}`
property bool ready: false
property string previousHyprlandInstanceSignature: ""
property bool isNewHyprlandInstance: previousHyprlandInstanceSignature !== states.hyprlandInstanceSignature
onReadyChanged: {
root.previousHyprlandInstanceSignature = root.states.hyprlandInstanceSignature
root.states.hyprlandInstanceSignature = Quickshell.env("HYPRLAND_INSTANCE_SIGNATURE") || ""
}
Timer {
id: fileReloadTimer
interval: 100
repeat: false
onTriggered: {
persistentStatesFileView.reload()
}
}
Timer {
id: fileWriteTimer
interval: 100
repeat: false
onTriggered: {
persistentStatesFileView.writeAdapter()
}
}
FileView {
id: persistentStatesFileView
path: root.filePath
watchChanges: true
onFileChanged: fileReloadTimer.restart()
onAdapterUpdated: fileWriteTimer.restart()
onLoaded: root.ready = true
onLoadFailed: error => {
console.log("Failed to load persistent states file:", error);
if (error == FileViewError.FileNotFound) {
fileWriteTimer.restart();
}
}
adapter: JsonAdapter {
id: persistentStatesJsonAdapter
property string hyprlandInstanceSignature: ""
property JsonObject ai: JsonObject {
property string model: "gemini-2.5-flash"
property real temperature: 0.5
}
property JsonObject cheatsheet: JsonObject {
property int tabIndex: 0
}
property JsonObject sidebar: JsonObject {
property JsonObject bottomGroup: JsonObject {
property bool collapsed: false
property int tab: 0
}
}
property JsonObject booru: JsonObject {
property bool allowNsfw: false
property string provider: "yandere"
}
property JsonObject idle: JsonObject {
property bool inhibit: false
}
property JsonObject overlay: JsonObject {
property list<string> open: ["crosshair", "recorder", "volumeMixer", "resources"]
property JsonObject crosshair: JsonObject {
property bool pinned: false
property bool clickthrough: true
property real x: 827
property real y: 441
property real width: 250
property real height: 100
}
property JsonObject floatingImage: JsonObject {
property bool pinned: false
property bool clickthrough: false
property real x: 1650
property real y: 390
property real width: 0
property real height: 0
}
property JsonObject fpsLimiter: JsonObject {
property bool pinned: false
property bool clickthrough: false
property real x: 1570
property real y: 615
property real width: 280
property real height: 80
}
property JsonObject recorder: JsonObject {
property bool pinned: false
property bool clickthrough: false
property real x: 80
property real y: 80
property real width: 350
property real height: 130
}
property JsonObject resources: JsonObject {
property bool pinned: false
property bool clickthrough: true
property real x: 1500
property real y: 770
property real width: 350
property real height: 200
property int tabIndex: 0
}
property JsonObject volumeMixer: JsonObject {
property bool pinned: false
property bool clickthrough: false
property real x: 80
property real y: 280
property real width: 350
property real height: 600
property int tabIndex: 0
}
property JsonObject notes: JsonObject {
property bool pinned: false
property bool clickthrough: true
property real x: 1400
property real y: 42
property real width: 460
property real height: 330
}
}
property JsonObject timer: JsonObject {
property JsonObject pomodoro: JsonObject {
property bool running: false
property int start: 0
property bool isBreak: false
property int cycle: 0
}
property JsonObject stopwatch: JsonObject {
property bool running: false
property int start: 0
property list<var> laps: []
}
}
}
}
}

View file

@ -0,0 +1,36 @@
pragma Singleton
import Quickshell
import "myers.js" as Myers
/**
* Bit-parallel fuzzy matching helpers.
*/
Singleton {
/** Lowercase + NFC normalize a string. Same transform applied internally by prepare() and score(). */
function normalize(value) { return Myers.normalize(value) }
/** Precompute pattern state for reuse across comparisons. */
function prepare(pattern) { return Myers.prepare(pattern) }
/** Return Levenshtein edit distance. Normalizes text automatically. */
function distance(prepared, text) { return Myers.distance(prepared, text) }
/** Return Levenshtein edit distance. text must already be normalized. */
function distanceNormalized(prepared, text) { return Myers.distanceNormalized(prepared, text) }
/** Return normalized similarity in [0, 1]. */
function score(prepared, text) { return Myers.score(prepared, text) }
/** Compute one-off similarity without prepare(). */
function computeScore(pattern, text) { return Myers.computeScore(pattern, text) }
/** Compatibility wrapper for Levendist.computeTextMatchScore. */
function computeTextMatchScore(s1, s2) { return Myers.computeTextMatchScore(s1, s2) }
/**
* Return candidates sorted by descending similarity.
* opts: { key?: string, threshold?: number }
*/
function search(prepared, candidates, opts) { return Myers.search(prepared, candidates, opts) }
}

View file

@ -0,0 +1,174 @@
pragma Singleton
import Quickshell
Singleton {
id: root
/**
* Returns a color with the hue of color2 and the saturation, value, and alpha of color1.
*
* @param {string} color1 - The base color (any Qt.color-compatible string).
* @param {string} color2 - The color to take hue from.
* @returns {Qt.rgba} The resulting color.
*/
function colorWithHueOf(color1, color2) {
var c1 = Qt.color(color1);
var c2 = Qt.color(color2);
// Qt.color hsvHue/hsvSaturation/hsvValue/alpha return 0-1
var hue = c2.hsvHue;
var sat = c1.hsvSaturation;
var val = c1.hsvValue;
var alpha = c1.a;
return Qt.hsva(hue, sat, val, alpha);
}
/**
* Returns a color with the saturation of color2 and the hue/value/alpha of color1.
*
* @param {string} color1 - The base color (any Qt.color-compatible string).
* @param {string} color2 - The color to take saturation from.
* @returns {Qt.rgba} The resulting color.
*/
function colorWithSaturationOf(color1, color2) {
var c1 = Qt.color(color1);
var c2 = Qt.color(color2);
var hue = c1.hsvHue;
var sat = c2.hsvSaturation;
var val = c1.hsvValue;
var alpha = c1.a;
return Qt.hsva(hue, sat, val, alpha);
}
/**
* Returns a color with the given lightness and the hue, saturation, and alpha of the input color (using HSL).
*
* @param {string} color - The base color (any Qt.color-compatible string).
* @param {number} lightness - The lightness value to use (0-1).
* @returns {Qt.rgba} The resulting color.
*/
function colorWithLightness(color, lightness) {
var c = Qt.color(color);
return Qt.hsla(c.hslHue, c.hslSaturation, lightness, c.a);
}
/**
* Returns a color with the lightness of color2 and the hue, saturation, and alpha of color1 (using HSL).
*
* @param {string} color1 - The base color (any Qt.color-compatible string).
* @param {string} color2 - The color to take lightness from.
* @returns {Qt.rgba} The resulting color.
*/
function colorWithLightnessOf(color1, color2) {
var c2 = Qt.color(color2);
return colorWithLightness(color1, c2.hslLightness);
}
/**
* Adapts color1 to the accent (hue and saturation) of color2 using HSL, keeping lightness and alpha from color1.
*
* @param {string} color1 - The base color (any Qt.color-compatible string).
* @param {string} color2 - The accent color.
* @returns {Qt.rgba} The resulting color.
*/
function adaptToAccent(color1, color2) {
var c1 = Qt.color(color1);
var c2 = Qt.color(color2);
var hue = c2.hslHue;
var sat = c2.hslSaturation;
var light = c1.hslLightness;
var alpha = c1.a;
return Qt.hsla(hue, sat, light, alpha);
}
/**
* Mixes two colors by a given percentage.
*
* @param {string} color1 - The first color (any Qt.color-compatible string).
* @param {string} color2 - The second color.
* @param {number} percentage - The mix ratio (0-1). 1 = all color1, 0 = all color2.
* @returns {Qt.rgba} The resulting mixed color.
*/
function mix(color1, color2, percentage = 0.5) {
var c1 = Qt.color(color1);
var c2 = Qt.color(color2);
return Qt.rgba(percentage * c1.r + (1 - percentage) * c2.r, percentage * c1.g + (1 - percentage) * c2.g, percentage * c1.b + (1 - percentage) * c2.b, percentage * c1.a + (1 - percentage) * c2.a);
}
/**
* Transparentizes a color by a given percentage.
*
* @param {string} color - The color (any Qt.color-compatible string).
* @param {number} percentage - The amount to transparentize (0-1).
* @returns {Qt.rgba} The resulting color.
*/
function transparentize(color, percentage = 1) {
var c = Qt.color(color);
return Qt.rgba(c.r, c.g, c.b, c.a * (1 - percentage));
}
/**
* Sets the alpha channel of a color.
*
* @param {string} color - The base color (any Qt.color-compatible string).
* @param {number} alpha - The desired alpha (0-1).
* @returns {Qt.rgba} The resulting color with applied alpha.
*/
function applyAlpha(color, alpha) {
var c = Qt.color(color);
var a = Math.max(0, Math.min(1, alpha));
return Qt.rgba(c.r, c.g, c.b, a);
}
/**
* Returns true if the color is considered "dark" (hslLightness < 0.5).
*
* @param {string} color - The color to check (any Qt.color-compatible string).
* @returns {boolean} True if dark, false otherwise.
*/
function isDark(color) {
var c = Qt.color(color);
return c.hslLightness < 0.5;
}
/**
* Clamps a value to the inclusive range [0, 1].
*
* @param {number} x - The value to clamp.
* @returns {number} The clamped value in the range [0, 1].
*/
function clamp01(x) {
return Math.min(1, Math.max(0, x));
}
/**
* Solves for the solid overlay color that, when composited over a base color
* with a given opacity, yields the target color.
*
* The compositing equation is:
* result = overlay * overlayOpacity + base * (1 - overlayOpacity)
*
* This function algebraically inverts that equation per channel.
*
* @param {Qt.rgba} baseColor - The base (background) color.
* @param {Qt.rgba} targetColor - The resulting color after compositing.
* @param {number} overlayOpacity - The overlay opacity (0-1).
* @returns {Qt.rgba} The solved overlay color
*/
function solveOverlayColor(baseColor, targetColor, overlayOpacity) {
const bc = Qt.color(baseColor);
const tc = Qt.color(targetColor);
let invA = 1.0 - overlayOpacity;
let r = (tc.r - bc.r * invA) / overlayOpacity;
let g = (tc.g - bc.g * invA) / overlayOpacity;
let b = (tc.b - bc.b * invA) / overlayOpacity;
return Qt.rgba(clamp01(r), clamp01(g), clamp01(b), overlayOpacity);
}
}

View file

@ -0,0 +1,27 @@
pragma Singleton
import Quickshell
Singleton {
id: root
function getFirstDayOfWeek(date, firstDay = 1) {
const d = new Date(date); // Copy
const day = d.getDay(); // 0 = Sunday, 1 = Monday, ..., 6 = Saturday
// Calculate difference to firstDay
const diff = (day - firstDay + 7) % 7;
d.setDate(d.getDate() - diff);
return d;
}
function sameDate(d1, d2) {
return (d1.getFullYear() === d2.getFullYear() && d1.getMonth() === d2.getMonth() && d1.getDate() === d2.getDate());
}
function getIthDayDateOfSameWeek(date, i, firstDay = 1) {
const firstDayDate = root.getFirstDayOfWeek(date, firstDay);
const targetDate = new Date(firstDayDate);
targetDate.setDate(firstDayDate.getDate() + i);
return targetDate;
}
}

View file

@ -0,0 +1,71 @@
pragma Singleton
import Quickshell
Singleton {
id: root
/**
* Trims the File protocol off the input string
* @param {string} str
* @returns {string}
*/
function trimFileProtocol(str) {
let s = str;
if (typeof s !== "string") s = str.toString(); // Convert to string if it's an url or whatever
return s.startsWith("file://") ? s.slice(7) : s;
}
/**
* Extracts the file name from a file path
* @param {string} str
* @returns {string}
*/
function fileNameForPath(str) {
if (typeof str !== "string") return "";
const trimmed = trimFileProtocol(str);
return trimmed.split(/[\\/]/).pop();
}
/**
* Extracts the folder name from a directory path
* @param {string} str
* @returns {string}
*/
function folderNameForPath(str) {
if (typeof str !== "string") return "";
const trimmed = trimFileProtocol(str);
// Remove trailing slash if present
const noTrailing = trimmed.endsWith("/") ? trimmed.slice(0, -1) : trimmed;
if (!noTrailing) return "";
return noTrailing.split(/[\\/]/).pop();
}
/**
* Removes the file extension from a file path or name
* @param {string} str
* @returns {string}
*/
function trimFileExt(str) {
if (typeof str !== "string") return "";
const trimmed = trimFileProtocol(str);
const lastDot = trimmed.lastIndexOf(".");
if (lastDot > -1 && lastDot > trimmed.lastIndexOf("/")) {
return trimmed.slice(0, lastDot);
}
return trimmed;
}
/**
* Returns the parent directory of a given file path
* @param {string} str
* @returns {string}
*/
function parentDirectory(str) {
if (typeof str !== "string") return "";
const trimmed = trimFileProtocol(str);
const parts = trimmed.split(/[\\/]/);
if (parts.length <= 1) return "";
parts.pop();
return parts.join("/");
}
}

View file

@ -0,0 +1,18 @@
pragma Singleton
import Quickshell
import "fuzzysort.js" as FuzzySort
/**
* Wrapper for FuzzySort to play nicely with Quickshell's imports
*/
Singleton {
function go(...args) {
return FuzzySort.go(...args)
}
function prepare(...args) {
return FuzzySort.prepare(...args)
}
}

View file

@ -0,0 +1,123 @@
pragma Singleton
import Quickshell
/**
* Layout-aware search helpers.
*/
Singleton {
id: root
// Physical layout maps. Bidirectional pairs are generated in the IIFE below.
readonly property var _layouts: (function() {
const list = [
{
name: "uk",
map: {
"й":"q","ц":"w","у":"e","к":"r","е":"t","н":"y","г":"u",
"ш":"i","щ":"o","з":"p","х":"[","ї":"]",
"Й":"Q","Ц":"W","У":"E","К":"R","Е":"T","Н":"Y","Г":"U",
"Ш":"I","Щ":"O","З":"P","Х":"{","Ї":"}",
"ф":"a","і":"s","в":"d","а":"f","п":"g","р":"h","о":"j",
"л":"k","д":"l","ж":";","є":"'",
"Ф":"A","І":"S","В":"D","А":"F","П":"G","Р":"H","О":"J",
"Л":"K","Д":"L","Ж":":","Є":'"',
"я":"z","ч":"x","с":"c","м":"v","и":"b","т":"n","ь":"m",
"б":",","ю":".",
"Я":"Z","Ч":"X","С":"C","М":"V","И":"B","Т":"N","Ь":"M",
"Б":"<","Ю":">"
}
},
{
name: "de",
map: {
"y":"z","Y":"Z","z":"y","Z":"Y",
"ü":"[","Ü":"{","ö":";","Ö":":","ä":"'","Ä":'"',"ß":"-"
}
},
{
name: "fr",
map: {
"a":"q","A":"Q","z":"w","Z":"W","q":"a","Q":"A","w":"z","W":"Z",
"é":"2","è":"7","ê":"[","à":"0","ù":"`","ç":"9","œ":"p",
"m":";","M":":",";":"m",":":"M"
}
}
];
// Make all layout maps bidirectional (for example, add "q":"й" for "uk").
for (const layout of list) {
const reversePairs = {};
for (const [key, value] of Object.entries(layout.map)) {
if (layout.map[value] === undefined) {
reversePairs[value] = key;
}
}
Object.assign(layout.map, reversePairs);
}
return list;
})()
// Compact Cyrillic-to-Latin transliteration for search.
readonly property var _cyrillicToLatin: ({
"а":"a","б":"b","в":"v","г":"g","ґ":"g",
"д":"d","е":"e","є":"e","ж":"z","з":"z",
"и":"i","і":"i","ї":"i","й":"y","к":"k",
"л":"l","м":"m","н":"n","о":"o","п":"p",
"р":"r","с":"s","т":"t","у":"u","ф":"f",
"х":"h","ц":"c","ч":"c","ш":"s","щ":"s",
"ь":"","ъ":"","ю":"u","я":"a",
"ы":"y","э":"e","ё":"o",
"А":"A","Б":"B","В":"V","Г":"G","Ґ":"G",
"Д":"D","Е":"E","Є":"E","Ж":"Z","З":"Z",
"И":"I","І":"I","Ї":"I","Й":"Y","К":"K",
"Л":"L","М":"M","Н":"N","О":"O","П":"P",
"Р":"R","С":"S","Т":"T","У":"U","Ф":"F",
"Х":"H","Ц":"C","Ч":"C","Ш":"S","Щ":"S",
"Ь":"","Ъ":"","Ю":"U","Я":"A",
"Ы":"Y","Э":"E","Ё":"O"
})
// Internal character mapping helper.
function _mapWithLayout(str, layoutName) {
const layout = root._layouts.find(l => l.name === layoutName);
if (!layout) return str;
return str.split("").map(ch => {
const mapped = layout.map[ch];
return mapped !== undefined ? mapped : ch;
}).join("");
}
/**
* Return unique layout-corrected variants.
* Works in both directions for all configured layouts.
*/
function translateAll(str) {
if (!str) return [];
const results = [];
const seen = new Set([str]); // Keep input out of final results.
for (const layout of root._layouts) {
const translated = root._mapWithLayout(str, layout.name);
if (!seen.has(translated)) {
seen.add(translated);
results.push(translated);
}
}
return results;
}
/**
* Return transliterated Latin text.
* Return null when input contains no Cyrillic characters.
*/
function transliterate(str) {
if (!str || !/[Ѐ-ӿ]/.test(str)) return null;
const table = root._cyrillicToLatin;
return str.split("").map(ch => {
const mapped = table[ch];
return mapped !== undefined ? mapped : ch;
}).join("").replace(/\s+/g, " ").trim();
}
}

View file

@ -0,0 +1,18 @@
pragma Singleton
import Quickshell
import "levendist.js" as Levendist
/**
* Wrapper for levendist.js to play nicely with Quickshell's imports
*/
Singleton {
function computeScore(...args) {
return Levendist.computeScore(...args)
}
function computeTextMatchScore(...args) {
return Levendist.computeTextMatchScore(...args)
}
}

View file

@ -0,0 +1,111 @@
pragma Singleton
import Quickshell
Singleton {
id: root
/**
* @param { string } summary
* @returns { string }
*/
function findSuitableMaterialSymbol(summary = "") {
const defaultType = 'chat';
if (summary.length === 0) return defaultType;
const keywordsToTypes = {
'reboot': 'restart_alt',
'record': 'screen_record',
'battery': 'power',
'power': 'power',
'screenshot': 'screenshot_monitor',
'welcome': 'waving_hand',
'time': 'scheduleb',
'installed': 'download',
'configuration reloaded': 'reset_wrench',
'unable': 'question_mark',
"couldn't": 'question_mark',
'config': 'reset_wrench',
'update': 'update',
'ai response': 'neurology',
'control': 'settings',
'upsca': 'compare',
'music': 'queue_music',
'install': 'deployed_code_update',
'input': 'keyboard_alt',
'preedit': 'keyboard_alt',
'startswith:file': 'folder_copy', // Declarative startsWith check
};
const lowerSummary = summary.toLowerCase();
for (const [keyword, type] of Object.entries(keywordsToTypes)) {
if (keyword.startsWith('startswith:')) {
const startsWithKeyword = keyword.replace('startswith:', '');
if (lowerSummary.startsWith(startsWithKeyword)) {
return type;
}
} else if (lowerSummary.includes(keyword)) {
return type;
}
}
return defaultType;
}
/**
* @param { number | string | Date } timestamp
* @returns { string }
*/
function getFriendlyNotifTimeString(timestamp) {
if (!timestamp) return '';
const messageTime = new Date(timestamp);
const now = new Date();
const diffMs = now.getTime() - messageTime.getTime();
// Less than 1 minute
if (diffMs < 60000)
return 'Now';
// Same day - show relative time
if (messageTime.toDateString() === now.toDateString()) {
const diffMinutes = Math.floor(diffMs / 60000);
const diffHours = Math.floor(diffMs / 3600000);
if (diffHours > 0) {
return `${diffHours}h`;
} else {
return `${diffMinutes}m`;
}
}
// Yesterday
if (messageTime.toDateString() === new Date(now.getTime() - 86400000).toDateString())
return 'Yesterday';
// Older dates
return Qt.formatDateTime(messageTime, "MMMM dd");
}
function processNotificationBody(body, appName) {
let processedBody = body
// Clean Chromium-based browsers notifications - remove first line
if (appName) {
const lowerApp = appName.toLowerCase()
const chromiumBrowsers = [
"brave", "chrome", "chromium", "vivaldi", "opera", "microsoft edge"
]
if (chromiumBrowsers.some(name => lowerApp.includes(name))) {
const lines = body.split('\n\n')
if (lines.length > 1 && lines[0].startsWith('<a')) {
processedBody = lines.slice(1).join('\n\n')
}
}
}
processedBody = processedBody.replace(/<img/gi, '\n\n<img');
return processedBody
}
}

View file

@ -0,0 +1,98 @@
pragma Singleton
import Quickshell
Singleton {
id: root
function toPlainObject(qtObj) {
if (qtObj === null || typeof qtObj !== "object") return qtObj;
// Handle true arrays
if (Array.isArray(qtObj)) {
return qtObj.map(item => toPlainObject(item));
}
// Handle array-like Qt objects (e.g., have length and numeric keys)
if (
typeof qtObj.length === "number" &&
qtObj.length > 0 &&
Object.keys(qtObj).every(
key => !isNaN(key) || key === "length"
)
) {
let arr = [];
for (let i = 0; i < qtObj.length; i++) {
arr.push(toPlainObject(qtObj[i]));
}
return arr;
}
const result = ({});
for (let key in qtObj) {
if (
typeof qtObj[key] !== "function" &&
!key.startsWith("objectName") &&
!key.startsWith("children") &&
!key.startsWith("object") &&
!key.startsWith("parent") &&
!key.startsWith("metaObject") &&
!key.startsWith("destroyed") &&
!key.startsWith("reloadableId")
) {
result[key] = toPlainObject(qtObj[key]);
}
}
// console.log(JSON.stringify(result))
return result;
}
function applyToQtObject(qtObj, jsonObj) {
// console.log("applyToQtObject", JSON.stringify(qtObj, null, 2), "<<", JSON.stringify(jsonObj, null, 2));
if (!qtObj || typeof jsonObj !== "object" || jsonObj === null) return;
// Detect array-like Qt objects
const isQtArrayLike = obj => {
return obj && typeof obj === "object" &&
typeof obj.length === "number" &&
obj.length > 0 &&
Object.keys(obj).every(key => !isNaN(key) || key === "length");
};
// If both are arrays or array-like, update in place or replace
if ((Array.isArray(qtObj) || isQtArrayLike(qtObj)) && Array.isArray(jsonObj)) {
qtObj.length = 0;
for (let i = 0; i < jsonObj.length; i++) {
qtObj.push(jsonObj[i]);
}
return;
}
// If target is array or array-like but source is not, clear
if ((Array.isArray(qtObj) || isQtArrayLike(qtObj)) && !Array.isArray(jsonObj)) {
qtObj.length = 0;
return;
}
// If source is array but target is not, assign directly if possible
if (!(Array.isArray(qtObj) || isQtArrayLike(qtObj)) && Array.isArray(jsonObj)) {
return jsonObj;
}
for (let key in jsonObj) {
if (!qtObj.hasOwnProperty(key)) continue;
const value = qtObj[key];
const jsonValue = jsonObj[key];
// console.log("applying to qt obj key:", value, "jsonValue:", jsonValue);
if ((Array.isArray(value) || isQtArrayLike(value)) && Array.isArray(jsonValue)) {
value.length = 0;
for (let i = 0; i < jsonValue.length; i++) {
value.push(jsonValue[i]);
}
} else if (value && typeof value === "object" && !Array.isArray(value) && !isQtArrayLike(value)) {
applyToQtObject(value, jsonValue);
} else {
qtObj[key] = jsonValue;
}
}
}
}

View file

@ -0,0 +1,63 @@
pragma Singleton
import Quickshell
import Quickshell.Services.Mpris
import qs.services
import qs.modules.common
Singleton {
id: root
function closeAllWindows() {
HyprlandData.windowList.map(w => w.pid).forEach(pid => {
Quickshell.execDetached(["kill", pid]);
});
}
function changePassword() {
Quickshell.execDetached(["bash", "-c", `${Config.options.apps.changePassword}`]);
}
function lock() {
Quickshell.execDetached(["loginctl", "lock-session"]);
}
function suspend() {
pauseAllPlayers();
Quickshell.execDetached(["bash", "-c", "systemctl suspend || loginctl suspend"]);
}
function logout() {
closeAllWindows();
Quickshell.execDetached(["pkill", "-i", "Hyprland"]);
}
function launchTaskManager() {
Quickshell.execDetached(["bash", "-c", `${Config.options.apps.taskManager}`]);
}
function hibernate() {
pauseAllPlayers();
Quickshell.execDetached(["bash", "-c", `systemctl hibernate || loginctl hibernate`]);
}
function pauseAllPlayers() {
for (const player of Mpris.players.values) {
if (player.canPause) player.pause();
}
}
function poweroff() {
closeAllWindows();
Quickshell.execDetached(["bash", "-c", `systemctl poweroff || loginctl poweroff`]);
}
function reboot() {
closeAllWindows();
Quickshell.execDetached(["bash", "-c", `reboot || loginctl reboot`]);
}
function rebootToFirmware() {
closeAllWindows();
Quickshell.execDetached(["bash", "-c", `systemctl reboot --firmware-setup || loginctl reboot --firmware-setup`]);
}
}

View file

@ -0,0 +1,375 @@
pragma Singleton
import Quickshell
Singleton {
id: root
/**
* Formats a string according to the args that are passed inc
* @param { string } str
* @param {...any} args
* @returns { string }
*/
function format(str, ...args) {
return str.replace(/{(\d+)}/g, (match, index) => typeof args[index] !== 'undefined' ? args[index] : match);
}
/**
* Returns the domain of the passed in url or null
* @param { string } url
* @returns { string| null }
*/
function getDomain(url) {
const match = url.match(/^(?:https?:\/\/)?(?:www\.)?([^\/]+)/);
return match ? match[1] : null;
}
/**
* Returns the base url of the passed in url or null
* @param { string } url
* @returns { string | null }
*/
function getBaseUrl(url) {
const match = url.match(/^(https?:\/\/[^\/]+)(\/.*)?$/);
return match ? match[1] : null;
}
/**
* Escapes single quotes in shell commands
* @param { string } str
* @returns { string }
*/
function shellSingleQuoteEscape(str) {
return String(str)
// .replace(/\\/g, '\\\\')
.replace(/'/g, "'\\''");
}
/**
* Splits markdown blocks into three different types: text, think, and code.
* @param { string } markdown
* @returns {Array<{type: "text" | "think" | "code", content: string, lang?: string, completed?: boolean}>}
*/
function splitMarkdownBlocks(markdown) {
const regex = /```(\w+)?\n([\s\S]*?)```|<think>([\s\S]*?)<\/think>/g;
/**
* @type {{type: "text" | "think" | "code"; content: string; lang: string | undefined; completed: boolean | undefined}[]}
*/
let result = [];
let lastIndex = 0;
let match;
while ((match = regex.exec(markdown)) !== null) {
if (match.index > lastIndex) {
const text = markdown.slice(lastIndex, match.index);
if (text.trim()) {
result.push({
type: "text",
content: text
});
}
}
if (match[0].startsWith('```')) {
if (match[2] && match[2].trim()) {
result.push({
type: "code",
lang: match[1] || "",
content: match[2],
completed: true
});
}
} else if (match[0].startsWith('<think>')) {
if (match[3] && match[3].trim()) {
result.push({
type: "think",
content: match[3],
completed: true
});
}
}
lastIndex = regex.lastIndex;
}
// Handle any remaining text after the last match
if (lastIndex < markdown.length) {
const text = markdown.slice(lastIndex);
// Check for unfinished <think> block
const thinkStart = text.indexOf('<think>');
const codeStart = text.indexOf('```');
if (thinkStart !== -1 && (codeStart === -1 || thinkStart < codeStart)) {
const beforeThink = text.slice(0, thinkStart);
if (beforeThink.trim()) {
result.push({
type: "text",
content: beforeThink
});
}
const thinkContent = text.slice(thinkStart + 7);
if (thinkContent.trim()) {
result.push({
type: "think",
content: thinkContent,
completed: false
});
}
} else if (codeStart !== -1) {
const beforeCode = text.slice(0, codeStart);
if (beforeCode.trim()) {
result.push({
type: "text",
content: beforeCode
});
}
// Try to detect language after ```
const codeLangMatch = text.slice(codeStart + 3).match(/^(\w+)?\n/);
let lang = "";
let codeContentStart = codeStart + 3;
if (codeLangMatch) {
lang = codeLangMatch[1] || "";
codeContentStart += codeLangMatch[0].length;
} else if (text[codeStart + 3] === '\n') {
codeContentStart += 1;
}
const codeContent = text.slice(codeContentStart);
if (codeContent.trim()) {
result.push({
type: "code",
lang,
content: codeContent,
completed: false
});
}
} else if (text.trim()) {
result.push({
type: "text",
content: text
});
}
}
// console.log(JSON.stringify(result, null, 2));
return result;
}
/**
* Returns the original string with backslashes escaped
* @param { string } str
* @returns { string }
*/
function escapeBackslashes(str) {
return str.replace(/\\/g, '\\\\');
}
// Pronunciation fixes for VibeVoice — word-boundary replacements so the
// model says names and acronyms correctly. Ported from lettabot's Matrix
// TTS (src/channels/matrix/tts.ts), tuned for this voice over time.
readonly property var _ttsPronunciation: ({
"Xzaviar": "X-zay-V-ar", "xzaviar": "X-zay-V-ar",
"Jean Luc": "Zhan-Look", "jean luc": "Zhan-Look",
"Sebastian": "Se-BASS-chen", "sebastian": "Se-BASS-chen",
"API": "A P I", "SDK": "S D K",
"E2EE": "end-to-end encrypted",
"TTS": "text to speech", "STT": "speech to text",
})
function _applyTtsPronunciation(text) {
let result = text;
for (const wrong in root._ttsPronunciation) {
const right = root._ttsPronunciation[wrong];
result = result.replace(new RegExp(`\\b${wrong.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`, "gi"), right);
}
return result;
}
/**
* Cleans assistant reply text for text-to-speech: strips <think> blocks,
* agent control tags, HTML, code fences, markdown, and most emoji, and
* applies pronunciation fixes — so only the spoken voice remains.
* Returns the cleaned text (empty string if nothing speakable survives).
* Order (ported from lettabot's cleanTextForTTS, plus Souveraine think
* blocks): control tags → color/spoiler → HTML → think → code/markdown →
* emoji → pronunciation → whitespace.
* @param { string } text
* @returns { string }
*/
function ttsClean(text) {
if (!text)
return "";
let cleaned = text;
// Strip agent control tags
cleaned = cleaned.replace(/\[silent\]/gi, "");
cleaned = cleaned.replace(/\[chromatophore\]/gi, "");
cleaned = cleaned.replace(/\[!c\]/gi, "");
cleaned = cleaned.replace(/\[!s\]/gi, "");
cleaned = cleaned.replace(/\[react:[^\]]*\]/gi, "");
// Strip color syntax {color|text} → keep text; spoilers ||text|| → keep text
cleaned = cleaned.replace(/\{[^}|]+\|([^}]+)\}/g, "$1");
// dotAll via [\s\S] (not the 's' flag — older Qt/V8 rejects it)
cleaned = cleaned.replace(/\|\|([\s\S]+?)\|\|/g, "$1");
// Remove <think>...</think> blocks entirely (Souveraine reasoning).
// MUST run before the HTML-tag strip, or the tags get eaten first and
// the reasoning survives as plain text. Also drop an unclosed <think>
// (stream cut mid-reasoning) so we never speak a half-thought — by
// default we want only the actual message, not the thinking.
cleaned = cleaned.replace(/<think>[\s\S]*?<\/think>/g, " ");
cleaned = cleaned.replace(/<think>[\s\S]*$/g, " ");
// Strip HTML tags (keep content)
cleaned = cleaned.replace(/<[^>]+>/g, "");
// Remove markdown headers, bold, italic, code fences
cleaned = cleaned.replace(/```[\s\S]*?```/g, " ");
cleaned = cleaned.replace(/#{1,6}\s/g, " ");
cleaned = cleaned.replace(/\*\*([^*]+)\*\*/g, "$1");
cleaned = cleaned.replace(/\*([^*]+)\*/g, "$1");
cleaned = cleaned.replace(/`([^`]+)`/g, "$1");
// Remove markdown links and images
cleaned = cleaned.replace(/!\[([^\]]*)\]\([^)]+\)/g, "$1");
cleaned = cleaned.replace(/\[([^\]]*)\]\([^)]+\)/g, "$1");
// Emoji: preserve ✨ and 🎤 (the model says those fine), strip the rest.
// Marker-swap so the preserved ones survive the broad Unicode strip.
const sparkle = cleaned.includes("✨"), mic = cleaned.includes("🎤");
cleaned = cleaned.replace(/✨/g, "SPK").replace(/🎤/g, "MIC");
cleaned = cleaned.replace(/[\u{1F600}-\u{1F64F}\u{1F300}-\u{1F5FF}\u{1F680}-\u{1F6FF}\u{1F1E0}-\u{1F1FF}\u{2702}-\u{27B0}\u{24C2}-\u{1F251}\u{1F900}-\u{1FAFF}]/gu, "");
cleaned = cleaned.replace(/SPK/g, sparkle ? "✨" : "").replace(/MIC/g, mic ? "🎤" : "");
// Pronunciation fixes
cleaned = root._applyTtsPronunciation(cleaned);
// Collapse whitespace
cleaned = cleaned.replace(/\s+/g, " ").trim();
return cleaned;
}
/**
* Wraps words to supplied maximum length
* @param { string | null } str
* @param { number } maxLen
* @returns { string }
*/
function wordWrap(str, maxLen) {
if (!str)
return "";
let words = str.split(" ");
let lines = [];
let current = "";
for (let i = 0; i < words.length; ++i) {
if ((current + (current.length > 0 ? " " : "") + words[i]).length > maxLen) {
if (current.length > 0)
lines.push(current);
current = words[i];
} else {
current += (current.length > 0 ? " " : "") + words[i];
}
}
if (current.length > 0)
lines.push(current);
return lines.join("\n");
}
/**
* Cleans up a music title by removing bracketed and special characters.
* @param { string } title
* @returns { string }
*/
function cleanMusicTitle(title) {
if (!title)
return "";
// Brackets
title = title.replace(/^ *\([^)]*\) */g, " "); // Round brackets
title = title.replace(/^ *\[[^\]]*\] */g, " "); // Square brackets
title = title.replace(/^ *\{[^\}]*\} */g, " "); // Curly brackets
// Japenis brackets
title = title.replace(/^ *【[^】]*】/, ""); // Touhou
title = title.replace(/^ *《[^》]*》/, ""); // ??
title = title.replace(/^ *「[^」]*」/, ""); // OP/ED thingie
title = title.replace(/^ *『[^』]*』/, ""); // OP/ED thingie
return title.trim();
}
/**
* Converts seconds to a friendly time string (e.g. 1:23 or 1:02:03).
* @param { number } seconds
* @returns { string }
*/
function friendlyTimeForSeconds(seconds) {
if (isNaN(seconds) || seconds < 0)
return "0:00";
seconds = Math.floor(seconds);
const h = Math.floor(seconds / 3600);
const m = Math.floor((seconds % 3600) / 60);
const s = seconds % 60;
if (h > 0) {
return `${h}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`;
} else {
return `${m}:${s.toString().padStart(2, '0')}`;
}
}
/**
* Escapes HTML special characters in a string.
* @param { string } str
* @returns { string }
*/
function escapeHtml(str) {
if (typeof str !== 'string')
return str;
return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#39;');
}
/**
* Cleans a cliphist entry by removing leading digits and tab.
* @param { string } str
* @returns { string }
*/
function cleanCliphistEntry(str: string): string {
return str.replace(/^\d+\t/, "");
}
/**
* Checks if any substring in the list is contained in the string.
* @param { string } str
* @param { string[] } substrings
* @returns { boolean }
*/
function stringListContainsSubstring(str, substrings) {
for (let i = 0; i < substrings.length; ++i) {
if (str.includes(substrings[i])) {
return true;
}
}
return false;
}
/**
* Removes the given prefix from the string if present.
* @param { string } str
* @param { string } prefix
* @returns { string }
*/
function cleanPrefix(str, prefix) {
if (str.startsWith(prefix)) {
return str.slice(prefix.length);
}
return str;
}
/**
* Removes the first matching prefix from the string if present.
* @param { string } str
* @param { string[] } prefixes
* @returns { string }
*/
function cleanOnePrefix(str, prefixes) {
for (let i = 0; i < prefixes.length; ++i) {
if (str.startsWith(prefixes[i])) {
return str.slice(prefixes[i].length);
}
}
return str;
}
function toTitleCase(str) {
// Replace "-" and "_" with space, then capitalize each word
return str.replace(/[-_]/g, " ").replace(
/\w\S*/g,
function(txt) {
return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();
}
);
}
}

View file

@ -0,0 +1,682 @@
.pragma library
// https://github.com/farzher/fuzzysort
// License: MIT | Copyright (c) 2018 Stephen Kamenar
// A copy of the license is available in the `licenses` folder of this repository
var single = (search, target) => {
if(!search || !target) return NULL
var preparedSearch = getPreparedSearch(search)
if(!isPrepared(target)) target = getPrepared(target)
var searchBitflags = preparedSearch.bitflags
if((searchBitflags & target._bitflags) !== searchBitflags) return NULL
return algorithm(preparedSearch, target)
}
var go = (search, targets, options) => {
if(!search) return options?.all ? all(targets, options) : noResults
var preparedSearch = getPreparedSearch(search)
var searchBitflags = preparedSearch.bitflags
var containsSpace = preparedSearch.containsSpace
var threshold = denormalizeScore( options?.threshold || 0 )
var limit = options?.limit || INFINITY
var resultsLen = 0; var limitedCount = 0
var targetsLen = targets.length
function push_result(result) {
if(resultsLen < limit) { q.add(result); ++resultsLen }
else {
++limitedCount
if(result._score > q.peek()._score) q.replaceTop(result)
}
}
// This code is copy/pasted 3 times for performance reasons [options.key, options.keys, no keys]
// options.key
if(options?.key) {
var key = options.key
for(var i = 0; i < targetsLen; ++i) { var obj = targets[i]
var target = getValue(obj, key)
if(!target) continue
if(!isPrepared(target)) target = getPrepared(target)
if((searchBitflags & target._bitflags) !== searchBitflags) continue
var result = algorithm(preparedSearch, target)
if(result === NULL) continue
if(result._score < threshold) continue
result.obj = obj
push_result(result)
}
// options.keys
} else if(options?.keys) {
var keys = options.keys
var keysLen = keys.length
outer: for(var i = 0; i < targetsLen; ++i) { var obj = targets[i]
{ // early out based on bitflags
var keysBitflags = 0
for (var keyI = 0; keyI < keysLen; ++keyI) {
var key = keys[keyI]
var target = getValue(obj, key)
if(!target) { tmpTargets[keyI] = noTarget; continue }
if(!isPrepared(target)) target = getPrepared(target)
tmpTargets[keyI] = target
keysBitflags |= target._bitflags
}
if((searchBitflags & keysBitflags) !== searchBitflags) continue
}
if(containsSpace) for(let i=0; i<preparedSearch.spaceSearches.length; i++) keysSpacesBestScores[i] = NEGATIVE_INFINITY
for (var keyI = 0; keyI < keysLen; ++keyI) {
target = tmpTargets[keyI]
if(target === noTarget) { tmpResults[keyI] = noTarget; continue }
tmpResults[keyI] = algorithm(preparedSearch, target, /*allowSpaces=*/false, /*allowPartialMatch=*/containsSpace)
if(tmpResults[keyI] === NULL) { tmpResults[keyI] = noTarget; continue }
// todo: this seems weird and wrong. like what if our first match wasn't good. this should just replace it instead of averaging with it
// if our second match isn't good we ignore it instead of averaging with it
if(containsSpace) for(let i=0; i<preparedSearch.spaceSearches.length; i++) {
if(allowPartialMatchScores[i] > -1000) {
if(keysSpacesBestScores[i] > NEGATIVE_INFINITY) {
var tmp = (keysSpacesBestScores[i] + allowPartialMatchScores[i]) / 4/*bonus score for having multiple matches*/
if(tmp > keysSpacesBestScores[i]) keysSpacesBestScores[i] = tmp
}
}
if(allowPartialMatchScores[i] > keysSpacesBestScores[i]) keysSpacesBestScores[i] = allowPartialMatchScores[i]
}
}
if(containsSpace) {
for(let i=0; i<preparedSearch.spaceSearches.length; i++) { if(keysSpacesBestScores[i] === NEGATIVE_INFINITY) continue outer }
} else {
var hasAtLeast1Match = false
for(let i=0; i < keysLen; i++) { if(tmpResults[i]._score !== NEGATIVE_INFINITY) { hasAtLeast1Match = true; break } }
if(!hasAtLeast1Match) continue
}
var objResults = new KeysResult(keysLen)
for(let i=0; i < keysLen; i++) { objResults[i] = tmpResults[i] }
if(containsSpace) {
var score = 0
for(let i=0; i<preparedSearch.spaceSearches.length; i++) score += keysSpacesBestScores[i]
} else {
// todo could rewrite this scoring to be more similar to when there's spaces
// if we match multiple keys give us bonus points
var score = NEGATIVE_INFINITY
for(let i=0; i<keysLen; i++) {
var result = objResults[i]
if(result._score > -1000) {
if(score > NEGATIVE_INFINITY) {
var tmp = (score + result._score) / 4/*bonus score for having multiple matches*/
if(tmp > score) score = tmp
}
}
if(result._score > score) score = result._score
}
}
objResults.obj = obj
objResults._score = score
if(options?.scoreFn) {
score = options.scoreFn(objResults)
if(!score) continue
score = denormalizeScore(score)
objResults._score = score
}
if(score < threshold) continue
push_result(objResults)
}
// no keys
} else {
for(var i = 0; i < targetsLen; ++i) { var target = targets[i]
if(!target) continue
if(!isPrepared(target)) target = getPrepared(target)
if((searchBitflags & target._bitflags) !== searchBitflags) continue
var result = algorithm(preparedSearch, target)
if(result === NULL) continue
if(result._score < threshold) continue
push_result(result)
}
}
if(resultsLen === 0) return noResults
var results = new Array(resultsLen)
for(var i = resultsLen - 1; i >= 0; --i) results[i] = q.poll()
results.total = resultsLen + limitedCount
return results
}
// this is written as 1 function instead of 2 for minification. perf seems fine ...
// except when minified. the perf is very slow
var highlight = (result, open='<b>', close='</b>') => {
var callback = typeof open === 'function' ? open : undefined
var target = result.target
var targetLen = target.length
var indexes = result.indexes
var highlighted = ''
var matchI = 0
var indexesI = 0
var opened = false
var parts = []
for(var i = 0; i < targetLen; ++i) { var char = target[i]
if(indexes[indexesI] === i) {
++indexesI
if(!opened) { opened = true
if(callback) {
parts.push(highlighted); highlighted = ''
} else {
highlighted += open
}
}
if(indexesI === indexes.length) {
if(callback) {
highlighted += char
parts.push(callback(highlighted, matchI++)); highlighted = ''
parts.push(target.substr(i+1))
} else {
highlighted += char + close + target.substr(i+1)
}
break
}
} else {
if(opened) { opened = false
if(callback) {
parts.push(callback(highlighted, matchI++)); highlighted = ''
} else {
highlighted += close
}
}
}
highlighted += char
}
return callback ? parts : highlighted
}
var prepare = (target) => {
if(typeof target === 'number') target = ''+target
else if(typeof target !== 'string') target = ''
var info = prepareLowerInfo(target)
return new_result(target, {_targetLower:info._lower, _targetLowerCodes:info.lowerCodes, _bitflags:info.bitflags})
}
var cleanup = () => { preparedCache.clear(); preparedSearchCache.clear() }
// Below this point is only internal code
// Below this point is only internal code
// Below this point is only internal code
// Below this point is only internal code
class Result {
get ['indexes']() { return this._indexes.slice(0, this._indexes.len).sort((a,b)=>a-b) }
set ['indexes'](indexes) { return this._indexes = indexes }
['highlight'](open, close) { return highlight(this, open, close) }
get ['score']() { return normalizeScore(this._score) }
set ['score'](score) { this._score = denormalizeScore(score) }
}
class KeysResult extends Array {
get ['score']() { return normalizeScore(this._score) }
set ['score'](score) { this._score = denormalizeScore(score) }
}
var new_result = (target, options) => {
const result = new Result()
result['target'] = target
result['obj'] = options.obj ?? NULL
result._score = options._score ?? NEGATIVE_INFINITY
result._indexes = options._indexes ?? []
result._targetLower = options._targetLower ?? ''
result._targetLowerCodes = options._targetLowerCodes ?? NULL
result._nextBeginningIndexes = options._nextBeginningIndexes ?? NULL
result._bitflags = options._bitflags ?? 0
return result
}
var normalizeScore = score => {
if(score === NEGATIVE_INFINITY) return 0
if(score > 1) return score
return Math.E ** ( ((-score + 1)**.04307 - 1) * -2)
}
var denormalizeScore = normalizedScore => {
if(normalizedScore === 0) return NEGATIVE_INFINITY
if(normalizedScore > 1) return normalizedScore
return 1 - Math.pow((Math.log(normalizedScore) / -2 + 1), 1 / 0.04307)
}
var prepareSearch = (search) => {
if(typeof search === 'number') search = ''+search
else if(typeof search !== 'string') search = ''
search = search.trim()
var info = prepareLowerInfo(search)
var spaceSearches = []
if(info.containsSpace) {
var searches = search.split(/\s+/)
searches = [...new Set(searches)] // distinct
for(var i=0; i<searches.length; i++) {
if(searches[i] === '') continue
var _info = prepareLowerInfo(searches[i])
spaceSearches.push({lowerCodes:_info.lowerCodes, _lower:searches[i].toLowerCase(), containsSpace:false})
}
}
return {lowerCodes: info.lowerCodes, _lower: info._lower, containsSpace: info.containsSpace, bitflags: info.bitflags, spaceSearches: spaceSearches}
}
var getPrepared = (target) => {
if(target.length > 999) return prepare(target) // don't cache huge targets
var targetPrepared = preparedCache.get(target)
if(targetPrepared !== undefined) return targetPrepared
targetPrepared = prepare(target)
preparedCache.set(target, targetPrepared)
return targetPrepared
}
var getPreparedSearch = (search) => {
if(search.length > 999) return prepareSearch(search) // don't cache huge searches
var searchPrepared = preparedSearchCache.get(search)
if(searchPrepared !== undefined) return searchPrepared
searchPrepared = prepareSearch(search)
preparedSearchCache.set(search, searchPrepared)
return searchPrepared
}
var all = (targets, options) => {
var results = []; results.total = targets.length // this total can be wrong if some targets are skipped
var limit = options?.limit || INFINITY
if(options?.key) {
for(var i=0;i<targets.length;i++) { var obj = targets[i]
var target = getValue(obj, options.key)
if(target == NULL) continue
if(!isPrepared(target)) target = getPrepared(target)
var result = new_result(target.target, {_score: target._score, obj: obj})
results.push(result); if(results.length >= limit) return results
}
} else if(options?.keys) {
for(var i=0;i<targets.length;i++) { var obj = targets[i]
var objResults = new KeysResult(options.keys.length)
for (var keyI = options.keys.length - 1; keyI >= 0; --keyI) {
var target = getValue(obj, options.keys[keyI])
if(!target) { objResults[keyI] = noTarget; continue }
if(!isPrepared(target)) target = getPrepared(target)
target._score = NEGATIVE_INFINITY
target._indexes.len = 0
objResults[keyI] = target
}
objResults.obj = obj
objResults._score = NEGATIVE_INFINITY
results.push(objResults); if(results.length >= limit) return results
}
} else {
for(var i=0;i<targets.length;i++) { var target = targets[i]
if(target == NULL) continue
if(!isPrepared(target)) target = getPrepared(target)
target._score = NEGATIVE_INFINITY
target._indexes.len = 0
results.push(target); if(results.length >= limit) return results
}
}
return results
}
var algorithm = (preparedSearch, prepared, allowSpaces=false, allowPartialMatch=false) => {
if(allowSpaces===false && preparedSearch.containsSpace) return algorithmSpaces(preparedSearch, prepared, allowPartialMatch)
var searchLower = preparedSearch._lower
var searchLowerCodes = preparedSearch.lowerCodes
var searchLowerCode = searchLowerCodes[0]
var targetLowerCodes = prepared._targetLowerCodes
var searchLen = searchLowerCodes.length
var targetLen = targetLowerCodes.length
var searchI = 0 // where we at
var targetI = 0 // where you at
var matchesSimpleLen = 0
// very basic fuzzy match; to remove non-matching targets ASAP!
// walk through target. find sequential matches.
// if all chars aren't found then exit
for(;;) {
var isMatch = searchLowerCode === targetLowerCodes[targetI]
if(isMatch) {
matchesSimple[matchesSimpleLen++] = targetI
++searchI; if(searchI === searchLen) break
searchLowerCode = searchLowerCodes[searchI]
}
++targetI; if(targetI >= targetLen) return NULL // Failed to find searchI
}
var searchI = 0
var successStrict = false
var matchesStrictLen = 0
var nextBeginningIndexes = prepared._nextBeginningIndexes
if(nextBeginningIndexes === NULL) nextBeginningIndexes = prepared._nextBeginningIndexes = prepareNextBeginningIndexes(prepared.target)
targetI = matchesSimple[0]===0 ? 0 : nextBeginningIndexes[matchesSimple[0]-1]
// Our target string successfully matched all characters in sequence!
// Let's try a more advanced and strict test to improve the score
// only count it as a match if it's consecutive or a beginning character!
var backtrackCount = 0
if(targetI !== targetLen) for(;;) {
if(targetI >= targetLen) {
// We failed to find a good spot for this search char, go back to the previous search char and force it forward
if(searchI <= 0) break // We failed to push chars forward for a better match
++backtrackCount; if(backtrackCount > 200) break // exponential backtracking is taking too long, just give up and return a bad match
--searchI
var lastMatch = matchesStrict[--matchesStrictLen]
targetI = nextBeginningIndexes[lastMatch]
} else {
var isMatch = searchLowerCodes[searchI] === targetLowerCodes[targetI]
if(isMatch) {
matchesStrict[matchesStrictLen++] = targetI
++searchI; if(searchI === searchLen) { successStrict = true; break }
++targetI
} else {
targetI = nextBeginningIndexes[targetI]
}
}
}
// check if it's a substring match
var substringIndex = searchLen <= 1 ? -1 : prepared._targetLower.indexOf(searchLower, matchesSimple[0]) // perf: this is slow
var isSubstring = !!~substringIndex
var isSubstringBeginning = !isSubstring ? false : substringIndex===0 || prepared._nextBeginningIndexes[substringIndex-1] === substringIndex
// if it's a substring match but not at a beginning index, let's try to find a substring starting at a beginning index for a better score
if(isSubstring && !isSubstringBeginning) {
for(var i=0; i<nextBeginningIndexes.length; i=nextBeginningIndexes[i]) {
if(i <= substringIndex) continue
for(var s=0; s<searchLen; s++) if(searchLowerCodes[s] !== prepared._targetLowerCodes[i+s]) break
if(s === searchLen) { substringIndex = i; isSubstringBeginning = true; break }
}
}
// tally up the score & keep track of matches for highlighting later
// if it's a simple match, we'll switch to a substring match if a substring exists
// if it's a strict match, we'll switch to a substring match only if that's a better score
var calculateScore = matches => {
var score = 0
var extraMatchGroupCount = 0
for(var i = 1; i < searchLen; ++i) {
if(matches[i] - matches[i-1] !== 1) {score -= matches[i]; ++extraMatchGroupCount}
}
var unmatchedDistance = matches[searchLen-1] - matches[0] - (searchLen-1)
score -= (12+unmatchedDistance) * extraMatchGroupCount // penality for more groups
if(matches[0] !== 0) score -= matches[0]*matches[0]*.2 // penality for not starting near the beginning
if(!successStrict) {
score *= 1000
} else {
// successStrict on a target with too many beginning indexes loses points for being a bad target
var uniqueBeginningIndexes = 1
for(var i = nextBeginningIndexes[0]; i < targetLen; i=nextBeginningIndexes[i]) ++uniqueBeginningIndexes
if(uniqueBeginningIndexes > 24) score *= (uniqueBeginningIndexes-24)*10 // quite arbitrary numbers here ...
}
score -= (targetLen - searchLen)/2 // penality for longer targets
if(isSubstring) score /= 1+searchLen*searchLen*1 // bonus for being a full substring
if(isSubstringBeginning) score /= 1+searchLen*searchLen*1 // bonus for substring starting on a beginningIndex
score -= (targetLen - searchLen)/2 // penality for longer targets
return score
}
if(!successStrict) {
if(isSubstring) for(var i=0; i<searchLen; ++i) matchesSimple[i] = substringIndex+i // at this point it's safe to overwrite matchehsSimple with substr matches
var matchesBest = matchesSimple
var score = calculateScore(matchesBest)
} else {
if(isSubstringBeginning) {
for(var i=0; i<searchLen; ++i) matchesSimple[i] = substringIndex+i // at this point it's safe to overwrite matchehsSimple with substr matches
var matchesBest = matchesSimple
var score = calculateScore(matchesSimple)
} else {
var matchesBest = matchesStrict
var score = calculateScore(matchesStrict)
}
}
prepared._score = score
for(var i = 0; i < searchLen; ++i) prepared._indexes[i] = matchesBest[i]
prepared._indexes.len = searchLen
const result = new Result()
result.target = prepared.target
result._score = prepared._score
result._indexes = prepared._indexes
return result
}
var algorithmSpaces = (preparedSearch, target, allowPartialMatch) => {
var seen_indexes = new Set()
var score = 0
var result = NULL
var first_seen_index_last_search = 0
var searches = preparedSearch.spaceSearches
var searchesLen = searches.length
var changeslen = 0
// Return _nextBeginningIndexes back to its normal state
var resetNextBeginningIndexes = () => {
for(let i=changeslen-1; i>=0; i--) target._nextBeginningIndexes[nextBeginningIndexesChanges[i*2 + 0]] = nextBeginningIndexesChanges[i*2 + 1]
}
var hasAtLeast1Match = false
for(var i=0; i<searchesLen; ++i) {
allowPartialMatchScores[i] = NEGATIVE_INFINITY
var search = searches[i]
result = algorithm(search, target)
if(allowPartialMatch) {
if(result === NULL) continue
hasAtLeast1Match = true
} else {
if(result === NULL) {resetNextBeginningIndexes(); return NULL}
}
// if not the last search, we need to mutate _nextBeginningIndexes for the next search
var isTheLastSearch = i === searchesLen - 1
if(!isTheLastSearch) {
var indexes = result._indexes
var indexesIsConsecutiveSubstring = true
for(let i=0; i<indexes.len-1; i++) {
if(indexes[i+1] - indexes[i] !== 1) {
indexesIsConsecutiveSubstring = false; break;
}
}
if(indexesIsConsecutiveSubstring) {
var newBeginningIndex = indexes[indexes.len-1] + 1
var toReplace = target._nextBeginningIndexes[newBeginningIndex-1]
for(let i=newBeginningIndex-1; i>=0; i--) {
if(toReplace !== target._nextBeginningIndexes[i]) break
target._nextBeginningIndexes[i] = newBeginningIndex
nextBeginningIndexesChanges[changeslen*2 + 0] = i
nextBeginningIndexesChanges[changeslen*2 + 1] = toReplace
changeslen++
}
}
}
score += result._score / searchesLen
allowPartialMatchScores[i] = result._score / searchesLen
// dock points based on order otherwise "c man" returns Manifest.cpp instead of CheatManager.h
if(result._indexes[0] < first_seen_index_last_search) {
score -= (first_seen_index_last_search - result._indexes[0]) * 2
}
first_seen_index_last_search = result._indexes[0]
for(var j=0; j<result._indexes.len; ++j) seen_indexes.add(result._indexes[j])
}
if(allowPartialMatch && !hasAtLeast1Match) return NULL
resetNextBeginningIndexes()
// allows a search with spaces that's an exact substring to score well
var allowSpacesResult = algorithm(preparedSearch, target, /*allowSpaces=*/true)
if(allowSpacesResult !== NULL && allowSpacesResult._score > score) {
if(allowPartialMatch) {
for(var i=0; i<searchesLen; ++i) {
allowPartialMatchScores[i] = allowSpacesResult._score / searchesLen
}
}
return allowSpacesResult
}
if(allowPartialMatch) result = target
result._score = score
var i = 0
for (let index of seen_indexes) result._indexes[i++] = index
result._indexes.len = i
return result
}
// we use this instead of just .normalize('NFD').replace(/[\u0300-\u036f]/g, '') because that screws with japanese characters
var remove_accents = (str) => str.replace(/\p{Script=Latin}+/gu, match => match.normalize('NFD')).replace(/[\u0300-\u036f]/g, '')
var prepareLowerInfo = (str) => {
str = remove_accents(str)
var strLen = str.length
var lower = str.toLowerCase()
var lowerCodes = [] // new Array(strLen) sparse array is too slow
var bitflags = 0
var containsSpace = false // space isn't stored in bitflags because of how searching with a space works
for(var i = 0; i < strLen; ++i) {
var lowerCode = lowerCodes[i] = lower.charCodeAt(i)
if(lowerCode === 32) {
containsSpace = true
continue // it's important that we don't set any bitflags for space
}
var bit = lowerCode>=97&&lowerCode<=122 ? lowerCode-97 // alphabet
: lowerCode>=48&&lowerCode<=57 ? 26 // numbers
// 3 bits available
: lowerCode<=127 ? 30 // other ascii
: 31 // other utf8
bitflags |= 1<<bit
}
return {lowerCodes:lowerCodes, bitflags:bitflags, containsSpace:containsSpace, _lower:lower}
}
var prepareBeginningIndexes = (target) => {
var targetLen = target.length
var beginningIndexes = []; var beginningIndexesLen = 0
var wasUpper = false
var wasAlphanum = false
for(var i = 0; i < targetLen; ++i) {
var targetCode = target.charCodeAt(i)
var isUpper = targetCode>=65&&targetCode<=90
var isAlphanum = isUpper || targetCode>=97&&targetCode<=122 || targetCode>=48&&targetCode<=57
var isBeginning = isUpper && !wasUpper || !wasAlphanum || !isAlphanum
wasUpper = isUpper
wasAlphanum = isAlphanum
if(isBeginning) beginningIndexes[beginningIndexesLen++] = i
}
return beginningIndexes
}
var prepareNextBeginningIndexes = (target) => {
target = remove_accents(target)
var targetLen = target.length
var beginningIndexes = prepareBeginningIndexes(target)
var nextBeginningIndexes = [] // new Array(targetLen) sparse array is too slow
var lastIsBeginning = beginningIndexes[0]
var lastIsBeginningI = 0
for(var i = 0; i < targetLen; ++i) {
if(lastIsBeginning > i) {
nextBeginningIndexes[i] = lastIsBeginning
} else {
lastIsBeginning = beginningIndexes[++lastIsBeginningI]
nextBeginningIndexes[i] = lastIsBeginning===undefined ? targetLen : lastIsBeginning
}
}
return nextBeginningIndexes
}
var preparedCache = new Map()
var preparedSearchCache = new Map()
// the theory behind these being globals is to reduce garbage collection by not making new arrays
var matchesSimple = []; var matchesStrict = []
var nextBeginningIndexesChanges = [] // allows straw berry to match strawberry well, by modifying the end of a substring to be considered a beginning index for the rest of the search
var keysSpacesBestScores = []; var allowPartialMatchScores = []
var tmpTargets = []; var tmpResults = []
// prop = 'key' 2.5ms optimized for this case, seems to be about as fast as direct obj[prop]
// prop = 'key1.key2' 10ms
// prop = ['key1', 'key2'] 27ms
// prop = obj => obj.tags.join() ??ms
var getValue = (obj, prop) => {
var tmp = obj[prop]; if(tmp !== undefined) return tmp
if(typeof prop === 'function') return prop(obj) // this should run first. but that makes string props slower
var segs = prop
if(!Array.isArray(prop)) segs = prop.split('.')
var len = segs.length
var i = -1
while (obj && (++i < len)) obj = obj[segs[i]]
return obj
}
var isPrepared = (x) => { return typeof x === 'object' && typeof x._bitflags === 'number' }
var INFINITY = Infinity; var NEGATIVE_INFINITY = -INFINITY
var noResults = []; noResults.total = 0
var NULL = null
var noTarget = prepare('')
// Hacked version of https://github.com/lemire/FastPriorityQueue.js
var fastpriorityqueue=r=>{var e=[],o=0,a={},v=r=>{for(var a=0,v=e[a],c=1;c<o;){var s=c+1;a=c,s<o&&e[s]._score<e[c]._score&&(a=s),e[a-1>>1]=e[a],c=1+(a<<1)}for(var f=a-1>>1;a>0&&v._score<e[f]._score;f=(a=f)-1>>1)e[a]=e[f];e[a]=v};return a.add=(r=>{var a=o;e[o++]=r;for(var v=a-1>>1;a>0&&r._score<e[v]._score;v=(a=v)-1>>1)e[a]=e[v];e[a]=r}),a.poll=(r=>{if(0!==o){var a=e[0];return e[0]=e[--o],v(),a}}),a.peek=(r=>{if(0!==o)return e[0]}),a.replaceTop=(r=>{e[0]=r,v()}),a}
var q = fastpriorityqueue() // reuse this

View file

@ -0,0 +1,141 @@
// Original code from https://github.com/koeqaife/hyprland-material-you
// Original code license: GPLv3
// Translated to Js from Cython with an LLM and reviewed
function min3(a, b, c) {
return a < b && a < c ? a : b < c ? b : c;
}
function max3(a, b, c) {
return a > b && a > c ? a : b > c ? b : c;
}
function min2(a, b) {
return a < b ? a : b;
}
function max2(a, b) {
return a > b ? a : b;
}
function levenshteinDistance(s1, s2) {
let len1 = s1.length;
let len2 = s2.length;
if (len1 === 0) return len2;
if (len2 === 0) return len1;
if (len2 > len1) {
[s1, s2] = [s2, s1];
[len1, len2] = [len2, len1];
}
let prev = new Array(len2 + 1);
let curr = new Array(len2 + 1);
for (let j = 0; j <= len2; j++) {
prev[j] = j;
}
for (let i = 1; i <= len1; i++) {
curr[0] = i;
for (let j = 1; j <= len2; j++) {
let cost = s1[i - 1] === s2[j - 1] ? 0 : 1;
curr[j] = min3(prev[j] + 1, curr[j - 1] + 1, prev[j - 1] + cost);
}
[prev, curr] = [curr, prev];
}
return prev[len2];
}
function partialRatio(shortS, longS) {
let lenS = shortS.length;
let lenL = longS.length;
let best = 0.0;
if (lenS === 0) return 1.0;
for (let i = 0; i <= lenL - lenS; i++) {
let sub = longS.slice(i, i + lenS);
let dist = levenshteinDistance(shortS, sub);
let score = 1.0 - (dist / lenS);
if (score > best) best = score;
}
return best;
}
function computeScore(s1, s2) {
if (s1 === s2) return 1.0;
let dist = levenshteinDistance(s1, s2);
let maxLen = max2(s1.length, s2.length);
if (maxLen === 0) return 1.0;
let full = 1.0 - (dist / maxLen);
let part = s1.length < s2.length ? partialRatio(s1, s2) : partialRatio(s2, s1);
let score = 0.85 * full + 0.15 * part;
if (s1 && s2 && s1[0] !== s2[0]) {
score -= 0.05;
}
let lenDiff = Math.abs(s1.length - s2.length);
if (lenDiff >= 3) {
score -= 0.05 * lenDiff / maxLen;
}
let commonPrefixLen = 0;
let minLen = min2(s1.length, s2.length);
for (let i = 0; i < minLen; i++) {
if (s1[i] === s2[i]) {
commonPrefixLen++;
} else {
break;
}
}
score += 0.02 * commonPrefixLen;
if (s1.includes(s2) || s2.includes(s1)) {
score += 0.06;
}
return Math.max(0.0, Math.min(1.0, score));
}
function computeTextMatchScore(s1, s2) {
if (s1 === s2) return 1.0;
let dist = levenshteinDistance(s1, s2);
let maxLen = max2(s1.length, s2.length);
if (maxLen === 0) return 1.0;
let full = 1.0 - (dist / maxLen);
let part = s1.length < s2.length ? partialRatio(s1, s2) : partialRatio(s2, s1);
let score = 0.4 * full + 0.6 * part;
let lenDiff = Math.abs(s1.length - s2.length);
if (lenDiff >= 10) {
score -= 0.02 * lenDiff / maxLen;
}
let commonPrefixLen = 0;
let minLen = min2(s1.length, s2.length);
for (let i = 0; i < minLen; i++) {
if (s1[i] === s2[i]) {
commonPrefixLen++;
} else {
break;
}
}
score += 0.01 * commonPrefixLen;
if (s1.includes(s2) || s2.includes(s1)) {
score += 0.2;
}
return Math.max(0.0, Math.min(1.0, score));
}

View file

@ -0,0 +1,200 @@
// Bit-parallel approximate string matching — Myers (1999).
// Time: O(n · ⌈m/WORD_SIZE⌉) after prepare(); O(n) for typical short queries.
// 30, not 32: JS bitwise ops work on signed 32-bit integers, so
// (1 << 31) = -2147483648 and (1 << 32) wraps to 1. 30 keeps all
// mask values positive and avoids sign-bit surprises throughout.
const WORD_SIZE = 30;
// ─── Normalization ────────────────────────────────────────────────────────────
// Single normalization function used by every public API.
// NFC composes canonically equivalent sequences so "é" and "é"
// use the same representation and hash to the same character in Peq.
// Guard against older Qt/QML runtimes where String.normalize may be absent.
function normalize(value) {
const str = String(value == null ? "" : value).toLowerCase();
return typeof str.normalize === "function" ? str.normalize("NFC") : str;
}
// ─── Public API ───────────────────────────────────────────────────────────────
// Precompute equality bitmasks for a pattern.
// Call once per query; reuse the result across all candidate comparisons.
function prepare(pattern) {
const norm = normalize(pattern);
const m = norm.length;
if (m === 0) return { Peq: [], m: 0, words: 0 };
const words = Math.ceil(m / WORD_SIZE);
const Peq = new Array(words);
for (let w = 0; w < words; w++) Peq[w] = Object.create(null);
for (let i = 0; i < m; i++) {
const c = norm[i];
const w = (i / WORD_SIZE) | 0;
Peq[w][c] = (Peq[w][c] | 0) | (1 << (i % WORD_SIZE));
}
return { Peq, m, words };
}
// Raw edit distance. text must already be normalized.
// Keeping normalization out of the hot path lets callers normalize once per batch.
// Use distance() or score() if you want automatic normalization.
function distanceNormalized(prepared, text) {
const { Peq, m, words } = prepared;
const n = text.length;
if (m === 0) return n;
if (n === 0) return m;
return words === 1
? _distSingle(Peq[0], m, text, n)
: _distMulti(Peq, m, words, text, n);
}
// Edit distance with automatic text normalization. Safe to call with any string.
function distance(prepared, text) {
return distanceNormalized(prepared, normalize(text));
}
// Normalized similarity in [0, 1]. 1 = identical, 0 = completely different.
function score(prepared, text) {
const normText = normalize(text);
const maxLen = Math.max(prepared.m, normText.length);
if (maxLen === 0) return 1.0;
return 1.0 - distanceNormalized(prepared, normText) / maxLen;
}
// One-off score without a separate prepare() call.
function computeScore(pattern, text) {
return score(prepare(pattern), text);
}
// Drop-in replacement for levendist.js computeTextMatchScore.
function computeTextMatchScore(s1, s2) {
return computeScore(s1, s2);
}
// Rank candidates by descending similarity.
// opts.key — property name to extract text from each candidate (null = use candidate directly)
// opts.threshold — minimum score to include; results with score <= threshold are dropped.
// Uses > not >= so threshold=0 means "include anything with a positive score."
function search(prepared, candidates, opts) {
const key = opts != null ? opts.key : null;
const threshold = opts != null && opts.threshold != null ? opts.threshold : 0;
const len = candidates.length;
const results = [];
for (let i = 0; i < len; i++) {
const c = candidates[i];
const raw = key != null ? c[key] : c;
const s = score(prepared, raw);
if (s > threshold) results.push({ item: c, s: s, i: i });
}
// Secondary sort by original index keeps equal-score results stable
// so the list doesn't visually shuffle between keystrokes.
results.sort((a, b) => (b.s - a.s) || (a.i - b.i));
const out = new Array(results.length);
for (let i = 0; i < results.length; i++) out[i] = results[i].item;
return out;
}
// ─── Single-word Myers (m ≤ WORD_SIZE = 30) ─────────────────────────────────
function _distSingle(peq, m, text, n) {
const FULL = (1 << m) - 1;
const TOP = 1 << (m - 1);
let Pv = FULL, Mv = 0, score = m;
for (let j = 0; j < n; j++) {
const Eq = peq[text[j]] | 0;
const Xv = Eq | Mv;
const Xh = (((Eq & Pv) + Pv) ^ Pv) | Eq;
let Ph = (Mv | ~(Xh | Pv)) & FULL;
let Mh = Pv & Xh & FULL;
if (Ph & TOP) score++;
if (Mh & TOP) score--;
Ph = ((Ph << 1) | 1) & FULL;
Mh = (Mh << 1) & FULL;
Pv = (Mh | ~(Xv | Ph)) & FULL;
Mv = Ph & Xv & FULL;
}
return score;
}
// ─── Multi-word Myers (m > WORD_SIZE) ───────────────────────────────────────
//
// Shared scratch buffers to avoid per-call allocation.
// Safe for synchronous JS; do not call re-entrantly (e.g. from a callback inside distance()).
let _sharedPv = new Int32Array(16);
let _sharedMv = new Int32Array(16);
let _sharedFull = new Int32Array(16);
function _distMulti(Peq, m, words, text, n) {
const lastBits = m % WORD_SIZE || WORD_SIZE;
if (words > _sharedPv.length) {
const newSize = Math.max(words, _sharedPv.length * 2);
_sharedPv = new Int32Array(newSize);
_sharedMv = new Int32Array(newSize);
_sharedFull = new Int32Array(newSize);
}
for (let w = 0; w < words - 1; w++) _sharedFull[w] = (1 << WORD_SIZE) - 1;
_sharedFull[words - 1] = (1 << lastBits) - 1;
const LAST_TOP = 1 << (lastBits - 1);
for (let w = 0; w < words; w++) {
_sharedPv[w] = _sharedFull[w];
_sharedMv[w] = 0;
}
let score = m;
for (let j = 0; j < n; j++) {
const c = text[j];
let addCarry = 0, phCarry = 1, mhCarry = 0;
for (let w = 0; w < words; w++) {
const fw = _sharedFull[w];
const Eq = Peq[w][c] | 0;
const Xv = (Eq | _sharedMv[w]) & fw;
const sumBig = (Eq & _sharedPv[w]) + _sharedPv[w] + addCarry;
addCarry = sumBig > fw ? 1 : 0;
const Xh = (((sumBig & fw) ^ _sharedPv[w]) | Eq) & fw;
let Ph = (_sharedMv[w] | ~(Xh | _sharedPv[w])) & fw;
let Mh = _sharedPv[w] & Xh & fw;
if (w === words - 1) {
if (Ph & LAST_TOP) score++;
if (Mh & LAST_TOP) score--;
}
const phTop = (Ph >>> (WORD_SIZE - 1)) & 1;
const mhTop = (Mh >>> (WORD_SIZE - 1)) & 1;
Ph = ((Ph << 1) | phCarry) & fw;
Mh = ((Mh << 1) | mhCarry) & fw;
phCarry = phTop;
mhCarry = mhTop;
_sharedPv[w] = (Mh | ~(Xv | Ph)) & fw;
_sharedMv[w] = Ph & Xv & fw;
}
}
return score;
}

View file

@ -0,0 +1,27 @@
import QtQuick
import qs.modules.common
import qs.modules.common.functions
/**
* Material color scheme adapted to a given color. It's incomplete but enough for what we need...
*/
QtObject {
id: root
required property color color
readonly property bool colorIsDark: color.hslLightness < 0.5
property color colLayer0: ColorUtils.mix(Appearance.colors.colLayer0, root.color, (colorIsDark && Appearance.m3colors.darkmode) ? 0.6 : 0.5)
property color colLayer1: ColorUtils.mix(Appearance.colors.colLayer1, root.color, 0.5)
property color colOnLayer0: ColorUtils.mix(Appearance.colors.colOnLayer0, root.color, 0.5)
property color colOnLayer1: ColorUtils.mix(Appearance.colors.colOnLayer1, root.color, 0.5)
property color colSubtext: ColorUtils.mix(Appearance.colors.colOnLayer1, root.color, 0.5)
property color colPrimary: ColorUtils.mix(ColorUtils.adaptToAccent(Appearance.colors.colPrimary, root.color), root.color, 0.5)
property color colPrimaryHover: ColorUtils.mix(ColorUtils.adaptToAccent(Appearance.colors.colPrimaryHover, root.color), root.color, 0.3)
property color colPrimaryActive: ColorUtils.mix(ColorUtils.adaptToAccent(Appearance.colors.colPrimaryActive, root.color), root.color, 0.3)
property color colSecondary: ColorUtils.mix(ColorUtils.adaptToAccent(Appearance.colors.colSecondary, root.color), root.color, 0.5)
property color colSecondaryContainer: ColorUtils.mix(Appearance.m3colors.m3secondaryContainer, root.color, 0.15)
property color colSecondaryContainerHover: ColorUtils.mix(Appearance.colors.colSecondaryContainerHover, root.color, 0.3)
property color colSecondaryContainerActive: ColorUtils.mix(Appearance.colors.colSecondaryContainerActive, root.color, 0.5)
property color colOnPrimary: ColorUtils.mix(ColorUtils.adaptToAccent(Appearance.m3colors.m3onPrimary, root.color), root.color, 0.5)
property color colOnSecondaryContainer: ColorUtils.mix(Appearance.m3colors.m3onSecondaryContainer, root.color, 0.5)
}

View file

@ -0,0 +1,26 @@
import QtQuick
// idx1 is the "leading" indicator position, idx2 is the "following" one
// The former animates faster than the latter, see the NumberAnimations below
QtObject {
id: root
required property int index
property real idx1: index
property real idx2: index
property int idx1Duration: 100
property int idx2Duration: 300
Behavior on idx1 {
NumberAnimation {
duration: root.idx1Duration
easing.type: Easing.OutSine
}
}
Behavior on idx2 {
NumberAnimation {
duration: root.idx2Duration
easing.type: Easing.OutSine
}
}
}

View file

@ -0,0 +1,53 @@
import QtQuick
import Qt.labs.folderlistmodel
FolderListModel {
id: root
property list<url> folderHistory: []
property int currentFolderHistoryIndex: -1
property bool historyNavigationLock: false
function lockNextNavigation() {
historyNavigationLock = true;
}
function pushToHistory(path) {
if (folderHistory[currentFolderHistoryIndex] === path)
return;
folderHistory = folderHistory.slice(0, currentFolderHistoryIndex + 1);
folderHistory.push(path);
currentFolderHistoryIndex = folderHistory.length - 1;
}
function navigateUp() {
root.folder = root.parentFolder;
}
function navigateBack() {
if (currentFolderHistoryIndex === 0)
return;
currentFolderHistoryIndex--;
lockNextNavigation();
root.folder = folderHistory[currentFolderHistoryIndex];
}
function navigateForward() {
if (currentFolderHistoryIndex >= folderHistory.length - 1) return;
currentFolderHistoryIndex++;
lockNextNavigation();
root.folder = folderHistory[currentFolderHistoryIndex];
}
onFolderChanged: {
if (historyNavigationLock) {
historyNavigationLock = false;
return;
}
pushToHistory(folder);
}
Component.onCompleted: {
root.folderHistory = [root.folder]
root.currentFolderHistoryIndex = 0
}
}

View file

@ -0,0 +1,6 @@
import Quickshell
ScriptModel {
required property int count
values: Array(count).map((_, i) => i)
}

View file

@ -0,0 +1,32 @@
import QtQuick
import Quickshell
QtObject {
enum IconType { Material, Text, System, None }
enum FontType { Normal, Monospace }
// General stuff
property string type: ""
property var fontType: LauncherSearchResult.FontType.Normal
property string name: ""
property string rawValue: ""
property string iconName: ""
property var iconType: LauncherSearchResult.IconType.None
property string verb: ""
property bool blurImage: false
property var execute: () => {
print("Not implemented");
}
property var actions: []
// Stuff needed for DesktopEntry
property string id: ""
property bool shown: true
property string comment: ""
property bool runInTerminal: false
property string genericName: ""
property list<string> keywords: []
// Extra stuff to allow for more flexibility
property string category: type
}

View file

@ -0,0 +1,6 @@
import QtQuick
// QtObject that allows stuff to be freely declared inside
QtObject {
default property list<QtObject> data
}

View file

@ -0,0 +1,52 @@
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import qs.modules.common.functions
import qs.modules.common.utils
import qs.services
import qs.modules.common
import ".."
NestableObject {
id: root
enum State {
Done, Preparing, Processing, Error
}
signal finished()
signal error(message: string)
property int errorCode
property string errorMessage: ""
property var outputData
property var state: GCloudApi.State.Done
function resetState() {
root.state = GCloudApi.State.Done;
root.errorMessage = "";
root.outputData = undefined;
}
function handleApiOutput(out: string): bool {
try {
root.outputData = JSON.parse(out);
if (outputData.error) {
print("API error: " + JSON.stringify(outputData.error, null, 2))
root.state = GCloudApi.State.Error;
root.errorCode = outputData.error.code;
root.errorMessage = outputData.error.message;
root.error(outputData.error.message);
return false;
}
root.finished();
root.state = GCloudApi.State.Done;
return true
} catch (e) {
print("Failed to parse API response: " + e + "\n" + out)
root.state = GCloudApi.State.Error;
root.errorMessage = "Failed to parse API response";
root.error(root.errorMessage);
return false;
}
}
}

View file

@ -0,0 +1,57 @@
pragma ComponentBehavior: Bound
import QtQuick
import qs.modules.common.functions
import qs.modules.common.utils
import qs.services
import ".."
GCloudApi {
id: root
property list<string> pendingStrings
property bool setupReady: false
readonly property bool preparationReady: GoogleCloud.tokenReady && setupReady
function translateStrings(strings: list<string>) {
GoogleCloud.load();
root.setupReady = false;
root.pendingStrings = strings;
root.state = GCloudApi.State.Preparing;
root.setupReady = true;
}
onPreparationReadyChanged: {
if (!preparationReady) return;
root.state = GCloudApi.State.Processing;
const targetLang = Translation.languageCode;
const payload = {
"targetLanguageCode": targetLang,
"contents": root.pendingStrings,
"mimeType": "text/plain"
};
// print("PENDING STRINGS:", root.pendingStrings)
var seq = [];
seq.push([ //
"bash", "-c", //
`curl -sL -X POST \
-H "Authorization: Bearer ${GoogleCloud.token}" \
-H "x-goog-user-project: ${GoogleCloud.projectId}" \
-H "Content-Type: application/json" \
-d '${StringUtils.shellSingleQuoteEscape(JSON.stringify(payload))}' \
"https://translation.googleapis.com/v3/projects/${GoogleCloud.projectId}:translateText"`
]);
seq.push(((out) => {
root.handleApiOutput(out);
}));
multiproc.runSequence(seq);
}
MultiTurnProcess {
id: multiproc
}
}

View file

@ -0,0 +1,83 @@
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import qs.modules.common.functions
import qs.modules.common.utils
import qs.services
import qs.modules.common
import ".."
GCloudApi {
id: root
readonly property string imageBase64FilePath: `${Directories.screenshotTemp}/vision_base64.txt`
readonly property string payloadFilePath: `${Directories.screenshotTemp}/vision_payload.json`
property string uploadEndpoint: "https://uguu.se/upload"
property bool tokenReady: GoogleCloud.tokenReady
property bool onlineImageReady: false
readonly property bool preparationReady: tokenReady && onlineImageReady
function annotateImage(imageUri: string) {
resetState();
root.state = GCloudApi.State.Preparing;
root.onlineImageReady = false
GoogleCloud.load();
var seq = []; // command sequence
const niceFilePath = StringUtils.shellSingleQuoteEscape(FileUtils.trimFileProtocol(imageUri))
seq = [ //
["bash", "-c", `mkdir -p '${Directories.screenshotTemp}'; base64 '${niceFilePath}' -w 0 > '${imageBase64FilePath}'`], //
(out) => { //
root.onlineImageReady = true; //
}
]
// Execute the base64 conversion & load the token
prepMultiproc.runSequence(seq);
}
onPreparationReadyChanged: {
if (!preparationReady) return;
if (GoogleCloud.tokenError || GoogleCloud.keyError) {
root.state = GCloudApi.State.Error;
root.error(Translation.tr("Set your Google Cloud service account key"));
return;
}
root.state = GCloudApi.State.Processing;
var seq = []; // command sequence
// Construct the JSON payload using jq to read from the base64 file
seq.push([
"bash", "-c",
`jq -n --rawfile content '${imageBase64FilePath}' \
'{"requests": [{"image": {"content": $content}, "features": [{"type": "DOCUMENT_TEXT_DETECTION"}]}]}' \
> '${payloadFilePath}'`
]);
seq.push([
"bash", "-c",
`curl -s -X POST \
-H "Authorization: Bearer ${GoogleCloud.token}" \
-H "x-goog-user-project: ${GoogleCloud.projectId}" \
-H "Content-Type: application/json" \
https://vision.googleapis.com/v1/images:annotate \
-d @'${payloadFilePath}'`
]);
seq.push((out) => {
root.handleApiOutput(out);
});
lookMultiproc.runSequence(seq);
}
MultiTurnProcess {
id: prepMultiproc
}
MultiTurnProcess {
id: lookMultiproc
}
}

View file

@ -0,0 +1,90 @@
pragma ComponentBehavior: Bound
import QtQuick
import ".."
NestableObject {
id: root
property real confidenceThreshold: 0.5 // TODO tune this
property var rawData
property var rawBlocks
property var rawParagraphs
property var coherentParagraphs
function initializeWithData(apiOutputData: var): void {
// Null check
if (!apiOutputData) {
print("[GCloudVisionResult] Data is null/undefined")
return;
}
// Raw data
root.rawData = apiOutputData
// Raw blocks
var pages = apiOutputData.responses[0].fullTextAnnotation.pages
var blocks = [];
for (var i = 0; i < pages.length; i++) {
// print("this page", JSON.stringify(pages[i]))
var blocksThisPage = pages[i].blocks;
for (var j = 0; j < blocksThisPage.length; j++) {
const block = blocksThisPage[j];
// print("new block with confidence", block.confidence, ":", JSON.stringify(block, null, 2))
if (block.confidence > root.confidenceThreshold) {
blocks.push(block);
}
}
}
root.rawBlocks = blocks
// print("RAW BLOCKS:", blocks)
// Raw paragraphs
var paragraphs = []
for (var i = 0; i < blocks.length; i++) {
var blockParagraphs = blocks[i].paragraphs;
for (var j = 0; j < blockParagraphs.length; j++) {
const para = blockParagraphs[j];
// print("new paragraph", JSON.stringify(para))
paragraphs.push(para);
}
}
root.rawParagraphs = [...paragraphs];
// print("RAW PARAGRAPHS", paragraphs)
// Coherent paragraphs
// (raw data can be as granular as symbols)
// We're interested in paragraph level of granularity as it's good for translations
for (var i = 0; i < paragraphs.length; i++) {
const paragraph = paragraphs[i];
const words = paragraph.words;
var strList = []
for (var j = 0; j < words.length; j++) {
const symbols = words[j].symbols;
for (var k = 0; k < symbols.length; k++) {
const sym = symbols[k];
strList.push(sym.text);
// print("CHAR:", JSON.stringify(sym, null, 2));
// Breaks
// Reference: https://docs.cloud.google.com/vision/docs/reference/rpc/google.cloud.vision.v1#breaktype
if (sym.property?.detectedBreak.type == "SPACE" || sym.property?.detectedBreak.type == "UNKNOWN") {
strList.push(" ");
} else if (sym.property?.detectedBreak.type == "SURE_SPACE") {
strList.push(" ");
} else if (sym.property?.detectedBreak.type == "EOL_SURE_SPACE" || sym.property?.detectedBreak.type == "LINE_BREAK") {
strList.push("\n");
} else if (sym.property?.detectedBreak.type == "HYPHEN") {
strList.push("-\n");
}
}
}
// print("STR LIST:", strList)
paragraphs[i].text = strList.join("").trim();
// print("PARA TEXT:", paragraphs[i].text)
}
root.coherentParagraphs = paragraphs
// print("COHERENT PARAGRAPHS", JSON.stringify(paragraphs))
}
}

View file

@ -0,0 +1,63 @@
pragma ComponentBehavior: Bound
import QtQml
import QtQuick
import Quickshell.Io
import qs.services
import "../"
NestableObject {
id: root
required property string key
property alias fetching: fetchProc.running
property bool set
property var value
Component.onCompleted: fetch()
Connections {
target: HyprlandConfig
function onReloaded() {
root.fetch();
}
}
function fetch() {
fetchProc.command = fetchProc.baseCommand.concat([root.key]);
fetchProc.running = true;
}
function setValue(newValue) {
HyprlandConfig.set(root.key, newValue)
}
function reset() {
HyprlandConfig.reset(root.key)
}
Process {
id: fetchProc
property list<string> baseCommand: ["hyprctl", "getoption", "-j"]
stdout: StdioCollector {
onStreamFinished: {
if (text == "no such option")
return;
try {
const obj = JSON.parse(text);
// Note that the value is returned as "<data type>": <value>
// It's the only field that isn't always in the same key so we put it in an else
for (const key in obj) {
if (key == "option")
continue;
else if (key == "set")
root.set = obj[key];
else
root.value = obj[key];
}
} catch (e) {
console.log(`[HyprlandConfigOption] Failed to fetch option "${root.key}":\n - Output: ${text.trim()}\n - Error: ${e}`);
}
}
}
}
}

View file

@ -0,0 +1,17 @@
import QtQuick
import qs.services
import qs.modules.common
import qs.modules.common.functions
import qs.modules.common.widgets
QuickToggleModel {
name: HyprlandAntiFlashbangShader.enabled ? (HyprlandAntiFlashbangShader.weak ? Translation.tr("Anti-flash: Weak") : Translation.tr("Anti-flash: Strong")) : Translation.tr("Anti-flashbang")
tooltipText: `${Translation.tr("Anti-flashbang")}: ${HyprlandAntiFlashbangShader.enabled ? (HyprlandAntiFlashbangShader.weak ? Translation.tr("Weak") : Translation.tr("Strong")) : Translation.tr("Off")}`
icon: HyprlandAntiFlashbangShader.enabled ? (!HyprlandAntiFlashbangShader.weak ? "flash_off" : "sunny_snowing") : "flash_on"
toggled: HyprlandAntiFlashbangShader.enabled
mainAction: () => {
HyprlandAntiFlashbangShader.cycle()
}
hasMenu: true
}

View file

@ -0,0 +1,17 @@
import QtQuick
import qs.services
import qs.modules.common
import qs.modules.common.functions
import qs.modules.common.widgets
QuickToggleModel {
name: Translation.tr("Audio output")
statusText: toggled ? Translation.tr("Unmuted") : Translation.tr("Muted")
tooltipText: Translation.tr("Audio output | Right-click for volume mixer & device selector")
toggled: !Audio.sink?.audio?.muted
icon: Audio.sink?.audio?.muted ? "volume_off" : "volume_up"
mainAction: () => {
Audio.toggleMute()
}
hasMenu: true
}

View file

@ -0,0 +1,23 @@
import QtQuick
import Quickshell.Bluetooth
import qs.services
import qs.modules.common
import qs.modules.common.functions
import qs.modules.common.widgets
QuickToggleModel {
name: Translation.tr("Bluetooth")
statusText: BluetoothStatus.firstActiveDevice?.name ?? Translation.tr("Not connected")
tooltipText: Translation.tr("%1 | Right-click to configure").arg(
(BluetoothStatus.firstActiveDevice?.name ?? Translation.tr("Bluetooth"))
+ (BluetoothStatus.activeDeviceCount > 1 ? ` +${BluetoothStatus.activeDeviceCount - 1}` : "")
)
icon: BluetoothStatus.connected ? "bluetooth_connected" : BluetoothStatus.enabled ? "bluetooth" : "bluetooth_disabled"
available: BluetoothStatus.available
toggled: BluetoothStatus.enabled
mainAction: () => {
Bluetooth.defaultAdapter.enabled = !Bluetooth.defaultAdapter?.enabled
}
hasMenu: true
}

View file

@ -0,0 +1,78 @@
import QtQuick
import qs.services
import qs.modules.common
import qs.modules.common.functions
import qs.modules.common.widgets
import Quickshell
import Quickshell.Io
QuickToggleModel {
id: root
name: Translation.tr("Cloudflare WARP")
toggled: false
icon: "cloud_lock"
mainAction: () => {
if (toggled) {
root.toggled = false
Quickshell.execDetached(["warp-cli", "disconnect"])
} else {
root.toggled = true
Quickshell.execDetached(["warp-cli", "connect"])
}
}
Process {
id: connectProc
command: ["warp-cli", "connect"]
onExited: (exitCode, exitStatus) => {
if (exitCode !== 0) {
Quickshell.execDetached(["notify-send",
Translation.tr("Cloudflare WARP"),
Translation.tr("Connection failed. Please inspect manually with the <tt>warp-cli</tt> command")
, "-a", "Shell"
])
}
}
}
Process {
id: registrationProc
command: ["warp-cli", "registration", "new"]
onExited: (exitCode, exitStatus) => {
console.log("Warp registration exited with code and status:", exitCode, exitStatus)
if (exitCode === 0) {
connectProc.running = true
} else {
Quickshell.execDetached(["notify-send",
Translation.tr("Cloudflare WARP"),
Translation.tr("Registration failed. Please inspect manually with the <tt>warp-cli</tt> command"),
"-a", "Shell"
])
}
}
}
Process {
id: fetchActiveState
running: true
command: ["bash", "-c", "warp-cli status"]
stdout: StdioCollector {
id: warpStatusCollector
onStreamFinished: {
if (warpStatusCollector.text.length > 0) {
root.available = true
}
if (warpStatusCollector.text.includes("Unable")) {
registrationProc.running = true
} else if (warpStatusCollector.text.includes("Connected")) {
root.toggled = true
} else if (warpStatusCollector.text.includes("Disconnected")) {
root.toggled = false
}
}
}
}
tooltipText: Translation.tr("Cloudflare WARP (1.1.1.1)")
}

View file

@ -0,0 +1,29 @@
import QtQuick
import Quickshell
import qs
import qs.services
import qs.modules.common
import qs.modules.common.functions
import qs.modules.common.widgets
QuickToggleModel {
name: Translation.tr("Color picker")
hasStatusText: false
toggled: false
icon: "colorize"
mainAction: () => {
GlobalStates.sidebarRightOpen = false;
delayedActionTimer.start();
}
Timer {
id: delayedActionTimer
interval: 300
repeat: false
onTriggered: {
Quickshell.execDetached(["hyprpicker", "-a"]);
}
}
tooltipText: Translation.tr("Color picker")
}

View file

@ -0,0 +1,25 @@
import QtQuick
import Quickshell
import qs
import qs.services
import qs.modules.common
import qs.modules.common.functions
import qs.modules.common.widgets
QuickToggleModel {
name: Translation.tr("Dark Mode")
statusText: Appearance.m3colors.darkmode ? Translation.tr("Dark") : Translation.tr("Light")
toggled: Appearance.m3colors.darkmode
icon: "contrast"
mainAction: () => {
if (Appearance.m3colors.darkmode) {
Quickshell.execDetached([Directories.wallpaperSwitchScriptPath, "--mode", "light", "--noswitch"]);
} else {
Quickshell.execDetached([Directories.wallpaperSwitchScriptPath, "--mode", "dark", "--noswitch"]);
}
}
tooltipText: Translation.tr("Dark Mode")
}

View file

@ -0,0 +1,30 @@
import QtQuick
import Quickshell
import qs
import qs.services
import qs.modules.common
import qs.modules.common.functions
import qs.modules.common.widgets
QuickToggleModel {
name: Translation.tr("EasyEffects")
available: EasyEffects.available
toggled: EasyEffects.active
icon: "graphic_eq"
Component.onCompleted: {
EasyEffects.fetchActiveState()
}
mainAction: () => {
EasyEffects.toggle()
}
altAction: () => {
Quickshell.execDetached(["bash", "-c", "flatpak run com.github.wwmm.easyeffects || easyeffects"])
GlobalStates.sidebarRightOpen = false
}
tooltipText: Translation.tr("EasyEffects | Right-click to configure")
}

View file

@ -0,0 +1,49 @@
import QtQuick
import Quickshell
import Quickshell.Io
import qs
import qs.services
import qs.modules.common
/**
* Whole-screen capture, as opposed to ScreenSnipToggle's region select.
*
* `fullScreenshot` had been listed in the phone's config for long enough that
* everyone assumed a component existed; none ever did, in any tree or any
* commit. It rendered nothing because there was nothing to render.
*/
QuickToggleModel {
id: root
name: Translation.tr("Screenshot")
hasStatusText: false
toggled: false
icon: "screenshot_monitor"
// Close the panel first, or the screenshot is a picture of the panel.
// Same 300 ms the region snip uses — the close animation has to finish.
mainAction: () => {
GlobalStates.sidebarRightOpen = false;
captureDelay.start();
}
Timer {
id: captureDelay
interval: 300
repeat: false
onTriggered: captureProc.running = true
}
// To a file AND the clipboard: on the phone there is rarely anywhere to
// paste it right away, and on the laptop the file is the annoying half.
Process {
id: captureProc
command: ["bash", "-c",
"mkdir -p \"$HOME/Pictures\"; "
+ "f=\"$HOME/Pictures/screenshot-$(date +%Y%m%d-%H%M%S).png\"; "
+ "grim \"$f\" || { notify-send Screenshot 'Capture failed.' -a Shell; exit 1; }; "
+ "wl-copy -t image/png < \"$f\"; "
+ "notify-send Screenshot \"${f##*/} — saved and copied\" -a Shell"]
}
tooltipText: Translation.tr("Whole screen to ~/Pictures and the clipboard")
}

View file

@ -0,0 +1,45 @@
import QtQuick
import Quickshell.Io
import qs.modules.common.models.hyprland
import qs.services
QuickToggleModel {
id: root
name: Translation.tr("Game mode")
toggled: !confOpt.value
icon: "gamepad"
mainAction: () => {
root.toggled = !root.toggled;
if (root.toggled) {
HyprlandConfig.setMany({
"animations:enabled": 0,
"decoration:shadow:enabled": 0,
"decoration:blur:enabled": 0,
"general:gaps_in": 0,
"general:gaps_out": 0,
"general:border_size": 1,
"decoration:rounding": 0,
"general:allow_tearing": 1
});
} else {
HyprlandConfig.resetMany([ //
"animations:enabled", //
"decoration:shadow:enabled", //
"decoration:blur:enabled", //
"general:gaps_in", //
"general:gaps_out", //
"general:border_size", //
"decoration:rounding", //
"general:allow_tearing", //
]);
}
}
HyprlandConfigOption {
id: confOpt
key: "animations:enabled"
}
tooltipText: Translation.tr("Game mode")
}

View file

@ -0,0 +1,25 @@
import QtQuick
import Quickshell
import qs
import qs.services
import qs.modules.common
import qs.modules.common.functions
import qs.modules.common.widgets
QuickToggleModel {
toggled: Idle.inhibit
property bool autoIdleInhibit: Idle.autoIdleInhibit
name: Translation.tr("Keep awake")
statusText: autoIdleInhibit ? Translation.tr("auto Idle: enabled") : Translation.tr("auto Idle: disabled")
icon: autoIdleInhibit ? "emoji_food_beverage" : "coffee"
tooltipText: Translation.tr("Keep system awake | Right click to toggle auto mode")
mainAction: () => {
Idle.toggleInhibit()
}
altAction: () => {
Idle.toggleAutoInhibit()
}
}

View file

@ -0,0 +1,20 @@
import QtQuick
import Quickshell
import qs
import qs.services
import qs.modules.common
import qs.modules.common.functions
import qs.modules.common.widgets
QuickToggleModel {
name: Translation.tr("Audio input")
statusText: toggled ? Translation.tr("Enabled") : Translation.tr("Muted")
toggled: !Audio.source?.audio?.muted
icon: Audio.source?.audio?.muted ? "mic_off" : "mic"
mainAction: () => {
Audio.toggleMicMute()
}
hasMenu: true
tooltipText: Translation.tr("Audio input | Right-click for volume mixer & device selector")
}

View file

@ -0,0 +1,25 @@
import QtQuick
import Quickshell
import qs
import qs.services
import qs.modules.common
import qs.modules.common.functions
import qs.modules.common.widgets
QuickToggleModel {
toggled: SongRec.running
property bool sourceIsMonitor: SongRec.monitorSource === SongRec.MonitorSource.Monitor
name: Translation.tr("Identify Music")
statusText: toggled ? Translation.tr("Listening...") : sourceIsMonitor ? Translation.tr("System sound") : Translation.tr("Microphone")
icon: toggled ? "music_cast" : (sourceIsMonitor ? "music_note" : "frame_person_mic")
tooltipText: Translation.tr("Recognize music | Right-click to toggle source")
mainAction: () => {
SongRec.toggleRunning()
}
altAction: () => {
SongRec.toggleMonitorSource()
}
}

View file

@ -0,0 +1,16 @@
import QtQuick
import qs.services
import qs.modules.common
import qs.modules.common.functions
import qs.modules.common.widgets
QuickToggleModel {
name: Translation.tr("Internet")
statusText: Network.networkName
tooltipText: Translation.tr("%1 | Right-click to configure").arg(Network.networkName)
icon: Network.materialSymbol
toggled: Network.wifiStatus !== "disabled"
mainAction: () => Network.toggleWifi()
hasMenu: true
}

View file

@ -0,0 +1,28 @@
import QtQuick
import Quickshell
import qs
import qs.services
import qs.modules.common
import qs.modules.common.functions
import qs.modules.common.widgets
QuickToggleModel {
property bool auto: Config.options.light.night.automatic
name: Translation.tr("Night Light")
statusText: (auto ? Translation.tr("Auto, ") : "") + (toggled ? Translation.tr("Active") : Translation.tr("Inactive"))
toggled: Hyprsunset.temperatureActive
icon: auto ? "night_sight_auto" : "bedtime"
mainAction: () => {
Hyprsunset.toggleTemperature()
}
hasMenu: true
Component.onCompleted: {
Hyprsunset.fetchState()
}
tooltipText: Translation.tr("Night Light | Right-click to configure")
}

View file

@ -0,0 +1,20 @@
import QtQuick
import Quickshell
import qs
import qs.services
import qs.modules.common
import qs.modules.common.functions
import qs.modules.common.widgets
QuickToggleModel {
name: Translation.tr("Notifications")
statusText: toggled ? Translation.tr("Show") : Translation.tr("Silent")
toggled: !Notifications.silent
icon: toggled ? "notifications_active" : "notifications_paused"
mainAction: () => {
Notifications.silent = !Notifications.silent;
}
tooltipText: Translation.tr("Show notifications")
}

View file

@ -0,0 +1,19 @@
import QtQuick
import Quickshell
import qs
import qs.services
import qs.modules.common
import qs.modules.common.functions
import qs.modules.common.widgets
QuickToggleModel {
name: Translation.tr("Virtual Keyboard")
toggled: GlobalStates.oskOpen
icon: toggled ? "keyboard_hide" : "keyboard"
mainAction: () => {
GlobalStates.oskOpen = !GlobalStates.oskOpen
}
tooltipText: Translation.tr("On-screen keyboard")
}

View file

@ -0,0 +1,39 @@
import QtQuick
import Quickshell
import Quickshell.Services.UPower
import qs
import qs.services
import qs.modules.common
import qs.modules.common.functions
import qs.modules.common.widgets
QuickToggleModel {
name: Translation.tr("Power Profile")
toggled: PowerProfiles.profile !== PowerProfile.Balanced
icon: switch(PowerProfiles.profile) {
case PowerProfile.PowerSaver: return "energy_savings_leaf"
case PowerProfile.Balanced: return "airwave"
case PowerProfile.Performance: return "local_fire_department"
}
statusText: switch(PowerProfiles.profile) {
case PowerProfile.PowerSaver: return "Power Saver"
case PowerProfile.Balanced: return "Balanced"
case PowerProfile.Performance: return "Performance"
}
mainAction: () => {
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
}
}
tooltipText: Translation.tr("Click to cycle through power profiles")
}

View file

@ -0,0 +1,22 @@
import QtQuick
QtObject {
// Textual info
required property string name
property string statusText
property string tooltipText: ""
property string icon: "close"
// State
property bool hasStatusText: true
property bool available: true
property bool toggled: false
// Interactions
required property var mainAction
property bool hasMenu: false
property var altAction: null
// Allow stuff like Processes to be declared freely
default property list<QtObject> data
}

View file

@ -0,0 +1,29 @@
import QtQuick
import Quickshell
import qs
import qs.services
import qs.modules.common
import qs.modules.common.functions
import qs.modules.common.widgets
QuickToggleModel {
name: Translation.tr("Screen snip")
hasStatusText: false
toggled: false
icon: "screenshot_region"
mainAction: () => {
GlobalStates.sidebarRightOpen = false;
delayedActionTimer.start();
}
Timer {
id: delayedActionTimer
interval: 300
repeat: false
onTriggered: {
Quickshell.execDetached(["qs", "-p", Quickshell.shellPath(""), "ipc", "call", "region", "screenshot"]);
}
}
tooltipText: Translation.tr("Screen snip")
}

View file

@ -0,0 +1,176 @@
import QtQuick
import qs.services
import qs.modules.common
import Quickshell
import Quickshell.Io
/**
* A WireGuard tunnel, toggled deliberately.
*
* DELIBERATE, NEVER AUTOMATIC — and that is the design, not a limitation.
* An autonomous gate held this tunnel on 2026-07-30: it recycled 652 times in
* 90 minutes, through doze tiers whose contract is "network fetchers stopped",
* and the user could not switch it off because `nmcli connection down` fired
* the NM dispatcher that immediately brought it back up. A control surface the
* owner cannot overrule is not a control surface.
*
* So there is no daemon, no timer and no dispatcher hook deciding this. The
* toggle is the whole of it. If tunnel posture later becomes something the
* device decides for itself, it belongs in sessiond's state machine as an
* Action (DEVICE-STATE-MACHINE §12) and reachable as a verb (doctrine §13) —
* not as another actor off to the side.
*
* No privilege needed: polkit already lets the seat user activate a system
* connection, verified on device.
*/
QuickToggleModel {
id: root
/// The NetworkManager connection this drives. A property rather than a
/// constant so a second profile does not need a second component.
///
/// Give each device its OWN peer. A profile that shares an address and a
/// private key between two machines lets WireGuard connect only one of
/// them at a time, and one dialling a port with a key the server no longer
/// lists will send and never receive. Set this per deployment.
property string connectionName: ""
/// True while an up/down is in flight, so a double tap cannot race itself.
property bool busy: false
/// A host that only exists at the far end of the tunnel. Reaching it is the
/// only honest proof the tunnel carries traffic.
property string probeHost: ""
/// "off" | "connecting" | "limited" | "online".
///
/// NOT a bool, and that is the point. On 2026-07-31 this toggle read "On"
/// for an hour while the tunnel was deaf: NetworkManager said `activated`,
/// `wg` said 368 B received against 5.3 KiB sent, and every service behind
/// it — STT, TTS, gitea — was unreachable. "The interface is up" and "the
/// tunnel carries traffic" are different claims and only one of them is
/// worth showing.
///
/// Sailfish models exactly this distinction (`MobileDataConnection.status`
/// is Online / Limited / Connecting, never a bool) and Android refuses to
/// let an unvalidated network win at all. `limited` is the state both have
/// and we did not.
///
/// The probe is deliberately NOT an internet check. This is a split tunnel
/// whose whole purpose is a private range that no public probe can
/// see; a link that fails a general-internet probe may be exactly the one
/// we want. The question is per-consumer — "can this carry *this*" — so we
/// ask the far end directly.
property string status: "off"
name: Translation.tr("VPN")
icon: root.status === "online" ? "vpn_lock"
: root.status === "limited" ? "vpn_lock_off"
: "vpn_key"
statusText: {
if (root.busy)
return Translation.tr("…");
switch (root.status) {
case "online": return Translation.tr("On");
case "limited": return Translation.tr("No route");
default: return Translation.tr("Off");
}
}
tooltipText: {
switch (root.status) {
case "online":
return Translation.tr("VPN — the private range routed over the tunnel");
case "limited":
return Translation.tr("VPN — connected but not carrying traffic. The tunnel is up and the far side is unreachable; check which link the handshake is leaving by.");
default:
return Translation.tr("VPN — off");
}
}
mainAction: () => {
if (root.busy)
return;
root.busy = true;
if (root.toggled)
downProc.running = true;
else
upProc.running = true;
}
function notify(body) {
Quickshell.execDetached(["notify-send", Translation.tr("VPN"), body, "-a", "Shell"]);
}
Process {
id: upProc
command: ["nmcli", "connection", "up", root.connectionName]
onExited: (exitCode, exitStatus) => {
root.busy = false;
refreshProc.running = true;
if (exitCode !== 0)
root.notify(Translation.tr("Could not connect."));
}
}
Process {
id: downProc
command: ["nmcli", "connection", "down", root.connectionName]
onExited: (exitCode, exitStatus) => {
root.busy = false;
refreshProc.running = true;
if (exitCode !== 0)
root.notify(Translation.tr("Could not disconnect."));
}
}
// Read NM rather than tracking a local bool: the connection can also be
// brought up or down from nmcli, from Settings, or by NM itself, and a
// shadow copy that disagrees with the owning subsystem is the failure
// doctrine §4 names ("never a hand-tracked bool").
// Two questions, asked together because the answer to the first does not
// imply the second: is the connection active, and does it carry traffic.
// A single `nmcli` reply can only answer the first, which is how this
// reported "On" through an hour of a dead tunnel.
//
// The probe is one UDP DNS round-trip to the far-side resolver with a 2 s
// deadline — cheap enough for a 15 s poll, and it fails in exactly the case
// that matters (interface up, nothing crossing it). `getent` is not used:
// it would consult the whole resolver list and could be answered by a
// nameserver on another link, which is the same conflation this is here to
// end.
Process {
id: refreshProc
running: true
command: ["bash", "-c",
`if ! nmcli -t -f NAME connection show --active | grep -qx '${root.connectionName}'; then echo off; exit 0; fi;`
+ ` if timeout 2 bash -c 'echo > /dev/udp/${root.probeHost}/53' 2>/dev/null`
+ ` && timeout 2 ping -c1 -W2 ${root.probeHost} >/dev/null 2>&1; then echo online; else echo limited; fi`]
stdout: StdioCollector {
id: stateCollector
onStreamFinished: {
const reply = stateCollector.text.trim();
if (reply.length === 0)
return;
root.status = reply;
// `toggled` stays the user-facing "is it switched on", so the
// button still lights while the tunnel is limited — the state
// is reported in statusText rather than by silently un-toggling
// something the user turned on. Sailfish does the same: the
// switch stays checked and the description says "Limited".
root.toggled = (reply !== "off");
}
}
}
// Cheap resync for changes made outside the panel. One nmcli call every
// 15 s while the shell runs; deliberately not a decision loop.
Timer {
interval: 15000
running: true
repeat: true
onTriggered: {
if (!root.busy)
refreshProc.running = true;
}
}
}

View file

@ -0,0 +1,137 @@
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
property bool alsoInhibitIdle: false
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(alsoInhibitIdle = false) {
root.alsoInhibitIdle = alsoInhibitIdle;
root.unlockInProgress = true;
pam.start();
}
function tryFingerUnlock() {
if (root.fingerprintsConfigured) {
fingerPam.start();
}
}
function stopFingerPam() {
if (fingerPam.active) {
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()
}
}
}
}

View file

@ -0,0 +1,163 @@
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
required property Component lockSurface
property alias context: lockContext
property Component sessionLockSurface: WlSessionLockSurface {
id: sessionLockSurface
color: "transparent"
Loader {
active: GlobalStates.screenLocked
anchors.fill: parent
opacity: active ? 1 : 0
Behavior on opacity {
animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this)
}
sourceComponent: root.lockSurface
}
}
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")]
})
}
// 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();
// Отключаем экран по таймауту, если параметр больше нуля
if (Config.options.lock.dpmsTimeout > 0) {
Quickshell.execDetached(["bash", "-c", `sleep ${Config.options.lock.dpmsTimeout} && hyprctl dispatch dpms off`]);
}
}
}
}
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(); // Async
// Выполняем пользовательский хук разблокировки, если он задан
if (Config.options.lock.unlockHook && Config.options.lock.unlockHook !== "") {
Quickshell.execDetached(["bash", "-c", Config.options.lock.unlockHook]);
}
// Unlock the screen before exiting, or the compositor will display a
// fallback lock you can't interact with.
GlobalStates.screenLocked = false;
// Reset
lockContext.reset();
// Post-unlock actions
if (lockContext.alsoInhibitIdle) {
lockContext.alsoInhibitIdle = false;
Idle.toggleInhibit(true);
}
}
}
WlSessionLock {
id: lock
locked: GlobalStates.screenLocked
surface: root.sessionLockSurface
}
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();
}
}
}

View file

@ -0,0 +1 @@
auth sufficient pam_fprintd.so

View file

@ -0,0 +1,47 @@
import QtQuick
import Quickshell
import Quickshell.Io
import qs.modules.common
import qs.modules.common.functions
Process {
id: root
signal done(string path, int width, int height);
required property string filePath;
required property string sourceUrl;
property string downloadUserAgent: Config.options?.networking.userAgent ?? ""
function processFilePath() {
return StringUtils.shellSingleQuoteEscape(FileUtils.trimFileProtocol(filePath));
}
function processSourceUrl() {
return StringUtils.shellSingleQuoteEscape(sourceUrl);
}
function curlUserAgentArg() {
if (!downloadUserAgent) {
return "";
}
return ` -H 'User-Agent: ${StringUtils.shellSingleQuoteEscape(downloadUserAgent)}'`;
}
running: true
command: ["bash", "-c",
`mkdir -p $(dirname '${processFilePath()}'); [ -f '${processFilePath()}' ] || curl -sSL '${processSourceUrl()}'${curlUserAgentArg()} -o '${processFilePath()}' && file '${processFilePath()}'`
]
stdout: StdioCollector {
id: imageSizeOutputCollector
onStreamFinished: {
const output = imageSizeOutputCollector.text.trim();
const match = output.match(/(\d+)\s*x\s*(\d+)/);
if (match) {
const width = Number(match[1]);
const height = Number(match[2]);
root.done(root.filePath, width, height);
}
}
}
}

View file

@ -0,0 +1,46 @@
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell.Io
Process {
id: proc
signal finished(string output)
property list<var> sequence: []
property int index: 0
// Runs a sequence of command and functions
function runSequence(seq) {
if (seq.length == 0)
return;
sequence = seq;
running = false;
index = -1;
step();
}
function step(output) {
index++;
if (index >= sequence.length) {
finished(output);
return;
}
const nextItem = sequence[index];
if (typeof nextItem === "function") {
const nextOutput = nextItem(output);
step(nextOutput);
} else {
// If empty command then not set; this allows setting it with previous function
if (nextItem.length > 0)
command = nextItem;
running = true;
}
}
stdout: StdioCollector {
onStreamFinished: {
proc.step(text);
}
}
}

View file

@ -0,0 +1,81 @@
pragma ComponentBehavior: Bound
pragma Singleton
import qs.modules.common
import qs.modules.common.utils
import qs.modules.common.functions
import qs.modules.common.widgets
import qs.services
import QtQuick
import QtQuick.Controls
import Qt.labs.synchronizer
import Quickshell
Singleton {
id: root
enum Action {
Copy,
Edit,
Search,
CharRecognition,
Record,
RecordWithSound
}
property string imageSearchEngineBaseUrl: Config.options.search.imageSearch.imageSearchEngineBaseUrl
property string fileUploadApiEndpoint: "https://uguu.se/upload"
function getCommand(x, y, width, height, screenshotPath, action, saveDir = "") {
// Set command for action
const rx = Math.round(x);
const ry = Math.round(y);
const rw = Math.round(width);
const rh = Math.round(height);
const cropBase = `magick ${StringUtils.shellSingleQuoteEscape(screenshotPath)} `
+ `-crop ${rw}x${rh}+${rx}+${ry} +repage`
const cropToStdout = `${cropBase} -`
const cropInPlace = `${cropBase} '${StringUtils.shellSingleQuoteEscape(screenshotPath)}'`
const cleanup = `rm '${StringUtils.shellSingleQuoteEscape(screenshotPath)}'`
const slurpRegion = `${rx},${ry} ${rw}x${rh}`
const uploadAndGetUrl = (filePath) => {
return `curl -sF files[]=@'${StringUtils.shellSingleQuoteEscape(filePath)}' ${root.fileUploadApiEndpoint} | jq -r '.files[0].url'`
}
const annotationCommand = `${Config.options.regionSelector.annotation.useSatty ? "satty" : "swappy"} -f -`;
switch (action) {
case ScreenshotAction.Action.Copy:
if (saveDir === "") {
// not saving the screenshot, just copy to clipboard
return ["bash", "-c", `${cropToStdout} | wl-copy && ${cleanup}`]
break;
}
return [
"bash", "-c",
`mkdir -p '${StringUtils.shellSingleQuoteEscape(saveDir)}' && \
saveFileName="screenshot-$(date '+%Y-%m-%d_%H.%M.%S').png" && \
savePath="${saveDir}/$saveFileName" && \
${cropToStdout} | tee >(wl-copy) > "$savePath" && \
${cleanup}`
]
break;
case ScreenshotAction.Action.Edit:
return ["bash", "-c", `${cropToStdout} | ${annotationCommand} && ${cleanup}`]
break;
case ScreenshotAction.Action.Search:
return ["bash", "-c", `${cropInPlace} && xdg-open "${root.imageSearchEngineBaseUrl}$(${uploadAndGetUrl(screenshotPath)})" && ${cleanup}`]
break;
case ScreenshotAction.Action.CharRecognition:
return ["bash", "-c", `${cropInPlace} && tesseract '${StringUtils.shellSingleQuoteEscape(screenshotPath)}' stdout -l $(tesseract --list-langs | awk 'NR>1{print $1}' | tr '\\n' '+' | sed 's/\\+$/\\n/') | wl-copy && ${cleanup}`]
break;
case ScreenshotAction.Action.Record:
return ["bash", "-c", `${Directories.recordScriptPath} --region '${slurpRegion}'`]
break;
case ScreenshotAction.Action.RecordWithSound:
return ["bash", "-c", `${Directories.recordScriptPath} --region '${slurpRegion}' --sound`]
break;
default:
console.warn("[Region Selector] Unknown snip action, skipping snip.");
return;
}
}
}

View file

@ -0,0 +1,14 @@
import QtQuick
import Quickshell
import Quickshell.Io
import qs.modules.common
import qs.modules.common.functions
Process {
id: screenshotProc
running: true
property string screenshotDir: Directories.screenshotTemp
required property ShellScreen screen
property string screenshotPath: `${screenshotDir}/image-${screen.name}`
command: ["bash", "-c", `mkdir -p '${StringUtils.shellSingleQuoteEscape(screenshotDir)}' && grim -o '${StringUtils.shellSingleQuoteEscape(screen.name)}' '${StringUtils.shellSingleQuoteEscape(screenshotPath)}'`]
}

View file

@ -0,0 +1,120 @@
import QtQuick
import QtQuick.Layouts
import qs.services
import qs.modules.common
import qs.modules.common.widgets
import qs.modules.common.functions
Rectangle {
id: root
required property var directory
property bool showBreadcrumb: true
onShowBreadcrumbChanged: {
addressInput.text = root.directory;
}
signal navigateToDirectory(string path)
property real padding: 6
implicitWidth: mainLayout.implicitWidth + padding * 2
implicitHeight: mainLayout.implicitHeight + padding * 2
color: Appearance.colors.colLayer2
function focusBreadcrumb() {
root.showBreadcrumb = false;
addressInput.forceActiveFocus();
}
RowLayout {
id: mainLayout
anchors {
fill: parent
margins: root.padding
}
spacing: 8
RippleButton {
id: parentDirButton
downAction: () => root.navigateToDirectory(FileUtils.parentDirectory(root.directory))
contentItem: MaterialSymbol {
text: "drive_folder_upload"
iconSize: Appearance.font.pixelSize.larger
}
}
Item {
Layout.fillWidth: true
Layout.fillHeight: true
Rectangle {
id: directoryEntry
visible: !root.showBreadcrumb
anchors.fill: parent
color: Appearance.colors.colLayer1
radius: Appearance.rounding.full
implicitWidth: addressInput.implicitWidth
implicitHeight: addressInput.implicitHeight
Keys.onPressed: event => {
if (directoryEntry.visible && event.key === Qt.Key_Escape) {
root.showBreadcrumb = true;
event.accepted = true;
return;
}
event.accepted = false;
}
StyledTextInput {
id: addressInput
anchors.fill: parent
padding: 10
text: root.directory
Keys.onPressed: event => {
if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
root.navigateToDirectory(text);
root.showBreadcrumb = true;
event.accepted = true;
}
}
MouseArea {
// I-beam cursor
anchors.fill: parent
acceptedButtons: Qt.NoButton
hoverEnabled: true
cursorShape: Qt.IBeamCursor
}
}
}
Loader {
id: breadcrumbLoader
active: root.showBreadcrumb
visible: root.showBreadcrumb
anchors.fill: parent
sourceComponent: AddressBreadcrumb {
directory: root.directory
onNavigateToDirectory: dir => {
root.navigateToDirectory(dir);
}
}
}
}
RippleButton {
id: dirEditButton
toggled: !root.showBreadcrumb
downAction: () => root.showBreadcrumb = !root.showBreadcrumb
contentItem: MaterialSymbol {
text: "edit"
iconSize: Appearance.font.pixelSize.larger
color: dirEditButton.toggled ? Appearance.colors.colOnPrimary : Appearance.colors.colOnLayer2
}
StyledToolTip {
text: Translation.tr("Edit directory")
}
}
}
}

View file

@ -0,0 +1,41 @@
import QtQuick
import QtQuick.Layouts
import qs.services
import qs.modules.common
import qs.modules.common.widgets
import qs.modules.common.functions
ListView {
id: root
required property var directory
property var breadcrumbDirectory: ""
Component.onCompleted: breadcrumbDirectory = directory;
onDirectoryChanged: {
if (breadcrumbDirectory.startsWith(directory)) return;
breadcrumbDirectory = directory
}
signal navigateToDirectory(string path)
orientation: ListView.Horizontal
clip: true
spacing: 2
model: breadcrumbDirectory.split("/")
delegate: SelectionGroupButton {
id: folderButton
required property var modelData
required property int index
buttonText: index === 0 ? "/" : modelData
toggled: {
if (directory.trim() === "/") return index === 0;
return index === directory.split("/").length - 1
}
leftmost: index === 0
rightmost: index === breadcrumbDirectory.split("/").length - 1
onClicked: {
root.navigateToDirectory(breadcrumbDirectory.split("/").slice(0, index + 1).join("/"))
}
}
}

View file

@ -0,0 +1,49 @@
import qs.modules.common
import qs.modules.common.widgets
import QtQuick
import QtQuick.Layouts
/**
* A container that supports GroupButton children for bounciness.
* See https://m3.material.io/components/button-groups/overview
*/
Rectangle {
id: root
default property alias groupData: rowLayout.data
property alias uniformCellSizes: rowLayout.uniformCellSizes
property real spacing: 5
property real padding: 0
property alias clickIndex: rowLayout.clickIndex
property alias childrenCount: rowLayout.childrenCount
property real contentWidth: {
let total = 0;
for (let i = 0; i < rowLayout.children.length; ++i) {
const child = rowLayout.children[i];
if (!child.visible) continue;
total += child.baseWidth ?? child.implicitWidth ?? child.width;
}
return total + rowLayout.spacing * (rowLayout.children.length - 1);
}
topLeftRadius: rowLayout.children.length > 0 ? (rowLayout.children[0].radius + padding) :
Appearance?.rounding?.small
bottomLeftRadius: topLeftRadius
topRightRadius: rowLayout.children.length > 0 ? (rowLayout.children[rowLayout.children.length - 1].radius + padding) :
Appearance?.rounding?.small
bottomRightRadius: topRightRadius
color: "transparent"
width: root.contentWidth + padding * 2
implicitHeight: rowLayout.implicitHeight + padding * 2
implicitWidth: root.contentWidth + padding * 2
children: [RowLayout {
id: rowLayout
anchors.fill: parent
anchors.margins: root.padding
spacing: root.spacing
property int clickIndex: -1
property int childrenCount: children.length
}]
}

View file

@ -0,0 +1,123 @@
pragma ComponentBehavior: Bound
import QtQml
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import qs
import qs.services
import qs.modules.common
import qs.modules.common.widgets
import qs.modules.common.functions
import qs.modules.waffle.looks
Item {
id: root
// Expose delegate
property Component delegate: Text {
required property var model
text: model.day
}
// Configuration
property int paddingWeeks: 2 // 1 should be sufficient with proper clipping and no padding
property var locale: Qt.locale() // Should be of type Locale but QML is being funny
// Scrolling
function scrollMonthsAndSnap(x) { // Scroll x months and snap to month
const focusedDate = root.focusedDate;
const focusedMonth = focusedDate.getMonth();
const focusedYear = focusedDate.getFullYear();
const targetMonth = focusedMonth + x;
const targetDate = new Date(focusedYear, targetMonth, 1);
const currentFirstShownDate = new Date(root.dateInFirstWeek.getTime() + (root.paddingWeeks * root.millisPerWeek));
const diffMillis = targetDate.getTime() - currentFirstShownDate.getTime();
const diffWeeks = Math.round(diffMillis / root.millisPerWeek);
root.targetWeekDiff += diffWeeks;
}
property int weeksPerScroll: 1
property real targetWeekDiff: 0
property real weekDiff: targetWeekDiff
property int contentWeekDiff: weekDiff // whole part of weekDiff
property bool scrolling: false
Behavior on weekDiff {
id: weekScrollBehavior
animation: Looks.transition.scroll.createObject(this)
}
Timer {
id: scrollAnimationCheckTimer
interval: 30 // Should be plenty for 60fps
onTriggered: root.scrolling = false;
}
onWeekDiffChanged: {
scrolling = true;
scrollAnimationCheckTimer.restart();
}
MouseArea {
anchors.fill: parent
onWheel: wheel => {
root.targetWeekDiff += wheel.angleDelta.y / 120 * -root.weeksPerScroll; // Reverse cuz scrolling down should advance
}
}
// Date calculations
readonly property int millisPerWeek: 7 * 24 * 60 * 60 * 1000
readonly property int totalWeeks: 6 + (paddingWeeks * 2)
readonly property int focusedWeekIndex: 2 // The third row, 0-indexed
readonly property int focusDayOfWeekIndex: 6
property date dateInFirstWeek: {
const currentDate = new Date();
const currentMonth = currentDate.getMonth();
const currentYear = currentDate.getFullYear();
const firstDayThisMonth = new Date(currentYear, currentMonth, 1);
return new Date(firstDayThisMonth.getTime() - (paddingWeeks * millisPerWeek) + contentWeekDiff * millisPerWeek);
}
property date focusedDate: {
// The last day of 3rd week shown is considered the focused month
const addedTime = (root.paddingWeeks + root.focusedWeekIndex) * root.millisPerWeek
const dateInTargetWeek = new Date(root.dateInFirstWeek.getTime() + addedTime);
return DateUtils.getIthDayDateOfSameWeek(dateInTargetWeek, root.focusDayOfWeekIndex - root.locale.firstDayOfWeek, root.locale.firstdayOfWeek); // 4 = Thursday
}
property int focusedMonth: focusedDate.getMonth() + 1 // 0-indexed -> 1-indexed
// Sizes
property real verticalPadding: 0
property real buttonSize: 40
property real buttonSpacing: 2
property real buttonVerticalSpacing: buttonSpacing
implicitHeight: (6 * buttonSize) + (5 * buttonVerticalSpacing) + (2 * verticalPadding)
implicitWidth: weeksColumn.implicitWidth
clip: true
ColumnLayout {
id: weeksColumn
anchors {
left: parent.left
right: parent.right
}
y: {
const spacePerExtraRow = root.buttonSize + root.buttonVerticalSpacing;
const origin = -(spacePerExtraRow * root.paddingWeeks);
const diff = root.weekDiff * spacePerExtraRow;
return origin + (-diff % spacePerExtraRow) + root.verticalPadding;
}
spacing: root.buttonVerticalSpacing
Repeater {
model: root.totalWeeks
WeekRow {
required property int index
locale: root.locale
date: new Date(root.dateInFirstWeek.getTime() + (index * root.millisPerWeek))
Layout.fillWidth: true
spacing: root.buttonSpacing
delegate: root.delegate
}
}
}
}

View file

@ -0,0 +1,9 @@
import QtQuick
Rectangle {
property double diameter
implicitWidth: diameter
implicitHeight: diameter
radius: diameter / 2
}

View file

@ -0,0 +1,88 @@
import QtQuick
import QtQuick.Shapes
import qs.modules.common
/**
* Material 3 circular progress. See https://m3.material.io/components/progress-indicators/specs
*/
Item {
id: root
property int implicitSize: 30
property int lineWidth: 2
property real value: 0
property color colPrimary: Appearance.m3colors.m3onSecondaryContainer
property color colSecondary: Appearance.colors.colSecondaryContainer
property real gapAngle: 360 / 18
property bool fill: false
property int fillOverflow: 2
property bool enableAnimation: true
property int animationDuration: 800
property var easingType: Easing.OutCubic
implicitWidth: implicitSize
implicitHeight: implicitSize
property real degree: value * 360
property real centerX: root.width / 2
property real centerY: root.height / 2
property real arcRadius: root.implicitSize / 2 - root.lineWidth
property real startAngle: -90
Behavior on degree {
enabled: root.enableAnimation
NumberAnimation {
duration: root.animationDuration
easing.type: root.easingType
}
}
Loader {
active: root.fill
anchors.fill: parent
sourceComponent: Rectangle {
radius: 9999
color: root.colSecondary
}
}
Shape {
anchors.fill: parent
layer.enabled: true
layer.smooth: true
preferredRendererType: Shape.CurveRenderer
ShapePath {
id: secondaryPath
strokeColor: root.colSecondary
strokeWidth: root.lineWidth
capStyle: ShapePath.RoundCap
fillColor: "transparent"
PathAngleArc {
centerX: root.centerX
centerY: root.centerY
radiusX: root.arcRadius
radiusY: root.arcRadius
startAngle: root.startAngle - root.gapAngle
sweepAngle: -(360 - root.degree - 2 * root.gapAngle)
}
}
ShapePath {
id: primaryPath
strokeColor: root.colPrimary
strokeWidth: root.lineWidth
capStyle: ShapePath.RoundCap
fillColor: "transparent"
PathAngleArc {
centerX: root.centerX
centerY: root.centerY
radiusX: root.arcRadius
radiusY: root.arcRadius
startAngle: root.startAngle
sweepAngle: root.degree
}
}
}
}

View file

@ -0,0 +1,129 @@
import qs.modules.common
import qs.modules.common.widgets
import qs.services
import qs.modules.common.functions
import Qt5Compat.GraphicalEffects
import QtQuick
import Quickshell
import Quickshell.Io
Rectangle {
id: root
property string entry
property real maxWidth
property real maxHeight
property bool blur: false
property string blurText: "Image hidden"
property string imageDecodePath: Directories.cliphistDecode
property string imageDecodeFileName: `${entryNumber}`
property string imageDecodeFilePath: `${imageDecodePath}/${imageDecodeFileName}`
property string source
property int entryNumber: {
if (!root.entry)
return 0;
const match = root.entry.match(/^(\d+)\t/);
return match ? parseInt(match[1]) : 0;
}
property int imageWidth: {
if (!root.entry)
return 0;
const match = root.entry.match(/(\d+)x(\d+)/);
return match ? parseInt(match[1]) : 0;
}
property int imageHeight: {
if (!root.entry)
return 0;
const match = root.entry.match(/(\d+)x(\d+)/);
return match ? parseInt(match[2]) : 0;
}
property real scale: {
return Math.min(root.maxWidth / imageWidth, root.maxHeight / imageHeight, 1);
}
color: Appearance.colors.colLayer1
radius: Appearance.rounding.small
implicitHeight: imageHeight * scale
implicitWidth: imageWidth * scale
Component.onCompleted: {
decodeImageProcess.running = true;
}
Process {
id: decodeImageProcess
command: ["bash", "-c", `[ -f ${imageDecodeFilePath} ] || echo '${StringUtils.shellSingleQuoteEscape(root.entry)}' | ${Cliphist.cliphistBinary} decode > '${imageDecodeFilePath}'`]
onExited: (exitCode, exitStatus) => {
if (exitCode === 0) {
root.source = imageDecodeFilePath;
} else {
console.error("[CliphistImage] Failed to decode image for entry:", root.entry);
root.source = "";
}
}
}
Component.onDestruction: {
Quickshell.execDetached(["bash", "-c", `[ -f '${imageDecodeFilePath}' ] && rm -f '${imageDecodeFilePath}'`]);
}
layer.enabled: true
layer.effect: OpacityMask {
maskSource: Rectangle {
width: image.width
height: image.height
radius: root.radius
}
}
StyledImage {
id: image
anchors.fill: parent
source: Qt.resolvedUrl(root.source)
fillMode: Image.PreserveAspectFit
antialiasing: true
asynchronous: true
width: root.imageWidth * root.scale
height: root.imageHeight * root.scale
}
Loader {
id: blurLoader
active: root.blur
anchors.fill: image
sourceComponent: GaussianBlur {
source: image
radius: 35
samples: radius * 2 + 1
Rectangle {
anchors.fill: parent
color: ColorUtils.transparentize(Appearance.colors.colLayer0, 0.5)
Column {
anchors {
left: parent.left
right: parent.right
verticalCenter: parent.verticalCenter
}
MaterialSymbol {
visible: width <= image.width
anchors.horizontalCenter: parent.horizontalCenter
text: "visibility_off"
font.pixelSize: 28
}
StyledText {
visible: width <= image.width
anchors.horizontalCenter: parent.horizontalCenter
text: root.blurText
color: Appearance.colors.colOnSurface
font.pixelSize: Appearance.font.pixelSize.smallie
}
}
}
}
}
}

View file

@ -0,0 +1,98 @@
import qs.modules.common
import qs.modules.common.functions
import QtQuick
import QtQuick.Shapes
import Qt5Compat.GraphicalEffects
Item {
id: root
property int implicitSize: 18
property int lineWidth: 2
property real value: 0
property color colPrimary: Appearance?.colors.colOnSecondaryContainer ?? "#685496"
property color colSecondary: ColorUtils.transparentize(colPrimary, 0.5) ?? "#F1D3F9"
property real gapAngle: 360 / 18
property bool fill: true
property int fillOverflow: 2
property bool enableAnimation: true
property int animationDuration: 800
property var easingType: Easing.OutCubic
property bool accountForLightBleeding: true
default property Item textMask: Item {
width: implicitSize
height: implicitSize
StyledText {
anchors.centerIn: parent
text: Math.round(root.value * 100)
font.pixelSize: 12
font.weight: Font.Medium
}
}
implicitWidth: implicitSize
implicitHeight: implicitSize
property real degree: value * 360
property real centerX: root.width / 2
property real centerY: root.height / 2
property real arcRadius: root.implicitSize / 2 - root.lineWidth / 2 - (0.5 * root.accountForLightBleeding)
property real startAngle: -90
Behavior on degree {
enabled: root.enableAnimation
NumberAnimation {
duration: root.animationDuration
easing.type: root.easingType
}
}
Rectangle {
id: contentItem
anchors.fill: parent
radius: implicitSize / 2
color: root.colSecondary
visible: false
layer.enabled: true
layer.smooth: true
Shape {
anchors.fill: parent
preferredRendererType: Shape.CurveRenderer
ShapePath {
id: primaryPath
pathHints: ShapePath.PathSolid & ShapePath.PathNonIntersecting
strokeColor: root.colPrimary
strokeWidth: root.lineWidth
capStyle: ShapePath.RoundCap
fillColor: root.colPrimary
startX: root.centerX
startY: root.centerY
PathAngleArc {
moveToStart: false
centerX: root.centerX
centerY: root.centerY
radiusX: root.arcRadius
radiusY: root.arcRadius
startAngle: root.startAngle
sweepAngle: root.degree
}
PathLine {
x: primaryPath.startX
y: primaryPath.startY
}
}
}
}
OpacityMask {
anchors.fill: parent
source: contentItem
invert: true
maskSource: root.textMask
}
}

View file

@ -0,0 +1,101 @@
import qs.modules.common
import qs.modules.common.functions
import qs.modules.common.widgets
import QtQuick
import QtQuick.Controls
import Qt5Compat.GraphicalEffects
/**
* A progress bar with both ends rounded and text acts as clipping like OneUI 7's battery indicator.
*/
ProgressBar {
id: root
property bool vertical: false
property real valueBarWidth: 30
property real valueBarHeight: 18
property color highlightColor: Appearance?.colors.colOnSecondaryContainer ?? "#685496"
property color trackColor: ColorUtils.transparentize(highlightColor, 0.5) ?? "#F1D3F9"
property alias radius: contentItem.radius
property string text
default property Item textMask: Item {
width: valueBarWidth
height: valueBarHeight
StyledText {
anchors.centerIn: parent
font: root.font
text: root.text
}
}
text: Math.round(value * 100)
font {
pixelSize: 13
weight: text.length > 2 ? Font.Medium : Font.DemiBold
}
background: Item {
implicitHeight: valueBarHeight
implicitWidth: valueBarWidth
}
contentItem: Rectangle {
id: contentItem
anchors.fill: parent
radius: 9999
color: root.trackColor
visible: false
Rectangle {
id: progressFill
anchors {
top: parent.top
bottom: parent.bottom
left: parent.left
right: undefined
}
width: parent.width * root.visualPosition
height: parent.height
states: State {
name: "vertical"
when: root.vertical
AnchorChanges {
target: progressFill
anchors {
top: undefined
bottom: parent.bottom
left: parent.left
right: parent.right
}
}
PropertyChanges {
target: progressFill
width: parent.width
height: parent.height * root.visualPosition
}
}
radius: Appearance.rounding.unsharpen
color: root.highlightColor
}
}
OpacityMask {
id: roundingMask
visible: false
anchors.fill: parent
source: contentItem
maskSource: Rectangle {
width: contentItem.width
height: contentItem.height
radius: contentItem.radius
}
}
OpacityMask {
anchors.fill: parent
source: roundingMask
invert: true
maskSource: root.textMask
}
}

View file

@ -0,0 +1,8 @@
import QtQuick
import QtQuick.Layouts
RowLayout {
property bool uniform: false
spacing: 4
uniformCellSizes: uniform
}

View file

@ -0,0 +1,54 @@
import QtQuick
import QtQuick.Layouts
import qs.services
import qs.modules.common
import qs.modules.common.widgets
import qs.modules.common.functions
Flow {
id: root
Layout.fillWidth: true
spacing: 2
property list<var> options: [
{
"displayName": "Option 1",
"icon": "check",
"value": 1
},
{
"displayName": "Option 2",
"icon": "close",
"value": 2
},
]
property var currentValue: null
signal selected(var newValue)
Repeater {
model: root.options
delegate: SelectionGroupButton {
id: paletteButton
required property var modelData
required property int index
onYChanged: {
if (index === 0) {
paletteButton.leftmost = true
} else {
var prev = root.children[index - 1]
var thisIsOnNewLine = prev && prev.y !== paletteButton.y
paletteButton.leftmost = thisIsOnNewLine
prev.rightmost = thisIsOnNewLine
}
}
leftmost: index === 0
rightmost: index === root.options.length - 1
buttonIcon: modelData.icon || ""
buttonText: modelData.displayName
toggled: root.currentValue == modelData.value
onClicked: {
root.selected(modelData.value);
}
}
}
}

View file

@ -0,0 +1,47 @@
import qs.modules.common.widgets
import qs.modules.common
import QtQuick
import QtQuick.Layouts
import qs.services
RowLayout {
id: root
spacing: 10
Layout.leftMargin: 8
Layout.rightMargin: 8
property string text: ""
property string buttonIcon: ""
property alias value: slider.value
property alias stopIndicatorValues: slider.stopIndicatorValues
property bool usePercentTooltip: true
property real from: slider.from
property real to: slider.to
property real textWidth: 120
RowLayout {
id: row
spacing: 10
OptionalMaterialSymbol {
id: iconWidget
icon: root.buttonIcon
iconSize: Appearance.font.pixelSize.larger
}
StyledText {
id: labelWidget
Layout.preferredWidth: root.textWidth
text: root.text
color: Appearance.colors.colOnSecondaryContainer
}
}
StyledSlider {
id: slider
configuration: StyledSlider.Configuration.XS
usePercentTooltip: root.usePercentTooltip
value: root.value
from: root.from
to: root.to
}
}

View file

@ -0,0 +1,38 @@
import qs.modules.common.widgets
import qs.modules.common
import QtQuick
import QtQuick.Layouts
RowLayout {
id: root
property string text: ""
property string icon
property alias value: spinBoxWidget.value
property alias stepSize: spinBoxWidget.stepSize
property alias from: spinBoxWidget.from
property alias to: spinBoxWidget.to
spacing: 10
Layout.leftMargin: 8
Layout.rightMargin: 8
RowLayout {
spacing: 10
OptionalMaterialSymbol {
icon: root.icon
opacity: root.enabled ? 1 : 0.4
}
StyledText {
id: labelWidget
Layout.fillWidth: true
text: root.text
color: Appearance.colors.colOnSecondaryContainer
opacity: root.enabled ? 1 : 0.4
}
}
StyledSpinBox {
id: spinBoxWidget
Layout.fillWidth: false
value: root.value
}
}

View file

@ -0,0 +1,43 @@
import qs.modules.common.widgets
import qs.modules.common
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
RippleButton {
id: root
property string buttonIcon
property alias iconSize: iconWidget.iconSize
Layout.fillWidth: true
implicitHeight: contentItem.implicitHeight + 8 * 2
font.pixelSize: Appearance.font.pixelSize.small
onClicked: checked = !checked
contentItem: RowLayout {
spacing: 10
OptionalMaterialSymbol {
id: iconWidget
icon: root.buttonIcon
opacity: root.enabled ? 1 : 0.4
iconSize: Appearance.font.pixelSize.larger
}
StyledText {
id: labelWidget
Layout.fillWidth: true
text: root.text
font: root.font
color: Appearance.colors.colOnSecondaryContainer
opacity: root.enabled ? 1 : 0.4
}
StyledSwitch {
id: switchWidget
down: root.down
Layout.fillWidth: false
checked: root.checked
onClicked: root.clicked()
}
}
}

View file

@ -0,0 +1,29 @@
import QtQuick
import QtQuick.Layouts
import qs.modules.common
import qs.modules.common.widgets
StyledFlickable {
id: root
property real baseWidth: 600
property bool forceWidth: false
property real bottomContentPadding: 100
default property alias contentData: contentColumn.data
clip: true
contentHeight: contentColumn.implicitHeight + root.bottomContentPadding // Add some padding at the bottom
implicitWidth: contentColumn.implicitWidth
ColumnLayout {
id: contentColumn
width: root.forceWidth ? root.baseWidth : Math.max(root.baseWidth, implicitWidth)
anchors {
top: parent.top
horizontalCenter: parent.horizontalCenter
margins: 20
}
spacing: 30
}
}

View file

@ -0,0 +1,36 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.modules.common
import qs.modules.common.widgets
ColumnLayout {
id: root
property string title
property string icon: ""
default property alias contentData: sectionContent.data
Layout.fillWidth: true
spacing: 6
RowLayout {
spacing: 6
OptionalMaterialSymbol {
icon: root.icon
iconSize: Appearance.font.pixelSize.hugeass
}
StyledText {
text: root.title
font.pixelSize: Appearance.font.pixelSize.larger
font.weight: Font.Medium
color: Appearance.colors.colOnSecondaryContainer
}
}
ColumnLayout {
id: sectionContent
Layout.fillWidth: true
spacing: 4
}
}

View file

@ -0,0 +1,46 @@
import QtQuick
import QtQuick.Layouts
import qs.modules.common
import qs.modules.common.widgets
ColumnLayout {
id: root
property string title: ""
property string tooltip: ""
default property alias contentData: sectionContent.data
Layout.fillWidth: true
Layout.topMargin: 4
spacing: 2
RowLayout {
ContentSubsectionLabel {
visible: root.title && root.title.length > 0
text: root.title
}
MaterialSymbol {
visible: root.tooltip && root.tooltip.length > 0
text: "info"
iconSize: Appearance.font.pixelSize.large
color: Appearance.colors.colSubtext
MouseArea {
id: infoMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.WhatsThisCursor
StyledToolTip {
extraVisibleCondition: false
alternativeVisibleCondition: infoMouseArea.containsMouse
text: root.tooltip
}
}
}
Item { Layout.fillWidth: true }
}
ColumnLayout {
id: sectionContent
Layout.fillWidth: true
spacing: 2
}
}

View file

@ -0,0 +1,10 @@
import QtQuick
import QtQuick.Layouts
import qs.modules.common
import qs.modules.common.widgets
StyledText {
text: "Subsection"
color: Appearance.colors.colSubtext
Layout.leftMargin: 2
}

View file

@ -0,0 +1,37 @@
import QtQuick
import Quickshell
import Quickshell.Widgets
import Qt5Compat.GraphicalEffects
Item {
id: root
property bool colorize: false
property color color
property string source: ""
property string iconFolder: Qt.resolvedUrl(Quickshell.shellPath("assets/icons")) // The folder to check first
width: 30
height: 30
IconImage {
id: iconImage
anchors.fill: parent
source: {
const fullPathWhenSourceIsIconName = iconFolder + "/" + root.source;
if (iconFolder && fullPathWhenSourceIsIconName) {
return fullPathWhenSourceIsIconName
}
return root.source
}
implicitSize: root.height
}
Loader {
active: root.colorize
anchors.fill: iconImage
sourceComponent: ColorOverlay {
source: iconImage
color: root.color
}
}
}

View file

@ -0,0 +1,28 @@
import QtQuick
import qs.modules.common
import qs.modules.common.functions
Canvas {
id: root
property color color: "#ffffff"
property int dashLength: 6
property int gapLength: 4
property int borderWidth: 1
onDashLengthChanged: requestPaint()
onGapLengthChanged: requestPaint()
onWidthChanged: requestPaint()
onHeightChanged: requestPaint()
onPaint: {
var ctx = getContext("2d");
ctx.clearRect(0, 0, width, height);
ctx.save();
ctx.strokeStyle = root.color;
ctx.lineWidth = root.borderWidth;
if (root.gapLength > 0) {
ctx.setLineDash([root.dashLength, root.gapLength]); // Set dash pattern
}
ctx.strokeRect(root.borderWidth / 2, root.borderWidth / 2, width - root.borderWidth, height - root.borderWidth); // Draw it
ctx.restore();
}
}

View file

@ -0,0 +1,40 @@
import qs.modules.common
import qs.modules.common.functions
import qs.modules.common.widgets
import QtQuick
/**
* Material 3 dialog button. See https://m3.material.io/components/dialogs/overview
*/
RippleButton {
id: root
property string buttonText
padding: 14
implicitHeight: 36
implicitWidth: buttonTextWidget.implicitWidth + padding * 2
buttonRadius: Appearance?.rounding.full ?? 9999
property color colEnabled: Appearance?.colors.colPrimary ?? "#65558F"
property color colDisabled: Appearance?.m3colors.m3outline ?? "#8D8C96"
colBackground: ColorUtils.transparentize(Appearance.colors.colLayer3)
colBackgroundHover: Appearance.colors.colLayer3Hover
colRipple: Appearance.colors.colLayer3Active
property alias colText: buttonTextWidget.color
contentItem: StyledText {
id: buttonTextWidget
anchors.fill: parent
anchors.leftMargin: root.padding
anchors.rightMargin: root.padding
text: buttonText
horizontalAlignment: Text.AlignHCenter
font.pixelSize: Appearance?.font.pixelSize.small ?? 12
color: root.enabled ? root.colEnabled : root.colDisabled
Behavior on color {
animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this)
}
}
}

View file

@ -0,0 +1,25 @@
import qs.modules.common
import qs.modules.common.functions
import qs.modules.common.widgets
import QtQuick
RippleButton {
id: root
property bool active: false
horizontalPadding: Appearance.rounding.large
verticalPadding: 12
clip: true
pointingHandCursor: !active
implicitWidth: contentItem.implicitWidth + horizontalPadding * 2
implicitHeight: contentItem.implicitHeight + verticalPadding * 2
Behavior on implicitHeight {
animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
}
colBackground: ColorUtils.transparentize(Appearance.colors.colLayer3)
colBackgroundHover: active ? colBackground : Appearance.colors.colLayer3Hover
colRipple: Appearance.colors.colLayer3Active
buttonRadius: 0
}

View file

@ -0,0 +1,41 @@
import QtQuick
import Quickshell
import Quickshell.Io
import qs.modules.common
import qs.modules.common.functions
// From https://github.com/caelestia-dots/shell with modifications.
// License: GPLv3
StyledImage {
id: root
required property var fileModelData
asynchronous: true
fillMode: Image.PreserveAspectFit
source: {
if (!fileModelData.fileIsDir)
return Quickshell.iconPath("application-x-zerosize");
if ([Directories.documents, Directories.downloads, Directories.music, Directories.pictures, Directories.videos].some(dir => FileUtils.trimFileProtocol(dir) === fileModelData.filePath))
return Quickshell.iconPath(`folder-${fileModelData.fileName.toLowerCase()}`);
return Quickshell.iconPath("inode-directory");
}
onStatusChanged: {
if (status === Image.Error)
source = Quickshell.iconPath("error");
}
Process {
running: !fileModelData.fileIsDir
command: ["file", "--mime", "-b", fileModelData.filePath]
stdout: StdioCollector {
onStreamFinished: {
const mime = text.split(";")[0].replace("/", "-");
root.source = Images.validImageTypes.some(t => mime === `image-${t}`) ? fileModelData.fileUrl : Quickshell.iconPath(mime, "image-missing");
}
}
}
}

View file

@ -0,0 +1,76 @@
import qs.modules.common
import qs.services
import QtQuick
/**
* A convenience MouseArea for handling drag events.
*/
MouseArea {
id: root
hoverEnabled: true
acceptedButtons: Qt.LeftButton
property bool interactive: true
property bool automaticallyReset: true
readonly property real dragDiffX: _dragDiffX
readonly property real dragDiffY: _dragDiffY
property real startX: 0
property real startY: 0
property real regionTopLeftX: Math.min(startX, startX + _dragDiffX)
property real regionTopLeftY: Math.min(startY, startY + _dragDiffY)
property real regionWidth: Math.abs(_dragDiffX)
property real regionHeight: Math.abs(_dragDiffY)
signal dragPressed(diffX: real, diffY: real)
signal dragReleased(diffX: real, diffY: real)
property bool dragging: false
property real _dragDiffX: 0
property real _dragDiffY: 0
function resetDrag() {
_dragDiffX = 0
_dragDiffY = 0
}
onPressed: (mouse) => {
if (!root.interactive) {
if (mouse.button === Qt.LeftButton) {
mouse.accepted = false;
}
return;
}
if (mouse.button === Qt.LeftButton) {
startX = mouse.x
startY = mouse.y
}
}
onReleased: (mouse) => {
if (!root.interactive) {
return;
}
dragging = false
root.dragReleased(_dragDiffX, _dragDiffY);
if (root.automaticallyReset) {
root.resetDrag();
}
}
onPositionChanged: (mouse) => {
if (!root.interactive) {
return;
}
if (mouse.buttons & Qt.LeftButton) {
root._dragDiffX = mouse.x - startX
root._dragDiffY = mouse.y - startY
const dist = Math.sqrt(root._dragDiffX * root._dragDiffX + root._dragDiffY * root._dragDiffY);
root.dragPressed(_dragDiffX, _dragDiffY);
root.dragging = true;
}
}
onCanceled: (mouse) => {
if (!root.interactive) {
return;
}
released(mouse);
}
}

View file

@ -0,0 +1,16 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
SequentialAnimation {
id: root
required property Item target
property real distance: 30
NumberAnimation { target: root.target; property: "Layout.leftMargin"; to: -root.distance; duration: 50 }
NumberAnimation { target: root.target; property: "Layout.leftMargin"; to: root.distance; duration: 50 }
NumberAnimation { target: root.target; property: "Layout.leftMargin"; to: -root.distance / 2; duration: 40 }
NumberAnimation { target: root.target; property: "Layout.leftMargin"; to: root.distance / 2; duration: 40 }
NumberAnimation { target: root.target; property: "Layout.leftMargin"; to: 0; duration: 30 }
}

View file

@ -0,0 +1,18 @@
import QtQuick
import qs.modules.common
Loader {
id: root
property bool shown: true
property alias fade: opacityBehavior.enabled
property alias animation: opacityBehavior.animation
opacity: shown ? 1 : 0
visible: opacity > 0
active: opacity > 0
Behavior on opacity {
id: opacityBehavior
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
}
}

View file

@ -0,0 +1,48 @@
import qs.modules.common
import qs.modules.common.widgets
import qs.services
import qs.modules.common.functions
import Qt5Compat.GraphicalEffects
import QtQuick
import Quickshell.Io
import Quickshell.Widgets
IconImage {
id: root
property string url
property string displayText
property real size: 32
property string downloadUserAgent: Config.options?.networking.userAgent ?? ""
property string faviconDownloadPath: Directories.favicons
property string domainName: url.includes("vertexaisearch") ? displayText : StringUtils.getDomain(url)
property string faviconUrl: `https://www.google.com/s2/favicons?domain=${domainName}&sz=32`
property string fileName: `${domainName}.ico`
property string faviconFilePath: `${faviconDownloadPath}/${fileName}`
property string urlToLoad
Process {
id: faviconDownloadProcess
running: false
command: ["bash", "-c", `[ -f ${faviconFilePath} ] || curl -s '${root.faviconUrl}' -o '${faviconFilePath}' -L -H 'User-Agent: ${downloadUserAgent}'`]
onExited: (exitCode, exitStatus) => {
root.urlToLoad = root.faviconFilePath
}
}
Component.onCompleted: {
faviconDownloadProcess.running = true
}
source: Qt.resolvedUrl(root.urlToLoad)
implicitSize: root.size
layer.enabled: true
layer.effect: OpacityMask {
maskSource: Rectangle {
width: root.implicitSize
height: root.implicitSize
radius: Appearance.rounding.full
}
}
}

View file

@ -0,0 +1,64 @@
import QtQuick
import QtQuick.Layouts
import qs.modules.common
import qs.modules.common.widgets
/**
* Material 3 FAB.
*/
RippleButton {
id: root
property string iconText: "add"
property bool expanded: false
property real baseSize: 56
property real elementSpacing: 5
implicitWidth: expanded ? (Math.max(contentRowLayout.implicitWidth + 10 * 2, baseSize)) : baseSize
implicitHeight: baseSize
buttonRadius: baseSize / 14 * 4
colBackground: Appearance.colors.colPrimaryContainer
colBackgroundHover: Appearance.colors.colPrimaryContainerHover
colRipple: Appearance.colors.colPrimaryContainerActive
property color colOnBackground: Appearance.colors.colOnPrimaryContainer
contentItem: Row {
id: contentRowLayout
property real horizontalMargins: (root.baseSize - icon.width) / 2
anchors {
verticalCenter: parent?.verticalCenter
left: parent?.left
leftMargin: contentRowLayout.horizontalMargins
}
spacing: 0
MaterialSymbol {
id: icon
anchors.verticalCenter: parent.verticalCenter
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
iconSize: 26
color: root.colOnBackground
text: root.iconText
}
Loader {
anchors.verticalCenter: parent.verticalCenter
visible: root.buttonText?.length > 0
active: true
sourceComponent: Revealer {
visible: root.expanded || implicitWidth > 0
reveal: root.expanded
implicitWidth: reveal ? (buttonText.implicitWidth + root.elementSpacing + contentRowLayout.horizontalMargins) : 0
StyledText {
id: buttonText
anchors {
left: parent.left
leftMargin: root.elementSpacing
verticalCenter: parent.verticalCenter
}
text: root.buttonText
color: Appearance.colors.colOnPrimaryContainer
font.pixelSize: 14
font.weight: 450
}
}
}
}
}

View file

@ -0,0 +1,8 @@
import QtQuick
/**
* This is just to make sure `RippleButton`s can be used in a Flow layout.
*/
Flow {
property int clickIndex: -1
}

View file

@ -0,0 +1,56 @@
import QtQuick
MouseArea { // Right side | scroll to change volume
id: root
signal scrollUp(delta: int)
signal scrollDown(delta: int)
signal movedAway()
property bool hovered: false
property real lastScrollX: 0
property real lastScrollY: 0
property bool trackingScroll: false
property real moveThreshold: 20
acceptedButtons: Qt.LeftButton
hoverEnabled: true
onEntered: {
root.hovered = true;
}
onExited: {
root.hovered = false;
root.trackingScroll = false;
}
onWheel: event => {
if (event.angleDelta.y < 0)
root.scrollDown(event.angleDelta.y);
else if (event.angleDelta.y > 0)
root.scrollUp(event.angleDelta.y);
// Store the mouse position and start tracking
root.lastScrollX = event.x;
root.lastScrollY = event.y;
root.trackingScroll = true;
}
onPositionChanged: mouse => {
if (root.trackingScroll) {
const dx = mouse.x - root.lastScrollX;
const dy = mouse.y - root.lastScrollY;
if (Math.sqrt(dx * dx + dy * dy) > root.moveThreshold) {
root.movedAway();
root.trackingScroll = false;
}
}
}
onContainsMouseChanged: {
if (!root.containsMouse && root.trackingScroll) {
root.movedAway();
root.trackingScroll = false;
}
}
}

View file

@ -0,0 +1,44 @@
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 Quickshell
import Quickshell.Wayland
Scope {
id: root
required property Component contentComponent
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
Loader {
anchors.fill: parent
sourceComponent: root.contentComponent
}
}
}
}
}

View file

@ -0,0 +1,51 @@
import QtQuick
import qs.modules.common
import qs.modules.common.functions
/*
* Simple one value line graph
*/
Canvas {
id: root
enum Alignment { Left, Right }
required property list<real> values
property int points: values.length
property color color: Appearance.colors.colPrimary
property real fillOpacity: 0.5
property var alignment: Graph.Alignment.Left
onValuesChanged: root.requestPaint()
onPaint: {
var ctx = getContext("2d")
ctx.clearRect(0, 0, width, height)
if (!root.values || root.values.length < 2)
return
var n = root.points
var dx = width / (n - 1)
ctx.strokeStyle = root.color
ctx.fillStyle = ColorUtils.transparentize(root.color, 1 - root.fillOpacity)
ctx.lineWidth = 2
ctx.beginPath()
for (var i = 0; i < n; ++i) {
var valueIndex = (root.alignment === Graph.Alignment.Right) ? root.values.length - n + i : i
if (valueIndex < 0 || valueIndex >= root.values.length) {
continue; // No data for this point
}
var x = i * dx
var norm = root.values[valueIndex] // already in 0-1 range
var y = height - norm * height
if (valueIndex === 0) {
ctx.moveTo(x, height)
ctx.lineTo(x, y)
} else {
ctx.lineTo(x, y)
}
}
ctx.stroke()
ctx.lineTo(width, height)
ctx.fill()
}
}

View file

@ -0,0 +1,147 @@
import qs.modules.common
import qs.modules.common.widgets
import qs.modules.common.functions
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
/**
* Material 3 button with expressive bounciness.
* See https://m3.material.io/components/button-groups/overview
*/
Button {
id: root
property bool toggled
property string buttonText
property real buttonRadius: Appearance?.rounding?.small ?? 8
property real buttonRadiusPressed: Appearance?.rounding?.small ?? 6
property var downAction // When left clicking (down)
property var releaseAction // When left clicking (release)
property var altAction // When right clicking
property var middleClickAction // When middle clicking
property bool bounce: true
property real baseWidth: contentItem.implicitWidth + horizontalPadding * 2
property real baseHeight: contentItem.implicitHeight + verticalPadding * 2
property bool enableImplicitWidthAnimation: true
property bool enableImplicitHeightAnimation: true
property real clickedWidth: baseWidth + (isAtSide ? 10 : 20)
property real clickedHeight: baseHeight
property var parentGroup: root.parent
property int indexInParent: parentGroup?.children.indexOf(root) ?? -1
property int clickIndex: parentGroup?.clickIndex ?? -1
property bool isAtSide: indexInParent === 0 || indexInParent === (parentGroup?.childrenCount - 1)
Layout.fillWidth: (clickIndex - 1 <= indexInParent && indexInParent <= clickIndex + 1)
Layout.fillHeight: (clickIndex - 1 <= indexInParent && indexInParent <= clickIndex + 1)
implicitWidth: (root.down && bounce) ? clickedWidth : baseWidth
implicitHeight: (root.down && bounce) ? clickedHeight : baseHeight
property color colBackground: ColorUtils.transparentize(colBackgroundHover, 1) || "transparent"
property color colBackgroundHover: Appearance?.colors.colLayer1Hover ?? "#E5DFED"
property color colBackgroundActive: Appearance?.colors.colLayer1Active ?? "#D6CEE2"
property color colBackgroundToggled: Appearance?.colors.colPrimary ?? "#65558F"
property color colBackgroundToggledHover: Appearance?.colors.colPrimaryHover ?? "#77699C"
property color colBackgroundToggledActive: Appearance?.colors.colPrimaryActive ?? "#D6CEE2"
property real radius: root.down ? root.buttonRadiusPressed : root.buttonRadius
property real leftRadius: root.down ? root.buttonRadiusPressed : root.buttonRadius
property real rightRadius: root.down ? root.buttonRadiusPressed : root.buttonRadius
property color color: root.enabled ? (root.toggled ?
(root.down ? colBackgroundToggledActive :
root.hovered ? colBackgroundToggledHover :
colBackgroundToggled) :
(root.down ? colBackgroundActive :
root.hovered ? colBackgroundHover :
colBackground)) : colBackground
onDownChanged: {
if (root.down) {
if (root.parent.clickIndex !== undefined) {
root.parent.clickIndex = parent.children.indexOf(root)
}
}
}
Behavior on implicitWidth {
enabled: root.enableImplicitWidthAnimation
animation: Appearance.animation.clickBounce.numberAnimation.createObject(this)
}
Behavior on implicitHeight {
enabled: root.enableImplicitHeightAnimation
animation: Appearance.animation.clickBounce.numberAnimation.createObject(this)
}
Behavior on leftRadius {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
}
Behavior on rightRadius {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
}
property alias mouseArea: buttonMouseArea
MouseArea {
id: buttonMouseArea
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
onPressed: (event) => {
if(event.button === Qt.RightButton) {
if (root.altAction) root.altAction();
return;
}
if(event.button === Qt.MiddleButton) {
if (root.middleClickAction) root.middleClickAction();
return;
}
root.down = true
if (root.downAction) root.downAction();
}
onReleased: (event) => {
root.down = false
if (event.button != Qt.LeftButton) return;
if (root.releaseAction) root.releaseAction();
}
onClicked: (event) => {
if (event.button != Qt.LeftButton) return;
root.click()
}
onCanceled: (event) => {
root.down = false
}
onPressAndHold: () => {
// Guarded like releaseAction above. altAction is null for every
// toggle without a menu (AndroidQuickToggleButton sets it from
// hasMenu), so an unguarded call threw
// "Property 'altAction' ... is not a function" on any long-press
// of one. It only started showing up when the toggles that had
// been silently filtered out of the panel began rendering.
if (root.altAction) root.altAction();
root.down = false;
root.clicked = false;
};
}
property bool tabbedTo: root.focus && (focusReason === Qt.TabFocusReason || focusReason === Qt.BacktabFocusReason)
background: Rectangle {
id: buttonBackground
topLeftRadius: root.leftRadius
topRightRadius: root.rightRadius
bottomLeftRadius: root.leftRadius
bottomRightRadius: root.rightRadius
implicitHeight: 50
color: root.color
Behavior on color {
animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this)
}
border.width: root.tabbedTo ? 2 : 0
border.color: Appearance.colors.colSecondary
}
contentItem: StyledText {
text: root.buttonText
}
}

View file

@ -0,0 +1,33 @@
import QtQuick
import QtQuick.Layouts
import qs.modules.common
ToolbarButton {
id: iconBtn
required property string iconText
colBackgroundToggled: Appearance.colors.colSecondaryContainer
colBackgroundToggledHover: Appearance.colors.colSecondaryContainerHover
colRippleToggled: Appearance.colors.colSecondaryContainerActive
property color colText: toggled ? Appearance.colors.colOnSecondaryContainer : Appearance.colors.colOnSurfaceVariant
contentItem: Row {
anchors.centerIn: parent
spacing: 4
MaterialSymbol {
anchors.verticalCenter: parent.verticalCenter
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
iconSize: 22
text: iconBtn.iconText
color: iconBtn.colText
}
StyledText {
visible: iconBtn.iconText.length > 0 && iconBtn.text.length > 0
anchors.verticalCenter: parent.verticalCenter
color: iconBtn.colText
text: iconBtn.text
}
}
}

View file

@ -0,0 +1,23 @@
import QtQuick
import QtQuick.Layouts
import qs.modules.common
ToolbarButton {
id: iconBtn
implicitWidth: height
colBackgroundToggled: Appearance.colors.colSecondaryContainer
colBackgroundToggledHover: Appearance.colors.colSecondaryContainerHover
colRippleToggled: Appearance.colors.colSecondaryContainerActive
property color colText: toggled ? Appearance.colors.colOnSecondaryContainer : Appearance.colors.colOnSurfaceVariant
contentItem: MaterialSymbol {
anchors.centerIn: parent
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
iconSize: 22
text: iconBtn.text
color: iconBtn.colText
animateChange: true
}
}

View file

@ -0,0 +1,43 @@
import qs.modules.common
import QtQuick
Rectangle {
id: root
property string key
property real horizontalPadding: 6
property real verticalPadding: 1
property real borderWidth: 1
property real extraBottomBorderWidth: 2
property color borderColor: Appearance.colors.colOnLayer0
property real borderRadius: 5
property real pixelSize: Appearance.font.pixelSize.smaller
property color keyColor: Appearance.m3colors.m3surfaceContainerLow
implicitWidth: keyFace.implicitWidth + borderWidth * 2
implicitHeight: keyFace.implicitHeight + borderWidth * 2 + extraBottomBorderWidth
radius: borderRadius
color: borderColor
Rectangle {
id: keyFace
anchors {
fill: parent
topMargin: borderWidth
leftMargin: borderWidth
rightMargin: borderWidth
bottomMargin: extraBottomBorderWidth + borderWidth
}
implicitWidth: keyText.implicitWidth + horizontalPadding * 2
implicitHeight: keyText.implicitHeight + verticalPadding * 2
color: keyColor
radius: borderRadius - borderWidth
StyledText {
id: keyText
anchors.centerIn: parent
font.family: Appearance.font.family.monospace
font.pixelSize: root.pixelSize
text: key
}
}
}

View file

@ -0,0 +1,122 @@
import qs.services
import qs.modules.common
import qs.modules.common.widgets
import qs.modules.common.functions
import QtQuick
import QtQuick.Layouts
import Quickshell
RippleButton {
id: lightDarkButtonRoot
required property bool dark
property color previewBg: dark ? ColorUtils.colorWithHueOf("#3f3838", Appearance.m3colors.m3primary) :
ColorUtils.colorWithHueOf("#F7F9FF", Appearance.m3colors.m3primary)
property color previewFg: dark ? Qt.lighter(previewBg, 2.2) : ColorUtils.mix(previewBg, "#292929", 0.85)
padding: 5
Layout.fillWidth: true
colBackground: Appearance.colors.colLayer2
toggled: Appearance.m3colors.darkmode === dark
onClicked: {
Quickshell.execDetached(["bash", "-c", `${Directories.wallpaperSwitchScriptPath} --mode ${dark ? "dark" : "light"} --noswitch`])
}
contentItem: Item {
anchors.centerIn: parent
implicitWidth: buttonContentLayout.implicitWidth
implicitHeight: buttonContentLayout.implicitHeight
ColumnLayout {
id: buttonContentLayout
anchors.centerIn: parent
Rectangle {
Layout.alignment: Qt.AlignHCenter
implicitWidth: 250
implicitHeight: skeletonColumnLayout.implicitHeight + 10 * 2
radius: lightDarkButtonRoot.buttonRadius - lightDarkButtonRoot.padding
color: lightDarkButtonRoot.previewBg
border {
width: 1
color: Appearance.m3colors.m3outlineVariant
}
// Some skeleton items
ColumnLayout {
id: skeletonColumnLayout
anchors.fill: parent
anchors.margins: 10
spacing: 10
RowLayout {
Rectangle {
radius: Appearance.rounding.full
color: lightDarkButtonRoot.previewFg
implicitWidth: 50
implicitHeight: 50
}
ColumnLayout {
spacing: 4
Rectangle {
radius: Appearance.rounding.unsharpenmore
color: lightDarkButtonRoot.previewFg
Layout.fillWidth: true
implicitHeight: 22
}
Rectangle {
radius: Appearance.rounding.unsharpenmore
color: lightDarkButtonRoot.previewFg
Layout.fillWidth: true
Layout.rightMargin: 45
implicitHeight: 18
}
}
}
StyledProgressBar {
Layout.topMargin: 5
Layout.bottomMargin: 5
Layout.fillWidth: true
value: 0.7
wavy: true
animateWave: lightDarkButtonRoot.toggled
highlightColor: lightDarkButtonRoot.toggled ? Appearance.m3colors.m3primary : lightDarkButtonRoot.previewFg
trackColor: ColorUtils.mix(lightDarkButtonRoot.previewBg, lightDarkButtonRoot.previewFg, 0.5)
}
RowLayout {
spacing: 2
Rectangle {
radius: Appearance.rounding.full
color: lightDarkButtonRoot.toggled ? Appearance.m3colors.m3primary : lightDarkButtonRoot.previewFg
Layout.fillWidth: true
implicitHeight: 30
MaterialSymbol {
visible: lightDarkButtonRoot.toggled
anchors.centerIn: parent
horizontalAlignment: Text.AlignHCenter
text: "check"
iconSize: 20
color: lightDarkButtonRoot.toggled ? Appearance.m3colors.m3onPrimary : lightDarkButtonRoot.previewBg
}
}
Rectangle {
radius: Appearance.rounding.unsharpenmore
color: lightDarkButtonRoot.toggled ? Appearance.m3colors.m3secondaryContainer : lightDarkButtonRoot.previewFg
Layout.fillWidth: true
implicitHeight: 30
}
Rectangle {
topLeftRadius: Appearance.rounding.unsharpenmore
bottomLeftRadius: Appearance.rounding.unsharpenmore
topRightRadius: Appearance.rounding.full
bottomRightRadius: Appearance.rounding.full
color: lightDarkButtonRoot.toggled ? Appearance.m3colors.m3secondaryContainer : lightDarkButtonRoot.previewFg
Layout.fillWidth: true
implicitHeight: 30
}
}
}
}
StyledText {
Layout.fillWidth: true
text: dark ? Translation.tr("Dark") : Translation.tr("Light")
color: lightDarkButtonRoot.toggled ? Appearance.m3colors.m3onPrimary : Appearance.colors.colOnLayer2
horizontalAlignment: Text.AlignHCenter
}
}
}
}

View file

@ -0,0 +1,9 @@
import QtQuick
import QtQuick.Effects
// Note: You still have to set sizes yourself
MultiEffect {
maskEnabled: true
maskThresholdMin: 0.5
maskSpreadAtMin: 1
}

View file

@ -0,0 +1,39 @@
import QtQuick
import QtQuick.Shapes
import Quickshell
import qs.modules.common
import qs.modules.common.widgets.shapes
import "shapes/geometry/offset.js" as Offset
import "shapes/shapes/corner-rounding.js" as CornerRounding
import "shapes/shapes/rounded-polygon.js" as RoundedPolygon
import "shapes/material-shapes.js" as MaterialShapes
Item {
id: root
property int sides: 12
property int implicitSize: 100
property alias color: shapeCanvas.color
implicitWidth: implicitSize
implicitHeight: implicitSize
property var cornerRounding: new CornerRounding.CornerRounding((sides < 17 ? 1.5 : 1.1) / Math.max(sides, 1))
ShapeCanvas {
id: shapeCanvas
anchors.fill: parent
roundedPolygon: switch(sides) {
case 0: return MaterialShapes.getCircle();
case 1: return MaterialShapes.getCircle();
case 4: return MaterialShapes.getCookie4Sided();
case 6: return MaterialShapes.getCookie6Sided();
case 7: return MaterialShapes.getCookie7Sided();
case 9: return MaterialShapes.getCookie9Sided();
case 12: return MaterialShapes.getCookie12Sided();
default: return RoundedPolygon.RoundedPolygon.star(sides, 1, 0.8, root.cornerRounding)
.transformed((x, y) => MaterialShapes.rotate30.map(new Offset.Offset(x, y)))
.normalized();
}
}
}

View file

@ -0,0 +1,89 @@
pragma ComponentBehavior: Bound
import QtQuick
import qs.modules.common
import qs.modules.common.functions
import qs.modules.common.widgets
Rectangle {
id: root
property bool loading: true
property double pullProgress: 0
// Size, color
property double implicitSize: 48
implicitWidth: implicitSize
implicitHeight: implicitSize
radius: Math.min(width, height) / 2
color: Appearance.colors.colPrimaryContainer
property double baseShapeSize: root.implicitSize * 0.7
property double leapZoomSize: root.baseShapeSize * 1.2
property double leapZoomProgress: 0
// Shape
property list<var> shapes: [
MaterialShape.Shape.SoftBurst,
MaterialShape.Shape.Cookie9Sided,
MaterialShape.Shape.Pentagon,
MaterialShape.Shape.Pill,
MaterialShape.Shape.Sunny,
MaterialShape.Shape.Cookie4Sided,
MaterialShape.Shape.Oval,
]
property int shapeIndex: 0
property double pullRotation: root.loading ? 0 : -(root.pullProgress * 360)
property double continuousRotation: 0
property double leapRotation: 0
rotation: pullRotation + continuousRotation + leapRotation
RotationAnimation on continuousRotation {
running: root.loading
duration: 12000
easing.type: Easing.Linear
loops: Animation.Infinite
from: 0
to: 360
}
Timer {
interval: 800
running: root.loading
repeat: true
onTriggered: leapAnimation.start()
}
ParallelAnimation {
id: leapAnimation
PropertyAction { target: root; property: "shapeIndex"; value: (root.shapeIndex + 1) % root.shapes.length }
RotationAnimation {
target: root
direction: RotationAnimation.Shortest
property: "leapRotation"
to: (root.leapRotation + 90) % 360
duration: 350
easing.type: Easing.InOutQuad
}
NumberAnimation {
target: root
property: "leapZoomProgress"
from: 0
to: 1
duration: 750
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.animationCurves.standard
}
}
MaterialShape {
id: shape
anchors.centerIn: parent
shape: root.shapes[root.shapeIndex]
implicitSize: {
const leapZoomDiff = root.leapZoomSize - root.baseShapeSize
const progressFirstHalf = Math.min(root.leapZoomProgress, 0.5) * 2;
const progressSecondHalf = Math.max(root.leapZoomProgress - 0.5, 0) * 2;
return root.baseShapeSize + leapZoomDiff * progressFirstHalf - leapZoomDiff * progressSecondHalf;
}
color: Appearance.colors.colOnPrimaryContainer
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
}
}

View file

@ -0,0 +1,88 @@
import qs.modules.common.widgets.shapes
import "shapes/material-shapes.js" as MaterialShapes
ShapeCanvas {
id: root
enum Shape {
Circle,
Square,
Slanted,
Arch,
Fan,
Arrow,
SemiCircle,
Oval,
Pill,
Triangle,
Diamond,
ClamShell,
Pentagon,
Gem,
Sunny,
VerySunny,
Cookie4Sided,
Cookie6Sided,
Cookie7Sided,
Cookie9Sided,
Cookie12Sided,
Ghostish,
Clover4Leaf,
Clover8Leaf,
Burst,
SoftBurst,
Boom,
SoftBoom,
Flower,
Puffy,
PuffyDiamond,
PixelCircle,
PixelTriangle,
Bun,
Heart
}
required property var shape
property double implicitSize
implicitHeight: implicitSize
implicitWidth: implicitSize
polygonIsNormalized: true
roundedPolygon: {
switch (root.shape) {
case MaterialShape.Shape.Circle: return MaterialShapes.getCircle();
case MaterialShape.Shape.Square: return MaterialShapes.getSquare();
case MaterialShape.Shape.Slanted: return MaterialShapes.getSlanted();
case MaterialShape.Shape.Arch: return MaterialShapes.getArch();
case MaterialShape.Shape.Fan: return MaterialShapes.getFan();
case MaterialShape.Shape.Arrow: return MaterialShapes.getArrow();
case MaterialShape.Shape.SemiCircle: return MaterialShapes.getSemiCircle();
case MaterialShape.Shape.Oval: return MaterialShapes.getOval();
case MaterialShape.Shape.Pill: return MaterialShapes.getPill();
case MaterialShape.Shape.Triangle: return MaterialShapes.getTriangle();
case MaterialShape.Shape.Diamond: return MaterialShapes.getDiamond();
case MaterialShape.Shape.ClamShell: return MaterialShapes.getClamShell();
case MaterialShape.Shape.Pentagon: return MaterialShapes.getPentagon();
case MaterialShape.Shape.Gem: return MaterialShapes.getGem();
case MaterialShape.Shape.Sunny: return MaterialShapes.getSunny();
case MaterialShape.Shape.VerySunny: return MaterialShapes.getVerySunny();
case MaterialShape.Shape.Cookie4Sided: return MaterialShapes.getCookie4Sided();
case MaterialShape.Shape.Cookie6Sided: return MaterialShapes.getCookie6Sided();
case MaterialShape.Shape.Cookie7Sided: return MaterialShapes.getCookie7Sided();
case MaterialShape.Shape.Cookie9Sided: return MaterialShapes.getCookie9Sided();
case MaterialShape.Shape.Cookie12Sided: return MaterialShapes.getCookie12Sided();
case MaterialShape.Shape.Ghostish: return MaterialShapes.getGhostish();
case MaterialShape.Shape.Clover4Leaf: return MaterialShapes.getClover4Leaf();
case MaterialShape.Shape.Clover8Leaf: return MaterialShapes.getClover8Leaf();
case MaterialShape.Shape.Burst: return MaterialShapes.getBurst();
case MaterialShape.Shape.SoftBurst: return MaterialShapes.getSoftBurst();
case MaterialShape.Shape.Boom: return MaterialShapes.getBoom();
case MaterialShape.Shape.SoftBoom: return MaterialShapes.getSoftBoom();
case MaterialShape.Shape.Flower: return MaterialShapes.getFlower();
case MaterialShape.Shape.Puffy: return MaterialShapes.getPuffy();
case MaterialShape.Shape.PuffyDiamond: return MaterialShapes.getPuffyDiamond();
case MaterialShape.Shape.PixelCircle: return MaterialShapes.getPixelCircle();
case MaterialShape.Shape.PixelTriangle: return MaterialShapes.getPixelTriangle();
case MaterialShape.Shape.Bun: return MaterialShapes.getBun();
case MaterialShape.Shape.Heart: return MaterialShapes.getHeart();
default: return MaterialShapes.getCircle();
}
}
}

View file

@ -0,0 +1,23 @@
import QtQuick
import qs.modules.common
import qs.modules.common.widgets
MaterialShape {
id: root
property alias text: symbol.text
property alias iconSize: symbol.iconSize
property alias font: symbol.font
property alias colSymbol: symbol.color
property real padding: 6
color: Appearance.colors.colSecondaryContainer
colSymbol: Appearance.colors.colOnSecondaryContainer
shape: MaterialShape.Shape.Clover4Leaf
implicitSize: Math.max(symbol.implicitWidth, symbol.implicitHeight) + padding * 2
MaterialSymbol {
id: symbol
anchors.centerIn: parent
color: root.colSymbol
}
}

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