add noctalia and fuzzel

This commit is contained in:
2026-05-23 21:20:58 +02:00
parent 364801f1a3
commit 9a3eceb3ab
599 changed files with 204318 additions and 0 deletions
@@ -0,0 +1,473 @@
pragma Singleton
import QtQuick
import Quickshell
import Quickshell.Io
import qs.Commons
/*
Noctalia is not strictly a Material Design project, it supports both some predefined
color schemes and dynamic color generation from the wallpaper.
We ultimately decided to use a restricted set of colors that follows the
Material Design 3 naming convention.
NOTE: All color names are prefixed with 'm' (e.g., mPrimary) to prevent QML from
misinterpreting them as signals (e.g., the 'onPrimary' property name).
*/
Singleton {
id: root
property bool reloadColors: false
// Suppress transition animations until the first colors.json load completes
property bool skipTransition: true
// Flag indicating theme colors are currently transitioning (for widgets to disable their own animations)
property bool isTransitioning: false
// Timer to reset isTransitioning after animation completes
Timer {
id: transitionTimer
interval: Style.animationSlowest + 50 // Small buffer after animation
onTriggered: root.isTransitioning = false
}
// --- Key Colors: These are the main accent colors that define your app's style
property color mPrimary: defaultColors.mPrimary
property color mOnPrimary: defaultColors.mOnPrimary
property color mSecondary: defaultColors.mSecondary
property color mOnSecondary: defaultColors.mOnSecondary
property color mTertiary: defaultColors.mTertiary
property color mOnTertiary: defaultColors.mOnTertiary
// --- Utility Colors: These colors serve specific, universal purposes like indicating errors
property color mError: defaultColors.mError
property color mOnError: defaultColors.mOnError
// --- Surface and Variant Colors: These provide additional options for surfaces and their contents, creating visual hierarchy
property color mSurface: defaultColors.mSurface
property color mOnSurface: defaultColors.mOnSurface
property color mSurfaceVariant: defaultColors.mSurfaceVariant
property color mOnSurfaceVariant: defaultColors.mOnSurfaceVariant
property color mOutline: defaultColors.mOutline
property color mShadow: defaultColors.mShadow
property color mHover: defaultColors.mHover
property color mOnHover: defaultColors.mOnHover
// --- Color transition animations ---
Behavior on mPrimary {
enabled: !root.skipTransition
ColorAnimation {
duration: Style.animationSlowest
easing.type: Easing.OutCubic
}
}
Behavior on mOnPrimary {
enabled: !root.skipTransition
ColorAnimation {
duration: Style.animationSlowest
easing.type: Easing.OutCubic
}
}
Behavior on mSecondary {
enabled: !root.skipTransition
ColorAnimation {
duration: Style.animationSlowest
easing.type: Easing.OutCubic
}
}
Behavior on mOnSecondary {
enabled: !root.skipTransition
ColorAnimation {
duration: Style.animationSlowest
easing.type: Easing.OutCubic
}
}
Behavior on mTertiary {
enabled: !root.skipTransition
ColorAnimation {
duration: Style.animationSlowest
easing.type: Easing.OutCubic
}
}
Behavior on mOnTertiary {
enabled: !root.skipTransition
ColorAnimation {
duration: Style.animationSlowest
easing.type: Easing.OutCubic
}
}
Behavior on mError {
enabled: !root.skipTransition
ColorAnimation {
duration: Style.animationSlowest
easing.type: Easing.OutCubic
}
}
Behavior on mOnError {
enabled: !root.skipTransition
ColorAnimation {
duration: Style.animationSlowest
easing.type: Easing.OutCubic
}
}
Behavior on mSurface {
enabled: !root.skipTransition
ColorAnimation {
duration: Style.animationSlowest
easing.type: Easing.OutCubic
}
}
Behavior on mOnSurface {
enabled: !root.skipTransition
ColorAnimation {
duration: Style.animationSlowest
easing.type: Easing.OutCubic
}
}
Behavior on mSurfaceVariant {
enabled: !root.skipTransition
ColorAnimation {
duration: Style.animationSlowest
easing.type: Easing.OutCubic
}
}
Behavior on mOnSurfaceVariant {
enabled: !root.skipTransition
ColorAnimation {
duration: Style.animationSlowest
easing.type: Easing.OutCubic
}
}
Behavior on mOutline {
enabled: !root.skipTransition
ColorAnimation {
duration: Style.animationSlowest
easing.type: Easing.OutCubic
}
}
Behavior on mShadow {
enabled: !root.skipTransition
ColorAnimation {
duration: Style.animationSlowest
easing.type: Easing.OutCubic
}
}
Behavior on mHover {
enabled: !root.skipTransition
ColorAnimation {
duration: Style.animationSlowest
easing.type: Easing.OutCubic
}
}
Behavior on mOnHover {
enabled: !root.skipTransition
ColorAnimation {
duration: Style.animationSlowest
easing.type: Easing.OutCubic
}
}
// Helper to start transition and update a color
function startTransition() {
root.isTransitioning = true;
transitionTimer.restart();
}
// Update colors when customColorsData changes (imperative assignment enables Behavior animations)
Connections {
target: customColorsData
function onMPrimaryChanged() {
if (!root.skipTransition) {
startTransition();
}
root.mPrimary = customColorsData.mPrimary;
}
function onMOnPrimaryChanged() {
if (!root.skipTransition) {
startTransition();
}
root.mOnPrimary = customColorsData.mOnPrimary;
}
function onMSecondaryChanged() {
if (!root.skipTransition) {
startTransition();
}
root.mSecondary = customColorsData.mSecondary;
}
function onMOnSecondaryChanged() {
if (!root.skipTransition) {
startTransition();
}
root.mOnSecondary = customColorsData.mOnSecondary;
}
function onMTertiaryChanged() {
if (!root.skipTransition) {
startTransition();
}
root.mTertiary = customColorsData.mTertiary;
}
function onMOnTertiaryChanged() {
if (!root.skipTransition) {
startTransition();
}
root.mOnTertiary = customColorsData.mOnTertiary;
}
function onMErrorChanged() {
if (!root.skipTransition) {
startTransition();
}
root.mError = customColorsData.mError;
}
function onMOnErrorChanged() {
if (!root.skipTransition) {
startTransition();
}
root.mOnError = customColorsData.mOnError;
}
function onMSurfaceChanged() {
if (!root.skipTransition) {
startTransition();
}
root.mSurface = customColorsData.mSurface;
}
function onMOnSurfaceChanged() {
if (!root.skipTransition) {
startTransition();
}
root.mOnSurface = customColorsData.mOnSurface;
}
function onMSurfaceVariantChanged() {
if (!root.skipTransition) {
startTransition();
}
root.mSurfaceVariant = customColorsData.mSurfaceVariant;
}
function onMOnSurfaceVariantChanged() {
if (!root.skipTransition) {
startTransition();
}
root.mOnSurfaceVariant = customColorsData.mOnSurfaceVariant;
}
function onMOutlineChanged() {
if (!root.skipTransition) {
startTransition();
}
root.mOutline = customColorsData.mOutline;
}
function onMShadowChanged() {
if (!root.skipTransition) {
startTransition();
}
root.mShadow = customColorsData.mShadow;
}
function onMHoverChanged() {
if (!root.skipTransition) {
startTransition();
}
root.mHover = customColorsData.mHover;
}
function onMOnHoverChanged() {
if (!root.skipTransition) {
startTransition();
}
root.mOnHover = customColorsData.mOnHover;
}
}
function resolveColorKey(key) {
switch (key) {
case "primary":
return root.mPrimary;
case "secondary":
return root.mSecondary;
case "tertiary":
return root.mTertiary;
case "error":
return root.mError;
default:
return root.mOnSurface;
}
}
function resolveOnColorKey(key) {
switch (key) {
case "primary":
return root.mOnPrimary;
case "secondary":
return root.mOnSecondary;
case "tertiary":
return root.mOnTertiary;
case "error":
return root.mOnError;
default:
return root.mSurface;
}
}
function resolveColorKeyOptional(key) {
switch (key) {
case "primary":
return root.mPrimary;
case "secondary":
return root.mSecondary;
case "tertiary":
return root.mTertiary;
case "error":
return root.mError;
default:
return "transparent";
}
}
// Adaptive opacity calculation: automatically makes light mode more transparent
function adaptiveOpacity(baseOpacity) {
return Settings.data.colorSchemes.darkMode ? baseOpacity : Math.pow(baseOpacity, 1.5);
}
function smartAlpha(baseColor, minAlpha = 0.4) {
if (!Settings.data.ui.translucentWidgets)
return baseColor;
let alpha = Math.max(adaptiveOpacity(Settings.data.ui.panelBackgroundOpacity), minAlpha);
// Combine with the base color's existing alpha
let resultAlpha = Math.max(0, baseColor.a - (1.0 - alpha));
return Qt.alpha(baseColor, resultAlpha);
}
readonly property var colorKeyModel: [
{
"key": "none",
"name": I18n.tr("common.none")
},
{
"key": "primary",
"name": I18n.tr("common.primary")
},
{
"key": "secondary",
"name": I18n.tr("common.secondary")
},
{
"key": "tertiary",
"name": I18n.tr("common.tertiary")
},
{
"key": "error",
"name": I18n.tr("common.error")
}
]
// --------------------------------
// Default colors: Noctalia (default) dark — must match Assets/ColorScheme/Noctalia-default
QtObject {
id: defaultColors
readonly property color mPrimary: "#fff59b"
readonly property color mOnPrimary: "#0e0e43"
readonly property color mSecondary: "#a9aefe"
readonly property color mOnSecondary: "#0e0e43"
readonly property color mTertiary: "#9BFECE"
readonly property color mOnTertiary: "#0e0e43"
readonly property color mError: "#FD4663"
readonly property color mOnError: "#0e0e43"
readonly property color mSurface: "#070722"
readonly property color mOnSurface: "#f3edf7"
readonly property color mSurfaceVariant: "#11112d"
readonly property color mOnSurfaceVariant: "#7c80b4"
readonly property color mOutline: "#21215F"
readonly property color mShadow: "#070722"
readonly property color mHover: "#9BFECE"
readonly property color mOnHover: "#0e0e43"
}
// ----------------------------------------------------------------
// FileView to load custom colors data from colors.json
FileView {
id: customColorsFile
path: Settings.directoriesCreated ? (Settings.configDir + "colors.json") : undefined
printErrors: false
watchChanges: true
onFileChanged: {
Logger.d("Color", "Reloading colors from disk");
reloadColors = true;
reload();
}
onAdapterUpdated: {
Logger.d("Color", "Writing colors to disk");
writeAdapter();
}
onLoaded: {
if (root.skipTransition) {
Qt.callLater(function () {
root.skipTransition = false;
});
}
}
// Trigger initial load when path changes from empty to actual path
onPathChanged: {
if (path !== undefined) {
reload();
}
}
onLoadFailed: function (error) {
if (reloadColors) {
reloadColors = false;
return;
}
if (root.skipTransition) {
Qt.callLater(function () {
root.skipTransition = false;
});
}
// Error code 2 = ENOENT (No such file or directory)
if (error === 2 || error.toString().includes("No such file")) {
// File doesn't exist, create it with default values
writeAdapter();
}
}
JsonAdapter {
id: customColorsData
property color mPrimary: defaultColors.mPrimary
property color mOnPrimary: defaultColors.mOnPrimary
property color mSecondary: defaultColors.mSecondary
property color mOnSecondary: defaultColors.mOnSecondary
property color mTertiary: defaultColors.mTertiary
property color mOnTertiary: defaultColors.mOnTertiary
property color mError: defaultColors.mError
property color mOnError: defaultColors.mOnError
property color mSurface: defaultColors.mSurface
property color mOnSurface: defaultColors.mOnSurface
property color mSurfaceVariant: defaultColors.mSurfaceVariant
property color mOnSurfaceVariant: defaultColors.mOnSurfaceVariant
property color mOutline: defaultColors.mOutline
property color mShadow: defaultColors.mShadow
property color mHover: defaultColors.mHover
property color mOnHover: defaultColors.mOnHover
}
}
}
@@ -0,0 +1,883 @@
pragma Singleton
import QtQuick
import Quickshell
Singleton {
id: root
// Public API
function go(search, targets, options) {
return _go(search, targets, options);
}
function single(search, target) {
return _single(search, target);
}
function highlight(result, open, close) {
if (open === undefined)
open = '<b>';
if (close === undefined)
close = '</b>';
return _highlight(result, open, close);
}
function prepare(target) {
return _prepare(target);
}
function cleanup() {
return _cleanup();
}
// Internal implementation
readonly property var _INFINITY: Infinity
readonly property var _NEGATIVE_INFINITY: -Infinity
readonly property var _NULL: null
property var _noResults: {
let r = [];
r.total = 0;
return r;
}
property var _noTarget: _prepare('')
property var _preparedCache: new Map()
property var _preparedSearchCache: new Map()
property var _matchesSimple: []
property var _matchesStrict: []
property var _nextBeginningIndexesChanges: []
property var _keysSpacesBestScores: []
property var _allowPartialMatchScores: []
property var _tmpTargets: []
property var _tmpResults: []
property var _q: _fastpriorityqueue()
function _fastpriorityqueue() {
var e = [], o = 0, a = {};
var v = function (r) {
for (var a = 0, vc = e[a], c = 1; c < o; ) {
var s = c + 1;
a = c;
if (s < o && e[s]._score < e[c]._score)
a = s;
e[a - 1 >> 1] = e[a];
c = 1 + (a << 1);
}
for (var f = a - 1 >> 1; a > 0 && vc._score < e[f]._score; f = (a = f) - 1 >> 1)
e[a] = e[f];
e[a] = vc;
};
a.add = function (r) {
var ac = o;
e[o++] = r;
for (var vc = ac - 1 >> 1; ac > 0 && r._score < e[vc]._score; vc = (ac = vc) - 1 >> 1)
e[ac] = e[vc];
e[ac] = r;
};
a.poll = function () {
if (o !== 0) {
var ac = e[0];
e[0] = e[--o];
v();
return ac;
}
};
a.peek = function () {
if (o !== 0)
return e[0];
};
a.replaceTop = function (r) {
e[0] = r;
v();
};
return a;
}
function _createResult() {
return {
target: '',
obj: _NULL,
_score: _NEGATIVE_INFINITY,
_indexes: [],
_targetLower: '',
_targetLowerCodes: _NULL,
_nextBeginningIndexes: _NULL,
_bitflags: 0,
get indexes() {
return this._indexes.slice(0, this._indexes.len).sort((a, b) => a - b);
},
set indexes(idx) {
this._indexes = idx;
},
highlight: function (open, close) {
return root._highlight(this, open, close);
},
get score() {
return root._normalizeScore(this._score);
},
set score(s) {
this._score = root._denormalizeScore(s);
}
};
}
function _createKeysResult(len) {
var arr = new Array(len);
arr._score = _NEGATIVE_INFINITY;
arr.obj = _NULL;
Object.defineProperty(arr, 'score', {
get: function () {
return root._normalizeScore(this._score);
},
set: function (s) {
this._score = root._denormalizeScore(s);
}
});
return arr;
}
function _new_result(target, options) {
var result = _createResult();
result.target = target;
result.obj = options.obj ?? _NULL;
result._score = options._score ?? _NEGATIVE_INFINITY;
result._indexes = options._indexes ?? [];
result._targetLower = options._targetLower ?? '';
result._targetLowerCodes = options._targetLowerCodes ?? _NULL;
result._nextBeginningIndexes = options._nextBeginningIndexes ?? _NULL;
result._bitflags = options._bitflags ?? 0;
return result;
}
function _normalizeScore(score) {
if (score === _NEGATIVE_INFINITY)
return 0;
if (score > 1)
return score;
return Math.E ** (((-score + 1) ** 0.04307 - 1) * -2);
}
function _denormalizeScore(normalizedScore) {
if (normalizedScore === 0)
return _NEGATIVE_INFINITY;
if (normalizedScore > 1)
return normalizedScore;
return 1 - Math.pow((Math.log(normalizedScore) / -2 + 1), 1 / 0.04307);
}
function _remove_accents(str) {
return str.replace(/\p{Script=Latin}+/gu, match => match.normalize('NFD')).replace(/[\u0300-\u036f]/g, '');
}
function _prepareLowerInfo(str) {
str = _remove_accents(str);
var strLen = str.length;
var lower = str.toLowerCase();
var lowerCodes = [];
var bitflags = 0;
var containsSpace = false;
for (var i = 0; i < strLen; ++i) {
var lowerCode = lowerCodes[i] = lower.charCodeAt(i);
if (lowerCode === 32) {
containsSpace = true;
continue;
}
var bit = lowerCode >= 97 && lowerCode <= 122 ? lowerCode - 97 : lowerCode >= 48 && lowerCode <= 57 ? 26 : lowerCode <= 127 ? 30 : 31;
bitflags |= 1 << bit;
}
return {
lowerCodes: lowerCodes,
bitflags: bitflags,
containsSpace: containsSpace,
_lower: lower
};
}
function _prepareBeginningIndexes(target) {
var targetLen = target.length;
var beginningIndexes = [];
var beginningIndexesLen = 0;
var wasUpper = false;
var wasAlphanum = false;
for (var i = 0; i < targetLen; ++i) {
var targetCode = target.charCodeAt(i);
var isUpper = targetCode >= 65 && targetCode <= 90;
var isAlphanum = isUpper || targetCode >= 97 && targetCode <= 122 || targetCode >= 48 && targetCode <= 57;
var isBeginning = isUpper && !wasUpper || !wasAlphanum || !isAlphanum;
wasUpper = isUpper;
wasAlphanum = isAlphanum;
if (isBeginning)
beginningIndexes[beginningIndexesLen++] = i;
}
return beginningIndexes;
}
function _prepareNextBeginningIndexes(target) {
target = _remove_accents(target);
var targetLen = target.length;
var beginningIndexes = _prepareBeginningIndexes(target);
var nextBeginningIndexes = [];
var lastIsBeginning = beginningIndexes[0];
var lastIsBeginningI = 0;
for (var i = 0; i < targetLen; ++i) {
if (lastIsBeginning > i) {
nextBeginningIndexes[i] = lastIsBeginning;
} else {
lastIsBeginning = beginningIndexes[++lastIsBeginningI];
nextBeginningIndexes[i] = lastIsBeginning === undefined ? targetLen : lastIsBeginning;
}
}
return nextBeginningIndexes;
}
function _prepareSearch(search) {
if (typeof search === 'number')
search = '' + search;
else if (typeof search !== 'string')
search = '';
search = search.trim();
var info = _prepareLowerInfo(search);
var spaceSearches = [];
if (info.containsSpace) {
var searches = search.split(/\s+/);
searches = [...new Set(searches)];
for (var i = 0; i < searches.length; i++) {
if (searches[i] === '')
continue;
var _info = _prepareLowerInfo(searches[i]);
spaceSearches.push({
lowerCodes: _info.lowerCodes,
_lower: searches[i].toLowerCase(),
containsSpace: false
});
}
}
return {
lowerCodes: info.lowerCodes,
_lower: info._lower,
containsSpace: info.containsSpace,
bitflags: info.bitflags,
spaceSearches: spaceSearches
};
}
function _prepare(target) {
if (typeof target === 'number')
target = '' + target;
else if (typeof target !== 'string')
target = '';
var info = _prepareLowerInfo(target);
return _new_result(target, {
_targetLower: info._lower,
_targetLowerCodes: info.lowerCodes,
_bitflags: info.bitflags
});
}
function _cleanup() {
_preparedCache.clear();
_preparedSearchCache.clear();
}
function _isPrepared(x) {
return typeof x === 'object' && typeof x._bitflags === 'number';
}
function _getPrepared(target) {
if (target.length > 999)
return _prepare(target);
var targetPrepared = _preparedCache.get(target);
if (targetPrepared !== undefined)
return targetPrepared;
targetPrepared = _prepare(target);
_preparedCache.set(target, targetPrepared);
return targetPrepared;
}
function _getPreparedSearch(search) {
if (search.length > 999)
return _prepareSearch(search);
var searchPrepared = _preparedSearchCache.get(search);
if (searchPrepared !== undefined)
return searchPrepared;
searchPrepared = _prepareSearch(search);
_preparedSearchCache.set(search, searchPrepared);
return searchPrepared;
}
function _getValue(obj, prop) {
var tmp = obj[prop];
if (tmp !== undefined)
return tmp;
if (typeof prop === 'function')
return prop(obj);
var segs = prop;
if (!Array.isArray(prop))
segs = prop.split('.');
var len = segs.length;
var i = -1;
while (obj && (++i < len))
obj = obj[segs[i]];
return obj;
}
function _single(search, target) {
if (!search || !target)
return _NULL;
var preparedSearch = _getPreparedSearch(search);
if (!_isPrepared(target))
target = _getPrepared(target);
var searchBitflags = preparedSearch.bitflags;
if ((searchBitflags & target._bitflags) !== searchBitflags)
return _NULL;
return _algorithm(preparedSearch, target);
}
function _highlight(result, open, close) {
if (open === undefined)
open = '<b>';
if (close === undefined)
close = '</b>';
var callback = typeof open === 'function' ? open : undefined;
var target = result.target;
var targetLen = target.length;
var indexes = result.indexes;
var highlighted = '';
var matchI = 0;
var indexesI = 0;
var opened = false;
var parts = [];
for (var i = 0; i < targetLen; ++i) {
var ch = target[i];
if (indexes[indexesI] === i) {
++indexesI;
if (!opened) {
opened = true;
if (callback) {
parts.push(highlighted);
highlighted = '';
} else {
highlighted += open;
}
}
if (indexesI === indexes.length) {
if (callback) {
highlighted += ch;
parts.push(callback(highlighted, matchI++));
highlighted = '';
parts.push(target.substr(i + 1));
} else {
highlighted += ch + close + target.substr(i + 1);
}
break;
}
} else {
if (opened) {
opened = false;
if (callback) {
parts.push(callback(highlighted, matchI++));
highlighted = '';
} else {
highlighted += close;
}
}
}
highlighted += ch;
}
return callback ? parts : highlighted;
}
function _all(targets, options) {
var results = [];
results.total = targets.length;
var limit = options?.limit || _INFINITY;
if (options?.key) {
for (var i = 0; i < targets.length; i++) {
var obj = targets[i];
var target = _getValue(obj, options.key);
if (target == _NULL)
continue;
if (!_isPrepared(target))
target = _getPrepared(target);
var result = _new_result(target.target, {
_score: target._score,
obj: obj
});
results.push(result);
if (results.length >= limit)
return results;
}
} else if (options?.keys) {
for (var i = 0; i < targets.length; i++) {
var obj = targets[i];
var objResults = _createKeysResult(options.keys.length);
for (var keyI = options.keys.length - 1; keyI >= 0; --keyI) {
var target = _getValue(obj, options.keys[keyI]);
if (!target) {
objResults[keyI] = _noTarget;
continue;
}
if (!_isPrepared(target))
target = _getPrepared(target);
target._score = _NEGATIVE_INFINITY;
target._indexes.len = 0;
objResults[keyI] = target;
}
objResults.obj = obj;
objResults._score = _NEGATIVE_INFINITY;
results.push(objResults);
if (results.length >= limit)
return results;
}
} else {
for (var i = 0; i < targets.length; i++) {
var target = targets[i];
if (target == _NULL)
continue;
if (!_isPrepared(target))
target = _getPrepared(target);
target._score = _NEGATIVE_INFINITY;
target._indexes.len = 0;
results.push(target);
if (results.length >= limit)
return results;
}
}
return results;
}
function _algorithm(preparedSearch, prepared, allowSpaces, allowPartialMatch) {
if (allowSpaces === undefined)
allowSpaces = false;
if (allowPartialMatch === undefined)
allowPartialMatch = false;
if (allowSpaces === false && preparedSearch.containsSpace)
return _algorithmSpaces(preparedSearch, prepared, allowPartialMatch);
var searchLower = preparedSearch._lower;
var searchLowerCodes = preparedSearch.lowerCodes;
var searchLowerCode = searchLowerCodes[0];
var targetLowerCodes = prepared._targetLowerCodes;
var searchLen = searchLowerCodes.length;
var targetLen = targetLowerCodes.length;
var searchI = 0;
var targetI = 0;
var matchesSimpleLen = 0;
for (; ; ) {
var isMatch = searchLowerCode === targetLowerCodes[targetI];
if (isMatch) {
_matchesSimple[matchesSimpleLen++] = targetI;
++searchI;
if (searchI === searchLen)
break;
searchLowerCode = searchLowerCodes[searchI];
}
++targetI;
if (targetI >= targetLen)
return _NULL;
}
searchI = 0;
var successStrict = false;
var matchesStrictLen = 0;
var nextBeginningIndexes = prepared._nextBeginningIndexes;
if (nextBeginningIndexes === _NULL)
nextBeginningIndexes = prepared._nextBeginningIndexes = _prepareNextBeginningIndexes(prepared.target);
targetI = _matchesSimple[0] === 0 ? 0 : nextBeginningIndexes[_matchesSimple[0] - 1];
var backtrackCount = 0;
if (targetI !== targetLen)
for (; ; ) {
if (targetI >= targetLen) {
if (searchI <= 0)
break;
++backtrackCount;
if (backtrackCount > 200)
break;
--searchI;
var lastMatch = _matchesStrict[--matchesStrictLen];
targetI = nextBeginningIndexes[lastMatch];
} else {
var isMatch = searchLowerCodes[searchI] === targetLowerCodes[targetI];
if (isMatch) {
_matchesStrict[matchesStrictLen++] = targetI;
++searchI;
if (searchI === searchLen) {
successStrict = true;
break;
}
++targetI;
} else {
targetI = nextBeginningIndexes[targetI];
}
}
}
var substringIndex = searchLen <= 1 ? -1 : prepared._targetLower.indexOf(searchLower, _matchesSimple[0]);
var isSubstring = !!~substringIndex;
var isSubstringBeginning = !isSubstring ? false : substringIndex === 0 || prepared._nextBeginningIndexes[substringIndex - 1] === substringIndex;
if (isSubstring && !isSubstringBeginning) {
for (var i = 0; i < nextBeginningIndexes.length; i = nextBeginningIndexes[i]) {
if (i <= substringIndex)
continue;
for (var s = 0; s < searchLen; s++)
if (searchLowerCodes[s] !== prepared._targetLowerCodes[i + s])
break;
if (s === searchLen) {
substringIndex = i;
isSubstringBeginning = true;
break;
}
}
}
var calculateScore = function (matches) {
var score = 0;
var extraMatchGroupCount = 0;
for (var i = 1; i < searchLen; ++i) {
if (matches[i] - matches[i - 1] !== 1) {
score -= matches[i];
++extraMatchGroupCount;
}
}
var unmatchedDistance = matches[searchLen - 1] - matches[0] - (searchLen - 1);
score -= (12 + unmatchedDistance) * extraMatchGroupCount;
if (matches[0] !== 0)
score -= matches[0] * matches[0] * 0.2;
if (!successStrict) {
score *= 1000;
} else {
var uniqueBeginningIndexes = 1;
for (var i = nextBeginningIndexes[0]; i < targetLen; i = nextBeginningIndexes[i])
++uniqueBeginningIndexes;
if (uniqueBeginningIndexes > 24)
score *= (uniqueBeginningIndexes - 24) * 10;
}
score -= (targetLen - searchLen) / 2;
if (isSubstring)
score /= 1 + searchLen * searchLen * 1;
if (isSubstringBeginning)
score /= 1 + searchLen * searchLen * 1;
score -= (targetLen - searchLen) / 2;
return score;
};
var matchesBest, score;
if (!successStrict) {
if (isSubstring)
for (var i = 0; i < searchLen; ++i)
_matchesSimple[i] = substringIndex + i;
matchesBest = _matchesSimple;
score = calculateScore(matchesBest);
} else {
if (isSubstringBeginning) {
for (var i = 0; i < searchLen; ++i)
_matchesSimple[i] = substringIndex + i;
matchesBest = _matchesSimple;
score = calculateScore(_matchesSimple);
} else {
matchesBest = _matchesStrict;
score = calculateScore(_matchesStrict);
}
}
prepared._score = score;
for (var i = 0; i < searchLen; ++i)
prepared._indexes[i] = matchesBest[i];
prepared._indexes.len = searchLen;
var result = _createResult();
result.target = prepared.target;
result._score = prepared._score;
result._indexes = prepared._indexes;
return result;
}
function _algorithmSpaces(preparedSearch, target, allowPartialMatch) {
var seen_indexes = new Set();
var score = 0;
var result = _NULL;
var first_seen_index_last_search = 0;
var searches = preparedSearch.spaceSearches;
var searchesLen = searches.length;
var changeslen = 0;
var resetNextBeginningIndexes = function () {
for (let i = changeslen - 1; i >= 0; i--)
target._nextBeginningIndexes[_nextBeginningIndexesChanges[i * 2 + 0]] = _nextBeginningIndexesChanges[i * 2 + 1];
};
var hasAtLeast1Match = false;
for (var i = 0; i < searchesLen; ++i) {
_allowPartialMatchScores[i] = _NEGATIVE_INFINITY;
var search = searches[i];
result = _algorithm(search, target);
if (allowPartialMatch) {
if (result === _NULL)
continue;
hasAtLeast1Match = true;
} else {
if (result === _NULL) {
resetNextBeginningIndexes();
return _NULL;
}
}
var isTheLastSearch = i === searchesLen - 1;
if (!isTheLastSearch) {
var indexes = result._indexes;
var indexesIsConsecutiveSubstring = true;
for (let j = 0; j < indexes.len - 1; j++) {
if (indexes[j + 1] - indexes[j] !== 1) {
indexesIsConsecutiveSubstring = false;
break;
}
}
if (indexesIsConsecutiveSubstring) {
var newBeginningIndex = indexes[indexes.len - 1] + 1;
var toReplace = target._nextBeginningIndexes[newBeginningIndex - 1];
for (let j = newBeginningIndex - 1; j >= 0; j--) {
if (toReplace !== target._nextBeginningIndexes[j])
break;
target._nextBeginningIndexes[j] = newBeginningIndex;
_nextBeginningIndexesChanges[changeslen * 2 + 0] = j;
_nextBeginningIndexesChanges[changeslen * 2 + 1] = toReplace;
changeslen++;
}
}
}
score += result._score / searchesLen;
_allowPartialMatchScores[i] = result._score / searchesLen;
if (result._indexes[0] < first_seen_index_last_search) {
score -= (first_seen_index_last_search - result._indexes[0]) * 2;
}
first_seen_index_last_search = result._indexes[0];
for (var j = 0; j < result._indexes.len; ++j)
seen_indexes.add(result._indexes[j]);
}
if (allowPartialMatch && !hasAtLeast1Match)
return _NULL;
resetNextBeginningIndexes();
var allowSpacesResult = _algorithm(preparedSearch, target, true);
if (allowSpacesResult !== _NULL && allowSpacesResult._score > score) {
if (allowPartialMatch) {
for (var i = 0; i < searchesLen; ++i) {
_allowPartialMatchScores[i] = allowSpacesResult._score / searchesLen;
}
}
return allowSpacesResult;
}
if (allowPartialMatch)
result = target;
result._score = score;
var idx = 0;
for (let index of seen_indexes)
result._indexes[idx++] = index;
result._indexes.len = idx;
return result;
}
function _go(search, targets, options) {
if (!search)
return options?.all ? _all(targets, options) : _noResults;
var preparedSearch = _getPreparedSearch(search);
var searchBitflags = preparedSearch.bitflags;
var containsSpace = preparedSearch.containsSpace;
var threshold = _denormalizeScore(options?.threshold ?? 0.35);
var limit = options?.limit || _INFINITY;
var resultsLen = 0;
var limitedCount = 0;
var targetsLen = targets.length;
function push_result(result) {
if (resultsLen < limit) {
_q.add(result);
++resultsLen;
} else {
++limitedCount;
if (result._score > _q.peek()._score)
_q.replaceTop(result);
}
}
if (options?.key) {
var key = options.key;
for (var i = 0; i < targetsLen; ++i) {
var obj = targets[i];
var target = _getValue(obj, key);
if (!target)
continue;
if (!_isPrepared(target))
target = _getPrepared(target);
if ((searchBitflags & target._bitflags) !== searchBitflags)
continue;
var result = _algorithm(preparedSearch, target);
if (result === _NULL)
continue;
if (result._score < threshold)
continue;
result.obj = obj;
push_result(result);
}
} else if (options?.keys) {
var keys = options.keys;
var keysLen = keys.length;
outer: for (var i = 0; i < targetsLen; ++i) {
var obj = targets[i];
var keysBitflags = 0;
for (var keyI = 0; keyI < keysLen; ++keyI) {
var key = keys[keyI];
var target = _getValue(obj, key);
if (!target) {
_tmpTargets[keyI] = _noTarget;
continue;
}
if (!_isPrepared(target))
target = _getPrepared(target);
_tmpTargets[keyI] = target;
keysBitflags |= target._bitflags;
}
if ((searchBitflags & keysBitflags) !== searchBitflags)
continue;
if (containsSpace)
for (let j = 0; j < preparedSearch.spaceSearches.length; j++)
_keysSpacesBestScores[j] = _NEGATIVE_INFINITY;
for (var keyI = 0; keyI < keysLen; ++keyI) {
target = _tmpTargets[keyI];
if (target === _noTarget) {
_tmpResults[keyI] = _noTarget;
continue;
}
_tmpResults[keyI] = _algorithm(preparedSearch, target, false, containsSpace);
if (_tmpResults[keyI] === _NULL) {
_tmpResults[keyI] = _noTarget;
continue;
}
if (containsSpace)
for (let j = 0; j < preparedSearch.spaceSearches.length; j++) {
if (_allowPartialMatchScores[j] > -1000) {
if (_keysSpacesBestScores[j] > _NEGATIVE_INFINITY) {
var tmp = (_keysSpacesBestScores[j] + _allowPartialMatchScores[j]) / 4;
if (tmp > _keysSpacesBestScores[j])
_keysSpacesBestScores[j] = tmp;
}
}
if (_allowPartialMatchScores[j] > _keysSpacesBestScores[j])
_keysSpacesBestScores[j] = _allowPartialMatchScores[j];
}
}
if (containsSpace) {
for (let j = 0; j < preparedSearch.spaceSearches.length; j++)
if (_keysSpacesBestScores[j] === _NEGATIVE_INFINITY)
continue outer;
} else {
var hasAtLeast1Match = false;
for (let j = 0; j < keysLen; j++)
if (_tmpResults[j]._score !== _NEGATIVE_INFINITY) {
hasAtLeast1Match = true;
break;
}
if (!hasAtLeast1Match)
continue;
}
var objResults = _createKeysResult(keysLen);
for (let j = 0; j < keysLen; j++)
objResults[j] = _tmpResults[j];
var score;
if (containsSpace) {
score = 0;
for (let j = 0; j < preparedSearch.spaceSearches.length; j++)
score += _keysSpacesBestScores[j];
} else {
score = _NEGATIVE_INFINITY;
for (let j = 0; j < keysLen; j++) {
var res = objResults[j];
if (res._score > -1000) {
if (score > _NEGATIVE_INFINITY) {
var tmp = (score + res._score) / 4;
if (tmp > score)
score = tmp;
}
}
if (res._score > score)
score = res._score;
}
}
objResults.obj = obj;
objResults._score = score;
if (options?.scoreFn) {
score = options.scoreFn(objResults);
if (!score)
continue;
score = _denormalizeScore(score);
objResults._score = score;
}
if (score < threshold)
continue;
push_result(objResults);
}
} else {
for (var i = 0; i < targetsLen; ++i) {
var target = targets[i];
if (!target)
continue;
if (!_isPrepared(target))
target = _getPrepared(target);
if ((searchBitflags & target._bitflags) !== searchBitflags)
continue;
var result = _algorithm(preparedSearch, target);
if (result === _NULL)
continue;
if (result._score < threshold)
continue;
push_result(result);
}
}
if (resultsLen === 0)
return _noResults;
var results = new Array(resultsLen);
for (var i = resultsLen - 1; i >= 0; --i)
results[i] = _q.poll();
results.total = resultsLen + limitedCount;
return results;
}
}
@@ -0,0 +1,373 @@
pragma Singleton
import QtQuick
import Quickshell
import Quickshell.Io
import qs.Commons
Singleton {
id: root
property bool isLoaded: false
property string langCode: ""
property var locale: Qt.locale()
property string systemDetectedLangCode: ""
property string fullLocaleCode: "" // Preserves regional locale variants
property var translations: ({})
property var fallbackTranslations: ({})
// Static list of available translations — update when adding/removing translation files
property var availableLanguages: ["en", "en-GB", "cs", "de", "es", "fr", "hu", "it", "ja", "ko-KR", "ku", "nl", "nn-HN", "nn-NO", "pl", "pt", "ru", "sv", "tr", "uk-UA", "vi", "zh-CN", "zh-TW"]
// Default date format per language (used by lock screen, etc.)
readonly property var dateFormats: ({
"de": "dddd, d. MMMM",
"en": "dddd, MMMM d",
"es": "dddd, d 'de' MMMM",
"fr": "dddd d MMMM",
"hu": "dddd, MMMM d.",
"it": "dddd d MMMM",
"ja": "yyyy年M月d日 dddd",
"ko": "yyyy년 M월 d일 dddd",
"ku": "dddd, dê MMMM",
"nl": "dddd d MMMM",
"nn": "dddd d. MMMM",
"pl": "dddd, d MMMM",
"pt": "dddd, d 'de' MMMM",
"ru": "dddd, d MMMM",
"sv": "dddd d MMMM",
"tr": "dddd, d MMMM",
"uk": "dddd, d MMMM",
"vi": "dddd, d MMMM",
"zh": "yyyy年M月d日 dddd"
})
// Signals for reactive updates
signal languageChanged(string newLanguage)
signal translationsLoaded
// FileView to load translation files
property FileView translationFile: FileView {
id: fileView
printErrors: false
watchChanges: true
onFileChanged: reload()
onLoaded: {
try {
var data = JSON.parse(text());
root.translations = data;
Logger.i("I18n", `Loaded translations for "${root.langCode}"`);
root.isLoaded = true;
root.translationsLoaded();
// Load English fallback for non-English languages (only after main file succeeds)
if (root.langCode !== "en") {
fallbackFileView.path = `file://${Quickshell.shellDir}/Assets/Translations/en.json`;
}
} catch (e) {
Logger.e("I18n", `Failed to parse translation file: ${e}`);
setLanguage("en");
}
}
onLoadFailed: function (error) {
if (root.langCode === "en") {
Logger.e("I18n", `Failed to load English translation file: ${error}`);
// English also failed - still emit signal to unblock startup
root.isLoaded = true;
root.translationsLoaded();
return;
}
// Qt.callLater is needed because FileView doesn't re-trigger when path
// is changed inside its own onLoadFailed handler
var strippedCode = stripScript(root.langCode);
if (strippedCode !== root.langCode) {
Logger.d("I18n", `Translation file for "${root.langCode}" not found, trying "${strippedCode}"`);
root.langCode = strippedCode;
Qt.callLater(loadTranslations);
} else {
// Try language-only code (e.g. "zh-CN" → "zh")
var shortCode = root.langCode.substring(0, 2);
if (shortCode !== root.langCode) {
Logger.d("I18n", `Translation file for "${root.langCode}" not found, trying "${shortCode}"`);
root.langCode = shortCode;
Qt.callLater(loadTranslations);
} else {
Logger.w("I18n", `Translation file for "${root.langCode}" not found, falling back to English`);
root.langCode = "en";
root.fullLocaleCode = "en";
root.locale = Qt.locale("en");
Qt.callLater(loadTranslations);
}
}
}
}
// FileView to load fallback translation files
property FileView fallbackTranslationFile: FileView {
id: fallbackFileView
watchChanges: true
onFileChanged: reload()
onLoaded: {
try {
var data = JSON.parse(text());
root.fallbackTranslations = data;
Logger.d("I18n", `Loaded english fallback translations`);
} catch (e) {
Logger.e("I18n", `Failed to parse fallback translation file: ${e}`);
}
}
onLoadFailed: function (error) {
Logger.e("I18n", `Failed to load fallback translation file: ${error}`);
}
}
// Correct language when settings finish loading from disk (or user changes it)
Connections {
target: Settings.data.general
function onLanguageChanged() {
var userLang = Settings.data.general.language;
if (userLang !== "" && userLang !== root.langCode && availableLanguages.includes(userLang)) {
Logger.i("I18n", `Applying user language preference: "${userLang}"`);
setLanguage(userLang);
} else if (userLang === "" && root.systemDetectedLangCode !== "" && root.systemDetectedLangCode !== root.langCode) {
Logger.i("I18n", `Language reset to default, reverting to system language: "${root.systemDetectedLangCode}"`);
setLanguage(root.systemDetectedLangCode);
}
}
}
Component.onCompleted: {
Logger.i("I18n", "Service started");
var lang = determineFastLanguage();
langCode = lang.code;
fullLocaleCode = lang.fullLocale;
locale = Qt.locale(lang.fullLocale);
systemDetectedLangCode = lang.code;
Logger.i("I18n", `Loading "${lang.code}" (locale: "${lang.fullLocale}")`);
loadTranslations();
}
// Strip 4-letter script subtag from a BCP 47 tag (e.g. "fr-Latn-FR" → "fr-FR")
function stripScript(tag) {
var parts = tag.split("-");
var result = [];
for (var i = 0; i < parts.length; i++) {
if (parts[i].length === 4 && /^[A-Za-z]{4}$/.test(parts[i])) {
continue;
}
result.push(parts[i]);
}
return result.join("-");
}
// Determine the best language match against availableLanguages
function determineFastLanguage() {
// User preference from Settings (defaults to "" if not yet loaded from disk)
var userLang = Settings.data.general.language;
if (userLang !== "" && availableLanguages.includes(userLang)) {
return {
code: userLang,
fullLocale: userLang
};
}
// Match system locale against available translations
for (var i = 0; i < Qt.locale().uiLanguages.length; i++) {
var fullLang = Qt.locale().uiLanguages[i];
// Exact match (e.g. "zh-CN")
if (availableLanguages.includes(fullLang)) {
return {
code: fullLang,
fullLocale: fullLang
};
}
// Script-stripped match (e.g. "zh-Hans-CN" → "zh-CN")
var stripped = stripScript(fullLang);
if (stripped !== fullLang && availableLanguages.includes(stripped)) {
return {
code: stripped,
fullLocale: fullLang
};
}
// Language-only match (e.g. "fr-FR" → "fr")
var short_ = fullLang.substring(0, 2);
if (availableLanguages.includes(short_)) {
return {
code: short_,
fullLocale: fullLang
};
}
}
return {
code: "en",
fullLocale: "en"
};
}
function dateFormat() {
var lang = langCode.split("-")[0];
return dateFormats[lang] || "dddd, d MMMM";
}
// -------------------------------------------
function setLanguage(newLangCode, fullLocale) {
if (typeof fullLocale === "undefined") {
fullLocale = newLangCode;
}
if (newLangCode !== langCode && availableLanguages.includes(newLangCode)) {
langCode = newLangCode;
fullLocaleCode = fullLocale;
locale = Qt.locale(fullLocale);
Logger.i("I18n", `Language set to "${langCode}" with locale "${fullLocale}"`);
languageChanged(langCode);
loadTranslations();
} else if (!availableLanguages.includes(newLangCode)) {
Logger.w("I18n", `Language "${newLangCode}" is not available`);
}
}
// -------------------------------------------
function loadTranslations() {
if (langCode === "")
return;
const filePath = `file://${Quickshell.shellDir}/Assets/Translations/${langCode}.json`;
fileView.path = filePath;
isLoaded = false;
}
// -------------------------------------------
// Check if a translation exists
function hasTranslation(key) {
if (!isLoaded)
return false;
const keys = key.split(".");
var value = translations;
for (var i = 0; i < keys.length; i++) {
if (value && typeof value === "object" && keys[i] in value) {
value = value[keys[i]];
} else {
return false;
}
}
return typeof value === "string";
}
// -------------------------------------------
// Get all translation keys (useful for debugging)
function getAllKeys(obj, prefix) {
if (typeof obj === "undefined")
obj = translations;
if (typeof prefix === "undefined")
prefix = "";
var keys = [];
for (var key in (obj || {})) {
const value = obj[key];
const fullKey = prefix ? `${prefix}.${key}` : key;
if (typeof value === "object" && value !== null) {
keys = keys.concat(getAllKeys(value, fullKey));
} else if (typeof value === "string") {
keys.push(fullKey);
}
}
return keys;
}
// -------------------------------------------
// Reload translations (useful for development)
function reload() {
Logger.d("I18n", "Reloading translations");
loadTranslations();
}
// -------------------------------------------
// Main translation function
function tr(key, interpolations) {
if (typeof interpolations === "undefined")
interpolations = {};
if (!isLoaded) {
//Logger.d("I18n", "Translations not loaded yet")
return key;
}
// Navigate nested keys (e.g., "menu.file.open")
const keys = key.split(".");
// Look-up translation in the active language
var value = translations;
var notFound = false;
for (var i = 0; i < keys.length; i++) {
if (value && typeof value === "object" && keys[i] in value) {
value = value[keys[i]];
} else {
Logger.d("I18n", `Translation key "${key}" not found at part "${keys[i]}"`);
Logger.d("I18n", `Available keys: ${Object.keys(value || {}).join(", ")}`);
notFound = true;
break;
}
}
// Fallback to english if not found
if (notFound && availableLanguages.includes("en") && langCode !== "en") {
value = fallbackTranslations;
for (var i = 0; i < keys.length; i++) {
if (value && typeof value === "object" && keys[i] in value) {
value = value[keys[i]];
} else {
// Indicate this key does not even exists in the english fallback
return `!!${key}!!`;
}
}
} else if (notFound) {
// No fallback available
return `!!${key}!!`;
}
if (typeof value !== "string") {
Logger.d("I18n", `Translation key "${key}" is not a string`);
return key;
}
// Handle interpolations (e.g., "Hello {name}!")
var result = value;
for (var placeholder in interpolations) {
const regex = new RegExp(`\\{${placeholder}\\}`, 'g');
result = result.replace(regex, interpolations[placeholder]);
}
return result;
}
// -------------------------------------------
// Plural translation function
function trp(key, count, interpolations) {
if (typeof interpolations === "undefined") {
interpolations = {};
}
// Use key for singular, key-plural for plural
const realKey = count === 1 ? key : `${key}-plural`;
// Merge interpolations with count
var finalInterpolations = {
"count": count
};
for (var prop in interpolations) {
finalInterpolations[prop] = interpolations[prop];
}
return tr(realKey, finalInterpolations);
}
}
@@ -0,0 +1,85 @@
pragma Singleton
import QtQuick
import QtQuick.Controls
import Quickshell
import qs.Commons
Singleton {
id: root
// Expose the font family name for easy access
readonly property string fontFamily: currentFontLoader ? currentFontLoader.name : ""
readonly property string defaultIcon: IconsTabler.defaultIcon
readonly property var icons: IconsTabler.icons
readonly property var aliases: IconsTabler.aliases
readonly property string fontPath: "/Assets/Fonts/tabler/noctalia-tabler-icons.ttf"
// Current active font loader
property FontLoader currentFontLoader: null
property int fontVersion: 0
// Create a unique cache-busting path
readonly property string cacheBustingPath: Quickshell.shellDir + fontPath + "?v=" + fontVersion + "&t=" + Date.now()
// Signal emitted when font is reloaded
signal fontReloaded
Component.onCompleted: {
Logger.i("Icons", "Service started");
loadFontWithCacheBusting();
}
Connections {
target: Quickshell
function onReloadCompleted() {
Logger.d("Icons", "Quickshell reload completed - forcing font reload");
reloadFont();
}
}
// ---------------------------------------
function get(iconName) {
// Check in aliases first
if (aliases[iconName] !== undefined) {
iconName = aliases[iconName];
}
// Find the appropriate codepoint
return icons[iconName];
}
function loadFontWithCacheBusting() {
Logger.d("Icons", "Loading font with cache busting");
// Destroy old loader first
if (currentFontLoader) {
currentFontLoader.destroy();
currentFontLoader = null;
}
// Create new loader with cache-busting URL
currentFontLoader = Qt.createQmlObject(`
import QtQuick
FontLoader {
source: "${cacheBustingPath}"
}
`, root, "dynamicFontLoader_" + fontVersion);
// Connect to the new loader's status changes
currentFontLoader.statusChanged.connect(function () {
if (currentFontLoader.status === FontLoader.Ready) {
Logger.d("Icons", "Font loaded successfully:", currentFontLoader.name, "(version " + fontVersion + ")");
fontReloaded();
} else if (currentFontLoader.status === FontLoader.Error) {
Logger.e("Icons", "Font failed to load (version " + fontVersion + ")");
}
});
}
function reloadFont() {
Logger.d("Icons", "Forcing font reload...");
fontVersion++;
loadFontWithCacheBusting();
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,184 @@
pragma Singleton
import QtQuick
QtObject {
function getKeybindString(event) {
let keyStr = "";
if (event.modifiers & Qt.ControlModifier)
keyStr += "Ctrl+";
if (event.modifiers & Qt.AltModifier)
keyStr += "Alt+";
if (event.modifiers & Qt.ShiftModifier)
keyStr += "Shift+";
let keyName = "";
let rawText = event.text;
if (event.key >= Qt.Key_A && event.key <= Qt.Key_Z || event.key >= Qt.Key_0 && event.key <= Qt.Key_9) {
keyName = String.fromCharCode(event.key);
} else if (event.key >= Qt.Key_F1 && event.key <= Qt.Key_F12) {
keyName = "F" + (event.key - Qt.Key_F1 + 1);
} else if (rawText && rawText.length > 0 && rawText.charCodeAt(0) > 31 && rawText.charCodeAt(0) !== 127) {
keyName = rawText.toUpperCase();
if (event.modifiers & Qt.ShiftModifier) {
const shiftMap = {
"!": "1",
"\"": "2",
"§": "3",
"$": "4",
"%": "5",
"&": "6",
"/": "7",
"(": "8",
")": "9",
"=": "0",
"@": "2",
"#": "3",
"^": "6",
"*": "8"
};
if (shiftMap[keyName]) {
keyName = shiftMap[keyName];
}
}
} else {
switch (event.key) {
case Qt.Key_Escape:
keyName = "Esc";
break;
case Qt.Key_Space:
keyName = "Space";
break;
case Qt.Key_Return:
keyName = "Return";
break;
case Qt.Key_Enter:
keyName = "Enter";
break;
case Qt.Key_Tab:
keyName = "Tab";
break;
case Qt.Key_Backspace:
keyName = "Backspace";
break;
case Qt.Key_Delete:
keyName = "Del";
break;
case Qt.Key_Insert:
keyName = "Ins";
break;
case Qt.Key_Home:
keyName = "Home";
break;
case Qt.Key_End:
keyName = "End";
break;
case Qt.Key_PageUp:
keyName = "PgUp";
break;
case Qt.Key_PageDown:
keyName = "PgDn";
break;
case Qt.Key_Left:
keyName = "Left";
break;
case Qt.Key_Right:
keyName = "Right";
break;
case Qt.Key_Up:
keyName = "Up";
break;
case Qt.Key_Down:
keyName = "Down";
break;
}
}
if (!keyName)
return "";
return keyStr + keyName;
}
function checkKey(event, settingName, settings) {
// Accept both simplified names ("remove") and full property names ("keyRemove")
// so callers are less error-prone.
var normalized = String(settingName || "");
if (!normalized)
return false;
var propName = normalized.startsWith("key") ? normalized : ("key" + normalized.charAt(0).toUpperCase() + normalized.slice(1));
var boundKeys = settings.data.general.keybinds[propName];
if (!boundKeys || boundKeys.length === 0)
return false;
var eventString = getKeybindString(event);
for (var i = 0; i < boundKeys.length; i++) {
if (boundKeys[i] === eventString)
return true;
}
return false;
}
/**
* Check if a keybind string conflicts with any other existing keybinds.
* @param {string} keyStr - The keybind string to check (e.g., "Ctrl+A").
* @param {string} currentPath - The settings path of the keybind being edited (to skip checking itself).
* @param {object} data - The settings data object (from Settings.data).
* @returns {string|null} - The name of the conflicting action, or null if no conflict.
*/
function getKeybindConflict(keyStr, currentPath, data) {
if (!keyStr || !data)
return null;
const searchKey = String(keyStr).trim().toLowerCase();
// 1. Check navigation keybinds
const navKeybinds = data.general ? data.general.keybinds : null;
const navMap = {
"keyUp": "Navigation: Up",
"keyDown": "Navigation: Down",
"keyLeft": "Navigation: Left",
"keyRight": "Navigation: Right",
"keyEnter": "Navigation: Enter",
"keyEscape": "Navigation: Escape"
};
if (navKeybinds) {
for (const prop in navMap) {
const fullPath = "general.keybinds." + prop;
if (fullPath === currentPath)
continue;
const boundKeys = navKeybinds[prop];
if (boundKeys && boundKeys.length !== undefined) {
for (let i = 0; i < boundKeys.length; i++) {
if (String(boundKeys[i]).trim().toLowerCase() === searchKey) {
return navMap[prop];
}
}
}
}
}
// 2. Check session menu power options
const sessionMenu = data.sessionMenu;
if (sessionMenu && sessionMenu.powerOptions) {
const powerOptions = sessionMenu.powerOptions;
for (let i = 0; i < powerOptions.length; i++) {
const entry = powerOptions[i];
const fullPath = "sessionMenu.powerOptions[" + i + "].keybind";
if (fullPath === currentPath)
continue;
if (entry.keybind && String(entry.keybind).trim().toLowerCase() === searchKey) {
// Capitalize action name
const actionName = entry.action ? entry.action.charAt(0).toUpperCase() + entry.action.slice(1) : "Unknown";
return "Session Menu: " + actionName;
}
}
}
return null;
}
}
@@ -0,0 +1,69 @@
pragma Singleton
import Quickshell
import qs.Commons
Singleton {
id: root
function _formatMessage(...args) {
var t = Time.getFormattedTimestamp();
if (args.length > 1) {
const maxLength = 14;
var module = args.shift().substring(0, maxLength).padStart(maxLength, " ");
return `\x1b[36m[${t}]\x1b[0m \x1b[35m${module}\x1b[0m ` + args.join(" ");
} else {
return `[\x1b[36m[${t}]\x1b[0m ` + args.join(" ");
}
}
function _getStackTrace() {
try {
throw new Error("Stack trace");
} catch (e) {
return e.stack;
}
}
// Debug log (only when Settings.isDebug is true)
function d(...args) {
if (Settings?.isDebug) {
var msg = _formatMessage(...args);
console.debug(msg);
}
}
// Info log (always visible)
function i(...args) {
var msg = _formatMessage(...args);
console.info(msg);
}
// Warning log (always visible)
function w(...args) {
var msg = _formatMessage(...args);
console.warn(msg);
}
// Error log (always visible)
function e(...args) {
var msg = _formatMessage(...args);
console.error(msg);
}
function callStack() {
var stack = _getStackTrace();
Logger.i("Debug", "--------------------------");
Logger.i("Debug", "Current call stack");
// Split the stack into lines and log each one
var stackLines = stack.split('\n');
for (var i = 0; i < stackLines.length; i++) {
var line = stackLines[i].trim(); // Remove leading/trailing whitespace
if (line.length > 0) {
// Only log non-empty lines
Logger.i("Debug", `- ${line}`);
}
}
Logger.i("Debug", "--------------------------");
}
}
@@ -0,0 +1,23 @@
import QtQuick
QtObject {
id: root
// Migrate from version < 27 to version 27
// Converts settingsPanelAttachToBar boolean to settingsPanelMode string
function migrate(adapter, logger, rawJson) {
logger.i("Settings", "Migrating settings to v27");
// Check rawJson for old property (adapter doesn't expose removed properties)
if (rawJson?.ui?.settingsPanelAttachToBar !== undefined) {
if (rawJson.ui.settingsPanelAttachToBar === true) {
adapter.ui.settingsPanelMode = "attached";
} else {
adapter.ui.settingsPanelMode = "centered";
}
logger.i("Settings", "Migrated settingsPanelAttachToBar to settingsPanelMode: " + adapter.ui.settingsPanelMode);
}
return true;
}
}
@@ -0,0 +1,37 @@
import QtQuick
QtObject {
id: root
// Migrate TaskbarGrouped widgets to Workspace with showApplications: true
function migrate(adapter, logger, rawJson) {
logger.i("Settings", "Migrating settings to v28");
// Check bar widgets in all sections
const sections = ["left", "center", "right"];
for (const section of sections) {
if (!rawJson?.bar?.widgets?.[section])
continue;
const widgets = rawJson.bar.widgets[section];
for (let i = 0; i < widgets.length; i++) {
if (widgets[i].id === "TaskbarGrouped") {
// Convert TaskbarGrouped to Workspace with showApplications
const oldWidget = widgets[i];
adapter.bar.widgets[section][i] = {
id: "Workspace",
showApplications: true,
labelMode: oldWidget.labelMode || "index",
hideUnoccupied: oldWidget.hideUnoccupied || false,
showLabelsOnlyWhenOccupied: oldWidget.showLabelsOnlyWhenOccupied ?? true,
colorizeIcons: oldWidget.colorizeIcons || false
};
logger.i("Settings", "Migrated TaskbarGrouped to Workspace in " + section + " section");
}
}
}
return true;
}
}
@@ -0,0 +1,18 @@
import QtQuick
QtObject {
id: root
// Migrate systemMonitor.enableNvidiaGpu to systemMonitor.enableDgpuMonitoring
function migrate(adapter, logger, rawJson) {
logger.i("Settings", "Migrating settings to v32");
// Check rawJson for old property (adapter doesn't expose removed properties)
if (rawJson?.systemMonitor?.enableNvidiaGpu !== undefined) {
adapter.systemMonitor.enableDgpuMonitoring = rawJson.systemMonitor.enableNvidiaGpu;
logger.i("Settings", "Migrated systemMonitor.enableNvidiaGpu to enableDgpuMonitoring: " + adapter.systemMonitor.enableDgpuMonitoring);
}
return true;
}
}
@@ -0,0 +1,26 @@
import QtQuick
QtObject {
id: root
// Migrate wallpaper.randomEnabled to wallpaper.wallpaperChangeMode
function migrate(adapter, logger, rawJson) {
logger.i("Settings", "Migrating settings to v33");
if (rawJson?.wallpaper?.randomEnabled !== undefined) {
// We'll keep randomEnabled for backward compatibility but set wallpaperChangeMode
if (rawJson.wallpaper.randomEnabled === true) {
adapter.wallpaper.wallpaperChangeMode = "random";
logger.i("Settings", "Migrated wallpaper.randomEnabled=true to wallpaperChangeMode=random");
} else {
adapter.wallpaper.wallpaperChangeMode = "random";
logger.i("Settings", "Set wallpaperChangeMode=random (randomEnabled was false)");
}
} else {
adapter.wallpaper.wallpaperChangeMode = "random";
logger.i("Settings", "Set default wallpaperChangeMode=random");
}
return true;
}
}
@@ -0,0 +1,20 @@
import QtQuick
QtObject {
id: root
// Migrate bar.transparent to bar.backgroundOpacity + bar.useSeparateOpacity
function migrate(adapter, logger, rawJson) {
logger.i("Settings", "Migrating settings to v35");
if (rawJson?.bar?.transparent !== undefined) {
if (rawJson.bar.transparent === true) {
adapter.bar.backgroundOpacity = 0;
adapter.bar.useSeparateOpacity = true;
logger.i("Settings", "Migrated bar.transparent=true to backgroundOpacity=0, useSeparateOpacity=true");
}
}
return true;
}
}
@@ -0,0 +1,29 @@
import QtQuick
import Quickshell
import qs.Commons
QtObject {
id: root
// Clear legacy emoji usage cache to adopt new format
function migrate(adapter, logger, rawJson) {
logger.i("Settings", "Migrating settings to v36");
const usagePath = Settings.cacheDir + "emoji_usage.json";
if (!usagePath.endsWith("emoji_usage.json")) {
logger.w("Settings", "Skipping emoji usage cleanup due to unexpected path: " + usagePath);
return true;
}
try {
// Ensure dir exists then remove the file
Quickshell.execDetached(["sh", "-c", `mkdir -p "${Settings.cacheDir}" && rm -f -- "${usagePath}"`]);
logger.i("Settings", "Cleared legacy emoji usage file at: " + usagePath);
} catch (e) {
logger.w("Settings", "Failed to clear emoji usage cache: " + e);
}
return true;
}
}
@@ -0,0 +1,79 @@
import QtQuick
QtObject {
id: root
// Rename WiFi widgets/shortcuts to Network
function migrate(adapter, logger, rawJson) {
logger.i("Settings", "Migrating settings to v37 (WiFi → Network)");
let changed = false;
function migrateArray(contextName, rawArr, adapterArr, setItem) {
if (!adapterArr)
return;
const len = adapterArr.length;
for (let i = 0; i < len; i++) {
// Prefer raw item if present, otherwise use adapter item
const item = rawArr && rawArr[i] !== undefined ? rawArr[i] : adapterArr[i];
if (item === undefined || item === null)
continue;
// Case 1: simple string entries
if (typeof item === "string") {
if (item === "WiFi") {
setItem(i, "Network");
changed = true;
logger.i("Settings", `Migrated ${contextName}[${i}] from WiFi to Network (string entry)`);
}
continue;
}
// Case 2: object entries with id
let id = item.id !== undefined ? item.id : (item.widgetId !== undefined ? item.widgetId : undefined);
if (id === "WiFi") {
var newObj = {};
for (var key in item)
newObj[key] = item[key];
if (newObj.id !== undefined)
newObj.id = "Network";
if (newObj.widgetId !== undefined && newObj.id === undefined)
newObj.widgetId = "Network"; // fallback if older schema used widgetId
setItem(i, newObj);
changed = true;
logger.i("Settings", `Migrated ${contextName}[${i}] from WiFi to Network (object entry)`);
}
}
}
// --- Bar widgets: left/center/right
const sections = ["left", "center", "right"];
for (const section of sections) {
const rawArr = rawJson?.bar?.widgets?.[section];
const adapterArr = adapter?.bar?.widgets?.[section];
migrateArray(`bar.widgets.${section}`, rawArr, adapterArr, function (i, v) {
adapter.bar.widgets[section][i] = v;
});
}
// --- Control Center shortcuts: left/right
const rawLeft = rawJson?.controlCenter?.shortcuts?.left;
const adLeft = adapter?.controlCenter?.shortcuts?.left;
migrateArray("controlCenter.shortcuts.left", rawLeft, adLeft, function (i, v) {
adapter.controlCenter.shortcuts.left[i] = v;
});
const rawRight = rawJson?.controlCenter?.shortcuts?.right;
const adRight = adapter?.controlCenter?.shortcuts?.right;
migrateArray("controlCenter.shortcuts.right", rawRight, adRight, function (i, v) {
adapter.controlCenter.shortcuts.right[i] = v;
});
if (!changed) {
logger.i("Settings", "No WiFi widget IDs found to migrate; leaving settings unchanged");
}
return true;
}
}
@@ -0,0 +1,27 @@
import QtQuick
import qs.Commons
QtObject {
function migrate(adapter, logger, rawJson) {
logger.i("Migration38", "Migrating bar margins from percentages to integers");
// Use rawJson to read original values, adapter to write new values (following migration patterns)
const rawVertical = rawJson?.bar?.marginVertical;
const rawHorizontal = rawJson?.bar?.marginHorizontal;
// Only migrate if values exist and are percentages (<= 1.0)
if (rawVertical !== undefined && typeof rawVertical === 'number' && rawVertical <= 1.0) {
const marginXL = 18; // Standard value of Style.marginXL
adapter.bar.marginVertical = Math.round(rawVertical * marginXL);
logger.d("Migration38", "Converted marginVertical from " + rawVertical + " to " + adapter.bar.marginVertical + "px");
}
if (rawHorizontal !== undefined && typeof rawHorizontal === 'number' && rawHorizontal <= 1.0) {
const marginXL = 18; // Standard value of Style.marginXL
adapter.bar.marginHorizontal = Math.round(rawHorizontal * marginXL);
logger.d("Migration38", "Converted marginHorizontal from " + rawHorizontal + " to " + adapter.bar.marginHorizontal + "px");
}
return true;
}
}
@@ -0,0 +1,43 @@
import QtQuick
QtObject {
id: root
// List of all template IDs that existed as booleans in the old format
readonly property var templateIds: ["gtk", "qt", "kcolorscheme", "alacritty", "kitty", "ghostty", "foot", "wezterm", "fuzzel", "discord", "pywalfox", "vicinae", "walker", "code", "spicetify", "telegram", "cava", "yazi", "emacs", "niri", "hyprland", "mango", "zed", "helix", "zenBrowser"]
function migrate(adapter, logger, rawJson) {
logger.i("Migration39", "Migrating templates from boolean format to activeTemplates array");
// Check if old format exists (any boolean template property)
const oldTemplates = rawJson?.templates;
if (!oldTemplates) {
logger.d("Migration39", "No templates section found, skipping migration");
return true;
}
// Check if already migrated (has activeTemplates array)
if (Array.isArray(oldTemplates.activeTemplates)) {
logger.d("Migration39", "Already has activeTemplates array, skipping migration");
return true;
}
// Build the new activeTemplates array from old boolean values
const activeTemplates = [];
for (const templateId of templateIds) {
if (oldTemplates[templateId] === true) {
activeTemplates.push({
"id": templateId,
"enabled": true
});
logger.d("Migration40", "Migrated enabled template: " + templateId);
}
}
// Write the new format
adapter.templates.activeTemplates = activeTemplates;
logger.i("Migration40", "Migrated " + activeTemplates.length + " templates to new array format");
return true;
}
}
@@ -0,0 +1,28 @@
import QtQuick
QtObject {
id: root
function migrate(adapter, logger, rawJson) {
logger.i("Migration42", "Migrating randomEnabled to automationEnabled");
const wallpaper = rawJson?.wallpaper;
if (!wallpaper) {
logger.d("Migration42", "No wallpaper section found, skipping migration");
return true;
}
// Check if already migrated (has automationEnabled)
if (wallpaper.automationEnabled !== undefined) {
logger.d("Migration42", "Already has automationEnabled, skipping migration");
return true;
}
// Migrate randomEnabled to automationEnabled
const oldValue = wallpaper.randomEnabled ?? false;
adapter.wallpaper.automationEnabled = oldValue;
logger.i("Migration42", "Migrated randomEnabled=" + oldValue + " to automationEnabled=" + oldValue);
return true;
}
}
@@ -0,0 +1,29 @@
import QtQuick
QtObject {
id: root
function migrate(adapter, logger, rawJson) {
logger.i("Migration43", "Migrating recursiveSearch to viewMode");
const wallpaper = rawJson?.wallpaper;
if (!wallpaper) {
logger.d("Migration43", "No wallpaper section found, skipping migration");
return true;
}
// Check if already migrated (has viewMode)
if (wallpaper.viewMode !== undefined) {
logger.d("Migration43", "Already has viewMode, skipping migration");
return true;
}
// Migrate recursiveSearch to viewMode
const oldValue = wallpaper.recursiveSearch ?? false;
const newValue = oldValue ? "recursive" : "single";
adapter.wallpaper.viewMode = newValue;
logger.i("Migration43", "Migrated recursiveSearch=" + oldValue + " to viewMode=" + newValue);
return true;
}
}
@@ -0,0 +1,35 @@
import QtQuick
import Quickshell
import qs.Commons
QtObject {
id: root
function migrate(adapter, logger, rawJson) {
logger.i("Migration44", "Updating PAM pam/password.conf");
const configDir = Settings.configDir;
const pamConfigDir = configDir + "pam";
const pamConfigFile = pamConfigDir + "/password.conf";
const pamConfigDirEsc = pamConfigDir.replace(/'/g, "'\\''");
const pamConfigFileEsc = pamConfigFile.replace(/'/g, "'\\''");
// Ensure directory exists
Quickshell.execDetached(["mkdir", "-p", pamConfigDir]);
// Generate the PAM config file content (updated version)
var configContent = "auth sufficient pam_fprintd.so timeout=-1\n";
configContent += "auth sufficient /run/current-system/sw/lib/security/pam_fprintd.so timeout=-1 # for NixOS\n";
configContent += "auth required pam_unix.so\n";
// Write the config file using heredoc
var script = `cat > '${pamConfigFileEsc}' << 'EOF'\n`;
script += configContent;
script += "EOF\n";
Quickshell.execDetached(["sh", "-c", script]);
logger.d("Migration44", "PAM config file updated at:", pamConfigFile);
return true;
}
}
@@ -0,0 +1,18 @@
import QtQuick
QtObject {
/**
* Migration 45: Migrate 'floating' bar setting to 'barType'
*/
function migrate(adapter, logger, rawJson) {
logger.i("Migration45", "Migrating bar settings...");
if (rawJson?.bar?.floating) {
adapter.bar.barType = "floating";
} else {
adapter.bar.barType = "simple";
}
return true;
}
}
@@ -0,0 +1,21 @@
import QtQuick
import Quickshell
import qs.Commons
QtObject {
id: root
function migrate(adapter, logger, rawJson) {
logger.i("Migration46", "Removing legacy PAM configuration file");
const configDir = Settings.configDir;
const pamConfigDir = configDir + "pam";
// Remove the entire pam directory if it exists
const script = `rm -rf '${pamConfigDir}'`;
Quickshell.execDetached(["sh", "-c", script]);
logger.d("Migration46", "Cleaned up legacy PAM config");
return true;
}
}
@@ -0,0 +1,18 @@
import QtQuick
import Quickshell
import qs.Commons
QtObject {
id: root
function migrate(adapter, logger, rawJson) {
logger.i("Migration47", "Removing network_stats.json cache");
const networkStatsFile = Settings.cacheDir + "network_stats.json";
Quickshell.execDetached(["rm", "-f", networkStatsFile]);
logger.d("Migration47", "Removed network_stats.json");
return true;
}
}
@@ -0,0 +1,22 @@
import QtQuick
QtObject {
id: root
// Migrate battery thresholds from notifications to systemMonitor
function migrate(adapter, logger, rawJson) {
logger.i("Settings", "Migrating settings to v48");
if (rawJson?.notifications?.batteryWarningThreshold !== undefined) {
adapter.systemMonitor.batteryWarningThreshold = rawJson.notifications.batteryWarningThreshold;
logger.i("Settings", "Migrated notifications.batteryWarningThreshold to systemMonitor: " + adapter.systemMonitor.batteryWarningThreshold);
}
if (rawJson?.notifications?.batteryCriticalThreshold !== undefined) {
adapter.systemMonitor.batteryCriticalThreshold = rawJson.notifications.batteryCriticalThreshold;
logger.i("Settings", "Migrated notifications.batteryCriticalThreshold to systemMonitor: " + adapter.systemMonitor.batteryCriticalThreshold);
}
return true;
}
}
@@ -0,0 +1,17 @@
import QtQuick
import Quickshell
import qs.Commons
QtObject {
id: root
// Remove old launcher_app_usage.json (usage tracking moved to ShellState)
function migrate(adapter, logger, rawJson) {
logger.i("Settings", "Migrating settings to v49");
Quickshell.execDetached(["rm", "-f", Settings.cacheDir + "launcher_app_usage.json"]);
logger.i("Settings", "Removed old launcher_app_usage.json");
return true;
}
}
@@ -0,0 +1,24 @@
import QtQuick
QtObject {
id: root
// Migrate keybinds from single strings to arrays of strings
function migrate(adapter, logger, rawJson) {
logger.i("Settings", "Migrating settings to v50");
const keybinds = rawJson?.general?.keybinds;
if (!keybinds)
return true;
const keys = ["keyUp", "keyDown", "keyLeft", "keyRight", "keyEnter", "keyEscape"];
for (const key of keys) {
if (keybinds[key] !== undefined && typeof keybinds[key] === "string") {
adapter.general.keybinds[key] = [keybinds[key]];
logger.i("Settings", "Migrated keybinds." + key + " from string to array: [" + keybinds[key] + "]");
}
}
return true;
}
}
@@ -0,0 +1,39 @@
import QtQuick
QtObject {
id: root
// Add default keybinds (1-6) to session menu power options if none are defined
function migrate(adapter, logger, rawJson) {
logger.i("Settings", "Migrating settings to v53");
const powerOptions = rawJson?.sessionMenu?.powerOptions;
if (!powerOptions || !Array.isArray(powerOptions))
return true;
// Check if any power option has a keybind defined
const hasAnyKeybind = powerOptions.some(opt => opt.keybind && opt.keybind !== "");
if (hasAnyKeybind)
return true;
// No keybinds defined — apply defaults matching the action order
const defaultKeybinds = {
"lock": "1",
"suspend": "2",
"hibernate": "3",
"reboot": "4",
"logout": "5",
"shutdown": "6"
};
for (let i = 0; i < powerOptions.length; i++) {
const action = powerOptions[i].action;
if (defaultKeybinds[action]) {
adapter.sessionMenu.powerOptions[i].keybind = defaultKeybinds[action];
logger.i("Settings", "Set keybind '" + defaultKeybinds[action] + "' for session menu action: " + action);
}
}
return true;
}
}
@@ -0,0 +1,28 @@
import QtQuick
QtObject {
id: root
// Add numpad Enter as a second default keybind for keyEnter
function migrate(adapter, logger, rawJson) {
logger.i("Settings", "Migrating settings to v54");
const keybinds = rawJson?.general?.keybinds;
if (!keybinds)
return true;
const keyEnter = keybinds.keyEnter;
if (!keyEnter || !Array.isArray(keyEnter))
return true;
// Only add "Enter" if the first entry is "Return" and "Enter" isn't already present
if (keyEnter[0] === "Return" && !keyEnter.includes("Enter")) {
var updated = Array.from(keyEnter);
updated.splice(1, 0, "Enter");
adapter.general.keybinds.keyEnter = updated;
logger.i("Settings", "Added 'Enter' (numpad) as second default keybind for keyEnter");
}
return true;
}
}
@@ -0,0 +1,22 @@
import QtQuick
QtObject {
id: root
function migrate(adapter, logger, rawJson) {
logger.i("Settings", "Migrating settings to v55");
// Check if the old setting exists
if (rawJson.controlCenter && rawJson.controlCenter.openAtMouseOnBarRightClick !== undefined) {
if (!rawJson.bar)
rawJson.bar = {};
rawJson.bar.rightClickFollowMouse = rawJson.controlCenter.openAtMouseOnBarRightClick;
delete rawJson.controlCenter.openAtMouseOnBarRightClick;
logger.i("Settings", "Successfully moved openAtMouseOnBarRightClick to bar.rightClickFollowMouse");
}
return true;
}
}
@@ -0,0 +1,21 @@
import QtQuick
import Quickshell
import qs.Commons
QtObject {
id: root
function migrate(adapter, logger, rawJson) {
logger.i("Settings", "Migrating settings to v56 (Color Scheme Migration)");
const scriptPath = Quickshell.shellDir + "/Scripts/python/src/theming/migrate-colorschemes.py";
const configDir = Settings.configDir;
logger.i("Settings", `Running color scheme migration script: ${scriptPath} with configDir: ${configDir}`);
// Run the migration script detached
Quickshell.execDetached(["python3", scriptPath, configDir]);
return true;
}
}
@@ -0,0 +1,16 @@
import QtQuick
QtObject {
id: root
function migrate(adapter, logger, rawJson) {
logger.i("Settings", "Migrating settings to v57 (cavaFrameRate -> spectrumFrameRate)");
if (rawJson && rawJson.audio && rawJson.audio.cavaFrameRate !== undefined) {
adapter.audio.spectrumFrameRate = rawJson.audio.cavaFrameRate;
logger.i("Settings", "Migrated cavaFrameRate:", rawJson.audio.cavaFrameRate, "-> spectrumFrameRate");
}
return true;
}
}
@@ -0,0 +1,14 @@
import QtQuick
QtObject {
function migrate(adapter, logger, rawJson) {
logger.i("Settings", "Migrating settings to v58 (dock.dockType: static -> attached)");
if (rawJson && rawJson.dock && rawJson.dock.dockType === "static") {
adapter.dock.dockType = "attached";
logger.i("Settings", "Migrated dock.dockType: static -> attached");
}
return true;
}
}
@@ -0,0 +1,25 @@
import QtQuick
QtObject {
function migrate(adapter, logger, rawJson) {
logger.i("Settings", "Migrating settings to v59 (wallpaper.transitionType: string -> array)");
if (rawJson && rawJson.wallpaper && typeof rawJson.wallpaper.transitionType === "string") {
var oldValue = rawJson.wallpaper.transitionType;
var newValue;
if (oldValue === "random") {
newValue = ["fade", "disc", "stripes", "wipe", "pixelate", "honeycomb"];
} else if (oldValue === "none") {
newValue = [];
} else {
newValue = [oldValue];
}
adapter.wallpaper.transitionType = newValue;
logger.i("Settings", "Migrated wallpaper.transitionType:", oldValue, "->", JSON.stringify(newValue));
}
return true;
}
}
@@ -0,0 +1,63 @@
pragma Singleton
import QtQuick
QtObject {
id: root
// Map of version number to migration component
readonly property var migrations: ({
27: migration27Component,
28: migration28Component,
32: migration32Component,
33: migration33Component,
35: migration35Component,
36: migration36Component,
37: migration37Component,
38: migration38Component,
40: migration40Component,
42: migration42Component,
43: migration43Component,
44: migration44Component,
45: migration45Component,
46: migration46Component,
47: migration47Component,
48: migration48Component,
49: migration49Component,
50: migration50Component,
53: migration53Component,
54: migration54Component,
55: migration55Component,
56: migration56Component,
57: migration57Component,
58: migration58Component,
59: migration59Component
})
// Migration components
property Component migration27Component: Migration27 {}
property Component migration28Component: Migration28 {}
property Component migration32Component: Migration32 {}
property Component migration33Component: Migration33 {}
property Component migration35Component: Migration35 {}
property Component migration36Component: Migration36 {}
property Component migration37Component: Migration37 {}
property Component migration38Component: Migration38 {}
property Component migration40Component: Migration40 {}
property Component migration42Component: Migration42 {}
property Component migration43Component: Migration43 {}
property Component migration44Component: Migration44 {}
property Component migration45Component: Migration45 {}
property Component migration46Component: Migration46 {}
property Component migration47Component: Migration47 {}
property Component migration48Component: Migration48 {}
property Component migration49Component: Migration49 {}
property Component migration50Component: Migration50 {}
property Component migration53Component: Migration53 {}
property Component migration54Component: Migration54 {}
property Component migration55Component: Migration55 {}
property Component migration56Component: Migration56 {}
property Component migration57Component: Migration57 {}
property Component migration58Component: Migration58 {}
property Component migration59Component: Migration59 {}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,287 @@
pragma Singleton
import QtQuick
import Quickshell
import Quickshell.Io
import "../Helpers/QtObj2JS.js" as QtObj2JS
import qs.Services.Power
import qs.Services.System
import qs.Services.UI
// Centralized shell state management for small cache files
Singleton {
id: root
property string stateFile: ""
property bool isLoaded: false
// State properties for different services
readonly property alias data: adapter
// Signals for state changes
signal displayStateChanged
signal notificationsStateChanged
signal changelogStateChanged
signal colorSchemesListChanged
Component.onCompleted: {
// Setup state file path (needs Settings to be available)
Qt.callLater(() => {
if (typeof Settings !== 'undefined' && Settings.cacheDir) {
stateFile = Settings.cacheDir + "shell-state.json";
stateFileView.path = stateFile;
}
});
}
// FileView for shell state
FileView {
id: stateFileView
printErrors: false
watchChanges: false
adapter: JsonAdapter {
id: adapter
// CompositorService: display scales
property var display: ({})
// NotificationService: notification state
property var notificationsState: ({
lastSeenTs: 0
})
// UpdateService: changelog state
property var changelogState: ({
lastSeenVersion: ""
})
// SchemeDownloader: color schemes list
property var colorSchemesList: ({
schemes: [],
timestamp: 0
})
// UI state: settings panel, etc.
property var ui: ({
settingsSidebarExpanded: true
})
// Telemetry state
property var telemetry: ({
instanceId: ""
})
// Launcher app usage counts
property var launcherUsage: ({})
}
onLoaded: {
root.isLoaded = true;
Logger.d("ShellState", "Loaded state file");
}
onLoadFailed: error => {
if (error === 2) {
// File doesn't exist, will be created on first write
root.isLoaded = true;
Logger.d("ShellState", "State file doesn't exist, will create on first write");
} else {
Logger.e("ShellState", "Failed to load state file:", error);
root.isLoaded = true;
}
}
}
// Launcher usage
function getLauncherUsageCount(key) {
const m = adapter.launcherUsage;
if (!m)
return 0;
const v = m[key];
return typeof v === 'number' && isFinite(v) ? v : 0;
}
function recordLauncherUsage(key) {
let counts = Object.assign({}, adapter.launcherUsage || {});
counts[key] = getLauncherUsageCount(key) + 1;
adapter.launcherUsage = counts;
save();
}
// Migrate usage from one key to another, merging counts in a single save
function migrateLauncherUsage(fromKey, toKey) {
let counts = Object.assign({}, adapter.launcherUsage || {});
const fromCount = typeof counts[fromKey] === 'number' && isFinite(counts[fromKey]) ? counts[fromKey] : 0;
const toCount = typeof counts[toKey] === 'number' && isFinite(counts[toKey]) ? counts[toKey] : 0;
counts[toKey] = toCount + fromCount;
delete counts[fromKey];
adapter.launcherUsage = counts;
save();
}
// Debounced save timer
Timer {
id: saveTimer
interval: 500
onTriggered: performSave()
}
property bool saveQueued: false
function save() {
saveQueued = true;
saveTimer.restart();
}
function performSave() {
if (!saveQueued || !stateFile) {
return;
}
saveQueued = false;
try {
// Ensure cache directory exists
Quickshell.execDetached(["mkdir", "-p", Settings.cacheDir]);
Qt.callLater(() => {
try {
stateFileView.writeAdapter();
Logger.d("ShellState", "Saved state file");
} catch (writeError) {
Logger.e("ShellState", "Failed to write state file:", writeError);
}
});
} catch (error) {
Logger.e("ShellState", "Failed to save state:", error);
}
}
// Convenience functions for each service
// Display state (CompositorService)
function setDisplay(displayData) {
adapter.display = displayData;
save();
displayStateChanged();
}
function getDisplay() {
return adapter.display || {};
}
// Notifications state (NotificationService)
function setNotificationsState(stateData) {
adapter.notificationsState = stateData;
save();
notificationsStateChanged();
}
function getNotificationsState() {
return adapter.notificationsState || {
lastSeenTs: 0
};
}
// Changelog state (UpdateService)
function setChangelogState(stateData) {
adapter.changelogState = stateData;
save();
changelogStateChanged();
}
function getChangelogState() {
return adapter.changelogState || {
lastSeenVersion: ""
};
}
// Color schemes list (SchemeDownloader)
function setColorSchemesList(listData) {
adapter.colorSchemesList = listData;
save();
colorSchemesListChanged();
}
function getColorSchemesList() {
return adapter.colorSchemesList || {
schemes: [],
timestamp: 0
};
}
// UI state
function setUiState(stateData) {
adapter.ui = stateData;
save();
}
function getUiState() {
return adapter.ui || {
settingsSidebarExpanded: true
};
}
function setSettingsSidebarExpanded(expanded) {
let uiState = getUiState();
uiState.settingsSidebarExpanded = expanded;
setUiState(uiState);
}
function getSettingsSidebarExpanded() {
return getUiState().settingsSidebarExpanded !== false; // default to true
}
// Telemetry state
function setTelemetryState(stateData) {
adapter.telemetry = stateData;
save();
}
function getTelemetryState() {
return adapter.telemetry || {
instanceId: ""
};
}
function getTelemetryInstanceId() {
return getTelemetryState().instanceId || "";
}
function setTelemetryInstanceId(instanceId) {
let state = getTelemetryState();
state.instanceId = instanceId;
setTelemetryState(state);
}
// -----------------------------------------------------
function buildStateSnapshot() {
try {
const settingsData = QtObj2JS.qtObjectToPlainObject(Settings.data);
const shellStateData = ShellState?.data ? QtObj2JS.qtObjectToPlainObject(ShellState.data) || {} : {};
return {
settings: settingsData,
state: {
doNotDisturb: NotificationService.doNotDisturb,
noctaliaPerformanceMode: PowerProfileService.noctaliaPerformanceMode,
barVisible: BarService.isVisible,
openedPanel: PanelService.openedPanel?.objectName || "",
lockScreenActive: PanelService.lockScreen?.active || false,
wallpapers: WallpaperService.getWallpapersEffectiveMap(),
desktopWidgetsEditMode: DesktopWidgetRegistry.editMode || false,
// -------------
display: shellStateData.display || {},
notificationsState: shellStateData.notificationsState || {},
changelogState: shellStateData.changelogState || {},
colorSchemesList: shellStateData.colorSchemesList || {},
ui: shellStateData.ui || {}
}
};
} catch (error) {
Logger.e("Settings", "Failed to build state snapshot:", error);
return null;
}
}
}
@@ -0,0 +1,247 @@
pragma Singleton
import QtQuick
import Quickshell
import qs.Services.Power
Singleton {
id: root
// Font size
readonly property real fontSizeXXS: 8
readonly property real fontSizeXS: 9
readonly property real fontSizeS: 10
readonly property real fontSizeM: 11
readonly property real fontSizeL: 13
readonly property real fontSizeXL: 16
readonly property real fontSizeXXL: 18
readonly property real fontSizeXXXL: 24
// Font weight
readonly property int fontWeightRegular: 400
readonly property int fontWeightMedium: 500
readonly property int fontWeightSemiBold: 600
readonly property int fontWeightBold: 700
// Container Radii: major layout sections (sidebars, cards, content panels)
readonly property int radiusXXXS: Math.round(3 * Settings.data.general.radiusRatio)
readonly property int radiusXXS: Math.round(4 * Settings.data.general.radiusRatio)
readonly property int radiusXS: Math.round(8 * Settings.data.general.radiusRatio)
readonly property int radiusS: Math.round(12 * Settings.data.general.radiusRatio)
readonly property int radiusM: Math.round(16 * Settings.data.general.radiusRatio)
readonly property int radiusL: Math.round(20 * Settings.data.general.radiusRatio)
// Input radii: interactive elements (buttons, toggles, text fields)
readonly property int iRadiusXXXS: Math.round(3 * Settings.data.general.iRadiusRatio)
readonly property int iRadiusXXS: Math.round(4 * Settings.data.general.iRadiusRatio)
readonly property int iRadiusXS: Math.round(8 * Settings.data.general.iRadiusRatio)
readonly property int iRadiusS: Math.round(12 * Settings.data.general.iRadiusRatio)
readonly property int iRadiusM: Math.round(16 * Settings.data.general.iRadiusRatio)
readonly property int iRadiusL: Math.round(20 * Settings.data.general.iRadiusRatio)
readonly property int screenRadius: Math.round(20 * Settings.data.general.screenRadiusRatio)
// Border
readonly property int borderS: Math.max(1, Math.round(1 * uiScaleRatio))
readonly property int borderM: Math.max(1, Math.round(2 * uiScaleRatio))
readonly property int borderL: Math.max(1, Math.round(3 * uiScaleRatio))
// Margins (for margins and spacing)
readonly property int marginXXXS: Math.round(1 * uiScaleRatio)
readonly property int marginXXS: Math.round(2 * uiScaleRatio)
readonly property int marginXS: Math.round(4 * uiScaleRatio)
readonly property int marginS: Math.round(6 * uiScaleRatio)
readonly property int marginM: Math.round(9 * uiScaleRatio)
readonly property int marginL: Math.round(13 * uiScaleRatio)
readonly property int marginXL: Math.round(18 * uiScaleRatio)
// Double margins, for proper container sizing only (e.g. height: id.implicitHeight + Style.margin2M)
readonly property int margin2XXXS: marginXXXS * 2
readonly property int margin2XXS: marginXXS * 2
readonly property int margin2XS: marginXS * 2
readonly property int margin2S: marginS * 2
readonly property int margin2M: marginM * 2
readonly property int margin2L: marginL * 2
readonly property int margin2XL: marginXL * 2
// Opacity
readonly property real opacityNone: 0.0
readonly property real opacityLight: 0.25
readonly property real opacityMedium: 0.5
readonly property real opacityHeavy: 0.75
readonly property real opacityAlmost: 0.95
readonly property real opacityFull: 1.0
// Shadows
readonly property real shadowOpacity: 0.85
readonly property real shadowBlur: 1.0
readonly property int shadowBlurMax: 22
readonly property real shadowHorizontalOffset: Settings.data.general.shadowOffsetX
readonly property real shadowVerticalOffset: Settings.data.general.shadowOffsetY
// Animation duration (ms)
readonly property int animationFaster: (Settings.data.general.animationDisabled || PowerProfileService.noctaliaPerformanceMode) ? 0 : Math.round(75 / Settings.data.general.animationSpeed)
readonly property int animationFast: (Settings.data.general.animationDisabled || PowerProfileService.noctaliaPerformanceMode) ? 0 : Math.round(150 / Settings.data.general.animationSpeed)
readonly property int animationNormal: (Settings.data.general.animationDisabled || PowerProfileService.noctaliaPerformanceMode) ? 0 : Math.round(300 / Settings.data.general.animationSpeed)
readonly property int animationSlow: (Settings.data.general.animationDisabled || PowerProfileService.noctaliaPerformanceMode) ? 0 : Math.round(450 / Settings.data.general.animationSpeed)
readonly property int animationSlowest: (Settings.data.general.animationDisabled || PowerProfileService.noctaliaPerformanceMode) ? 0 : Math.round(750 / Settings.data.general.animationSpeed)
// Delays
readonly property int tooltipDelay: 300
readonly property int tooltipDelayLong: 1200
readonly property int pillDelay: 500
// Widgets base size
readonly property real baseWidgetSize: 33
readonly property real sliderWidth: 200
readonly property real uiScaleRatio: Settings.data.general.scaleRatio
// Bar Height
readonly property real barHeight: {
let h;
switch (Settings.data.bar.density) {
case "mini":
h = (Settings.data.bar.position === "left" || Settings.data.bar.position === "right") ? 23 : 21;
break;
case "compact":
h = (Settings.data.bar.position === "left" || Settings.data.bar.position === "right") ? 27 : 25;
break;
case "comfortable":
h = (Settings.data.bar.position === "left" || Settings.data.bar.position === "right") ? 39 : 37;
break;
case "spacious":
h = (Settings.data.bar.position === "left" || Settings.data.bar.position === "right") ? 49 : 47;
break;
default:
case "default":
h = (Settings.data.bar.position === "left" || Settings.data.bar.position === "right") ? 33 : 31;
}
return toOdd(h);
}
// Capsule Height
// Note: capsule must always be smaller than barHeight to account for border rendering
// Qt Quick Rectangle borders are drawn centered on edges (half inside, half outside)
readonly property real capsuleHeight: {
let h;
switch (Settings.data.bar.density) {
case "mini":
h = Math.round(barHeight * 0.90);
break;
case "compact":
h = Math.round(barHeight * 0.85);
break;
case "comfortable":
h = Math.round(barHeight * 0.75);
break;
case "spacious":
h = Math.round(barHeight * 0.65);
break;
default:
h = Math.round(barHeight * 0.82);
break;
}
return toOdd(h);
}
// The base/default font size for all texts in the bar
readonly property real _barBaseFontSize: Math.max(1, (Style.barHeight / Style.capsuleHeight) * Style.fontSizeXXS)
readonly property real barFontSize: (Settings.data.bar.position === "left" || Settings.data.bar.position === "right") ? _barBaseFontSize * 0.9 * Settings.data.bar.fontScale : _barBaseFontSize * Settings.data.bar.fontScale
readonly property color capsuleColor: Settings.data.bar.showCapsule ? Qt.alpha(Settings.data.bar.capsuleColorKey !== "none" ? Color.resolveColorKey(Settings.data.bar.capsuleColorKey) : Color.mSurfaceVariant, Settings.data.bar.capsuleOpacity) : "transparent"
readonly property color capsuleBorderColor: Settings.data.bar.showOutline ? Color.mPrimary : "transparent"
readonly property int capsuleBorderWidth: Settings.data.bar.showOutline ? Style.borderS : 0
readonly property color boxBorderColor: Settings.data.ui.boxBorderEnabled ? Color.mOutline : "transparent"
// Pixel-perfect utility for centering content without subpixel positioning
function pixelAlignCenter(containerSize, contentSize) {
return Math.round((containerSize - contentSize) / 2);
}
// Ensures a number is always odd (rounds down to nearest odd)
function toOdd(n) {
return Math.floor(n / 2) * 2 + 1;
}
// Ensures a number is always even (rounds down to nearest even)
function toEven(n) {
return Math.floor(n / 2) * 2;
}
// Get bar height for a specific density and orientation
function getBarHeightForDensity(density, isVertical) {
let h;
switch (density) {
case "mini":
h = isVertical ? 23 : 21;
break;
case "compact":
h = isVertical ? 27 : 25;
break;
case "comfortable":
h = isVertical ? 39 : 37;
break;
case "spacious":
h = isVertical ? 49 : 47;
break;
default:
case "default":
h = isVertical ? 33 : 31;
}
return toOdd(h);
}
// Get capsule height for a specific density and bar height
function getCapsuleHeightForDensity(density, barHeight) {
let h;
switch (density) {
case "mini":
h = Math.round(barHeight * 0.90);
break;
case "compact":
h = Math.round(barHeight * 0.85);
break;
case "comfortable":
h = Math.round(barHeight * 0.75);
break;
case "spacious":
h = Math.round(barHeight * 0.65);
break;
default:
h = Math.round(barHeight * 0.82);
break;
}
return toOdd(h);
}
// Get bar font size for a specific bar height, capsule height, and orientation
function getBarFontSizeForDensity(barHeight, capsuleHeight, isVertical) {
const baseFontSize = Math.max(1, (barHeight / capsuleHeight) * Style.fontSizeXXS);
return isVertical ? baseFontSize * 0.9 * Settings.data.bar.fontScale : baseFontSize * Settings.data.bar.fontScale;
}
// Convenience functions for per-screen bar sizing
function getBarHeightForScreen(screenName) {
var density = Settings.getBarDensityForScreen(screenName);
var position = Settings.getBarPositionForScreen(screenName);
var isVertical = position === "left" || position === "right";
return getBarHeightForDensity(density, isVertical);
}
function getCapsuleHeightForScreen(screenName) {
var barHeight = getBarHeightForScreen(screenName);
var density = Settings.getBarDensityForScreen(screenName);
return getCapsuleHeightForDensity(density, barHeight);
}
function getBarFontSizeForScreen(screenName) {
var barHeight = getBarHeightForScreen(screenName);
var capsuleHeight = getCapsuleHeightForScreen(screenName);
var position = Settings.getBarPositionForScreen(screenName);
var isVertical = position === "left" || position === "right";
return getBarFontSizeForDensity(barHeight, capsuleHeight, isVertical);
}
}
@@ -0,0 +1,289 @@
pragma Singleton
import QtQuick
import Quickshell
import qs.Commons
Singleton {
id: root
property real scoreThreshold: 0.2
// Manual overrides for tricky apps
property var substitutions: ({
"code-url-handler": "visual-studio-code",
"Code": "visual-studio-code",
"gnome-tweaks": "org.gnome.tweaks",
"pavucontrol-qt": "pavucontrol",
"wps": "wps-office2019-kprometheus",
"wpsoffice": "wps-office2019-kprometheus",
"footclient": "foot"
})
// Dynamic fixups
property var regexSubstitutions: [
{
"regex": /^steam_app_(\d+)$/,
"replace": "steam_icon_$1"
},
{
"regex": /Minecraft.*/,
"replace": "minecraft-launcher"
},
{
"regex": /.*polkit.*/,
"replace": "system-lock-screen"
},
{
"regex": /gcr.prompter/,
"replace": "system-lock-screen"
}
]
property list<DesktopEntry> entryList: []
property var preppedNames: []
property var preppedIcons: []
property var preppedIds: []
Component.onCompleted: refreshEntries()
Connections {
target: DesktopEntries.applications
function onValuesChanged() {
refreshEntries();
}
}
function refreshEntries() {
if (typeof DesktopEntries === 'undefined')
return;
const values = Array.from(DesktopEntries.applications.values);
if (values) {
entryList = values.sort((a, b) => a.name.localeCompare(b.name));
updatePreppedData();
}
}
function updatePreppedData() {
if (typeof FuzzySort === 'undefined')
return;
const list = Array.from(entryList);
preppedNames = list.map(a => ({
name: FuzzySort.prepare(`${a.name} `),
entry: a
}));
preppedIcons = list.map(a => ({
name: FuzzySort.prepare(`${a.icon} `),
entry: a
}));
preppedIds = list.map(a => ({
name: FuzzySort.prepare(`${a.id} `),
entry: a
}));
}
function iconForAppId(appId, fallbackName) {
const fallback = fallbackName || "application-x-executable";
if (!appId)
return iconFromName(fallback, fallback);
const entry = findAppEntry(appId);
if (entry) {
return iconFromName(entry.icon, fallback);
}
return iconFromName(appId, fallback);
}
// Robust lookup strategy
function findAppEntry(str) {
if (!str || str.length === 0)
return null;
let result = null;
if (result = checkHeuristic(str))
return result;
if (result = checkSubstitutions(str))
return result;
if (result = checkRegex(str))
return result;
if (result = checkSimpleTransforms(str))
return result;
if (result = checkFuzzySearch(str))
return result;
if (result = checkCleanMatch(str))
return result;
return null;
}
function iconFromName(iconName, fallbackName) {
const fallback = fallbackName || "application-x-executable";
try {
if (iconName && typeof Quickshell !== 'undefined' && Quickshell.iconPath) {
const p = Quickshell.iconPath(iconName, fallback);
if (p && p !== "")
return p;
}
} catch (e) {}
try {
return Quickshell.iconPath ? (Quickshell.iconPath(fallback, true) || "") : "";
} catch (e2) {
return "";
}
}
function distroLogoPath() {
try {
return (typeof OSInfo !== 'undefined' && OSInfo.distroIconPath) ? OSInfo.distroIconPath : "";
} catch (e) {
return "";
}
}
// --- Lookup Helpers ---
function checkHeuristic(str) {
if (typeof DesktopEntries !== 'undefined' && DesktopEntries.heuristicLookup) {
const entry = DesktopEntries.heuristicLookup(str);
if (entry)
return entry;
}
return null;
}
function checkSubstitutions(str) {
let effectiveStr = substitutions[str];
if (!effectiveStr)
effectiveStr = substitutions[str.toLowerCase()];
if (effectiveStr && effectiveStr !== str) {
return findAppEntry(effectiveStr);
}
return null;
}
function checkRegex(str) {
for (let i = 0; i < regexSubstitutions.length; i++) {
const sub = regexSubstitutions[i];
const replaced = str.replace(sub.regex, sub.replace);
if (replaced !== str) {
return findAppEntry(replaced);
}
}
return null;
}
function checkSimpleTransforms(str) {
if (typeof DesktopEntries === 'undefined' || !DesktopEntries.byId)
return null;
const lower = str.toLowerCase();
const variants = [str, lower, getFromReverseDomain(str), getFromReverseDomain(str)?.toLowerCase(), normalizeWithHyphens(str), str.replace(/_/g, '-').toLowerCase(), str.replace(/-/g, '_').toLowerCase()];
for (let i = 0; i < variants.length; i++) {
const variant = variants[i];
if (variant) {
const entry = DesktopEntries.byId(variant);
if (entry)
return entry;
}
}
return null;
}
function checkFuzzySearch(str) {
if (typeof FuzzySort === 'undefined')
return null;
// Check filenames (IDs) first
if (preppedIds.length > 0) {
let results = fuzzyQuery(str, preppedIds);
if (results.length === 0) {
const underscored = str.replace(/-/g, '_').toLowerCase();
if (underscored !== str)
results = fuzzyQuery(underscored, preppedIds);
}
if (results.length > 0)
return results[0];
}
// Then icons
if (preppedIcons.length > 0) {
const results = fuzzyQuery(str, preppedIcons);
if (results.length > 0)
return results[0];
}
// Then names
if (preppedNames.length > 0) {
const results = fuzzyQuery(str, preppedNames);
if (results.length > 0)
return results[0];
}
return null;
}
function checkCleanMatch(str) {
if (!str || str.length <= 3)
return null;
if (typeof DesktopEntries === 'undefined' || !DesktopEntries.byId)
return null;
// Aggressive fallback: strip all separators
const cleanStr = str.toLowerCase().replace(/[\.\-_]/g, '');
const list = Array.from(entryList);
for (let i = 0; i < list.length; i++) {
const entry = list[i];
const cleanId = (entry.id || "").toLowerCase().replace(/[\.\-_]/g, '');
if (cleanId.includes(cleanStr) || cleanStr.includes(cleanId)) {
return entry;
}
}
return null;
}
function fuzzyQuery(search, preppedData) {
if (!search || !preppedData || preppedData.length === 0)
return [];
return FuzzySort.go(search, preppedData, {
all: true,
key: "name"
}).map(r => r.obj.entry);
}
function iconExists(iconName) {
if (!iconName || iconName.length === 0)
return false;
if (iconName.startsWith("/"))
return true;
const path = Quickshell.iconPath(iconName, true);
return path && path.length > 0 && !path.includes("image-missing");
}
function getFromReverseDomain(str) {
if (!str)
return "";
return str.split('.').slice(-1)[0];
}
function normalizeWithHyphens(str) {
if (!str)
return "";
return str.toLowerCase().replace(/\s+/g, "-");
}
// Deprecated shim
function guessIcon(str) {
const entry = findAppEntry(str);
return entry ? entry.icon : "image-missing";
}
}
@@ -0,0 +1,232 @@
pragma Singleton
import QtQuick
import Quickshell
import qs.Commons
import qs.Services.System
Singleton {
id: root
// Current date
property var now: new Date()
// Unix timestamp of the last update
property real _lastUpdateTs: Date.now()
// Signal emitted when a significant time jump is detected (e.g. system resume)
signal resumed
// Returns a Unix Timestamp (in seconds)
readonly property int timestamp: {
return Math.floor(root.now / 1000);
}
// Timer state (for countdown/stopwatch)
property bool timerRunning: false
property bool timerStopwatchMode: false
property int timerRemainingSeconds: 0
property int timerTotalSeconds: 0
property int timerElapsedSeconds: 0
property bool timerSoundPlaying: false
property int timerStartTimestamp: 0 // Unix timestamp when timer was started
property int timerPausedAt: 0 // Value when paused (for resuming)
Timer {
id: updateTimer
interval: 1000
repeat: true
running: true
triggeredOnStart: false
onTriggered: {
var newTime = new Date();
var currentTs = newTime.getTime();
// Detect time jump (e.g. system resume) - threshold: 5 seconds
if (currentTs - root._lastUpdateTs > 5000) {
Logger.i("Time", "Time jump detected (" + Math.round((currentTs - root._lastUpdateTs) / 1000) + "s) - likely system resume");
root.resumed();
}
root._lastUpdateTs = currentTs;
root.now = newTime;
// Update timer if running
if (root.timerRunning && root.timerStartTimestamp > 0) {
const elapsedSinceStart = root.timestamp - root.timerStartTimestamp;
if (root.timerStopwatchMode) {
root.timerElapsedSeconds = root.timerPausedAt + elapsedSinceStart;
} else {
root.timerRemainingSeconds = root.timerTotalSeconds - elapsedSinceStart;
if (root.timerRemainingSeconds <= 0) {
root.timerOnFinished();
}
}
}
// Adjust next interval to sync with the start of the next second
var msIntoSecond = newTime.getMilliseconds();
if (msIntoSecond > 100) {
// If we're more than 100ms into the second, adjust for next time
updateTimer.interval = 1000 - msIntoSecond + 10; // +10ms buffer
updateTimer.restart();
} else {
updateTimer.interval = 1000;
}
}
}
Component.onCompleted: {
// Start by syncing to the next second boundary
var now = new Date();
var msUntilNextSecond = 1000 - now.getMilliseconds();
updateTimer.interval = msUntilNextSecond + 10; // +10ms buffer
updateTimer.restart();
}
// Formats a Date object into a YYYYMMDD-HHMMSS string.
function getFormattedTimestamp(date) {
if (!date) {
date = new Date();
}
const year = date.getFullYear();
// getMonth() is zero-based, so we add 1
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
const seconds = String(date.getSeconds()).padStart(2, '0');
return `${year}${month}${day}-${hours}${minutes}${seconds}`;
}
// Format an easy to read approximate duration ex: 4h 32m
// Used to display the time remaining on the Battery widget, computer uptime, etc..
function formatVagueHumanReadableDuration(totalSeconds) {
if (typeof totalSeconds !== 'number' || totalSeconds < 0) {
return '0s';
}
// Floor the input to handle decimal seconds
totalSeconds = Math.floor(totalSeconds);
const days = Math.floor(totalSeconds / 86400);
const hours = Math.floor((totalSeconds % 86400) / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
const seconds = totalSeconds % 60;
const parts = [];
if (days)
parts.push(`${days}d`);
if (hours)
parts.push(`${hours}h`);
if (minutes)
parts.push(`${minutes}m`);
// Only show seconds if no hours and no minutes
if (!hours && !minutes) {
parts.push(`${seconds}s`);
}
return parts.join(' ');
}
// Format a date into
function formatRelativeTime(date) {
if (!date)
return "";
const diff = Date.now() - date.getTime();
if (diff < 60000)
return I18n.tr("notifications.time.now");
if (diff < 120000)
return I18n.tr("notifications.time.diff-m");
if (diff < 3600000)
return I18n.tr("notifications.time.diff-mm", {
"diff": Math.floor(diff / 60000)
});
if (diff < 7200000)
return I18n.tr("notifications.time.diff-h");
if (diff < 86400000)
return I18n.tr("notifications.time.diff-hh", {
"diff": Math.floor(diff / 3600000)
});
if (diff < 172800000)
return I18n.tr("notifications.time.diff-d");
return I18n.tr("notifications.time.diff-dd", {
"diff": Math.floor(diff / 86400000)
});
}
// Timer functions
function timerStart() {
if (root.timerStopwatchMode) {
root.timerRunning = true;
root.timerStartTimestamp = root.timestamp;
root.timerPausedAt = root.timerElapsedSeconds;
} else {
if (root.timerRemainingSeconds <= 0) {
return;
}
root.timerRunning = true;
root.timerTotalSeconds = root.timerRemainingSeconds;
root.timerStartTimestamp = root.timestamp;
root.timerPausedAt = 0;
}
}
function timerPause() {
if (root.timerRunning) {
// Calculate and set remainingSeconds BEFORE changing timerRunning
// This ensures the UI sees the correct value when it reacts to timerRunning changing
if (root.timerStopwatchMode) {
root.timerPausedAt = root.timerElapsedSeconds;
} else {
// Calculate remaining seconds at the exact moment of pause
// Use current time directly to avoid stale timestamp issues
const currentTimestamp = Math.floor(Date.now() / 1000);
const elapsedSinceStart = currentTimestamp - root.timerStartTimestamp;
const currentRemaining = root.timerTotalSeconds - elapsedSinceStart;
root.timerPausedAt = Math.max(0, currentRemaining);
// CRITICAL: Update timerRemainingSeconds to the paused value BEFORE changing timerRunning
// This ensures UI sees correct value when it reacts to timerRunning change
root.timerRemainingSeconds = root.timerPausedAt;
}
}
root.timerRunning = false;
root.timerStartTimestamp = 0;
// Stop any repeating notification sound when pausing
SoundService.stopSound("alarm-beep.wav");
root.timerSoundPlaying = false;
}
function timerReset() {
root.timerRunning = false;
root.timerStartTimestamp = 0;
if (root.timerStopwatchMode) {
root.timerElapsedSeconds = 0;
root.timerPausedAt = 0;
} else {
root.timerRemainingSeconds = 0;
root.timerTotalSeconds = 0;
root.timerPausedAt = 0;
}
// Stop any repeating notification sound
SoundService.stopSound("alarm-beep.wav");
root.timerSoundPlaying = false;
}
function timerOnFinished() {
root.timerRunning = false;
root.timerRemainingSeconds = 0;
// Play notification sound with repeat at lower volume
root.timerSoundPlaying = true;
SoundService.playSound("alarm-beep.wav", {
repeat: true,
volume: 0.3
});
}
}