adds overview, removes old stuff
This commit is contained in:
@@ -0,0 +1,360 @@
|
||||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import "functions"
|
||||
import "." as Common
|
||||
import ".."
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
property string colorSource: Common.Config.options.appearance.colorSource
|
||||
property string caelestiaAccentProfile: Common.Config.options.appearance.caelestia.accentProfile
|
||||
property string lastCaelestiaPayload: ""
|
||||
property QtObject m3colors: {
|
||||
if (colorSource === "matugen" && matugenLoader.item)
|
||||
return matugenLoader.item;
|
||||
if (colorSource === "caelestia" && caelestiaPaletteLoaded)
|
||||
return caelestiaColors;
|
||||
return defaultColors;
|
||||
}
|
||||
property QtObject animation
|
||||
property QtObject animationCurves
|
||||
property QtObject colors
|
||||
property QtObject rounding
|
||||
property QtObject font
|
||||
property QtObject sizes
|
||||
property bool caelestiaPaletteLoaded: false
|
||||
|
||||
Loader {
|
||||
id: matugenLoader
|
||||
active: root.colorSource === "matugen"
|
||||
source: "Appearance.colors.qml"
|
||||
}
|
||||
|
||||
// Retuned to pull from this shell's own wallust palette (Theme/Colors) instead of the
|
||||
// upstream stock M3 purple palette, so the overview matches the bar's look. See
|
||||
// ../modules/overview/CREDITS.md.
|
||||
property QtObject defaultColors: QtObject {
|
||||
property bool darkmode: true
|
||||
property color m3primary: Theme.wsActive
|
||||
property color m3onPrimary: Colors.background
|
||||
property color m3primaryContainer: ColorUtils.mix(Theme.wsActive, Colors.background, 0.35)
|
||||
property color m3onPrimaryContainer: Colors.foreground
|
||||
property color m3secondary: Theme.wsActive
|
||||
property color m3onSecondary: Colors.background
|
||||
property color m3secondaryContainer: ColorUtils.mix(Theme.wsActive, Colors.background, 0.55)
|
||||
property color m3onSecondaryContainer: Colors.foreground
|
||||
property color m3background: Colors.background
|
||||
property color m3onBackground: Colors.foreground
|
||||
property color m3surface: Colors.background
|
||||
property color m3surfaceContainerLow: ColorUtils.mix(Colors.foreground, Colors.background, 0.06)
|
||||
property color m3surfaceContainer: ColorUtils.mix(Colors.foreground, Colors.background, 0.10)
|
||||
property color m3surfaceContainerHigh: ColorUtils.mix(Colors.foreground, Colors.background, 0.15)
|
||||
property color m3surfaceContainerHighest: ColorUtils.mix(Colors.foreground, Colors.background, 0.20)
|
||||
property color m3onSurface: Colors.foreground
|
||||
property color m3surfaceVariant: ColorUtils.mix(Colors.foreground, Colors.background, 0.25)
|
||||
property color m3onSurfaceVariant: Colors.foreground
|
||||
property color m3inverseSurface: Colors.foreground
|
||||
property color m3inverseOnSurface: Colors.background
|
||||
property color m3outline: Colors.color8
|
||||
property color m3outlineVariant: ColorUtils.mix(Colors.foreground, Colors.background, 0.30)
|
||||
property color m3shadow: "#000000"
|
||||
}
|
||||
|
||||
property QtObject caelestiaColors: QtObject {
|
||||
property bool darkmode: defaultColors.darkmode
|
||||
property color m3primary: defaultColors.m3primary
|
||||
property color m3onPrimary: defaultColors.m3onPrimary
|
||||
property color m3primaryContainer: defaultColors.m3primaryContainer
|
||||
property color m3onPrimaryContainer: defaultColors.m3onPrimaryContainer
|
||||
property color m3secondary: defaultColors.m3secondary
|
||||
property color m3onSecondary: defaultColors.m3onSecondary
|
||||
property color m3secondaryContainer: defaultColors.m3secondaryContainer
|
||||
property color m3onSecondaryContainer: defaultColors.m3onSecondaryContainer
|
||||
property color m3background: defaultColors.m3background
|
||||
property color m3onBackground: defaultColors.m3onBackground
|
||||
property color m3surface: defaultColors.m3surface
|
||||
property color m3surfaceContainerLow: defaultColors.m3surfaceContainerLow
|
||||
property color m3surfaceContainer: defaultColors.m3surfaceContainer
|
||||
property color m3surfaceContainerHigh: defaultColors.m3surfaceContainerHigh
|
||||
property color m3surfaceContainerHighest: defaultColors.m3surfaceContainerHighest
|
||||
property color m3onSurface: defaultColors.m3onSurface
|
||||
property color m3surfaceVariant: defaultColors.m3surfaceVariant
|
||||
property color m3onSurfaceVariant: defaultColors.m3onSurfaceVariant
|
||||
property color m3inverseSurface: defaultColors.m3inverseSurface
|
||||
property color m3inverseOnSurface: defaultColors.m3inverseOnSurface
|
||||
property color m3outline: defaultColors.m3outline
|
||||
property color m3outlineVariant: defaultColors.m3outlineVariant
|
||||
property color m3shadow: defaultColors.m3shadow
|
||||
}
|
||||
|
||||
function loadCaelestiaPalette() {
|
||||
getCaelestiaScheme.running = true;
|
||||
}
|
||||
|
||||
function relativeLuminance(color) {
|
||||
const c = Qt.color(color);
|
||||
function channel(v) {
|
||||
return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
|
||||
}
|
||||
return (0.2126 * channel(c.r)) + (0.7152 * channel(c.g)) + (0.0722 * channel(c.b));
|
||||
}
|
||||
|
||||
function bestOnColor(backgroundColor) {
|
||||
return relativeLuminance(backgroundColor) > 0.5 ? "#121212" : "#f5f5f5";
|
||||
}
|
||||
|
||||
function firstColor(palette, keys, fallback) {
|
||||
for (const key of keys) {
|
||||
if (palette[key])
|
||||
return palette[key];
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
function applyCaelestiaPalette(palette, mode) {
|
||||
if (caelestiaAccentProfile === "vibrant") {
|
||||
const primary = firstColor(palette, ["blue", "klink", "term12", "primary"], defaultColors.m3primary);
|
||||
const secondary = firstColor(palette, ["mauve", "lavender", "term13", "secondary"], defaultColors.m3secondary);
|
||||
const tertiary = firstColor(palette, ["pink", "rosewater", "term11", "tertiary"], defaultColors.m3secondaryContainer);
|
||||
const primaryContainer = firstColor(palette, ["sapphire", "klinkSelection", "primaryContainer"], defaultColors.m3primaryContainer);
|
||||
const secondaryContainer = firstColor(palette, ["surface2", "secondaryContainer"], defaultColors.m3secondaryContainer);
|
||||
|
||||
caelestiaColors.m3primary = primary;
|
||||
caelestiaColors.m3onPrimary = bestOnColor(primary);
|
||||
caelestiaColors.m3primaryContainer = primaryContainer;
|
||||
caelestiaColors.m3onPrimaryContainer = bestOnColor(primaryContainer);
|
||||
caelestiaColors.m3secondary = secondary;
|
||||
caelestiaColors.m3onSecondary = bestOnColor(secondary);
|
||||
caelestiaColors.m3secondaryContainer = secondaryContainer;
|
||||
caelestiaColors.m3onSecondaryContainer = bestOnColor(secondaryContainer);
|
||||
// Preserve a stronger accent presence across UI mixes.
|
||||
caelestiaColors.m3surfaceVariant = firstColor(palette, ["surface1", "surfaceVariant"], defaultColors.m3surfaceVariant);
|
||||
caelestiaColors.m3outline = firstColor(palette, ["overlay2", "outline"], defaultColors.m3outline);
|
||||
caelestiaColors.m3outlineVariant = firstColor(palette, ["overlay0", "outlineVariant"], defaultColors.m3outlineVariant);
|
||||
if (tertiary)
|
||||
caelestiaColors.m3secondaryContainer = ColorUtils.mix(secondaryContainer, tertiary, 0.7);
|
||||
} else {
|
||||
const map = {
|
||||
"primary": "m3primary",
|
||||
"onPrimary": "m3onPrimary",
|
||||
"primaryContainer": "m3primaryContainer",
|
||||
"onPrimaryContainer": "m3onPrimaryContainer",
|
||||
"secondary": "m3secondary",
|
||||
"onSecondary": "m3onSecondary",
|
||||
"secondaryContainer": "m3secondaryContainer",
|
||||
"onSecondaryContainer": "m3onSecondaryContainer",
|
||||
"surfaceVariant": "m3surfaceVariant",
|
||||
"outline": "m3outline",
|
||||
"outlineVariant": "m3outlineVariant"
|
||||
};
|
||||
for (const key in map) {
|
||||
if (palette[key])
|
||||
caelestiaColors[map[key]] = palette[key];
|
||||
}
|
||||
}
|
||||
|
||||
// Keep foundational tones from Material keys for readability.
|
||||
const baseMap = {
|
||||
"background": "m3background",
|
||||
"onBackground": "m3onBackground",
|
||||
"surface": "m3surface",
|
||||
"surfaceContainerLow": "m3surfaceContainerLow",
|
||||
"surfaceContainer": "m3surfaceContainer",
|
||||
"surfaceContainerHigh": "m3surfaceContainerHigh",
|
||||
"surfaceContainerHighest": "m3surfaceContainerHighest",
|
||||
"onSurface": "m3onSurface",
|
||||
"inverseSurface": "m3inverseSurface",
|
||||
"inverseOnSurface": "m3inverseOnSurface",
|
||||
"shadow": "m3shadow"
|
||||
};
|
||||
for (const key in baseMap) {
|
||||
if (palette[key])
|
||||
caelestiaColors[baseMap[key]] = palette[key];
|
||||
}
|
||||
|
||||
if (palette["onSurfaceVariant"])
|
||||
caelestiaColors.m3onSurfaceVariant = palette["onSurfaceVariant"];
|
||||
|
||||
if (mode === "light")
|
||||
caelestiaColors.darkmode = false;
|
||||
else if (mode === "dark")
|
||||
caelestiaColors.darkmode = true;
|
||||
}
|
||||
|
||||
Process {
|
||||
id: getCaelestiaScheme
|
||||
command: ["sh", "-lc", "caelestia scheme get 2>/dev/null || true"]
|
||||
stdout: StdioCollector {
|
||||
id: caelestiaCollector
|
||||
onStreamFinished: {
|
||||
const text = caelestiaCollector.text;
|
||||
if (!text || !text.trim()) {
|
||||
root.caelestiaPaletteLoaded = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const ansiPattern = /\x1b\[[0-9;]*m/g;
|
||||
const lines = text.split("\n");
|
||||
let mode = "";
|
||||
const palette = ({});
|
||||
|
||||
for (const rawLine of lines) {
|
||||
const line = rawLine.replace(ansiPattern, "").trim();
|
||||
|
||||
if (line.startsWith("Mode:")) {
|
||||
mode = line.split(":")[1]?.trim()?.toLowerCase() ?? "";
|
||||
continue;
|
||||
}
|
||||
|
||||
const match = line.match(/^([A-Za-z0-9_]+):\s*.*?([0-9a-fA-F]{6})$/);
|
||||
if (!match)
|
||||
continue;
|
||||
|
||||
palette[match[1]] = `#${match[2]}`;
|
||||
}
|
||||
|
||||
const normalized = JSON.stringify({ mode: mode, palette: palette, profile: caelestiaAccentProfile });
|
||||
if (normalized === root.lastCaelestiaPayload)
|
||||
return;
|
||||
|
||||
root.lastCaelestiaPayload = normalized;
|
||||
applyCaelestiaPalette(palette, mode);
|
||||
root.caelestiaPaletteLoaded = Object.keys(palette).length > 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: caelestiaRefreshTimer
|
||||
interval: Math.max(500, Common.Config.options.appearance.caelestia.refreshInterval)
|
||||
running: root.colorSource === "caelestia" && Common.Config.options.appearance.caelestia.autoRefresh
|
||||
repeat: true
|
||||
triggeredOnStart: false
|
||||
onTriggered: root.loadCaelestiaPalette()
|
||||
}
|
||||
|
||||
onColorSourceChanged: {
|
||||
if (colorSource === "caelestia")
|
||||
loadCaelestiaPalette();
|
||||
}
|
||||
|
||||
onCaelestiaAccentProfileChanged: {
|
||||
if (colorSource === "caelestia") {
|
||||
root.lastCaelestiaPayload = "";
|
||||
loadCaelestiaPalette();
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
if (colorSource === "caelestia")
|
||||
loadCaelestiaPalette();
|
||||
}
|
||||
|
||||
colors: QtObject {
|
||||
property color colSubtext: m3colors.m3outline
|
||||
property color colLayer0: m3colors.m3background
|
||||
property color colOnLayer0: m3colors.m3onBackground
|
||||
property color colLayer0Border: ColorUtils.mix(root.m3colors.m3outlineVariant, colLayer0, 0.4)
|
||||
property color colLayer1: m3colors.m3surfaceContainerLow
|
||||
property color colOnLayer1: m3colors.m3onSurfaceVariant
|
||||
property color colOnLayer1Inactive: ColorUtils.mix(colOnLayer1, colLayer1, 0.45)
|
||||
property color colLayer1Hover: ColorUtils.mix(colLayer1, colOnLayer1, 0.92)
|
||||
property color colLayer1Active: ColorUtils.mix(colLayer1, colOnLayer1, 0.85)
|
||||
property color colLayer2: m3colors.m3surfaceContainer
|
||||
property color colOnLayer2: m3colors.m3onSurface
|
||||
property color colLayer2Hover: ColorUtils.mix(colLayer2, colOnLayer2, 0.90)
|
||||
property color colLayer2Active: ColorUtils.mix(colLayer2, colOnLayer2, 0.80)
|
||||
property color colPrimary: m3colors.m3primary
|
||||
property color colOnPrimary: m3colors.m3onPrimary
|
||||
property color colSecondary: m3colors.m3secondary
|
||||
property color colSecondaryContainer: m3colors.m3secondaryContainer
|
||||
property color colOnSecondaryContainer: m3colors.m3onSecondaryContainer
|
||||
property color colTooltip: m3colors.m3inverseSurface
|
||||
property color colOnTooltip: m3colors.m3inverseOnSurface
|
||||
property color colShadow: ColorUtils.transparentize(m3colors.m3shadow, 0.7)
|
||||
property color colOutline: m3colors.m3outline
|
||||
}
|
||||
|
||||
rounding: QtObject {
|
||||
property int unsharpen: Common.Config.options.appearance.rounding.unsharpen
|
||||
property int verysmall: Common.Config.options.appearance.rounding.verysmall
|
||||
property int small: Common.Config.options.appearance.rounding.small
|
||||
property int normal: Common.Config.options.appearance.rounding.normal
|
||||
property int large: Common.Config.options.appearance.rounding.large
|
||||
property int full: Common.Config.options.appearance.rounding.full
|
||||
property int screenRounding: Common.Config.options.appearance.rounding.screenRounding
|
||||
property int windowRounding: Common.Config.options.appearance.rounding.windowRounding
|
||||
}
|
||||
|
||||
font: QtObject {
|
||||
property QtObject family: QtObject {
|
||||
property string main: Common.Config.options.appearance.font.family.main
|
||||
property string title: Common.Config.options.appearance.font.family.title
|
||||
property string expressive: Common.Config.options.appearance.font.family.expressive
|
||||
}
|
||||
property QtObject pixelSize: QtObject {
|
||||
property int smaller: Common.Config.options.appearance.font.pixelSize.smaller
|
||||
property int small: Common.Config.options.appearance.font.pixelSize.small
|
||||
property int normal: Common.Config.options.appearance.font.pixelSize.normal
|
||||
property int larger: Common.Config.options.appearance.font.pixelSize.larger
|
||||
property int huge: Common.Config.options.appearance.font.pixelSize.huge
|
||||
}
|
||||
}
|
||||
|
||||
animationCurves: QtObject {
|
||||
readonly property list<real> expressiveDefaultSpatial: [0.38, 1.21, 0.22, 1.00, 1, 1]
|
||||
readonly property list<real> expressiveEffects: [0.34, 0.80, 0.34, 1.00, 1, 1]
|
||||
readonly property list<real> emphasizedDecel: [0.05, 0.7, 0.1, 1, 1, 1]
|
||||
readonly property real expressiveDefaultSpatialDuration: Common.Config.options.appearance.animation.duration.elementMove
|
||||
readonly property real expressiveEffectsDuration: Common.Config.options.appearance.animation.duration.elementMoveFast
|
||||
}
|
||||
|
||||
animation: QtObject {
|
||||
property QtObject elementMove: QtObject {
|
||||
property int duration: animationCurves.expressiveDefaultSpatialDuration
|
||||
property int type: Easing.BezierSpline
|
||||
property list<real> bezierCurve: animationCurves.expressiveDefaultSpatial
|
||||
property Component numberAnimation: Component {
|
||||
NumberAnimation {
|
||||
duration: root.animation.elementMove.duration
|
||||
easing.type: root.animation.elementMove.type
|
||||
easing.bezierCurve: root.animation.elementMove.bezierCurve
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
property QtObject elementMoveEnter: QtObject {
|
||||
property int duration: Common.Config.options.appearance.animation.duration.elementMoveEnter
|
||||
property int type: Easing.BezierSpline
|
||||
property list<real> bezierCurve: animationCurves.emphasizedDecel
|
||||
property Component numberAnimation: Component {
|
||||
NumberAnimation {
|
||||
duration: root.animation.elementMoveEnter.duration
|
||||
easing.type: root.animation.elementMoveEnter.type
|
||||
easing.bezierCurve: root.animation.elementMoveEnter.bezierCurve
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
property QtObject elementMoveFast: QtObject {
|
||||
property int duration: animationCurves.expressiveEffectsDuration
|
||||
property int type: Easing.BezierSpline
|
||||
property list<real> bezierCurve: animationCurves.expressiveEffects
|
||||
property Component numberAnimation: Component {
|
||||
NumberAnimation {
|
||||
duration: root.animation.elementMoveFast.duration
|
||||
easing.type: root.animation.elementMoveFast.type
|
||||
easing.bezierCurve: root.animation.elementMoveFast.bezierCurve
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sizes: QtObject {
|
||||
property real elevationMargin: Common.Config.options.appearance.sizes.elevationMargin
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
property var userOptions: ({})
|
||||
|
||||
function read(path, fallback) {
|
||||
const parts = path.split(".");
|
||||
let current = userOptions;
|
||||
|
||||
for (const part of parts) {
|
||||
if (current === null || current === undefined || typeof current !== "object" || !(part in current)) {
|
||||
return fallback;
|
||||
}
|
||||
current = current[part];
|
||||
}
|
||||
|
||||
return current === undefined || current === null ? fallback : current;
|
||||
}
|
||||
|
||||
function readInt(path, fallback) {
|
||||
const value = read(path, fallback);
|
||||
const parsed = Number(value);
|
||||
return Number.isInteger(parsed) ? parsed : fallback;
|
||||
}
|
||||
|
||||
function readReal(path, fallback) {
|
||||
const value = read(path, fallback);
|
||||
const parsed = Number(value);
|
||||
return Number.isFinite(parsed) ? parsed : fallback;
|
||||
}
|
||||
|
||||
function readBool(path, fallback) {
|
||||
const value = read(path, fallback);
|
||||
return typeof value === "boolean" ? value : fallback;
|
||||
}
|
||||
|
||||
function readString(path, fallback) {
|
||||
const value = read(path, fallback);
|
||||
if (typeof value !== "string")
|
||||
return fallback;
|
||||
|
||||
const trimmed = value.trim();
|
||||
return trimmed.length > 0 ? trimmed : fallback;
|
||||
}
|
||||
|
||||
property QtObject options: QtObject {
|
||||
property QtObject appearance: QtObject {
|
||||
property string colorSource: root.readString(
|
||||
"appearance.colorSource",
|
||||
root.readBool("appearance.useMatugenColors", false) ? "matugen" : "default"
|
||||
)
|
||||
property bool useMatugenColors: colorSource === "matugen"
|
||||
property QtObject caelestia: QtObject {
|
||||
property bool autoRefresh: root.readBool("appearance.caelestia.autoRefresh", true)
|
||||
property int refreshInterval: root.readInt("appearance.caelestia.refreshInterval", 2000)
|
||||
property string accentProfile: root.readString("appearance.caelestia.accentProfile", "vibrant")
|
||||
}
|
||||
|
||||
property QtObject rounding: QtObject {
|
||||
property int unsharpen: root.readInt("appearance.rounding.unsharpen", 2)
|
||||
property int verysmall: root.readInt("appearance.rounding.verysmall", 8)
|
||||
property int small: root.readInt("appearance.rounding.small", 12)
|
||||
property int normal: root.readInt("appearance.rounding.normal", 17)
|
||||
property int large: root.readInt("appearance.rounding.large", 23)
|
||||
property int full: root.readInt("appearance.rounding.full", 9999)
|
||||
property int screenRounding: root.readInt("appearance.rounding.screenRounding", large)
|
||||
property int windowRounding: root.readInt("appearance.rounding.windowRounding", 18)
|
||||
}
|
||||
|
||||
property QtObject font: QtObject {
|
||||
property QtObject family: QtObject {
|
||||
property string main: root.readString("appearance.font.family.main", "sans-serif")
|
||||
property string title: root.readString("appearance.font.family.title", "sans-serif")
|
||||
property string expressive: root.readString("appearance.font.family.expressive", "sans-serif")
|
||||
}
|
||||
|
||||
property QtObject pixelSize: QtObject {
|
||||
property int smaller: root.readInt("appearance.font.pixelSize.smaller", 12)
|
||||
property int small: root.readInt("appearance.font.pixelSize.small", 15)
|
||||
property int normal: root.readInt("appearance.font.pixelSize.normal", 16)
|
||||
property int larger: root.readInt("appearance.font.pixelSize.larger", 19)
|
||||
property int huge: root.readInt("appearance.font.pixelSize.huge", 22)
|
||||
}
|
||||
}
|
||||
|
||||
property QtObject animation: QtObject {
|
||||
property QtObject duration: QtObject {
|
||||
property int elementMove: root.readInt("appearance.animation.duration.elementMove", 500)
|
||||
property int elementMoveEnter: root.readInt("appearance.animation.duration.elementMoveEnter", 400)
|
||||
property int elementMoveFast: root.readInt("appearance.animation.duration.elementMoveFast", 200)
|
||||
}
|
||||
}
|
||||
|
||||
property QtObject sizes: QtObject {
|
||||
property real elevationMargin: root.readReal("appearance.sizes.elevationMargin", 10)
|
||||
}
|
||||
}
|
||||
|
||||
property QtObject overview: QtObject {
|
||||
property int rows: root.readInt("overview.rows", 2)
|
||||
property int columns: root.readInt("overview.columns", 5)
|
||||
property real scale: root.readReal("overview.scale", 0.16)
|
||||
property bool enable: root.readBool("overview.enable", true)
|
||||
property bool hideEmptyRows: root.readBool("overview.hideEmptyRows", true)
|
||||
property bool closeOnFocusLoss: root.readBool("overview.closeOnFocusLoss", true)
|
||||
property bool useWorkspaceMap: root.readBool("overview.useWorkspaceMap", false)
|
||||
property var workspaceMap: root.read("overview.workspaceMap", [])
|
||||
property bool orderRightLeft: root.readBool("overview.orderRightLeft", false)
|
||||
property bool orderBottomUp: root.readBool("overview.orderBottomUp", false)
|
||||
property bool previewsEnabled: root.readBool("overview.previewsEnabled", true)
|
||||
property string previewMode: root.readString("overview.previewMode", "live")
|
||||
property bool includeInactiveMonitorPreviews: root.readBool("overview.includeInactiveMonitorPreviews", true)
|
||||
property int previewRecaptureDelayMs: root.readInt("overview.previewRecaptureDelayMs", 60)
|
||||
property bool showSpecialWorkspaces: root.readBool("overview.showSpecialWorkspaces", true)
|
||||
property var specialWorkspaces: root.read("overview.specialWorkspaces", [])
|
||||
property int specialWorkspaceColumns: root.readInt("overview.specialWorkspaceColumns", columns)
|
||||
property string emptyWorkspaceWallpaper: root.readString("overview.emptyWorkspaceWallpaper", "")
|
||||
property string specialEmptyWorkspaceWallpaper: root.readString("overview.specialEmptyWorkspaceWallpaper", "")
|
||||
property real workspaceSpacing: root.readReal("overview.workspaceSpacing", 5)
|
||||
property real backgroundPadding: root.readReal("overview.backgroundPadding", 10)
|
||||
property real workspaceNumberBaseSize: root.readReal("overview.workspaceNumberBaseSize", 250)
|
||||
property QtObject effects: QtObject {
|
||||
property bool enableBackdrop: root.readBool("overview.effects.enableBackdrop", false)
|
||||
property real backdropOpacity: root.readReal("overview.effects.backdropOpacity", 0.28)
|
||||
property real panelOpacity: root.readReal("overview.effects.panelOpacity", 0.92)
|
||||
property real workspaceOpacity: root.readReal("overview.effects.workspaceOpacity", 0.86)
|
||||
property real emptyWorkspaceWallpaperOverlayOpacity: root.readReal("overview.effects.emptyWorkspaceWallpaperOverlayOpacity", 0.18)
|
||||
property real windowOverlayOpacity: root.readReal("overview.effects.windowOverlayOpacity", 0.22)
|
||||
property bool enableBlur: root.readBool("overview.effects.enableBlur", false)
|
||||
property bool glassMode: root.readBool("overview.effects.glassMode", false)
|
||||
property real glassTintStrength: root.readReal("overview.effects.glassTintStrength", 0.35)
|
||||
property real glassBorderOpacity: root.readReal("overview.effects.glassBorderOpacity", 0.72)
|
||||
property real glassShineOpacity: root.readReal("overview.effects.glassShineOpacity", 0.14)
|
||||
}
|
||||
}
|
||||
|
||||
property QtObject position: QtObject {
|
||||
property int topMargin: root.readInt("position.topMargin", 100)
|
||||
}
|
||||
|
||||
property QtObject windowPreview: QtObject {
|
||||
property bool showIcons: root.readBool("windowPreview.showIcons", true)
|
||||
property real iconToWindowRatio: root.readReal("windowPreview.iconToWindowRatio", 0.25)
|
||||
property real iconToWindowRatioCompact: root.readReal("windowPreview.iconToWindowRatioCompact", 0.45)
|
||||
property real xwaylandIndicatorToIconRatio: root.readReal("windowPreview.xwaylandIndicatorToIconRatio", 0.35)
|
||||
property real inactiveMonitorOpacity: root.readReal("windowPreview.inactiveMonitorOpacity", 0.4)
|
||||
property bool cropToFill: root.readBool("windowPreview.cropToFill", false)
|
||||
}
|
||||
|
||||
property QtObject hacks: QtObject {
|
||||
property int arbitraryRaceConditionDelay: root.readInt("hacks.arbitraryRaceConditionDelay", 150)
|
||||
property int hyprlandEventDebounceMs: root.readInt("hacks.hyprlandEventDebounceMs", 40)
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: loadUserConfig
|
||||
command: [
|
||||
"sh",
|
||||
"-lc",
|
||||
"cfg=\"${XDG_CONFIG_HOME:-$HOME/.config}/quickshell/overview/config.json\"; [ -r \"$cfg\" ] && cat \"$cfg\""
|
||||
]
|
||||
stdout: StdioCollector {
|
||||
id: configCollector
|
||||
onStreamFinished: {
|
||||
const payload = configCollector.text.trim();
|
||||
if (!payload)
|
||||
return;
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(payload);
|
||||
if (typeof parsed === "object" && parsed !== null) {
|
||||
root.userOptions = parsed;
|
||||
} else {
|
||||
console.warn("overview: config.json must contain a JSON object; ignoring file");
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("overview: failed to parse user config.json; using defaults", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
loadUserConfig.running = true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
pragma Singleton
|
||||
import Quickshell
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
function colorWithHueOf(color1, color2) {
|
||||
var c1 = Qt.color(color1);
|
||||
var c2 = Qt.color(color2);
|
||||
var hue = c2.hsvHue;
|
||||
var sat = c1.hsvSaturation;
|
||||
var val = c1.hsvValue;
|
||||
var alpha = c1.a;
|
||||
return Qt.hsva(hue, sat, val, alpha);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
function colorWithLightness(color, lightness) {
|
||||
var c = Qt.color(color);
|
||||
return Qt.hsla(c.hslHue, c.hslSaturation, lightness, c.a);
|
||||
}
|
||||
|
||||
function colorWithLightnessOf(color1, color2) {
|
||||
var c2 = Qt.color(color2);
|
||||
return colorWithLightness(color1, c2.hslLightness);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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
|
||||
);
|
||||
}
|
||||
|
||||
function transparentize(color, percentage = 1) {
|
||||
var c = Qt.color(color);
|
||||
return Qt.rgba(c.r, c.g, c.b, c.a * (1 - percentage));
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
singleton ColorUtils 1.0 ColorUtils.qml
|
||||
@@ -0,0 +1,7 @@
|
||||
singleton Appearance 1.0 Appearance.qml
|
||||
singleton Config 1.0 Config.qml
|
||||
singleton ColorUtils 1.0 functions/ColorUtils.qml
|
||||
StyledText 1.0 widgets/StyledText.qml
|
||||
StyledRectangularShadow 1.0 widgets/StyledRectangularShadow.qml
|
||||
StyledToolTip 1.0 widgets/StyledToolTip.qml
|
||||
StyledToolTipContent 1.0 widgets/StyledToolTipContent.qml
|
||||
@@ -0,0 +1,14 @@
|
||||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
import ".."
|
||||
|
||||
RectangularShadow {
|
||||
required property var target
|
||||
anchors.fill: target
|
||||
radius: 20
|
||||
blur: 0.9 * Appearance.sizes.elevationMargin
|
||||
offset: Qt.vector2d(0.0, 1.0)
|
||||
spread: 1
|
||||
color: Appearance.colors.colShadow
|
||||
cached: true
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import QtQuick
|
||||
import ".."
|
||||
|
||||
Text {
|
||||
id: root
|
||||
property bool animateChange: false
|
||||
|
||||
renderType: Text.NativeRendering
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
font {
|
||||
hintingPreference: Font.PreferFullHinting
|
||||
family: Appearance?.font.family.main ?? "sans-serif"
|
||||
pixelSize: Appearance?.font.pixelSize.small ?? 15
|
||||
}
|
||||
color: Appearance?.m3colors.m3onBackground ?? "white"
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import "."
|
||||
|
||||
ToolTip {
|
||||
id: root
|
||||
property bool extraVisibleCondition: true
|
||||
property bool alternativeVisibleCondition: false
|
||||
readonly property bool internalVisibleCondition: (extraVisibleCondition && (parent.hovered === undefined || parent?.hovered)) || alternativeVisibleCondition
|
||||
verticalPadding: 5
|
||||
horizontalPadding: 10
|
||||
background: null
|
||||
|
||||
visible: internalVisibleCondition
|
||||
|
||||
contentItem: StyledToolTipContent {
|
||||
id: contentItem
|
||||
text: root.text
|
||||
shown: root.internalVisibleCondition
|
||||
horizontalPadding: root.horizontalPadding
|
||||
verticalPadding: root.verticalPadding
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import QtQuick
|
||||
import "."
|
||||
import "../"
|
||||
|
||||
Item {
|
||||
id: root
|
||||
required property string text
|
||||
property bool shown: false
|
||||
property real horizontalPadding: 10
|
||||
property real verticalPadding: 5
|
||||
implicitWidth: tooltipTextObject.implicitWidth + 2 * root.horizontalPadding
|
||||
implicitHeight: tooltipTextObject.implicitHeight + 2 * root.verticalPadding
|
||||
|
||||
property bool isVisible: backgroundRectangle.implicitHeight > 0
|
||||
|
||||
Rectangle {
|
||||
id: backgroundRectangle
|
||||
anchors {
|
||||
bottom: root.bottom
|
||||
horizontalCenter: root.horizontalCenter
|
||||
}
|
||||
color: Appearance?.colors.colTooltip ?? "#3C4043"
|
||||
radius: Appearance?.rounding.verysmall ?? 7
|
||||
opacity: shown ? 1 : 0
|
||||
implicitWidth: shown ? (tooltipTextObject.implicitWidth + 2 * root.horizontalPadding) : 0
|
||||
implicitHeight: shown ? (tooltipTextObject.implicitHeight + 2 * root.verticalPadding) : 0
|
||||
clip: true
|
||||
|
||||
Behavior on implicitWidth {
|
||||
animation: Appearance?.animation.elementMoveFast.numberAnimation.createObject(this)
|
||||
}
|
||||
Behavior on implicitHeight {
|
||||
animation: Appearance?.animation.elementMoveFast.numberAnimation.createObject(this)
|
||||
}
|
||||
Behavior on opacity {
|
||||
animation: Appearance?.animation.elementMoveFast.numberAnimation.createObject(this)
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: tooltipTextObject
|
||||
anchors.centerIn: parent
|
||||
text: root.text
|
||||
font.pixelSize: Appearance?.font.pixelSize.smaller ?? 14
|
||||
font.hintingPreference: Font.PreferNoHinting
|
||||
color: Appearance?.colors.colOnTooltip ?? "#FFFFFF"
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
StyledText 1.0 StyledText.qml
|
||||
StyledRectangularShadow 1.0 StyledRectangularShadow.qml
|
||||
StyledToolTip 1.0 StyledToolTip.qml
|
||||
StyledToolTipContent 1.0 StyledToolTipContent.qml
|
||||
@@ -0,0 +1,56 @@
|
||||
// BatteryWidget.qml - battery icon + percentage
|
||||
import QtQuick
|
||||
import Quickshell.Io
|
||||
import ".."
|
||||
|
||||
Pill {
|
||||
id: root
|
||||
property int capacity: 100
|
||||
property string status: "Unknown"
|
||||
property bool charging: status === "Charging"
|
||||
property bool plugged: status === "Full" || status === "Not charging"
|
||||
property bool critical: capacity <= 15 && !charging
|
||||
|
||||
property string icon: {
|
||||
if (charging) return " "
|
||||
if (plugged) return " "
|
||||
if (capacity > 80) return ""
|
||||
if (capacity > 60) return ""
|
||||
if (capacity > 40) return ""
|
||||
if (capacity > 20) return ""
|
||||
return ""
|
||||
}
|
||||
|
||||
Text {
|
||||
text: root.icon + " " + root.capacity + "%"
|
||||
font.family: Theme.fontSans
|
||||
font.pixelSize: Theme.fontSize
|
||||
color: root.critical ? Theme.color1 : Theme.pillText
|
||||
}
|
||||
|
||||
Process {
|
||||
id: batProc
|
||||
running: false
|
||||
command: ["bash", "-c",
|
||||
"cat /sys/class/power_supply/BAT0/capacity 2>/dev/null; " +
|
||||
"echo ---; " +
|
||||
"cat /sys/class/power_supply/BAT0/status 2>/dev/null"]
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
const parts = this.text.split("---")
|
||||
if (parts.length >= 2) {
|
||||
const cap = parseInt(parts[0].trim())
|
||||
if (!isNaN(cap)) root.capacity = cap
|
||||
root.status = parts[1].trim()
|
||||
}
|
||||
}
|
||||
}
|
||||
onExited: batProc.running = false
|
||||
}
|
||||
|
||||
Timer {
|
||||
interval: 5000; running: true; repeat: true
|
||||
triggeredOnStart: true
|
||||
onTriggered: batProc.running = true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
// modules/BluetoothWidget.qml - ᛒ status / device alias
|
||||
// Uses bluetoothctl via a polled Process (no BlueZ QML bindings in QS yet).
|
||||
import QtQuick
|
||||
import Quickshell.Io
|
||||
import ".."
|
||||
|
||||
Pill {
|
||||
id: root
|
||||
property string btStatus: "off" // "off" | "on" | "connected"
|
||||
property string devAlias: ""
|
||||
|
||||
readonly property var bgColors: ({
|
||||
"off": Qt.rgba(0.565, 0.545, 0.671, 0.3), // alpha(@color3, 0.3)
|
||||
"on": Theme.color2,
|
||||
"connected": Theme.color4,
|
||||
})
|
||||
// Override Pill's own color binding
|
||||
color: bgColors[btStatus] ?? Theme.pill
|
||||
|
||||
onClicked: (m) => {
|
||||
if (m.button === Qt.LeftButton)
|
||||
Exec.run(["blueman-manager"])
|
||||
}
|
||||
|
||||
Text {
|
||||
text: {
|
||||
var s = root.btStatus
|
||||
if (s === "connected") return "ᛒ " + (root.devAlias || "connected")
|
||||
if (s === "on") return "ᛒ on"
|
||||
return "ᛒ off"
|
||||
}
|
||||
font { family: Theme.fontSans; pixelSize: Theme.fontSize }
|
||||
color: Theme.pillText
|
||||
}
|
||||
|
||||
// Poll bluetoothctl show + info every 5 s
|
||||
Process {
|
||||
id: btProc
|
||||
running: false
|
||||
command: ["bash", "-c",
|
||||
"bluetoothctl show | grep -E 'Powered|Name'; " +
|
||||
"bluetoothctl info 2>/dev/null | grep -E 'Name|Connected'"]
|
||||
stdout: SplitParser {
|
||||
onRead: (line) => {
|
||||
if (/Powered:\s+no/i.test(line)) { root.btStatus = "off"; root.devAlias = "" }
|
||||
if (/Powered:\s+yes/i.test(line)) { if (root.btStatus === "off") root.btStatus = "on" }
|
||||
if (/Connected:\s+yes/i.test(line)) root.btStatus = "connected"
|
||||
if (/Connected:\s+no/i.test(line)) { if (root.btStatus === "connected") { root.btStatus = "on"; root.devAlias = "" } }
|
||||
var match = /^\s+Name:\s+(.+)/.exec(line)
|
||||
if (match && root.btStatus === "connected") root.devAlias = match[1].trim()
|
||||
}
|
||||
}
|
||||
onExited: btProc.running = false
|
||||
}
|
||||
|
||||
Timer {
|
||||
interval: 5000; running: true; repeat: true
|
||||
triggeredOnStart: true
|
||||
onTriggered: btProc.running = true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
// CavaWidget.qml - audio visualiser via cava raw output
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell.Io
|
||||
import ".."
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property var bars: Array(12).fill(0)
|
||||
property bool silence: bars.every(v => v === 0)
|
||||
property bool allBlank: bars.every(v => v < 28.5)
|
||||
|
||||
readonly property var blocks: [" ","▁","▂","▃","▄","▅","▆","▇","█"]
|
||||
|
||||
visible: !allBlank
|
||||
implicitWidth: cavaRow.implicitWidth + Theme.pillPadH * 2
|
||||
implicitHeight: Theme.barHeight
|
||||
radius: Theme.radius
|
||||
color: cavaHover.containsMouse ? Theme.pill : Theme.pill
|
||||
Behavior on color { ColorAnimation { duration: 150 } }
|
||||
|
||||
RowLayout {
|
||||
id: cavaRow
|
||||
anchors.centerIn: parent
|
||||
spacing: 1
|
||||
|
||||
Repeater {
|
||||
model: root.bars.length
|
||||
Text {
|
||||
required property int index
|
||||
text: root.silence
|
||||
? " "
|
||||
: root.blocks[Math.min(Math.floor(root.bars[index] / 28.5), 8)]
|
||||
font.family: Theme.fontMono
|
||||
font.pixelSize: Theme.fontSize + 1
|
||||
color: Theme.pillText
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: cavaHover
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onClicked: Exec.run(["pavucontrol"])
|
||||
}
|
||||
|
||||
// Write the cava config once at startup, then run cava pointing at it.
|
||||
Component.onCompleted: writeCfg.running = true
|
||||
|
||||
Process {
|
||||
id: writeCfg
|
||||
running: false
|
||||
// Plain double-quoted string - no JS interpolation, bash sees ${VAR} verbatim.
|
||||
command: ["bash", "-c",
|
||||
"mkdir -p /tmp/qs-cava && cat > /tmp/qs-cava/cava.ini <<'CFG'\n" +
|
||||
"[general]\n" +
|
||||
"framerate = 30\n" +
|
||||
"bars = 12\n" +
|
||||
"[input]\n" +
|
||||
"method = pipewire\n" +
|
||||
"source = auto\n" +
|
||||
"[smoothing]\n" +
|
||||
"noise_reduction = 77\n" +
|
||||
"monstercat = 1\n" +
|
||||
"[output]\n" +
|
||||
"method = raw\n" +
|
||||
"raw_target = /dev/stdout\n" +
|
||||
"data_format = ascii\n" +
|
||||
"ascii_max_range = 255\n" +
|
||||
"bar_delimiter = 59\n" +
|
||||
"CFG\n"
|
||||
]
|
||||
onExited: cavaProc.running = true
|
||||
}
|
||||
|
||||
Process {
|
||||
id: cavaProc
|
||||
running: false
|
||||
command: ["cava", "-p", "/tmp/qs-cava/cava.ini"]
|
||||
stdout: SplitParser {
|
||||
onRead: (line) => {
|
||||
const parts = line.trim().replace(/;$/, "").split(";");
|
||||
if (parts.length >= root.bars.length) {
|
||||
root.bars = parts.slice(0, root.bars.length)
|
||||
.map(v => parseInt(v) || 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
onExited: cavaProc.running = true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
// modules/ClockWidget.qml - " HH:MM DD Mon" (matches waybar clock format)
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell.Io
|
||||
import ".."
|
||||
|
||||
Pill {
|
||||
onClicked: (m) => {
|
||||
if (m.button === Qt.LeftButton)
|
||||
Exec.run(["kitty", "-e", "calcure", "--class=float", "-T", "calcure"])
|
||||
}
|
||||
|
||||
Text {
|
||||
text: " " + Qt.formatDateTime(clock.now, "HH:mm") +
|
||||
" " + Qt.formatDateTime(clock.now, "d MMM")
|
||||
font { family: Theme.fontSans; pixelSize: Theme.fontSize }
|
||||
color: Theme.pillText
|
||||
}
|
||||
|
||||
// Update every 10 s (no need for per-second ticks)
|
||||
QtObject {
|
||||
id: clock
|
||||
property var now: new Date()
|
||||
property var timer: Timer {
|
||||
interval: 10000; running: true; repeat: true
|
||||
triggeredOnStart: true
|
||||
onTriggered: clock.now = new Date()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
// CpuWidget.qml - "X.XGHz | Y%"
|
||||
import QtQuick
|
||||
import Quickshell.Io
|
||||
import ".."
|
||||
|
||||
Pill {
|
||||
id: root
|
||||
property real freqGhz: 0
|
||||
property int usagePct: 0
|
||||
|
||||
property int prevIdle: 0
|
||||
property int prevTotal: 0
|
||||
|
||||
onClicked: (m) => {
|
||||
if (m.button === Qt.LeftButton) Exec.run(["kitty", "-e", "btop"])
|
||||
}
|
||||
|
||||
Text {
|
||||
text: root.freqGhz.toFixed(1) + "GHz | " + root.usagePct + "%"
|
||||
font.family: Theme.fontSans
|
||||
font.pixelSize: Theme.fontSize
|
||||
color: Theme.pillText
|
||||
}
|
||||
|
||||
// /proc/stat - first line is total CPU
|
||||
Process {
|
||||
id: statProc
|
||||
running: false
|
||||
command: ["bash", "-c", "head -1 /proc/stat && cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq 2>/dev/null"]
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
const lines = this.text.split("\n")
|
||||
// line 0: cpu user nice system idle iowait irq softirq steal
|
||||
const nums = lines[0].replace(/^cpu\s+/, "").split(/\s+/).map(Number)
|
||||
const idle = (nums[3] || 0) + (nums[4] || 0)
|
||||
const total = nums.reduce((s, v) => s + v, 0)
|
||||
const dIdle = idle - root.prevIdle
|
||||
const dTotal = total - root.prevTotal
|
||||
if (dTotal > 0) root.usagePct = Math.round((1 - dIdle / dTotal) * 100)
|
||||
root.prevIdle = idle
|
||||
root.prevTotal = total
|
||||
// line 1: current frequency in kHz
|
||||
const khz = parseInt(lines[1] || "0")
|
||||
if (!isNaN(khz) && khz > 0) root.freqGhz = khz / 1e6
|
||||
}
|
||||
}
|
||||
onExited: statProc.running = false
|
||||
}
|
||||
|
||||
Timer {
|
||||
interval: 1500; running: true; repeat: true
|
||||
triggeredOnStart: true
|
||||
onTriggered: statProc.running = true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,248 @@
|
||||
// MediaCavaWidget.qml - cava visualiser + media info + volume in one pill
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell.Io
|
||||
import Quickshell.Services.Mpris
|
||||
import ".."
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
// ── Cava ──────────────────────────────────────────────────
|
||||
property var bars: Array(12).fill(0)
|
||||
property bool silence: bars.every(v => v === 0)
|
||||
property bool allBlank: bars.every(v => v < 28.5)
|
||||
property bool silentMode: false
|
||||
readonly property var blocks: [" ","▁","▂","▃","▄","▅","▆","▇","█"]
|
||||
|
||||
// ── Audio (wpctl - avoids PipeWire binding issues) ────────
|
||||
property real vol: 0.0
|
||||
property bool muted: false
|
||||
property string volIcon: {
|
||||
if (muted || vol === 0) return " "
|
||||
if (vol < 0.34) return " "
|
||||
if (vol < 0.67) return " "
|
||||
return " "
|
||||
}
|
||||
|
||||
// ── Media ─────────────────────────────────────────────────
|
||||
property MprisPlayer activePlayer: {
|
||||
var players = Mpris.players.values
|
||||
for (var i = 0; i < players.length; i++)
|
||||
if (players[i].identity.toLowerCase() === "spotify") return players[i]
|
||||
return players.length > 0 ? players[0] : null
|
||||
}
|
||||
|
||||
property string trackText: {
|
||||
if (!activePlayer) return ""
|
||||
var p = activePlayer
|
||||
var info = ""
|
||||
if (p.trackArtists && p.trackTitle)
|
||||
info = Array.from(p.trackArtists).join(", ") + " - " + p.trackTitle
|
||||
else if (p.trackTitle)
|
||||
info = p.trackTitle
|
||||
if (info.length > 45) info = info.substring(0, 45) + "..."
|
||||
if (p.playbackState !== MprisPlaybackState.Playing && info)
|
||||
info = " " + info
|
||||
return info + " "
|
||||
}
|
||||
|
||||
// ── Silence delay ─────────────────────────────────────────
|
||||
onAllBlankChanged: {
|
||||
if (allBlank) silenceTimer.start()
|
||||
else { silenceTimer.stop(); silentMode = false }
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: silenceTimer
|
||||
interval: Theme.cavaDissapearTime
|
||||
repeat: false
|
||||
onTriggered: root.silentMode = true
|
||||
}
|
||||
|
||||
// ── Hover → media popup ───────────────────────────────────
|
||||
property bool hovered: mouseArea.containsMouse
|
||||
onHoveredChanged: {
|
||||
if (hovered) GlobalStates.popups.open("media", mapToItem(null, width / 2, 0).x)
|
||||
}
|
||||
|
||||
// ── Appearance ────────────────────────────────────────────
|
||||
implicitHeight: Theme.barHeight
|
||||
implicitWidth: contentRow.implicitWidth + Theme.pillPadH * 2
|
||||
radius: Theme.radius
|
||||
clip: true
|
||||
color: root.hovered ? Theme.pillHover : Theme.pill
|
||||
Behavior on color { ColorAnimation { duration: 150 } }
|
||||
|
||||
// Volume fill - slightly more opaque than background, fills left→right by volume %
|
||||
Rectangle {
|
||||
anchors { left: parent.left; top: parent.top; bottom: parent.bottom }
|
||||
width: parent.width * Math.min(root.vol, 1.0)
|
||||
radius: Theme.radius
|
||||
color: root.hovered ? Theme.setColorAlpha(Theme.pill, 0.3) : Theme.setColorAlpha(Theme.pill, 0.9)
|
||||
Behavior on width { NumberAnimation { duration: 150; easing.type: Easing.OutCubic } }
|
||||
}
|
||||
|
||||
// ── Content ───────────────────────────────────────────────
|
||||
RowLayout {
|
||||
id: contentRow
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.pillPadH
|
||||
spacing: 4
|
||||
|
||||
// Cava bars -d slides right and collapses after silence delay
|
||||
Item {
|
||||
id: cavaContainer
|
||||
clip: true
|
||||
implicitHeight: Theme.barHeight
|
||||
implicitWidth: root.silentMode ? 0 : cavaRow.implicitWidth
|
||||
Behavior on implicitWidth {
|
||||
NumberAnimation { duration: 350; easing.type: Easing.InOutCubic }
|
||||
}
|
||||
|
||||
FontMetrics {
|
||||
id: barMetrics
|
||||
font.family: Theme.fontMono
|
||||
font.pixelSize: Theme.fontSize + 1
|
||||
}
|
||||
|
||||
Row {
|
||||
id: cavaRow
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 1
|
||||
x: root.silentMode ? 16 : 0
|
||||
Behavior on x {
|
||||
NumberAnimation { duration: 350; easing.type: Easing.InOutCubic }
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: root.bars.length
|
||||
Text {
|
||||
required property int index
|
||||
width: barMetrics.advanceWidth("█")
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: root.silence
|
||||
? " "
|
||||
: root.blocks[Math.min(Math.floor(root.bars[index] / 28.5), 8)]
|
||||
font { family: Theme.fontMono; pixelSize: Theme.fontSize + 1 }
|
||||
color: Theme.pillText
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Track info
|
||||
Text {
|
||||
visible: root.trackText !== ""
|
||||
text: root.trackText
|
||||
font { family: Theme.fontSans; pixelSize: Theme.fontSize }
|
||||
color: Theme.pillText
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: 1
|
||||
}
|
||||
|
||||
// Volume icon
|
||||
Text {
|
||||
text: root.volIcon
|
||||
font { family: Theme.fontMono; pixelSize: Theme.fontSize }
|
||||
color: "#fab387"
|
||||
}
|
||||
|
||||
// Volume percentage
|
||||
Text {
|
||||
text: root.muted ? "muted" : Math.round(root.vol * 100) + "%"
|
||||
font { family: Theme.fontSans; pixelSize: Theme.fontSize }
|
||||
color: Theme.pillText
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
|
||||
onClicked: (m) => {
|
||||
if (m.button === Qt.RightButton) {
|
||||
Exec.run(["wpctl", "set-mute", "@DEFAULT_AUDIO_SINK@", "toggle"])
|
||||
root.muted = !root.muted
|
||||
}
|
||||
}
|
||||
|
||||
onWheel: (w) => {
|
||||
var step = w.angleDelta.y > 0 ? "4%+" : "4%-"
|
||||
Exec.run(["wpctl", "set-volume", "@DEFAULT_AUDIO_SINK@", step])
|
||||
// Optimistic update - poll syncs it within 1s
|
||||
root.vol = Math.max(0, Math.min(1.5, root.vol + (w.angleDelta.y > 0 ? 0.04 : -0.04)))
|
||||
}
|
||||
}
|
||||
|
||||
// ── Volume poll (wpctl) ───────────────────────────────────
|
||||
Process {
|
||||
id: volProc
|
||||
running: false
|
||||
command: ["wpctl", "get-volume", "@DEFAULT_AUDIO_SINK@"]
|
||||
stdout: SplitParser {
|
||||
onRead: (line) => {
|
||||
// "Volume: 0.40" or "Volume: 0.40 [MUTED]"
|
||||
var m = /Volume:\s+([\d.]+)(.*)/.exec(line)
|
||||
if (m) {
|
||||
root.vol = parseFloat(m[1])
|
||||
root.muted = m[2].includes("MUTED")
|
||||
}
|
||||
}
|
||||
}
|
||||
onExited: volProc.running = false
|
||||
}
|
||||
|
||||
Timer {
|
||||
interval: 1000
|
||||
running: true
|
||||
repeat: true
|
||||
triggeredOnStart: true
|
||||
onTriggered: volProc.running = true
|
||||
}
|
||||
|
||||
// ── Cava process ──────────────────────────────────────────
|
||||
Component.onCompleted: writeCfg.running = true
|
||||
|
||||
Process {
|
||||
id: writeCfg
|
||||
running: false
|
||||
command: ["bash", "-c",
|
||||
"mkdir -p /tmp/qs-cava && cat > /tmp/qs-cava/cava.ini <<'CFG'\n" +
|
||||
"[general]\n" +
|
||||
"framerate = 30\n" +
|
||||
"bars = 12\n" +
|
||||
"[input]\n" +
|
||||
"method = pipewire\n" +
|
||||
"source = auto\n" +
|
||||
"[smoothing]\n" +
|
||||
"noise_reduction = 77\n" +
|
||||
"monstercat = 1\n" +
|
||||
"[output]\n" +
|
||||
"method = raw\n" +
|
||||
"raw_target = /dev/stdout\n" +
|
||||
"data_format = ascii\n" +
|
||||
"ascii_max_range = 255\n" +
|
||||
"bar_delimiter = 59\n" +
|
||||
"CFG\n"
|
||||
]
|
||||
onExited: cavaProc.running = true
|
||||
}
|
||||
|
||||
Process {
|
||||
id: cavaProc
|
||||
running: false
|
||||
command: ["cava", "-p", "/tmp/qs-cava/cava.ini"]
|
||||
stdout: SplitParser {
|
||||
onRead: (line) => {
|
||||
const parts = line.trim().replace(/;$/, "").split(";")
|
||||
if (parts.length >= root.bars.length)
|
||||
root.bars = parts.slice(0, root.bars.length).map(v => parseInt(v) || 0)
|
||||
}
|
||||
}
|
||||
onExited: cavaProc.running = true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
// MediaCavaWidget.qml - cava visualiser + media info + volume in one pill
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell.Io
|
||||
import Quickshell.Services.Mpris
|
||||
import ".."
|
||||
|
||||
Popup {
|
||||
id: root
|
||||
popupName: "media"
|
||||
|
||||
property MprisPlayer player: {
|
||||
var players = Mpris.players.values
|
||||
for (var i = 0; i < players.length; i++)
|
||||
if (players[i].identity.toLowerCase() === "spotify") return players[i]
|
||||
return players.length > 0 ? players[0] : null
|
||||
}
|
||||
|
||||
property bool isPlaying: player?.playbackState === MprisPlaybackState.Playing ?? false
|
||||
property real displayPosition: 0
|
||||
|
||||
Timer {
|
||||
interval: 1000
|
||||
running: root.visible && root.player !== null
|
||||
repeat: true
|
||||
triggeredOnStart: true
|
||||
onTriggered: root.displayPosition = root.player?.position ?? 0
|
||||
}
|
||||
|
||||
function formatTime(secs) {
|
||||
secs = Math.max(0, Math.floor(secs))
|
||||
return Math.floor(secs / 60) + ":" + String(secs % 60).padStart(2, "0")
|
||||
}
|
||||
|
||||
component CtrlBtn: Text {
|
||||
property string icon: ""
|
||||
property color baseColor: Theme.pillText
|
||||
property color hoverColor: Theme.pillHover
|
||||
signal activate()
|
||||
text: icon
|
||||
font { family: Theme.fontMono; pixelSize: 20 }
|
||||
color: ma.containsMouse ? hoverColor : baseColor
|
||||
Behavior on color { ColorAnimation { duration: 100 } }
|
||||
MouseArea {
|
||||
id: ma
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onClicked: parent.activate()
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
width: 200
|
||||
spacing: 8
|
||||
|
||||
// Album art - hidden when no art is available
|
||||
Rectangle {
|
||||
visible: artImage.status === Image.Ready
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.preferredWidth: 180
|
||||
Layout.preferredHeight: artImage.status === Image.Ready ? 180 : 0
|
||||
Layout.topMargin: artImage.status === Image.Ready ? 10 : 0
|
||||
radius: Theme.radius
|
||||
clip: true
|
||||
color: Theme.pill
|
||||
|
||||
Image {
|
||||
id: artImage
|
||||
anchors.fill: parent
|
||||
source: root.player?.trackArtUrl ?? ""
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
smooth: true
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (root.player)
|
||||
Exec.run(["hyprctl", "dispatch", "hl.dsp.focus({ window = 'class:" + root.player.identity + "' })"])
|
||||
GlobalStates.popups.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Track title with marquee scroll
|
||||
Item {
|
||||
id: titleClip
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 15
|
||||
Layout.rightMargin: 15
|
||||
Layout.topMargin: 4
|
||||
implicitHeight: titleText.implicitHeight
|
||||
clip: true
|
||||
|
||||
Text {
|
||||
id: titleText
|
||||
text: root.player?.trackTitle ?? "Nothing playing"
|
||||
font { family: Theme.fontSans; pixelSize: Theme.fontSize + 1; bold: true }
|
||||
color: titleMa.containsMouse && root.player ? Qt.lighter(Theme.pillText, 1.3) : Theme.pillText
|
||||
Behavior on color { ColorAnimation { duration: 100 } }
|
||||
|
||||
MouseArea {
|
||||
id: titleMa
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: root.player ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
onClicked: if (root.player) {
|
||||
Exec.run(["hyprctl", "dispatch", "hl.dsp.focus({ window = 'class:" + root.player.identity + "' })"])
|
||||
GlobalStates.popups.close()
|
||||
}
|
||||
}
|
||||
|
||||
onTextChanged: {
|
||||
marquee.stop()
|
||||
x = 0
|
||||
Qt.callLater(() => {
|
||||
if (implicitWidth > titleClip.width) marquee.start()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: Qt.callLater(() => {
|
||||
if (titleText.implicitWidth > width) marquee.start()
|
||||
})
|
||||
|
||||
SequentialAnimation {
|
||||
id: marquee
|
||||
loops: Animation.Infinite
|
||||
|
||||
PauseAnimation { duration: 1500 }
|
||||
NumberAnimation {
|
||||
target: titleText; property: "x"
|
||||
from: 0
|
||||
to: -(titleText.implicitWidth - titleClip.width + 10)
|
||||
duration: Math.max(1, titleText.implicitWidth - titleClip.width) * 18
|
||||
easing.type: Easing.Linear
|
||||
}
|
||||
PauseAnimation { duration: 800 }
|
||||
NumberAnimation {
|
||||
target: titleText; property: "x"
|
||||
to: 0; duration: 400; easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Artist
|
||||
Text {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 15
|
||||
Layout.rightMargin: 15
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: root.player?.trackArtists ? Array.from(root.player.trackArtists).join(", ") : ""
|
||||
font { family: Theme.fontSans; pixelSize: Theme.fontSize }
|
||||
color: Theme.pillText
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
// Progress bar
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 15
|
||||
Layout.rightMargin: 15
|
||||
height: 3
|
||||
radius: 2
|
||||
color: Theme.pill
|
||||
|
||||
Rectangle {
|
||||
width: root.player && root.player.length > 0
|
||||
? Math.min(1, root.displayPosition / root.player.length) * parent.width
|
||||
: 0
|
||||
height: parent.height
|
||||
radius: parent.radius
|
||||
color: Theme.color6
|
||||
}
|
||||
}
|
||||
|
||||
// Time labels
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 15
|
||||
Layout.rightMargin: 15
|
||||
Text {
|
||||
text: root.formatTime(root.displayPosition)
|
||||
font { family: Theme.fontMono; pixelSize: Theme.fontSize - 1 }
|
||||
color: Theme.pillText
|
||||
}
|
||||
Item { Layout.fillWidth: true }
|
||||
Text {
|
||||
text: root.formatTime(root.player?.length ?? 0)
|
||||
font { family: Theme.fontMono; pixelSize: Theme.fontSize - 1 }
|
||||
color: Theme.pillText
|
||||
}
|
||||
}
|
||||
|
||||
// Controls
|
||||
RowLayout {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.bottomMargin: 10
|
||||
spacing: 20
|
||||
CtrlBtn { text: ""; onActivate: root.player?.previous() }
|
||||
CtrlBtn { text: root.isPlaying ? "" : ""; onActivate: root.player?.togglePlaying() }
|
||||
CtrlBtn { text: ""; onActivate: root.player?.stop() }
|
||||
CtrlBtn { text: ""; onActivate: root.player?.next() }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
// modules/MediaWidget.qml
|
||||
// Mirrors waybar custom/spotify - uses MPRIS via Quickshell.Services.Mpris.
|
||||
// Shows: artist - title + spotify icon. Click to play/pause, scroll to skip.
|
||||
import QtQuick
|
||||
import Quickshell.Services.Mpris
|
||||
import ".."
|
||||
|
||||
Pill {
|
||||
id: root
|
||||
|
||||
// Pick the first active player (prefer spotify)
|
||||
property MprisPlayer activePlayer: {
|
||||
var players = Mpris.players.values
|
||||
for (var i = 0; i < players.length; i++)
|
||||
if (players[i].identity.toLowerCase() === "spotify") return players[i]
|
||||
return players.length > 0 ? players[0] : null
|
||||
}
|
||||
|
||||
property string trackText: {
|
||||
if (!activePlayer) return ""
|
||||
var p = activePlayer
|
||||
var info = ""
|
||||
if (p.trackArtists && p.trackTitle)
|
||||
info = Array.from(p.trackArtists).join(", ") + " - " + p.trackTitle
|
||||
else if (p.trackTitle)
|
||||
info = p.trackTitle
|
||||
if (info.length > 45) info = info.substring(0, 45) + "..."
|
||||
if (p.playbackState !== MprisPlaybackState.Playing && info)
|
||||
info = " " + info
|
||||
return info + " " // trailing Nerd Font Spotify icon
|
||||
}
|
||||
|
||||
visible: trackText !== ""
|
||||
|
||||
Text {
|
||||
text: root.trackText
|
||||
font { family: Theme.fontSans; pixelSize: Theme.fontSize }
|
||||
color: Theme.pillText
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: 1
|
||||
}
|
||||
|
||||
onClicked: (m) => {
|
||||
if (!root.activePlayer) return
|
||||
if (m.button === Qt.LeftButton)
|
||||
root.activePlayer.togglePlaying()
|
||||
}
|
||||
|
||||
onScrolled: (w) => {
|
||||
if (!root.activePlayer) return
|
||||
if (w.angleDelta.y > 0) root.activePlayer.next()
|
||||
else root.activePlayer.previous()
|
||||
}
|
||||
|
||||
onHoveredChanged: {
|
||||
if (hovered) GlobalStates.popups.open("media", mapToItem(null, width / 2, 0).x)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
// MemoryWidget.qml - " X.XX / Y GB"
|
||||
import QtQuick
|
||||
import Quickshell.Io
|
||||
import ".."
|
||||
|
||||
Pill {
|
||||
id: root
|
||||
property real usedGb: 0
|
||||
property real totalGb: 0
|
||||
|
||||
onClicked: (m) => {
|
||||
if (m.button === Qt.LeftButton) Exec.run(["kitty", "-e", "btop"])
|
||||
}
|
||||
|
||||
Text {
|
||||
text: " " + root.usedGb.toFixed(2) + " / " + root.totalGb.toFixed(0) + " GB"
|
||||
font.family: Theme.fontSans
|
||||
font.pixelSize: Theme.fontSize
|
||||
color: Theme.pillText
|
||||
}
|
||||
|
||||
Process {
|
||||
id: memProc
|
||||
running: false
|
||||
command: ["cat", "/proc/meminfo"]
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
const text = this.text
|
||||
const total = parseInt((/MemTotal:\s+(\d+)/.exec(text) || [])[1] || "0")
|
||||
const avail = parseInt((/MemAvailable:\s+(\d+)/.exec(text) || [])[1] || "0")
|
||||
root.totalGb = total / 1048576
|
||||
root.usedGb = (total - avail) / 1048576
|
||||
}
|
||||
}
|
||||
onExited: memProc.running = false
|
||||
}
|
||||
|
||||
Timer {
|
||||
interval: 5000; running: true; repeat: true
|
||||
triggeredOnStart: true
|
||||
onTriggered: memProc.running = true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// modules/PowerMenuWidget.qml - ⏻ button with inline popup menu.
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell.Io
|
||||
import ".."
|
||||
|
||||
Pill {
|
||||
id: root
|
||||
onHoveredChanged: {
|
||||
if (hovered) GlobalStates.popups.toggle("powerMenu", mapToItem(null, width / 2, 0).x)
|
||||
}
|
||||
onClicked: {
|
||||
GlobalStates.popups.toggle("powerMenu", mapToItem(null, width / 2, 0).x)
|
||||
}
|
||||
Text {
|
||||
text: " "
|
||||
font { family: Theme.fontMono; pixelSize: Theme.fontSize }
|
||||
color: Theme.pillText
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
// PowerProfilesWidget.qml - ⚡/⚖/🔋 + click-to-cycle
|
||||
import QtQuick
|
||||
import Quickshell.Io
|
||||
import ".."
|
||||
|
||||
Pill {
|
||||
id: root
|
||||
property string profile: "balanced"
|
||||
|
||||
readonly property var profileOrder: ["performance", "balanced", "power-saver"]
|
||||
readonly property var icons: ({
|
||||
"performance": "⚡",
|
||||
"balanced": "⚖",
|
||||
"power-saver": "🔋",
|
||||
})
|
||||
|
||||
onClicked: (m) => {
|
||||
if (m.button !== Qt.LeftButton) return;
|
||||
const i = profileOrder.indexOf(root.profile);
|
||||
const next = profileOrder[(i + 1) % profileOrder.length];
|
||||
setProc.command = ["powerprofilesctl", "set", next];
|
||||
setProc.running = true;
|
||||
root.profile = next; // optimistic update
|
||||
}
|
||||
|
||||
Text {
|
||||
text: (root.icons[root.profile] ?? "⚡")
|
||||
font.family: Theme.fontSans
|
||||
font.pixelSize: Theme.fontSize
|
||||
color: Theme.pillText
|
||||
}
|
||||
|
||||
// Read current profile periodically
|
||||
Process {
|
||||
id: readProc
|
||||
running: false
|
||||
command: ["powerprofilesctl", "get"]
|
||||
stdout: SplitParser {
|
||||
onRead: (line) => root.profile = line.trim()
|
||||
}
|
||||
onExited: readProc.running = false
|
||||
}
|
||||
|
||||
// Setter - command is rewritten on each click
|
||||
Process {
|
||||
id: setProc
|
||||
running: false
|
||||
command: ["true"]
|
||||
onExited: setProc.running = false
|
||||
}
|
||||
|
||||
Timer {
|
||||
interval: 2000
|
||||
running: true
|
||||
repeat: true
|
||||
triggeredOnStart: true
|
||||
onTriggered: readProc.running = true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
// modules/SysTrayWidget.qml - SNI system tray (nm-applet, blueman ...)
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell.Services.SystemTray
|
||||
import Quickshell
|
||||
import ".."
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
required property var parentWindow
|
||||
color: "transparent"
|
||||
radius: Theme.radius
|
||||
|
||||
implicitWidth: trayRow.implicitWidth + 6
|
||||
implicitHeight: Theme.barHeight
|
||||
|
||||
RowLayout {
|
||||
id: trayRow
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacing
|
||||
|
||||
Repeater {
|
||||
model: SystemTray.items
|
||||
|
||||
Rectangle {
|
||||
id: trayRect
|
||||
required property SystemTrayItem modelData
|
||||
width: Theme.barHeight-Theme.barPadding*2; height: Theme.barHeight-Theme.barPadding*2
|
||||
radius: Theme.radius
|
||||
color: trayHover.containsMouse
|
||||
? Theme.pillHover
|
||||
: Theme.pill
|
||||
|
||||
Behavior on color { ColorAnimation { duration: 150 } }
|
||||
|
||||
Image {
|
||||
anchors { fill: parent; margins: 4 }
|
||||
source: modelData.icon
|
||||
fillMode: Image.PreserveAspectFit
|
||||
smooth: true
|
||||
}
|
||||
|
||||
QsMenuAnchor {
|
||||
id: menuAnchor
|
||||
menu: modelData.menu
|
||||
anchor.window: root.parentWindow
|
||||
anchor.rect: Qt.rect(
|
||||
trayRect.mapToItem(null, 0, 0).x,
|
||||
trayRect.mapToItem(null, 0, 0).y,
|
||||
trayRect.width,
|
||||
trayRect.height
|
||||
)
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: trayHover
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
onClicked: (m) => {
|
||||
if (m.button === Qt.LeftButton)
|
||||
modelData.activate()
|
||||
else if (modelData.hasMenu)
|
||||
menuAnchor.open()
|
||||
}
|
||||
}
|
||||
|
||||
// Attention indicator dot
|
||||
Rectangle {
|
||||
visible: modelData.status === SystemTrayItem.NeedsAttention
|
||||
width: 5; height: 5; radius: 2.5
|
||||
color: Theme.wsUrgent
|
||||
anchors { bottom: parent.bottom; right: parent.right; margins: 1 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
// TemperatureWidget.qml - CPU package temperature
|
||||
import QtQuick
|
||||
import Quickshell.Io
|
||||
import ".."
|
||||
|
||||
Pill {
|
||||
id: root
|
||||
property int tempC: 0
|
||||
property bool critical: tempC >= 80
|
||||
|
||||
property string icon: tempC < 50 ? "" : tempC < 70 ? "" : ""
|
||||
|
||||
onClicked: (m) => {
|
||||
if (m.button === Qt.LeftButton) Exec.run(["xsensors"])
|
||||
}
|
||||
|
||||
Text {
|
||||
text: root.icon + " " + root.tempC + "°C"
|
||||
font.family: Theme.fontSans
|
||||
font.pixelSize: Theme.fontSize
|
||||
color: root.critical ? Theme.color1 : Theme.pillText
|
||||
}
|
||||
|
||||
// Read first available CPU package sensor - works regardless of hwmon number
|
||||
Process {
|
||||
id: tempProc
|
||||
running: false
|
||||
command: ["bash", "-c", "cat /sys/class/hwmon/hwmon*/temp1_input 2>/dev/null | head -1"]
|
||||
stdout: SplitParser {
|
||||
onRead: (line) => {
|
||||
const raw = parseInt(line.trim())
|
||||
if (!isNaN(raw)) root.tempC = Math.round(raw / 1000)
|
||||
}
|
||||
}
|
||||
onExited: tempProc.running = false
|
||||
}
|
||||
|
||||
Timer {
|
||||
interval: 4000; running: true; repeat: true
|
||||
triggeredOnStart: true
|
||||
onTriggered: tempProc.running = true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
// WallpaperPopup.qml - A popup for browsing and setting wallpapers from /usr/share/wallpapers
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Wayland
|
||||
import ".."
|
||||
|
||||
PanelWindow {
|
||||
id: root
|
||||
required property var screen
|
||||
|
||||
property bool logicalOpen: GlobalStates.popups.active === "wallpaper"
|
||||
property bool _keepVisible: false
|
||||
visible: logicalOpen || _keepVisible
|
||||
|
||||
onLogicalOpenChanged: {
|
||||
if (logicalOpen) {
|
||||
closeTimer.stop()
|
||||
_keepVisible = false
|
||||
if (wallpaperModel.count === 0) scanProc.running = true
|
||||
openY.start()
|
||||
openOpacity.start()
|
||||
focusTimer.start()
|
||||
} else {
|
||||
_keepVisible = true
|
||||
closeY.start()
|
||||
closeOpacity.start()
|
||||
closeTimer.restart()
|
||||
}
|
||||
}
|
||||
|
||||
Timer { id: closeTimer; interval: 280; onTriggered: root._keepVisible = false }
|
||||
Timer { id: focusTimer; interval: 50; onTriggered: grid.forceActiveFocus() }
|
||||
|
||||
WlrLayershell.layer: WlrLayer.Overlay
|
||||
WlrLayershell.namespace: "quickshell-popup-wallpaper"
|
||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand
|
||||
anchors { top: true; left: true; right: true; bottom: true }
|
||||
color: Qt.rgba(0, 0, 0, 0)
|
||||
|
||||
MouseArea { anchors.fill: parent; onClicked: GlobalStates.popups.close() }
|
||||
|
||||
NumberAnimation { id: openY; target: box; property: "y"; to: 1; duration: 250; easing.type: Easing.OutCubic }
|
||||
NumberAnimation { id: closeY; target: box; property: "y"; to: -340; duration: 200; easing.type: Easing.InCubic }
|
||||
NumberAnimation { id: openOpacity; target: box; property: "opacity"; to: Theme.popupOpacity; duration: 200 }
|
||||
NumberAnimation { id: closeOpacity; target: box; property: "opacity"; to: 0.0; duration: 200 }
|
||||
|
||||
// ── Named-pipe IPC ────────────────────────────────────────
|
||||
// Trigger from Hyprland keybind: echo 1 > /tmp/qs-wallpaper-ipc
|
||||
// Trigger from another QML popup: Exec.run(["bash", "-c", "echo 1 > /tmp/qs-wallpaper-ipc"])
|
||||
Process {
|
||||
id: ipcReader
|
||||
running: true
|
||||
command: ["bash", "-c",
|
||||
"rm -f /tmp/qs-wallpaper-ipc && " +
|
||||
"mkfifo /tmp/qs-wallpaper-ipc && " +
|
||||
"while true; do cat /tmp/qs-wallpaper-ipc; done"]
|
||||
stdout: SplitParser {
|
||||
onRead: () => GlobalStates.popups.toggle("wallpaper", 0)
|
||||
}
|
||||
onExited: running = true
|
||||
}
|
||||
|
||||
// ── Wallpaper scanner ─────────────────────────────────────
|
||||
ListModel { id: wallpaperModel }
|
||||
|
||||
Process {
|
||||
id: scanProc
|
||||
running: false
|
||||
command: ["bash", "-c",
|
||||
"find /usr/share/wallpapers -type f " +
|
||||
"\\( -iname '*.jpg' -o -iname '*.jpeg' -o -iname '*.png' -o -iname '*.webp' \\) " +
|
||||
"| sort"]
|
||||
stdout: SplitParser {
|
||||
onRead: (line) => {
|
||||
var p = line.trim()
|
||||
if (p) wallpaperModel.append({ path: p })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process { id: setProc; running: false }
|
||||
|
||||
// ── Popup box ─────────────────────────────────────────────
|
||||
Rectangle {
|
||||
id: box
|
||||
|
||||
// anchorX = 0 → centered; anchorX > 0 → centered under widget
|
||||
x: {
|
||||
var ax = GlobalStates.popups.anchorX
|
||||
var cx = ax > 0 ? ax - width / 2 : parent.width / 2 - width / 2
|
||||
return Math.max(4, Math.min(parent.width - width - 4, cx))
|
||||
}
|
||||
y: -340
|
||||
opacity: 0.0
|
||||
|
||||
width: 324
|
||||
height: 500
|
||||
|
||||
color: Theme.popupBackground
|
||||
radius: Theme.radius * 2
|
||||
clip: true
|
||||
border.color: Theme.popupBorderColor
|
||||
border.width: Theme.popupBorderWidth
|
||||
|
||||
MouseArea { anchors.fill: parent }
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
visible: wallpaperModel.count === 0
|
||||
text: "Scanning..."
|
||||
font { family: Theme.fontSans; pixelSize: Theme.fontSize }
|
||||
color: Theme.pillText
|
||||
opacity: 0.5
|
||||
}
|
||||
|
||||
GridView {
|
||||
id: grid
|
||||
anchors { fill: parent; margins: 8 }
|
||||
clip: true
|
||||
cellWidth: Math.floor(width / 2)
|
||||
cellHeight: 104
|
||||
focus: true
|
||||
keyNavigationEnabled: true
|
||||
currentIndex: 0
|
||||
|
||||
Keys.onReturnPressed: {
|
||||
var item = wallpaperModel.get(grid.currentIndex)
|
||||
if (item) {
|
||||
setProc.command = ["waypaper", "--wallpaper", item.path]
|
||||
setProc.running = true
|
||||
GlobalStates.popups.close()
|
||||
}
|
||||
}
|
||||
Keys.onEscapePressed: GlobalStates.popups.close()
|
||||
|
||||
model: wallpaperModel
|
||||
|
||||
highlight: Rectangle {
|
||||
z: 2
|
||||
radius: Theme.radius + 1
|
||||
color: "transparent"
|
||||
border.color: Theme.accent ?? "#cba6f7"
|
||||
border.width: 2
|
||||
}
|
||||
highlightFollowsCurrentItem: true
|
||||
highlightMoveDuration: 80
|
||||
|
||||
delegate: Item {
|
||||
required property string path
|
||||
required property int index
|
||||
|
||||
width: grid.cellWidth - 4
|
||||
height: 100
|
||||
|
||||
Rectangle {
|
||||
anchors { fill: parent; margins: 2 }
|
||||
radius: Theme.radius
|
||||
color: "black"
|
||||
clip: true
|
||||
|
||||
Image {
|
||||
anchors.fill: parent
|
||||
source: "file://" + path
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
sourceSize: Qt.size(152, 96)
|
||||
smooth: true
|
||||
asynchronous: true
|
||||
opacity: status === Image.Ready ? 1.0 : 0.0
|
||||
Behavior on opacity { NumberAnimation { duration: 150 } }
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: Theme.radius
|
||||
color: hover.containsMouse ? Qt.rgba(1, 1, 1, 0.2) : "transparent"
|
||||
Behavior on color { ColorAnimation { duration: 100 } }
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: hover
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
grid.currentIndex = index
|
||||
setProc.command = ["waypaper", "--wallpaper", path]
|
||||
setProc.running = true
|
||||
GlobalStates.popups.close()
|
||||
}
|
||||
onEntered: grid.currentIndex = index
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
// WallpaperWidget.qml - pill that opens the wallpaper changer popup
|
||||
import QtQuick
|
||||
import ".."
|
||||
|
||||
Pill {
|
||||
id: root
|
||||
onClicked: GlobalStates.popups.toggle("wallpaper", mapToItem(null, width / 2, 0).x)
|
||||
|
||||
Text {
|
||||
text: " "
|
||||
font { family: Theme.fontMono; pixelSize: Theme.fontSize }
|
||||
color: Theme.pillText
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
// modules/WeatherWidget.qml - wttr.in one-liner, refreshed hourly
|
||||
import QtQuick
|
||||
import Quickshell.Io
|
||||
import ".."
|
||||
|
||||
Pill {
|
||||
id: root
|
||||
property string weatherText: "..."
|
||||
|
||||
Text {
|
||||
text: weatherText
|
||||
font { family: Theme.fontSans; pixelSize: Theme.fontSize }
|
||||
color: Theme.pillText
|
||||
}
|
||||
|
||||
// ── Fetch via curl ────────────────────────────────────────
|
||||
Process {
|
||||
id: curl
|
||||
command: ["curl", "-s", "--max-time", "8", "https://wttr.in/?format=1"]
|
||||
running: false
|
||||
stdout: SplitParser {
|
||||
onRead: (line) => root.weatherText = line.trim()
|
||||
}
|
||||
onExited: curl.running = false
|
||||
}
|
||||
|
||||
Timer {
|
||||
interval: 3600000 // 1 hour
|
||||
running: true
|
||||
repeat: true
|
||||
triggeredOnStart: true
|
||||
onTriggered: curl.running = true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
// modules/WindowTitleWidget.qml - hyprland/window equivalent
|
||||
import QtQuick
|
||||
import Quickshell.Hyprland
|
||||
import ".."
|
||||
|
||||
Pill {
|
||||
id: root
|
||||
required property var screen
|
||||
|
||||
clip: true
|
||||
|
||||
Behavior on implicitWidth {
|
||||
NumberAnimation { duration: 250; easing.type: Easing.InOutCubic }
|
||||
}
|
||||
|
||||
readonly property string displayTitle: {
|
||||
let rawTitle = Hyprland.activeToplevel ? Hyprland.activeToplevel.title : "";
|
||||
if (rawTitle.length > 45) {
|
||||
return rawTitle.substring(0, Theme.titleLength) + "...";
|
||||
}
|
||||
return rawTitle;
|
||||
}
|
||||
Text {
|
||||
text: root.displayTitle
|
||||
font { family: Theme.fontSans; pixelSize: Theme.fontSize }
|
||||
color: Theme.pillText
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
// WorkspacesWidget.qml - shows workspaces and allows switching between them
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell.Hyprland
|
||||
import ".."
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
required property var screen
|
||||
|
||||
property var sortedWorkspaces: Array.from(Hyprland.workspaces.values).sort((a, b) => a.id - b.id)
|
||||
property int activeIndex: {
|
||||
var fid = Hyprland.focusedWorkspace?.id ?? -1
|
||||
for (var i = 0; i < sortedWorkspaces.length; i++)
|
||||
if (sortedWorkspaces[i].id === fid) return i
|
||||
return -1
|
||||
}
|
||||
|
||||
// Get the active workspace for this monitor
|
||||
property var monitorActiveWorkspace: {
|
||||
var activeWs = Hyprland.focusedWorkspace
|
||||
// Check if focused workspace is on this monitor
|
||||
if (activeWs && activeWs.monitor?.id === root.screen.id) {
|
||||
return activeWs
|
||||
}
|
||||
// Otherwise find the first workspace on this monitor
|
||||
for (var i = 0; i < sortedWorkspaces.length; i++) {
|
||||
if (sortedWorkspaces[i].monitor?.id === root.screen.id) {
|
||||
return sortedWorkspaces[i]
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function isWorkspaceOnThisMonitor(workspace) {
|
||||
// Find the monitor in Hyprland.monitors that matches this screen's name
|
||||
for (const monitor of Hyprland.monitors.values) {
|
||||
if (monitor.name === root.screen.name && monitor.id === workspace.monitor.id) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
readonly property int btnWidth: 32
|
||||
|
||||
color: Theme.pill
|
||||
radius: Theme.radius
|
||||
clip: true
|
||||
implicitWidth: wsRow.implicitWidth
|
||||
implicitHeight: Theme.barHeight
|
||||
Behavior on implicitWidth {
|
||||
NumberAnimation { duration: Theme.workspaceSlideTime; easing.type: Easing.InOutCubic }
|
||||
}
|
||||
|
||||
// Sliding active indicator - single Rectangle that travels between buttons
|
||||
Rectangle {
|
||||
visible: root.activeIndex >= 0
|
||||
x: root.activeIndex * root.btnWidth
|
||||
width: root.btnWidth
|
||||
height: parent.height
|
||||
color: Theme.wsActive
|
||||
|
||||
Behavior on x {
|
||||
NumberAnimation { duration: Theme.workspaceSlideTime; easing.type: Easing.InOutCubic }
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: wsRow
|
||||
anchors.fill: parent
|
||||
spacing: 0
|
||||
|
||||
Repeater {
|
||||
model: root.sortedWorkspaces
|
||||
|
||||
delegate: Rectangle {
|
||||
required property var modelData
|
||||
|
||||
Layout.fillHeight: true
|
||||
implicitWidth: root.btnWidth
|
||||
color: wsBtn.containsMouse ? Theme.pillHover : "transparent"
|
||||
// Reduce opacity for workspaces not on this monitor
|
||||
opacity: root.isWorkspaceOnThisMonitor(modelData) ? 1.0 : 0.4
|
||||
Behavior on opacity { NumberAnimation { duration: 150 } }
|
||||
Behavior on color { ColorAnimation { duration: 150 } }
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: modelData.id
|
||||
font { family: Theme.fontSans; pixelSize: Theme.fontSizeSecondary }
|
||||
color: Theme.pillText
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: wsBtn
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onClicked: Hyprland.dispatch("hl.dsp.focus({ workspace = '" + modelData.id + "' })")
|
||||
onWheel: (w) => Hyprland.dispatch(
|
||||
"hl.dsp.focus({ workspace = \"" + (w.angleDelta.y > 0 ? "e+1" : "e-1") + "\" })"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
# Overview module - credits
|
||||
|
||||
The files in this directory (`Overview.qml`, `OverviewWidget.qml`, `OverviewWindow.qml`) and
|
||||
their support singletons (`../../common/Appearance.qml`, `../../common/Config.qml`,
|
||||
`../../common/functions/ColorUtils.qml`, `../../common/widgets/Styled*.qml`,
|
||||
`../../services/HyprlandData.qml`) are copied from
|
||||
[Shanu-Kumawat/quickshell-overview](https://github.com/Shanu-Kumawat/quickshell-overview),
|
||||
which was itself extracted from the overview feature in
|
||||
[end-4/dots-hyprland](https://github.com/end-4/dots-hyprland) ("illogical-impulse") by
|
||||
[end-4](https://github.com/end-4).
|
||||
|
||||
Released under the GPL by the upstream project.
|
||||
|
||||
### Local deviations from upstream
|
||||
|
||||
- `Overview.qml` / `OverviewWidget.qml` / `OverviewWindow.qml`: added one `import "../.."` line
|
||||
each, so `GlobalStates.overviewOpen` (merged into `own/GlobalStates.qml`, see below) resolves
|
||||
from this subdirectory. No other logic changed.
|
||||
- `../../common/Appearance.qml`: `defaultColors` was retuned to pull from this shell's own
|
||||
`Theme`/`Colors` wallust singletons instead of the stock M3 purple palette, so the overview
|
||||
matches the bar's colors (needed one added `import ".."`). This is the module's own documented
|
||||
way to customize its palette (see upstream README, "Theme & Colors").
|
||||
- `../../common/Config.qml`: untouched. Transparency/rounding are instead tuned via
|
||||
`~/.config/quickshell/overview/config.json` (the module's own documented user-override file),
|
||||
kept separate from the upstream defaults it reads.
|
||||
|
||||
## Why a copy instead of a symlink/submodule
|
||||
|
||||
The module was merged into this shell's own process (`own/shell.qml`) so it runs in the same
|
||||
`quickshell -c own` instance as the bar, rather than as a second `qs -c overview` process.
|
||||
|
||||
An untouched clone of the upstream repo is kept side by side at
|
||||
`~/.config/quickshell/overview` - use it as the reference copy when checking for upstream
|
||||
updates (`git -C ~/.config/quickshell/overview pull`) and re-sync the files above by hand if
|
||||
anything changes there.
|
||||
|
||||
## Configuration
|
||||
|
||||
Runtime options (grid size, scale, effects, etc.) are unaffected by the merge - `Config.qml`
|
||||
still reads user overrides from `$XDG_CONFIG_HOME/quickshell/overview/config.json` regardless
|
||||
of where the QML itself lives. See `~/.config/quickshell/overview/config.example.json` and
|
||||
`~/.config/quickshell/overview/README.md` (in the untouched upstream clone) for the full list
|
||||
of options.
|
||||
|
||||
## Toggling it
|
||||
|
||||
The merged module still registers the same IPC target, just under the `own` shell instance:
|
||||
|
||||
```bash
|
||||
qs ipc -c own call overview toggle
|
||||
qs ipc -c own call overview open
|
||||
qs ipc -c own call overview close
|
||||
```
|
||||
@@ -0,0 +1,253 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Hyprland
|
||||
import "../../common"
|
||||
import "../../services"
|
||||
import "../.."
|
||||
import "."
|
||||
|
||||
Scope {
|
||||
id: overviewScope
|
||||
Variants {
|
||||
id: overviewVariants
|
||||
model: Quickshell.screens
|
||||
PanelWindow {
|
||||
id: root
|
||||
required property var modelData
|
||||
readonly property HyprlandMonitor monitor: Hyprland.monitorFor(root.screen)
|
||||
property bool monitorIsFocused: (Hyprland.focusedMonitor?.id == monitor?.id)
|
||||
property bool blurEnabled: Config.options.overview.effects.enableBlur
|
||||
property bool backdropEnabled: Config.options.overview.effects.enableBackdrop
|
||||
property real backdropOpacity: Math.max(0, Math.min(1, Config.options.overview.effects.backdropOpacity))
|
||||
property bool closeOnFocusLoss: Config.options.overview.closeOnFocusLoss ?? true
|
||||
screen: modelData
|
||||
visible: GlobalStates.overviewOpen
|
||||
|
||||
WlrLayershell.namespace: blurEnabled ? "quickshell:overview-blur" : "quickshell:overview"
|
||||
WlrLayershell.layer: WlrLayer.Overlay
|
||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
|
||||
color: "transparent"
|
||||
|
||||
anchors {
|
||||
top: true
|
||||
bottom: true
|
||||
left: true
|
||||
right: true
|
||||
}
|
||||
|
||||
HyprlandFocusGrab {
|
||||
id: grab
|
||||
windows: [root]
|
||||
property bool canBeActive: root.monitorIsFocused
|
||||
active: false
|
||||
onCleared: () => {
|
||||
// Only the monitor that owns the grab may close the overview
|
||||
if (root.closeOnFocusLoss && !active && canBeActive)
|
||||
GlobalStates.overviewOpen = false;
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: GlobalStates
|
||||
function onOverviewOpenChanged() {
|
||||
if (GlobalStates.overviewOpen) {
|
||||
delayedGrabTimer.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Re-evaluate grab ownership when focused monitor changes
|
||||
Connections {
|
||||
target: Hyprland
|
||||
function onFocusedMonitorChanged() {
|
||||
if (!GlobalStates.overviewOpen)
|
||||
return;
|
||||
// Transfer the grab to the newly focused monitor
|
||||
if (root.monitorIsFocused && !grab.active) {
|
||||
grab.active = true;
|
||||
} else if (!root.monitorIsFocused && grab.active) {
|
||||
grab.active = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: delayedGrabTimer
|
||||
interval: Config.options.hacks.arbitraryRaceConditionDelay
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
if (!grab.canBeActive)
|
||||
return;
|
||||
grab.active = GlobalStates.overviewOpen;
|
||||
}
|
||||
}
|
||||
|
||||
// Keep the layershell surface full-screen so backdrop/blur are not constrained by content size.
|
||||
implicitWidth: screen.width
|
||||
implicitHeight: screen.height
|
||||
|
||||
Item {
|
||||
id: keyHandler
|
||||
anchors.fill: parent
|
||||
visible: GlobalStates.overviewOpen
|
||||
focus: GlobalStates.overviewOpen
|
||||
z: 0
|
||||
|
||||
Rectangle {
|
||||
id: backdropLayer
|
||||
anchors.fill: parent
|
||||
visible: root.backdropEnabled
|
||||
color: "#000000"
|
||||
opacity: root.backdropOpacity
|
||||
z: 0
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: outsideClickCatcher
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
|
||||
enabled: root.closeOnFocusLoss && GlobalStates.overviewOpen
|
||||
z: 0
|
||||
onPressed: mouse => {
|
||||
GlobalStates.overviewOpen = false;
|
||||
mouse.accepted = true;
|
||||
}
|
||||
}
|
||||
|
||||
Keys.onPressed: event => {
|
||||
// close: Escape or Enter
|
||||
if (event.key === Qt.Key_Escape || event.key === Qt.Key_Return) {
|
||||
GlobalStates.overviewOpen = false;
|
||||
event.accepted = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// Helper: compute current group bounds
|
||||
const workspacesPerGroup = Config.options.overview.rows * Config.options.overview.columns;
|
||||
const currentId = Hyprland.focusedMonitor?.activeWorkspace?.id ?? 1;
|
||||
const useWorkspaceMap = Config.options.overview.useWorkspaceMap;
|
||||
const workspaceMap = Config.options.overview.workspaceMap ?? [];
|
||||
const focusedMonitorId = Hyprland.focusedMonitor?.id ?? root.monitor?.id ?? 0;
|
||||
const workspaceOffset = useWorkspaceMap ? Number(workspaceMap[focusedMonitorId] ?? 0) : 0;
|
||||
const currentGroup = Math.floor((currentId - workspaceOffset - 1) / workspacesPerGroup);
|
||||
const minWorkspaceId = currentGroup * workspacesPerGroup + 1 + workspaceOffset;
|
||||
const maxWorkspaceId = minWorkspaceId + workspacesPerGroup - 1;
|
||||
|
||||
const rows = Config.options.overview.rows;
|
||||
const columns = Config.options.overview.columns;
|
||||
const reverseColumns = Config.options.overview.orderRightLeft;
|
||||
const reverseRows = Config.options.overview.orderBottomUp;
|
||||
|
||||
const clampedIndex = Math.max(0, Math.min(workspacesPerGroup - 1, currentId - minWorkspaceId));
|
||||
const currentNormalRow = Math.floor(clampedIndex / columns);
|
||||
const currentNormalColumn = clampedIndex % columns;
|
||||
|
||||
function toVisualRow(normalRow) {
|
||||
return reverseRows ? (rows - normalRow - 1) : normalRow;
|
||||
}
|
||||
|
||||
function toVisualColumn(normalColumn) {
|
||||
return reverseColumns ? (columns - normalColumn - 1) : normalColumn;
|
||||
}
|
||||
|
||||
function toNormalRow(visualRow) {
|
||||
return reverseRows ? (rows - visualRow - 1) : visualRow;
|
||||
}
|
||||
|
||||
function toNormalColumn(visualColumn) {
|
||||
return reverseColumns ? (columns - visualColumn - 1) : visualColumn;
|
||||
}
|
||||
|
||||
let targetVisualRow = toVisualRow(currentNormalRow);
|
||||
let targetVisualColumn = toVisualColumn(currentNormalColumn);
|
||||
|
||||
let targetId = null;
|
||||
|
||||
// Arrow keys and vim-style hjkl
|
||||
if (event.key === Qt.Key_Left || event.key === Qt.Key_H) {
|
||||
targetVisualColumn = (targetVisualColumn - 1 + columns) % columns;
|
||||
} else if (event.key === Qt.Key_Right || event.key === Qt.Key_L) {
|
||||
targetVisualColumn = (targetVisualColumn + 1) % columns;
|
||||
} else if (event.key === Qt.Key_Up || event.key === Qt.Key_K) {
|
||||
targetVisualRow = (targetVisualRow - 1 + rows) % rows;
|
||||
} else if (event.key === Qt.Key_Down || event.key === Qt.Key_J) {
|
||||
targetVisualRow = (targetVisualRow + 1) % rows;
|
||||
}
|
||||
|
||||
// Number keys: jump to workspace within the current group
|
||||
// 1-9 map to positions 1-9, 0 maps to position 10
|
||||
else if (event.key >= Qt.Key_1 && event.key <= Qt.Key_9) {
|
||||
const position = event.key - Qt.Key_0; // 1-9
|
||||
if (position <= workspacesPerGroup) {
|
||||
targetId = minWorkspaceId + position - 1;
|
||||
}
|
||||
} else if (event.key === Qt.Key_0) {
|
||||
// 0 = 10th workspace in the group (if group has 10+ workspaces)
|
||||
if (workspacesPerGroup >= 10) {
|
||||
targetId = minWorkspaceId + 9; // 10th position = offset 9
|
||||
}
|
||||
}
|
||||
|
||||
if (targetId === null && (
|
||||
event.key === Qt.Key_Left || event.key === Qt.Key_H ||
|
||||
event.key === Qt.Key_Right || event.key === Qt.Key_L ||
|
||||
event.key === Qt.Key_Up || event.key === Qt.Key_K ||
|
||||
event.key === Qt.Key_Down || event.key === Qt.Key_J
|
||||
)) {
|
||||
const targetNormalRow = toNormalRow(targetVisualRow);
|
||||
const targetNormalColumn = toNormalColumn(targetVisualColumn);
|
||||
targetId = minWorkspaceId + targetNormalRow * columns + targetNormalColumn;
|
||||
}
|
||||
|
||||
if (targetId !== null) {
|
||||
const clampedTarget = Math.max(minWorkspaceId, Math.min(maxWorkspaceId, targetId));
|
||||
if (Hyprland.usingLua) {
|
||||
Hyprland.dispatch(`hl.dsp.focus({workspace = '${clampedTarget}'})`);
|
||||
} else {
|
||||
Hyprland.dispatch("workspace " + clampedTarget);
|
||||
}
|
||||
event.accepted = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: columnLayout
|
||||
visible: GlobalStates.overviewOpen
|
||||
z: 1
|
||||
anchors {
|
||||
horizontalCenter: parent.horizontalCenter
|
||||
top: parent.top
|
||||
topMargin: Config.options.position.topMargin
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: overviewLoader
|
||||
active: Config?.options.overview.enable ?? true
|
||||
sourceComponent: OverviewWidget {
|
||||
panelWindow: root
|
||||
visible: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
target: "overview"
|
||||
|
||||
function toggle() {
|
||||
GlobalStates.overviewOpen = !GlobalStates.overviewOpen;
|
||||
}
|
||||
function close() {
|
||||
GlobalStates.overviewOpen = false;
|
||||
}
|
||||
function open() {
|
||||
GlobalStates.overviewOpen = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,265 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Effects
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import "../../common"
|
||||
import "../../common/functions"
|
||||
import "../../services"
|
||||
import "../.."
|
||||
|
||||
Item { // Window
|
||||
id: root
|
||||
property var toplevel
|
||||
property var windowData
|
||||
property var monitorData
|
||||
property var widgetMonitorData
|
||||
property var scale
|
||||
property var availableWorkspaceWidth
|
||||
property var availableWorkspaceHeight
|
||||
property real positionBaseX: (monitorData?.x ?? 0) + (monitorData?.reserved?.[0] ?? 0)
|
||||
property real positionBaseY: (monitorData?.y ?? 0) + (monitorData?.reserved?.[1] ?? 0)
|
||||
property int recaptureToken: 0
|
||||
property bool restrictToWorkspace: true
|
||||
property real widthRatio: {
|
||||
if (!widgetMonitorData || !monitorData)
|
||||
return 1;
|
||||
|
||||
const widgetWidth = (widgetMonitorData.transform % 2 === 1) ? (widgetMonitorData.height ?? 1) : (widgetMonitorData.width ?? 1);
|
||||
const sourceWidth = (monitorData.transform % 2 === 1) ? (monitorData.height ?? 1) : (monitorData.width ?? 1);
|
||||
const sourceScale = monitorData.scale ?? 1;
|
||||
const widgetScale = widgetMonitorData.scale ?? 1;
|
||||
return (widgetWidth * sourceScale) / (sourceWidth * widgetScale);
|
||||
}
|
||||
property real heightRatio: {
|
||||
if (!widgetMonitorData || !monitorData)
|
||||
return 1;
|
||||
|
||||
const widgetHeight = (widgetMonitorData.transform % 2 === 1) ? (widgetMonitorData.width ?? 1) : (widgetMonitorData.height ?? 1);
|
||||
const sourceHeight = (monitorData.transform % 2 === 1) ? (monitorData.width ?? 1) : (monitorData.height ?? 1);
|
||||
const sourceScale = monitorData.scale ?? 1;
|
||||
const widgetScale = widgetMonitorData.scale ?? 1;
|
||||
return (widgetHeight * sourceScale) / (sourceHeight * widgetScale);
|
||||
}
|
||||
property real initX: Math.max(((windowData?.at[0] ?? 0) - positionBaseX) * root.scale * geometryScaleX, 0) + xOffset
|
||||
property real initY: Math.max(((windowData?.at[1] ?? 0) - positionBaseY) * root.scale * geometryScaleY, 0) + yOffset
|
||||
property real xOffset: 0
|
||||
property real yOffset: 0
|
||||
property int widgetMonitorId: 0
|
||||
property real geometryScaleX: widthRatio
|
||||
property real geometryScaleY: heightRatio
|
||||
|
||||
property var targetWindowWidth: (windowData?.size[0] ?? 100) * scale * geometryScaleX
|
||||
property var targetWindowHeight: (windowData?.size[1] ?? 100) * scale * geometryScaleY
|
||||
property bool hovered: false
|
||||
property bool pressed: false
|
||||
|
||||
property bool showIcons: Config.options.windowPreview.showIcons
|
||||
property var iconToWindowRatio: Config.options.windowPreview.iconToWindowRatio
|
||||
property var xwaylandIndicatorToIconRatio: Config.options.windowPreview.xwaylandIndicatorToIconRatio
|
||||
property var iconToWindowRatioCompact: Config.options.windowPreview.iconToWindowRatioCompact
|
||||
property bool cropToFill: Config.options.windowPreview.cropToFill
|
||||
property bool previewsEnabled: Config.options.overview.previewsEnabled
|
||||
property bool includeInactiveMonitorPreviews: Config.options.overview.includeInactiveMonitorPreviews
|
||||
property int previewRecaptureDelayMs: Config.options.overview.previewRecaptureDelayMs
|
||||
property real windowOverlayOpacity: Math.max(0, Math.min(1, Config.options.overview.effects.windowOverlayOpacity))
|
||||
property bool glassMode: Config.options.overview.effects.glassMode
|
||||
property real glassShineOpacity: Math.max(0, Math.min(1, Config.options.overview.effects.glassShineOpacity))
|
||||
property real effectiveWindowOverlayOpacity: glassMode ? Math.min(windowOverlayOpacity, 0.10) : windowOverlayOpacity
|
||||
property string previewModeRaw: Config.options.overview.previewMode
|
||||
property string previewMode: {
|
||||
const mode = `${previewModeRaw ?? "live"}`.trim().toLowerCase();
|
||||
return (mode === "event" || mode === "snapshot") ? "event" : "live";
|
||||
}
|
||||
property bool livePreviewEnabled: previewsEnabled && previewMode === "live"
|
||||
property bool shouldCapturePreview: {
|
||||
if (!GlobalStates.overviewOpen || !previewsEnabled || !previewCaptureEnabled)
|
||||
return false;
|
||||
if (includeInactiveMonitorPreviews)
|
||||
return true;
|
||||
return (windowData?.monitor ?? -1) === widgetMonitorId;
|
||||
}
|
||||
property var entry: {
|
||||
DesktopEntries.applications.values; // re-run when the entry index updates
|
||||
return DesktopEntries.heuristicLookup(windowData?.class);
|
||||
}
|
||||
property string iconName: {
|
||||
const raw = `${entry?.icon ?? ""}`.trim();
|
||||
const withoutProviderPrefix = raw.replace(/^image:\/\/icon\//, "");
|
||||
const withoutQuery = withoutProviderPrefix.split("?")[0].trim();
|
||||
return withoutQuery.length > 0 ? withoutQuery : "application-x-executable";
|
||||
}
|
||||
property var iconPath: Quickshell.iconPath(iconName, "image-missing")
|
||||
property bool compactMode: Appearance.font.pixelSize.smaller * 4 > targetWindowHeight || Appearance.font.pixelSize.smaller * 4 > targetWindowWidth
|
||||
|
||||
property bool indicateXWayland: windowData?.xwayland ?? false
|
||||
property bool previewCaptureEnabled: true
|
||||
property bool initialized: false
|
||||
property bool dragInProgress: false
|
||||
property bool suspendPositionAnimation: false
|
||||
property bool animateSize: true
|
||||
|
||||
x: initX
|
||||
y: initY
|
||||
width: Math.min(targetWindowWidth, availableWorkspaceWidth)
|
||||
height: Math.min(targetWindowHeight, availableWorkspaceHeight)
|
||||
opacity: (windowData?.monitor ?? -1) == widgetMonitorId ? 1 : Config.options.windowPreview.inactiveMonitorOpacity
|
||||
visible: {
|
||||
const thisWsId = windowData?.workspace?.id;
|
||||
const isFullscreen = (windowData?.fullscreen ?? 0) > 0;
|
||||
if (isFullscreen || thisWsId === undefined) return true;
|
||||
return !HyprlandData.windowList.some(w => w.workspace?.id === thisWsId && (w.fullscreen ?? 0) > 0);
|
||||
}
|
||||
|
||||
clip: true
|
||||
Component.onCompleted: Qt.callLater(() => root.initialized = true)
|
||||
|
||||
Behavior on x {
|
||||
enabled: root.initialized && !root.dragInProgress && !root.suspendPositionAnimation
|
||||
animation: Appearance.animation.elementMoveEnter.numberAnimation.createObject(this)
|
||||
}
|
||||
Behavior on y {
|
||||
enabled: root.initialized && !root.dragInProgress && !root.suspendPositionAnimation
|
||||
animation: Appearance.animation.elementMoveEnter.numberAnimation.createObject(this)
|
||||
}
|
||||
Behavior on width {
|
||||
enabled: root.initialized && root.animateSize && !root.dragInProgress && !root.suspendPositionAnimation
|
||||
animation: Appearance.animation.elementMoveEnter.numberAnimation.createObject(this)
|
||||
}
|
||||
Behavior on height {
|
||||
enabled: root.initialized && root.animateSize && !root.dragInProgress && !root.suspendPositionAnimation
|
||||
animation: Appearance.animation.elementMoveEnter.numberAnimation.createObject(this)
|
||||
}
|
||||
|
||||
// Opaque background for windows on the active monitor.
|
||||
// The simplest solution for making those windows fully opaque and not interacting with actual
|
||||
// windows behind the overview, e.g., applying blur to them.
|
||||
Rectangle {
|
||||
visible: (root.windowData?.monitor ?? -1) === root.widgetMonitorId
|
||||
anchors.fill: parent
|
||||
radius: Appearance.rounding.windowRounding * root.scale
|
||||
color: root.glassMode
|
||||
? ColorUtils.mix(Appearance.colors.colLayer2, Appearance.colors.colLayer0, 0.38)
|
||||
: Appearance.colors.colLayer2
|
||||
}
|
||||
|
||||
ScreencopyView {
|
||||
id: windowPreview
|
||||
readonly property real srcAspect: {
|
||||
const w = root.windowData?.size?.[0] ?? 0;
|
||||
const h = root.windowData?.size?.[1] ?? 0;
|
||||
return (w > 0 && h > 0) ? (w / h) : 1;
|
||||
}
|
||||
anchors.centerIn: parent
|
||||
width: root.cropToFill
|
||||
? Math.max(parent.width, parent.height * srcAspect)
|
||||
: Math.min(parent.width, parent.height * srcAspect)
|
||||
height: root.cropToFill
|
||||
? Math.max(parent.height, parent.width / srcAspect)
|
||||
: Math.min(parent.height, parent.width / srcAspect)
|
||||
captureSource: shouldCapturePreview ? root.toplevel : null
|
||||
live: livePreviewEnabled
|
||||
layer.enabled: true
|
||||
layer.smooth: true
|
||||
layer.effect: MultiEffect {
|
||||
maskEnabled: true
|
||||
maskSource: previewMask
|
||||
maskThresholdMin: 0.5
|
||||
maskSpreadAtMin: 1.0
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: Appearance.rounding.windowRounding * root.scale
|
||||
color: pressed ? ColorUtils.applyAlpha(Appearance.colors.colLayer2Active, Math.min(1, root.effectiveWindowOverlayOpacity + 0.30)) :
|
||||
hovered ? ColorUtils.applyAlpha(Appearance.colors.colLayer2Hover, Math.min(1, root.effectiveWindowOverlayOpacity + 0.20)) :
|
||||
ColorUtils.applyAlpha(
|
||||
root.glassMode ? ColorUtils.mix(Appearance.colors.colLayer2, Appearance.colors.colLayer0, 0.38) : Appearance.colors.colLayer2,
|
||||
root.effectiveWindowOverlayOpacity
|
||||
)
|
||||
border.color: root.glassMode
|
||||
? ColorUtils.applyAlpha(Appearance.m3colors.m3outline, 0.62)
|
||||
: ColorUtils.transparentize(Appearance.m3colors.m3outline, 0.7)
|
||||
border.width: 1
|
||||
|
||||
Rectangle {
|
||||
visible: root.glassMode
|
||||
anchors.fill: parent
|
||||
radius: parent.radius
|
||||
color: "transparent"
|
||||
gradient: Gradient {
|
||||
GradientStop { position: 0.0; color: ColorUtils.applyAlpha("#FFFFFF", root.glassShineOpacity * 0.24) }
|
||||
GradientStop { position: 0.5; color: ColorUtils.applyAlpha("#FFFFFF", 0.0) }
|
||||
GradientStop { position: 1.0; color: ColorUtils.applyAlpha("#000000", root.glassShineOpacity * 0.14) }
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
visible: root.glassMode
|
||||
anchors.fill: parent
|
||||
anchors.margins: 1
|
||||
radius: Math.max(parent.radius - 1, 0)
|
||||
color: "transparent"
|
||||
border.width: 1
|
||||
border.color: ColorUtils.applyAlpha("#FFFFFF", root.glassShineOpacity * 0.32)
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
spacing: Appearance.font.pixelSize.smaller * 0.5
|
||||
|
||||
Image {
|
||||
id: windowIcon
|
||||
visible: root.showIcons
|
||||
property var iconSize: {
|
||||
const renderedSize = Math.min(root.width, root.height);
|
||||
return renderedSize * (root.compactMode ? root.iconToWindowRatioCompact : root.iconToWindowRatio) / (root.monitorData?.scale ?? 1);
|
||||
}
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
source: root.iconPath
|
||||
width: iconSize
|
||||
height: iconSize
|
||||
sourceSize: Qt.size(Math.max(1, Math.round(iconSize)), Math.max(1, Math.round(iconSize)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: previewMask
|
||||
width: windowPreview.width
|
||||
height: windowPreview.height
|
||||
anchors.centerIn: parent
|
||||
visible: false
|
||||
layer.enabled: true
|
||||
layer.smooth: true
|
||||
Rectangle {
|
||||
anchors.centerIn: parent
|
||||
width: root.width
|
||||
height: root.height
|
||||
radius: Appearance.rounding.windowRounding * root.scale
|
||||
}
|
||||
}
|
||||
|
||||
function refreshCapture() {
|
||||
if (!GlobalStates.overviewOpen || livePreviewEnabled || !previewsEnabled)
|
||||
return;
|
||||
|
||||
root.previewCaptureEnabled = false;
|
||||
previewResetTimer.restart();
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: previewResetTimer
|
||||
interval: Math.max(1, previewRecaptureDelayMs)
|
||||
repeat: false
|
||||
onTriggered: root.previewCaptureEnabled = true
|
||||
}
|
||||
|
||||
onRecaptureTokenChanged: {
|
||||
if (recaptureToken > 0)
|
||||
root.refreshCapture();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
Overview 1.0 Overview.qml
|
||||
OverviewWidget 1.0 OverviewWidget.qml
|
||||
OverviewWindow 1.0 OverviewWindow.qml
|
||||
Reference in New Issue
Block a user