From 952aa63147c9fb28f6ace6f0bc7ccf45ced1299a Mon Sep 17 00:00:00 2001 From: Kiran George Date: Mon, 9 Jun 2025 11:30:08 +0530 Subject: Overview v2 --- .../modules/common/functions/color_utils.js | 85 +++ .../modules/common/functions/file_utils.js | 9 + .../modules/common/functions/fuzzysort.js | 682 +++++++++++++++++++++ .../modules/common/functions/levendist.js | 141 +++++ .../modules/common/functions/object_utils.js | 45 ++ .../modules/common/functions/string_utils.js | 188 ++++++ 6 files changed, 1150 insertions(+) create mode 100644 config/quickshell/modules/common/functions/color_utils.js create mode 100644 config/quickshell/modules/common/functions/file_utils.js create mode 100644 config/quickshell/modules/common/functions/fuzzysort.js create mode 100644 config/quickshell/modules/common/functions/levendist.js create mode 100644 config/quickshell/modules/common/functions/object_utils.js create mode 100644 config/quickshell/modules/common/functions/string_utils.js (limited to 'config/quickshell/modules/common/functions') diff --git a/config/quickshell/modules/common/functions/color_utils.js b/config/quickshell/modules/common/functions/color_utils.js new file mode 100644 index 00000000..c0ccfda9 --- /dev/null +++ b/config/quickshell/modules/common/functions/color_utils.js @@ -0,0 +1,85 @@ +// This module provides high level utility functions for color manipulation. + +/** + * Returns a color with the hue of color2 and the saturation, value, and alpha of color1. + * + * @param {string} color1 - The base color (any Qt.color-compatible string). + * @param {string} color2 - The color to take hue from. + * @returns {Qt.rgba} The resulting color. + */ +function colorWithHueOf(color1, color2) { + var c1 = Qt.color(color1); + var c2 = Qt.color(color2); + + // Qt.color hsvHue/hsvSaturation/hsvValue/alpha return 0-1 + var hue = c2.hsvHue; + var sat = c1.hsvSaturation; + var val = c1.hsvValue; + var alpha = c1.a; + + return Qt.hsva(hue, sat, val, alpha); +} + +/** + * Returns a color with the saturation of color2 and the hue/value/alpha of color1. + * + * @param {string} color1 - The base color (any Qt.color-compatible string). + * @param {string} color2 - The color to take saturation from. + * @returns {Qt.rgba} The resulting color. + */ +function colorWithSaturationOf(color1, color2) { + var c1 = Qt.color(color1); + var c2 = Qt.color(color2); + + var hue = c1.hsvHue; + var sat = c2.hsvSaturation; + var val = c1.hsvValue; + var alpha = c1.a; + + return Qt.hsva(hue, sat, val, alpha); +} + +/** + * Adapts color1 to the accent (hue and saturation) of color2 using HSL, keeping lightness and alpha from color1. + * + * @param {string} color1 - The base color (any Qt.color-compatible string). + * @param {string} color2 - The accent color. + * @returns {Qt.rgba} The resulting color. + */ +function adaptToAccent(color1, color2) { + var c1 = Qt.color(color1); + var c2 = Qt.color(color2); + + var hue = c2.hslHue; + var sat = c2.hslSaturation; + var light = c1.hslLightness; + var alpha = c1.a; + + return Qt.hsla(hue, sat, light, alpha); +} + +/** + * Mixes two colors by a given percentage. + * + * @param {string} color1 - The first color (any Qt.color-compatible string). + * @param {string} color2 - The second color. + * @param {number} percentage - The mix ratio (0-1). 1 = all color1, 0 = all color2. + * @returns {Qt.rgba} The resulting mixed color. + */ +function mix(color1, color2, percentage) { + var c1 = Qt.color(color1); + var c2 = Qt.color(color2); + return Qt.rgba(percentage * c1.r + (1 - percentage) * c2.r, percentage * c1.g + (1 - percentage) * c2.g, percentage * c1.b + (1 - percentage) * c2.b, percentage * c1.a + (1 - percentage) * c2.a); +} + +/** + * Transparentizes a color by a given percentage. + * + * @param {string} color - The color (any Qt.color-compatible string). + * @param {number} percentage - The amount to transparentize (0-1). + * @returns {Qt.rgba} The resulting color. + */ +function transparentize(color, percentage = 1) { + var c = Qt.color(color); + return Qt.rgba(c.r, c.g, c.b, c.a * (1 - percentage)); +} diff --git a/config/quickshell/modules/common/functions/file_utils.js b/config/quickshell/modules/common/functions/file_utils.js new file mode 100644 index 00000000..758950de --- /dev/null +++ b/config/quickshell/modules/common/functions/file_utils.js @@ -0,0 +1,9 @@ +/** + * Trims the File protocol off the input string + * @param {string} str + * @returns {string} + */ +function trimFileProtocol(str) { + return str.startsWith("file://") ? str.slice(7) : str; +} + diff --git a/config/quickshell/modules/common/functions/fuzzysort.js b/config/quickshell/modules/common/functions/fuzzysort.js new file mode 100644 index 00000000..1c1d9b9d --- /dev/null +++ b/config/quickshell/modules/common/functions/fuzzysort.js @@ -0,0 +1,682 @@ +.pragma library + +// https://github.com/farzher/fuzzysort +// License: MIT | Copyright (c) 2018 Stephen Kamenar +// A copy of the license is available in the `licenses` folder of this repository + +var single = (search, target) => { + if(!search || !target) return NULL + + var preparedSearch = getPreparedSearch(search) + if(!isPrepared(target)) target = getPrepared(target) + + var searchBitflags = preparedSearch.bitflags + if((searchBitflags & target._bitflags) !== searchBitflags) return NULL + + return algorithm(preparedSearch, target) +} + +var go = (search, targets, options) => { + if(!search) return options?.all ? all(targets, options) : noResults + + var preparedSearch = getPreparedSearch(search) + var searchBitflags = preparedSearch.bitflags + var containsSpace = preparedSearch.containsSpace + + var threshold = denormalizeScore( options?.threshold || 0 ) + var limit = options?.limit || INFINITY + + var resultsLen = 0; var limitedCount = 0 + var targetsLen = targets.length + + function push_result(result) { + if(resultsLen < limit) { q.add(result); ++resultsLen } + else { + ++limitedCount + if(result._score > q.peek()._score) q.replaceTop(result) + } + } + + // This code is copy/pasted 3 times for performance reasons [options.key, options.keys, no keys] + + // options.key + if(options?.key) { + var key = options.key + for(var i = 0; i < targetsLen; ++i) { var obj = targets[i] + var target = getValue(obj, key) + if(!target) continue + if(!isPrepared(target)) target = getPrepared(target) + + if((searchBitflags & target._bitflags) !== searchBitflags) continue + var result = algorithm(preparedSearch, target) + if(result === NULL) continue + if(result._score < threshold) continue + + result.obj = obj + push_result(result) + } + + // options.keys + } else if(options?.keys) { + var keys = options.keys + var keysLen = keys.length + + outer: for(var i = 0; i < targetsLen; ++i) { var obj = targets[i] + + { // early out based on bitflags + var keysBitflags = 0 + for (var keyI = 0; keyI < keysLen; ++keyI) { + var key = keys[keyI] + var target = getValue(obj, key) + if(!target) { tmpTargets[keyI] = noTarget; continue } + if(!isPrepared(target)) target = getPrepared(target) + tmpTargets[keyI] = target + + keysBitflags |= target._bitflags + } + + if((searchBitflags & keysBitflags) !== searchBitflags) continue + } + + if(containsSpace) for(let i=0; i -1000) { + if(keysSpacesBestScores[i] > NEGATIVE_INFINITY) { + var tmp = (keysSpacesBestScores[i] + allowPartialMatchScores[i]) / 4/*bonus score for having multiple matches*/ + if(tmp > keysSpacesBestScores[i]) keysSpacesBestScores[i] = tmp + } + } + if(allowPartialMatchScores[i] > keysSpacesBestScores[i]) keysSpacesBestScores[i] = allowPartialMatchScores[i] + } + } + + if(containsSpace) { + for(let i=0; i -1000) { + if(score > NEGATIVE_INFINITY) { + var tmp = (score + result._score) / 4/*bonus score for having multiple matches*/ + if(tmp > score) score = tmp + } + } + if(result._score > score) score = result._score + } + } + + objResults.obj = obj + objResults._score = score + if(options?.scoreFn) { + score = options.scoreFn(objResults) + if(!score) continue + score = denormalizeScore(score) + objResults._score = score + } + + if(score < threshold) continue + push_result(objResults) + } + + // no keys + } else { + for(var i = 0; i < targetsLen; ++i) { var target = targets[i] + if(!target) continue + if(!isPrepared(target)) target = getPrepared(target) + + if((searchBitflags & target._bitflags) !== searchBitflags) continue + var result = algorithm(preparedSearch, target) + if(result === NULL) continue + if(result._score < threshold) continue + + push_result(result) + } + } + + if(resultsLen === 0) return noResults + var results = new Array(resultsLen) + for(var i = resultsLen - 1; i >= 0; --i) results[i] = q.poll() + results.total = resultsLen + limitedCount + return results +} + + +// this is written as 1 function instead of 2 for minification. perf seems fine ... +// except when minified. the perf is very slow +var highlight = (result, open='', close='') => { + var callback = typeof open === 'function' ? open : undefined + + var target = result.target + var targetLen = target.length + var indexes = result.indexes + var highlighted = '' + var matchI = 0 + var indexesI = 0 + var opened = false + var parts = [] + + for(var i = 0; i < targetLen; ++i) { var char = target[i] + if(indexes[indexesI] === i) { + ++indexesI + if(!opened) { opened = true + if(callback) { + parts.push(highlighted); highlighted = '' + } else { + highlighted += open + } + } + + if(indexesI === indexes.length) { + if(callback) { + highlighted += char + parts.push(callback(highlighted, matchI++)); highlighted = '' + parts.push(target.substr(i+1)) + } else { + highlighted += char + close + target.substr(i+1) + } + break + } + } else { + if(opened) { opened = false + if(callback) { + parts.push(callback(highlighted, matchI++)); highlighted = '' + } else { + highlighted += close + } + } + } + highlighted += char + } + + return callback ? parts : highlighted +} + + +var prepare = (target) => { + if(typeof target === 'number') target = ''+target + else if(typeof target !== 'string') target = '' + var info = prepareLowerInfo(target) + return new_result(target, {_targetLower:info._lower, _targetLowerCodes:info.lowerCodes, _bitflags:info.bitflags}) +} + +var cleanup = () => { preparedCache.clear(); preparedSearchCache.clear() } + + +// Below this point is only internal code +// Below this point is only internal code +// Below this point is only internal code +// Below this point is only internal code + + +class Result { + get ['indexes']() { return this._indexes.slice(0, this._indexes.len).sort((a,b)=>a-b) } + set ['indexes'](indexes) { return this._indexes = indexes } + ['highlight'](open, close) { return highlight(this, open, close) } + get ['score']() { return normalizeScore(this._score) } + set ['score'](score) { this._score = denormalizeScore(score) } +} + +class KeysResult extends Array { + get ['score']() { return normalizeScore(this._score) } + set ['score'](score) { this._score = denormalizeScore(score) } +} + +var new_result = (target, options) => { + const result = new Result() + result['target'] = target + result['obj'] = options.obj ?? NULL + result._score = options._score ?? NEGATIVE_INFINITY + result._indexes = options._indexes ?? [] + result._targetLower = options._targetLower ?? '' + result._targetLowerCodes = options._targetLowerCodes ?? NULL + result._nextBeginningIndexes = options._nextBeginningIndexes ?? NULL + result._bitflags = options._bitflags ?? 0 + return result +} + + +var normalizeScore = score => { + if(score === NEGATIVE_INFINITY) return 0 + if(score > 1) return score + return Math.E ** ( ((-score + 1)**.04307 - 1) * -2) +} +var denormalizeScore = normalizedScore => { + if(normalizedScore === 0) return NEGATIVE_INFINITY + if(normalizedScore > 1) return normalizedScore + return 1 - Math.pow((Math.log(normalizedScore) / -2 + 1), 1 / 0.04307) +} + + +var prepareSearch = (search) => { + if(typeof search === 'number') search = ''+search + else if(typeof search !== 'string') search = '' + search = search.trim() + var info = prepareLowerInfo(search) + + var spaceSearches = [] + if(info.containsSpace) { + var searches = search.split(/\s+/) + searches = [...new Set(searches)] // distinct + for(var i=0; i { + if(target.length > 999) return prepare(target) // don't cache huge targets + var targetPrepared = preparedCache.get(target) + if(targetPrepared !== undefined) return targetPrepared + targetPrepared = prepare(target) + preparedCache.set(target, targetPrepared) + return targetPrepared +} +var getPreparedSearch = (search) => { + if(search.length > 999) return prepareSearch(search) // don't cache huge searches + var searchPrepared = preparedSearchCache.get(search) + if(searchPrepared !== undefined) return searchPrepared + searchPrepared = prepareSearch(search) + preparedSearchCache.set(search, searchPrepared) + return searchPrepared +} + + +var all = (targets, options) => { + var results = []; results.total = targets.length // this total can be wrong if some targets are skipped + + var limit = options?.limit || INFINITY + + if(options?.key) { + for(var i=0;i= limit) return results + } + } else if(options?.keys) { + for(var i=0;i= 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= limit) return results + } + } + + return results +} + + +var algorithm = (preparedSearch, prepared, allowSpaces=false, allowPartialMatch=false) => { + if(allowSpaces===false && preparedSearch.containsSpace) return algorithmSpaces(preparedSearch, prepared, allowPartialMatch) + + var searchLower = preparedSearch._lower + var searchLowerCodes = preparedSearch.lowerCodes + var searchLowerCode = searchLowerCodes[0] + var targetLowerCodes = prepared._targetLowerCodes + var searchLen = searchLowerCodes.length + var targetLen = targetLowerCodes.length + var searchI = 0 // where we at + var targetI = 0 // where you at + var matchesSimpleLen = 0 + + // very basic fuzzy match; to remove non-matching targets ASAP! + // walk through target. find sequential matches. + // if all chars aren't found then exit + for(;;) { + var isMatch = searchLowerCode === targetLowerCodes[targetI] + if(isMatch) { + matchesSimple[matchesSimpleLen++] = targetI + ++searchI; if(searchI === searchLen) break + searchLowerCode = searchLowerCodes[searchI] + } + ++targetI; if(targetI >= targetLen) return NULL // Failed to find searchI + } + + var searchI = 0 + var successStrict = false + var matchesStrictLen = 0 + + var nextBeginningIndexes = prepared._nextBeginningIndexes + if(nextBeginningIndexes === NULL) nextBeginningIndexes = prepared._nextBeginningIndexes = prepareNextBeginningIndexes(prepared.target) + targetI = matchesSimple[0]===0 ? 0 : nextBeginningIndexes[matchesSimple[0]-1] + + // Our target string successfully matched all characters in sequence! + // Let's try a more advanced and strict test to improve the score + // only count it as a match if it's consecutive or a beginning character! + var backtrackCount = 0 + if(targetI !== targetLen) for(;;) { + if(targetI >= targetLen) { + // We failed to find a good spot for this search char, go back to the previous search char and force it forward + if(searchI <= 0) break // We failed to push chars forward for a better match + + ++backtrackCount; if(backtrackCount > 200) break // exponential backtracking is taking too long, just give up and return a bad match + + --searchI + var lastMatch = matchesStrict[--matchesStrictLen] + targetI = nextBeginningIndexes[lastMatch] + + } else { + var isMatch = searchLowerCodes[searchI] === targetLowerCodes[targetI] + if(isMatch) { + matchesStrict[matchesStrictLen++] = targetI + ++searchI; if(searchI === searchLen) { successStrict = true; break } + ++targetI + } else { + targetI = nextBeginningIndexes[targetI] + } + } + } + + // check if it's a substring match + var substringIndex = searchLen <= 1 ? -1 : prepared._targetLower.indexOf(searchLower, matchesSimple[0]) // perf: this is slow + var isSubstring = !!~substringIndex + var isSubstringBeginning = !isSubstring ? false : substringIndex===0 || prepared._nextBeginningIndexes[substringIndex-1] === substringIndex + + // if it's a substring match but not at a beginning index, let's try to find a substring starting at a beginning index for a better score + if(isSubstring && !isSubstringBeginning) { + for(var i=0; i { + var score = 0 + + var extraMatchGroupCount = 0 + for(var i = 1; i < searchLen; ++i) { + if(matches[i] - matches[i-1] !== 1) {score -= matches[i]; ++extraMatchGroupCount} + } + var unmatchedDistance = matches[searchLen-1] - matches[0] - (searchLen-1) + + score -= (12+unmatchedDistance) * extraMatchGroupCount // penality for more groups + + if(matches[0] !== 0) score -= matches[0]*matches[0]*.2 // penality for not starting near the beginning + + if(!successStrict) { + score *= 1000 + } else { + // successStrict on a target with too many beginning indexes loses points for being a bad target + var uniqueBeginningIndexes = 1 + for(var i = nextBeginningIndexes[0]; i < targetLen; i=nextBeginningIndexes[i]) ++uniqueBeginningIndexes + + if(uniqueBeginningIndexes > 24) score *= (uniqueBeginningIndexes-24)*10 // quite arbitrary numbers here ... + } + + score -= (targetLen - searchLen)/2 // penality for longer targets + + if(isSubstring) score /= 1+searchLen*searchLen*1 // bonus for being a full substring + if(isSubstringBeginning) score /= 1+searchLen*searchLen*1 // bonus for substring starting on a beginningIndex + + score -= (targetLen - searchLen)/2 // penality for longer targets + + return score + } + + if(!successStrict) { + if(isSubstring) for(var i=0; i { + var seen_indexes = new Set() + var score = 0 + var result = NULL + + var first_seen_index_last_search = 0 + var searches = preparedSearch.spaceSearches + var searchesLen = searches.length + var changeslen = 0 + + // Return _nextBeginningIndexes back to its normal state + var resetNextBeginningIndexes = () => { + for(let i=changeslen-1; i>=0; i--) target._nextBeginningIndexes[nextBeginningIndexesChanges[i*2 + 0]] = nextBeginningIndexesChanges[i*2 + 1] + } + + var hasAtLeast1Match = false + for(var i=0; i=0; i--) { + if(toReplace !== target._nextBeginningIndexes[i]) break + target._nextBeginningIndexes[i] = newBeginningIndex + nextBeginningIndexesChanges[changeslen*2 + 0] = i + nextBeginningIndexesChanges[changeslen*2 + 1] = toReplace + changeslen++ + } + } + } + + score += result._score / searchesLen + allowPartialMatchScores[i] = result._score / searchesLen + + // dock points based on order otherwise "c man" returns Manifest.cpp instead of CheatManager.h + if(result._indexes[0] < first_seen_index_last_search) { + score -= (first_seen_index_last_search - result._indexes[0]) * 2 + } + first_seen_index_last_search = result._indexes[0] + + for(var j=0; j score) { + if(allowPartialMatch) { + for(var i=0; i str.replace(/\p{Script=Latin}+/gu, match => match.normalize('NFD')).replace(/[\u0300-\u036f]/g, '') + +var prepareLowerInfo = (str) => { + str = remove_accents(str) + var strLen = str.length + var lower = str.toLowerCase() + var lowerCodes = [] // new Array(strLen) sparse array is too slow + var bitflags = 0 + var containsSpace = false // space isn't stored in bitflags because of how searching with a space works + + for(var i = 0; i < strLen; ++i) { + var lowerCode = lowerCodes[i] = lower.charCodeAt(i) + + if(lowerCode === 32) { + containsSpace = true + continue // it's important that we don't set any bitflags for space + } + + var bit = lowerCode>=97&&lowerCode<=122 ? lowerCode-97 // alphabet + : lowerCode>=48&&lowerCode<=57 ? 26 // numbers + // 3 bits available + : lowerCode<=127 ? 30 // other ascii + : 31 // other utf8 + bitflags |= 1< { + var targetLen = target.length + var beginningIndexes = []; var beginningIndexesLen = 0 + var wasUpper = false + var wasAlphanum = false + for(var i = 0; i < targetLen; ++i) { + var targetCode = target.charCodeAt(i) + var isUpper = targetCode>=65&&targetCode<=90 + var isAlphanum = isUpper || targetCode>=97&&targetCode<=122 || targetCode>=48&&targetCode<=57 + var isBeginning = isUpper && !wasUpper || !wasAlphanum || !isAlphanum + wasUpper = isUpper + wasAlphanum = isAlphanum + if(isBeginning) beginningIndexes[beginningIndexesLen++] = i + } + return beginningIndexes +} +var prepareNextBeginningIndexes = (target) => { + target = remove_accents(target) + var targetLen = target.length + var beginningIndexes = prepareBeginningIndexes(target) + var nextBeginningIndexes = [] // new Array(targetLen) sparse array is too slow + var lastIsBeginning = beginningIndexes[0] + var lastIsBeginningI = 0 + for(var i = 0; i < targetLen; ++i) { + if(lastIsBeginning > i) { + nextBeginningIndexes[i] = lastIsBeginning + } else { + lastIsBeginning = beginningIndexes[++lastIsBeginningI] + nextBeginningIndexes[i] = lastIsBeginning===undefined ? targetLen : lastIsBeginning + } + } + return nextBeginningIndexes +} + +var preparedCache = new Map() +var preparedSearchCache = new Map() + +// the theory behind these being globals is to reduce garbage collection by not making new arrays +var matchesSimple = []; var matchesStrict = [] +var nextBeginningIndexesChanges = [] // allows straw berry to match strawberry well, by modifying the end of a substring to be considered a beginning index for the rest of the search +var keysSpacesBestScores = []; var allowPartialMatchScores = [] +var tmpTargets = []; var tmpResults = [] + +// prop = 'key' 2.5ms optimized for this case, seems to be about as fast as direct obj[prop] +// prop = 'key1.key2' 10ms +// prop = ['key1', 'key2'] 27ms +// prop = obj => obj.tags.join() ??ms +var getValue = (obj, prop) => { + var tmp = obj[prop]; if(tmp !== undefined) return tmp + if(typeof prop === 'function') return prop(obj) // this should run first. but that makes string props slower + var segs = prop + if(!Array.isArray(prop)) segs = prop.split('.') + var len = segs.length + var i = -1 + while (obj && (++i < len)) obj = obj[segs[i]] + return obj +} + +var isPrepared = (x) => { return typeof x === 'object' && typeof x._bitflags === 'number' } +var INFINITY = Infinity; var NEGATIVE_INFINITY = -INFINITY +var noResults = []; noResults.total = 0 +var NULL = null + +var noTarget = prepare('') + +// Hacked version of https://github.com/lemire/FastPriorityQueue.js +var fastpriorityqueue=r=>{var e=[],o=0,a={},v=r=>{for(var a=0,v=e[a],c=1;c>1]=e[a],c=1+(a<<1)}for(var f=a-1>>1;a>0&&v._score>1)e[a]=e[f];e[a]=v};return a.add=(r=>{var a=o;e[o++]=r;for(var v=a-1>>1;a>0&&r._score>1)e[a]=e[v];e[a]=r}),a.poll=(r=>{if(0!==o){var a=e[0];return e[0]=e[--o],v(),a}}),a.peek=(r=>{if(0!==o)return e[0]}),a.replaceTop=(r=>{e[0]=r,v()}),a} +var q = fastpriorityqueue() // reuse this diff --git a/config/quickshell/modules/common/functions/levendist.js b/config/quickshell/modules/common/functions/levendist.js new file mode 100644 index 00000000..90180d21 --- /dev/null +++ b/config/quickshell/modules/common/functions/levendist.js @@ -0,0 +1,141 @@ +// Original code from https://github.com/koeqaife/hyprland-material-you +// Original code license: GPLv3 +// Translated to Js from Cython with an LLM and reviewed + +function min3(a, b, c) { + return a < b && a < c ? a : b < c ? b : c; +} + +function max3(a, b, c) { + return a > b && a > c ? a : b > c ? b : c; +} + +function min2(a, b) { + return a < b ? a : b; +} + +function max2(a, b) { + return a > b ? a : b; +} + +function levenshteinDistance(s1, s2) { + let len1 = s1.length; + let len2 = s2.length; + + if (len1 === 0) return len2; + if (len2 === 0) return len1; + + if (len2 > len1) { + [s1, s2] = [s2, s1]; + [len1, len2] = [len2, len1]; + } + + let prev = new Array(len2 + 1); + let curr = new Array(len2 + 1); + + for (let j = 0; j <= len2; j++) { + prev[j] = j; + } + + for (let i = 1; i <= len1; i++) { + curr[0] = i; + for (let j = 1; j <= len2; j++) { + let cost = s1[i - 1] === s2[j - 1] ? 0 : 1; + curr[j] = min3(prev[j] + 1, curr[j - 1] + 1, prev[j - 1] + cost); + } + [prev, curr] = [curr, prev]; + } + + return prev[len2]; +} + +function partialRatio(shortS, longS) { + let lenS = shortS.length; + let lenL = longS.length; + let best = 0.0; + + if (lenS === 0) return 1.0; + + for (let i = 0; i <= lenL - lenS; i++) { + let sub = longS.slice(i, i + lenS); + let dist = levenshteinDistance(shortS, sub); + let score = 1.0 - (dist / lenS); + if (score > best) best = score; + } + + return best; +} + +function computeScore(s1, s2) { + if (s1 === s2) return 1.0; + + let dist = levenshteinDistance(s1, s2); + let maxLen = max2(s1.length, s2.length); + if (maxLen === 0) return 1.0; + + let full = 1.0 - (dist / maxLen); + let part = s1.length < s2.length ? partialRatio(s1, s2) : partialRatio(s2, s1); + + let score = 0.85 * full + 0.15 * part; + + if (s1 && s2 && s1[0] !== s2[0]) { + score -= 0.05; + } + + let lenDiff = Math.abs(s1.length - s2.length); + if (lenDiff >= 3) { + score -= 0.05 * lenDiff / maxLen; + } + + let commonPrefixLen = 0; + let minLen = min2(s1.length, s2.length); + for (let i = 0; i < minLen; i++) { + if (s1[i] === s2[i]) { + commonPrefixLen++; + } else { + break; + } + } + score += 0.02 * commonPrefixLen; + + if (s1.includes(s2) || s2.includes(s1)) { + score += 0.06; + } + + return Math.max(0.0, Math.min(1.0, score)); +} + +function computeTextMatchScore(s1, s2) { + if (s1 === s2) return 1.0; + + let dist = levenshteinDistance(s1, s2); + let maxLen = max2(s1.length, s2.length); + if (maxLen === 0) return 1.0; + + let full = 1.0 - (dist / maxLen); + let part = s1.length < s2.length ? partialRatio(s1, s2) : partialRatio(s2, s1); + + let score = 0.4 * full + 0.6 * part; + + let lenDiff = Math.abs(s1.length - s2.length); + if (lenDiff >= 10) { + score -= 0.02 * lenDiff / maxLen; + } + + let commonPrefixLen = 0; + let minLen = min2(s1.length, s2.length); + for (let i = 0; i < minLen; i++) { + if (s1[i] === s2[i]) { + commonPrefixLen++; + } else { + break; + } + } + score += 0.01 * commonPrefixLen; + + if (s1.includes(s2) || s2.includes(s1)) { + score += 0.2; + } + + return Math.max(0.0, Math.min(1.0, score)); +} diff --git a/config/quickshell/modules/common/functions/object_utils.js b/config/quickshell/modules/common/functions/object_utils.js new file mode 100644 index 00000000..96c632fd --- /dev/null +++ b/config/quickshell/modules/common/functions/object_utils.js @@ -0,0 +1,45 @@ +function toPlainObject(qtObj) { + if (qtObj === null || typeof qtObj !== "object") return qtObj; + + // Handle arrays + if (Array.isArray(qtObj)) { + return qtObj.map(toPlainObject); + } + + const result = ({}); + for (let key in qtObj) { + if ( + typeof qtObj[key] !== "function" && + !key.startsWith("objectName") && + !key.startsWith("children") && + !key.startsWith("object") && + !key.startsWith("parent") && + !key.startsWith("metaObject") && + !key.startsWith("destroyed") && + !key.startsWith("reloadableId") + ) { + result[key] = toPlainObject(qtObj[key]); + } + } + return result; +} + +function applyToQtObject(qtObj, jsonObj) { + if (!qtObj || typeof jsonObj !== "object" || jsonObj === null) return; + + for (let key in jsonObj) { + if (!qtObj.hasOwnProperty(key)) continue; + + // Check if the property is a QtObject (not a value) + const value = qtObj[key]; + const jsonValue = jsonObj[key]; + + // If it's an object and not an array, recurse + if (value && typeof value === "object" && !Array.isArray(value)) { + applyToQtObject(value, jsonValue); + } else { + // Otherwise, assign the value + qtObj[key] = jsonValue; + } + } +} diff --git a/config/quickshell/modules/common/functions/string_utils.js b/config/quickshell/modules/common/functions/string_utils.js new file mode 100644 index 00000000..c22671eb --- /dev/null +++ b/config/quickshell/modules/common/functions/string_utils.js @@ -0,0 +1,188 @@ +/** + * Formats a string according to the args that are passed in + * @param { string } str + * @param {...any} args + * @returns + */ +function format(str, ...args) { + return str.replace(/{(\d+)}/g, (match, index) => + typeof args[index] !== 'undefined' ? args[index] : match + ); +} + +/** + * Returns the domain of the passed in url or null + * @param { string } url + * @returns { string| null } + */ +function getDomain(url) { + const match = url.match(/^(?:https?:\/\/)?(?:www\.)?([^\/]+)/); + return match ? match[1] : null; +} + +/** + * Returns the base url of the passed in url or null + * @param { string } url + * @returns { string | null } + */ +function getBaseUrl(url) { + const match = url.match(/^(https?:\/\/[^\/]+)(\/.*)?$/); + return match ? match[1] : null; +} + +/** + * Escapes single quotes in shell commands + * @param { string } str + * @returns { string } + */ +function shellSingleQuoteEscape(str) { + // escape single quotes + return String(str) + // .replace(/\\/g, '\\\\') + .replace(/'/g, "'\\''"); +} + +/** + * Splits markdown blocks into three different types: text, think, and code. + * @param { string } markdown + */ +function splitMarkdownBlocks(markdown) { + const regex = /```(\w+)?\n([\s\S]*?)```|([\s\S]*?)<\/think>/g; + /** + * @type {{type: "text" | "think" | "code"; content: string; lang: string | undefined; completed: boolean | undefined}[]} + */ + let result = []; + let lastIndex = 0; + let match; + while ((match = regex.exec(markdown)) !== null) { + if (match.index > lastIndex) { + const text = markdown.slice(lastIndex, match.index); + if (text.trim()) { + result.push({ type: "text", content: text }); + } + } + if (match[0].startsWith('```')) { + if (match[2] && match[2].trim()) { + result.push({ type: "code", lang: match[1] || "", content: match[2], completed: true }); + } + } else if (match[0].startsWith('')) { + if (match[3] && match[3].trim()) { + result.push({ type: "think", content: match[3], completed: true }); + } + } + lastIndex = regex.lastIndex; + } + // Handle any remaining text after the last match + if (lastIndex < markdown.length) { + const text = markdown.slice(lastIndex); + // Check for unfinished block + const thinkStart = text.indexOf(''); + const codeStart = text.indexOf('```'); + if ( + thinkStart !== -1 && + (codeStart === -1 || thinkStart < codeStart) + ) { + const beforeThink = text.slice(0, thinkStart); + if (beforeThink.trim()) { + result.push({ type: "text", content: beforeThink }); + } + const thinkContent = text.slice(thinkStart + 7); + if (thinkContent.trim()) { + result.push({ type: "think", content: thinkContent, completed: false }); + } + } else if (codeStart !== -1) { + const beforeCode = text.slice(0, codeStart); + if (beforeCode.trim()) { + result.push({ type: "text", content: beforeCode }); + } + // Try to detect language after ``` + const codeLangMatch = text.slice(codeStart + 3).match(/^(\w+)?\n/); + let lang = ""; + let codeContentStart = codeStart + 3; + if (codeLangMatch) { + lang = codeLangMatch[1] || ""; + codeContentStart += codeLangMatch[0].length; + } else if (text[codeStart + 3] === '\n') { + codeContentStart += 1; + } + const codeContent = text.slice(codeContentStart); + if (codeContent.trim()) { + result.push({ type: "code", lang, content: codeContent, completed: false }); + } + } else if (text.trim()) { + result.push({ type: "text", content: text }); + } + } + // console.log(JSON.stringify(result, null, 2)); + return result; +} + +/** + * Returns the original string with backslashes escaped + * @param { string } str + * @returns { string } + */ +function escapeBackslashes(str) { + return str.replace(/\\/g, '\\\\'); +} + +/** + * Wraps words to supplied maximum length + * @param { string | null } str + * @param { number } maxLen + * @returns { string } + */ +function wordWrap(str, maxLen) { + if (!str) return ""; + let words = str.split(" "); + let lines = []; + let current = ""; + for (let i = 0; i < words.length; ++i) { + if ((current + (current.length > 0 ? " " : "") + words[i]).length > maxLen) { + if (current.length > 0) lines.push(current); + current = words[i]; + } else { + current += (current.length > 0 ? " " : "") + words[i]; + } + } + if (current.length > 0) lines.push(current); + return lines.join("\n"); +} + +function cleanMusicTitle(title) { + if (!title) return ""; + // Brackets + title = title.replace(/^ *\([^)]*\) */g, " "); // Round brackets + title = title.replace(/^ *\[[^\]]*\] */g, " "); // Square brackets + title = title.replace(/^ *\{[^\}]*\} */g, " "); // Curly brackets + // Japenis brackets + title = title.replace(/^ *【[^】]*】/, "") // Touhou + title = title.replace(/^ *《[^》]*》/, "") // ?? + title = title.replace(/^ *「[^」]*」/, "") // OP/ED + title = title.replace(/^ *『[^』]*』/, "") // OP/ED + + return title; +} + +function friendlyTimeForSeconds(seconds) { + if (isNaN(seconds) || seconds < 0) return "0:00"; + seconds = Math.floor(seconds); + const h = Math.floor(seconds / 3600); + const m = Math.floor((seconds % 3600) / 60); + const s = seconds % 60; + if (h > 0) { + return `${h}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`; + } else { + return `${m}:${s.toString().padStart(2, '0')}`; + } +} + +function escapeHtml(str) { + if (typeof str !== 'string') return str; + return str + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} -- cgit v1.2.3 From 0cda8f13953d0f4cc6126d4810c04452cc3375b8 Mon Sep 17 00:00:00 2001 From: Kiran George Date: Sat, 21 Jun 2025 17:26:23 +0530 Subject: Refactored for better colour and font expose and cleaned up unused code --- config/quickshell/.claude/settings.local.json | 9 ++ config/quickshell/GlobalStates.qml | 2 - config/quickshell/config.json | 25 ++-- config/quickshell/modules/common/Appearance.qml | 75 +++++------- config/quickshell/modules/common/Directories.qml | 3 +- .../modules/common/functions/string_utils.js | 135 --------------------- .../modules/common/widgets/DialogButton.qml | 4 +- .../modules/common/widgets/MaterialSymbol.qml | 9 +- .../modules/common/widgets/StyledText.qml | 7 +- .../modules/common/widgets/StyledTextArea.qml | 10 +- .../modules/common/widgets/StyledToolTip.qml | 2 +- .../quickshell/modules/overview/OverviewWidget.qml | 6 +- .../quickshell/modules/overview/OverviewWindow.qml | 8 +- config/quickshell/modules/overview/SearchItem.qml | 24 ++-- .../quickshell/modules/overview/SearchWidget.qml | 18 +-- config/quickshell/qml_color.json | 17 +++ config/quickshell/services/ConfigLoader.qml | 5 +- config/quickshell/services/MaterialThemeLoader.qml | 2 +- config/quickshell/shell.qml | 1 - config/wallust/templates/qml_color.json | 34 +++--- 20 files changed, 128 insertions(+), 268 deletions(-) create mode 100644 config/quickshell/.claude/settings.local.json create mode 100644 config/quickshell/qml_color.json (limited to 'config/quickshell/modules/common/functions') diff --git a/config/quickshell/.claude/settings.local.json b/config/quickshell/.claude/settings.local.json new file mode 100644 index 00000000..d474da6e --- /dev/null +++ b/config/quickshell/.claude/settings.local.json @@ -0,0 +1,9 @@ +{ + "permissions": { + "allow": [ + "Bash(find:*)", + "Bash(grep:*)" + ], + "deny": [] + } +} \ No newline at end of file diff --git a/config/quickshell/GlobalStates.qml b/config/quickshell/GlobalStates.qml index 7875645c..84b53c03 100644 --- a/config/quickshell/GlobalStates.qml +++ b/config/quickshell/GlobalStates.qml @@ -1,8 +1,6 @@ import "root:/modules/common/" import QtQuick import Quickshell -import Quickshell.Hyprland -import Quickshell.Io pragma Singleton pragma ComponentBehavior: Bound diff --git a/config/quickshell/config.json b/config/quickshell/config.json index f6c9e843..cd9c2c4d 100644 --- a/config/quickshell/config.json +++ b/config/quickshell/config.json @@ -16,7 +16,7 @@ "showXwaylandIndicator": true, "windowPadding": 6, "position": 1, - "workspaceNumberSize": 220 + "workspaceNumberSize": 0 }, "resources": { "updateInterval": 3000 @@ -38,23 +38,16 @@ }, "font": { "family": { - "main": "Open Sans", - "title": "JetBrains Mono NF", - "iconMaterial": "FiraConde Nerd Font", - "iconNerd": "SpaceMono NF", - "monospace": "JetBrains Mono NF", - "reading": "Readex Pro" + "uiFont": "Open Sans", + "iconFont": "FiraConde Nerd Font", + "codeFont": "JetBrains Mono NF" }, "pixelSize": { - "smallest": 10, - "smaller": 13, - "small": 15, - "normal": 16, - "large": 17, - "larger": 19, - "huge": 22, - "hugeass": 23, - "title": 28 + "textSmall": 13, + "textBase": 15, + "textMedium": 16, + "textLarge": 19, + "iconLarge": 22 } } } \ No newline at end of file diff --git a/config/quickshell/modules/common/Appearance.qml b/config/quickshell/modules/common/Appearance.qml index 29eca00c..675f1d1e 100644 --- a/config/quickshell/modules/common/Appearance.qml +++ b/config/quickshell/modules/common/Appearance.qml @@ -18,53 +18,47 @@ Singleton { property real transparency: 0.5 property real contentTransparency: 0.1 property real workpaceTransparency: 0.8 - // property real transparency: 0.15 - // property real contentTransparency: 0.5 property string background_image: Directories.config + "/rofi/.current_wallpaper" m3colors: QtObject { property bool darkmode: true property bool transparent: true - property color m3background: "#161217" - property color m3onBackground: "#EAE0E7" - property color m3surfaceContainerLow: "#1F1A1F" - property color m3surfaceContainer: "#231E23" - property color m3surfaceContainerHigh: "#2D282E" - property color m3surfaceContainerHighest: "#383339" - property color m3onSurface: "#EAE0E7" - property color m3onSurfaceVariant: "#CFC3CD" - property color m3outline: "#cba6f7" - property color m3scrim: "#000000" - property color m3shadow: "#000000" - property color m3primary: "#E5B6F2" - property color m3primaryContainer: "#5D386A" - property color m3secondary: "#D5C0D7" - property color m3secondaryContainer: "#534457" - property color m3onPrimary: "#452152" - property color m3onPrimaryContainer: "#F9D8FF" - property color m3onSecondaryContainer: "#F2DCF3" - property color m3outlineVariant: "#4C444D" + property color m3windowBackground: "#161217" + property color m3primaryText: "#EAE0E7" + property color m3layerBackground1: "#1F1A1F" + property color m3layerBackground2: "#231E23" + property color m3layerBackground3: "#2D282E" + property color m3surfaceText: "#EAE0E7" + property color m3secondaryText: "#CFC3CD" + property color m3borderPrimary: "#cba6f7" + property color m3shadowColor: "#000000" + property color m3accentPrimary: "#E5B6F2" + property color m3accentSecondary: "#D5C0D7" + property color m3selectionBackground: "#534457" + property color m3accentPrimaryText: "#452152" + property color m3selectionText: "#F2DCF3" + property color m3borderSecondary: "#4C444D" property color colTooltip: "#1e1e2e" property color colOnTooltip: "#F8F9FA" } colors: QtObject { - property color colSubtext: m3colors.m3outline - property color colLayer0: ColorUtils.transparentize(m3colors.m3background, root.transparency) - property color colLayer1: ColorUtils.transparentize(ColorUtils.mix(m3colors.m3surfaceContainerLow, m3colors.m3background, 0.7), root.contentTransparency); - property color colOnLayer1: m3colors.m3onSurfaceVariant; - property color colLayer2: ColorUtils.transparentize(ColorUtils.mix(m3colors.m3surfaceContainer, m3colors.m3surfaceContainerHigh, 0.55), root.contentTransparency) - property color colOnLayer2: m3colors.m3onSurface; + property color colSubtext: m3colors.m3borderPrimary + property color colLayer0: ColorUtils.transparentize(m3colors.m3windowBackground, root.transparency) + property color colLayer1: ColorUtils.transparentize(ColorUtils.mix(m3colors.m3layerBackground1, m3colors.m3windowBackground, 0.7), root.contentTransparency); + property color colOnLayer1: m3colors.m3secondaryText; + property color colLayer2: ColorUtils.transparentize(ColorUtils.mix(m3colors.m3layerBackground2, m3colors.m3layerBackground3, 0.55), root.contentTransparency) + property color colOnLayer2: m3colors.m3surfaceText; property color colLayer1Hover: ColorUtils.transparentize(ColorUtils.mix(colLayer1, colOnLayer1, 0.92), root.contentTransparency) property color colLayer1Active: ColorUtils.transparentize(ColorUtils.mix(colLayer1, colOnLayer1, 0.85), root.contentTransparency); property color colLayer2Hover: ColorUtils.transparentize(ColorUtils.mix(colLayer2, colOnLayer2, 0.90), root.contentTransparency) property color colLayer2Active: ColorUtils.transparentize(ColorUtils.mix(colLayer2, colOnLayer2, 0.80), root.contentTransparency); - property color colPrimary: m3colors.m3primary + property color colPrimary: m3colors.m3accentPrimary property color colPrimaryHover: ColorUtils.mix(colors.colPrimary, colLayer1Hover, 0.87) property color colPrimaryActive: ColorUtils.mix(colors.colPrimary, colLayer1Active, 0.7) - property color colShadow: ColorUtils.transparentize(m3colors.m3shadow, 0.7) + property color colShadow: ColorUtils.transparentize(m3colors.m3shadowColor, 0.7) } rounding: QtObject { @@ -82,23 +76,16 @@ Singleton { font: QtObject { property QtObject family: QtObject { - property string main: "Open Sans" - property string title: "JetBrains Mono NF" - property string iconMaterial: "FiraConde Nerd Font" - property string iconNerd: "SpaceMono NF" - property string monospace: "JetBrains Mono NF" - property string reading: "Readex Pro" + property string uiFont: "Open Sans" + property string iconFont: "FiraConde Nerd Font" + property string codeFont: "JetBrains Mono NF" } property QtObject pixelSize: QtObject { - property int smallest: 10 - property int smaller: 13 - property int small: 15 - property int normal: 16 - property int large: 17 - property int larger: 19 - property int huge: 22 - property int hugeass: 23 - property int title: 28 + property int textSmall: 13 + property int textBase: 15 + property int textMedium: 16 + property int textLarge: 19 + property int iconLarge: 22 } } diff --git a/config/quickshell/modules/common/Directories.qml b/config/quickshell/modules/common/Directories.qml index 9ddf43bd..694c73df 100644 --- a/config/quickshell/modules/common/Directories.qml +++ b/config/quickshell/modules/common/Directories.qml @@ -11,9 +11,10 @@ Singleton { // XDG Dirs, with "file://" readonly property string config: StandardPaths.standardLocations(StandardPaths.ConfigLocation)[0] readonly property string state: StandardPaths.standardLocations(StandardPaths.StateLocation)[0] + readonly property string gen_cache: StandardPaths.standardLocations(StandardPaths.GenericCacheLocation)[0] // Other dirs used by the shell, without "file://" property string shellConfig: FileUtils.trimFileProtocol(`${Directories.config}/quickshell`) property string shellConfigPath: `${Directories.shellConfig}/config.json` - property string generatedMaterialThemePath: `${Directories.shellConfig}/qml_color.json` + property string generatedMaterialThemePath: `${Directories.gen_cache}/hellwal/qml_color.json` } diff --git a/config/quickshell/modules/common/functions/string_utils.js b/config/quickshell/modules/common/functions/string_utils.js index c22671eb..c31edf49 100644 --- a/config/quickshell/modules/common/functions/string_utils.js +++ b/config/quickshell/modules/common/functions/string_utils.js @@ -42,141 +42,6 @@ function shellSingleQuoteEscape(str) { .replace(/'/g, "'\\''"); } -/** - * Splits markdown blocks into three different types: text, think, and code. - * @param { string } markdown - */ -function splitMarkdownBlocks(markdown) { - const regex = /```(\w+)?\n([\s\S]*?)```|([\s\S]*?)<\/think>/g; - /** - * @type {{type: "text" | "think" | "code"; content: string; lang: string | undefined; completed: boolean | undefined}[]} - */ - let result = []; - let lastIndex = 0; - let match; - while ((match = regex.exec(markdown)) !== null) { - if (match.index > lastIndex) { - const text = markdown.slice(lastIndex, match.index); - if (text.trim()) { - result.push({ type: "text", content: text }); - } - } - if (match[0].startsWith('```')) { - if (match[2] && match[2].trim()) { - result.push({ type: "code", lang: match[1] || "", content: match[2], completed: true }); - } - } else if (match[0].startsWith('')) { - if (match[3] && match[3].trim()) { - result.push({ type: "think", content: match[3], completed: true }); - } - } - lastIndex = regex.lastIndex; - } - // Handle any remaining text after the last match - if (lastIndex < markdown.length) { - const text = markdown.slice(lastIndex); - // Check for unfinished block - const thinkStart = text.indexOf(''); - const codeStart = text.indexOf('```'); - if ( - thinkStart !== -1 && - (codeStart === -1 || thinkStart < codeStart) - ) { - const beforeThink = text.slice(0, thinkStart); - if (beforeThink.trim()) { - result.push({ type: "text", content: beforeThink }); - } - const thinkContent = text.slice(thinkStart + 7); - if (thinkContent.trim()) { - result.push({ type: "think", content: thinkContent, completed: false }); - } - } else if (codeStart !== -1) { - const beforeCode = text.slice(0, codeStart); - if (beforeCode.trim()) { - result.push({ type: "text", content: beforeCode }); - } - // Try to detect language after ``` - const codeLangMatch = text.slice(codeStart + 3).match(/^(\w+)?\n/); - let lang = ""; - let codeContentStart = codeStart + 3; - if (codeLangMatch) { - lang = codeLangMatch[1] || ""; - codeContentStart += codeLangMatch[0].length; - } else if (text[codeStart + 3] === '\n') { - codeContentStart += 1; - } - const codeContent = text.slice(codeContentStart); - if (codeContent.trim()) { - result.push({ type: "code", lang, content: codeContent, completed: false }); - } - } else if (text.trim()) { - result.push({ type: "text", content: text }); - } - } - // console.log(JSON.stringify(result, null, 2)); - return result; -} - -/** - * Returns the original string with backslashes escaped - * @param { string } str - * @returns { string } - */ -function escapeBackslashes(str) { - return str.replace(/\\/g, '\\\\'); -} - -/** - * Wraps words to supplied maximum length - * @param { string | null } str - * @param { number } maxLen - * @returns { string } - */ -function wordWrap(str, maxLen) { - if (!str) return ""; - let words = str.split(" "); - let lines = []; - let current = ""; - for (let i = 0; i < words.length; ++i) { - if ((current + (current.length > 0 ? " " : "") + words[i]).length > maxLen) { - if (current.length > 0) lines.push(current); - current = words[i]; - } else { - current += (current.length > 0 ? " " : "") + words[i]; - } - } - if (current.length > 0) lines.push(current); - return lines.join("\n"); -} - -function cleanMusicTitle(title) { - if (!title) return ""; - // Brackets - title = title.replace(/^ *\([^)]*\) */g, " "); // Round brackets - title = title.replace(/^ *\[[^\]]*\] */g, " "); // Square brackets - title = title.replace(/^ *\{[^\}]*\} */g, " "); // Curly brackets - // Japenis brackets - title = title.replace(/^ *【[^】]*】/, "") // Touhou - title = title.replace(/^ *《[^》]*》/, "") // ?? - title = title.replace(/^ *「[^」]*」/, "") // OP/ED - title = title.replace(/^ *『[^』]*』/, "") // OP/ED - - return title; -} - -function friendlyTimeForSeconds(seconds) { - if (isNaN(seconds) || seconds < 0) return "0:00"; - seconds = Math.floor(seconds); - const h = Math.floor(seconds / 3600); - const m = Math.floor((seconds % 3600) / 60); - const s = seconds % 60; - if (h > 0) { - return `${h}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`; - } else { - return `${m}:${s.toString().padStart(2, '0')}`; - } -} - function escapeHtml(str) { if (typeof str !== 'string') return str; return str diff --git a/config/quickshell/modules/common/widgets/DialogButton.qml b/config/quickshell/modules/common/widgets/DialogButton.qml index 9e19a507..b799336a 100644 --- a/config/quickshell/modules/common/widgets/DialogButton.qml +++ b/config/quickshell/modules/common/widgets/DialogButton.qml @@ -18,7 +18,7 @@ RippleButton { buttonRadius: Appearance?.rounding.full ?? 9999 property color colEnabled: Appearance?.colors.colPrimary - property color colDisabled: Appearance?.m3colors.m3outline + property color colDisabled: Appearance?.m3colors.m3borderPrimary contentItem: StyledText { id: buttonTextWidget @@ -27,7 +27,7 @@ RippleButton { anchors.rightMargin: 15 text: buttonText horizontalAlignment: Text.AlignHCenter - font.pixelSize: Appearance?.font.pixelSize.small ?? 12 + font.pixelSize: Appearance?.font.pixelSize.textBase ?? 12 color: button.enabled ? button.colEnabled : button.colDisabled Behavior on color { diff --git a/config/quickshell/modules/common/widgets/MaterialSymbol.qml b/config/quickshell/modules/common/widgets/MaterialSymbol.qml index dbbfff00..214f838e 100644 --- a/config/quickshell/modules/common/widgets/MaterialSymbol.qml +++ b/config/quickshell/modules/common/widgets/MaterialSymbol.qml @@ -1,17 +1,16 @@ import "root:/modules/common/" import QtQuick -import QtQuick.Layouts Text { id: root - property real iconSize: Appearance?.font.pixelSize.small ?? 16 + property real iconSize: Appearance?.font.pixelSize.textBase ?? 16 property real fill: 0 renderType: Text.NativeRendering font.hintingPreference: Font.PreferFullHinting verticalAlignment: Text.AlignVCenter - font.family: Appearance?.font.family.iconMaterial ?? "Material Symbols Rounded" + font.family: Appearance?.font.family.iconFont ?? "Material Symbols Rounded" font.pixelSize: iconSize - color: Appearance.m3colors.m3onBackground + color: Appearance.m3colors.m3primaryText Behavior on fill { NumberAnimation { @@ -23,8 +22,6 @@ Text { font.variableAxes: { "FILL": fill, - // "wght": font.weight, - // "GRAD": 0, "opsz": iconSize, } } diff --git a/config/quickshell/modules/common/widgets/StyledText.qml b/config/quickshell/modules/common/widgets/StyledText.qml index 6eef5785..988c136d 100644 --- a/config/quickshell/modules/common/widgets/StyledText.qml +++ b/config/quickshell/modules/common/widgets/StyledText.qml @@ -1,14 +1,13 @@ import "root:/modules/common" import QtQuick -import QtQuick.Layouts Text { renderType: Text.NativeRendering verticalAlignment: Text.AlignVCenter font { hintingPreference: Font.PreferFullHinting - family: Appearance?.font.family.main ?? "sans-serif" - pixelSize: Appearance?.font.pixelSize.small ?? 15 + family: Appearance?.font.family.uiFont ?? "sans-serif" + pixelSize: Appearance?.font.pixelSize.textBase ?? 15 } - color: Appearance?.m3colors.m3onBackground ?? "black" + color: Appearance?.m3colors.m3primaryText ?? "black" } diff --git a/config/quickshell/modules/common/widgets/StyledTextArea.qml b/config/quickshell/modules/common/widgets/StyledTextArea.qml index 1ea9a349..af3cf34d 100644 --- a/config/quickshell/modules/common/widgets/StyledTextArea.qml +++ b/config/quickshell/modules/common/widgets/StyledTextArea.qml @@ -4,12 +4,12 @@ import QtQuick.Controls TextArea { renderType: Text.NativeRendering - selectedTextColor: Appearance.m3colors.m3onSecondaryContainer - selectionColor: Appearance.m3colors.m3secondaryContainer - placeholderTextColor: Appearance.m3colors.m3outline + selectedTextColor: Appearance.m3colors.m3selectionText + selectionColor: Appearance.m3colors.m3selectionBackground + placeholderTextColor: Appearance.m3colors.m3borderPrimary font { - family: Appearance?.font.family.main ?? "sans-serif" - pixelSize: Appearance?.font.pixelSize.small ?? 15 + family: Appearance?.font.family.uiFont ?? "sans-serif" + pixelSize: Appearance?.font.pixelSize.textBase ?? 15 hintingPreference: Font.PreferFullHinting } } diff --git a/config/quickshell/modules/common/widgets/StyledToolTip.qml b/config/quickshell/modules/common/widgets/StyledToolTip.qml index 2ca16df1..aaaad813 100644 --- a/config/quickshell/modules/common/widgets/StyledToolTip.qml +++ b/config/quickshell/modules/common/widgets/StyledToolTip.qml @@ -50,7 +50,7 @@ ToolTip { id: tooltipTextObject anchors.centerIn: parent text: content - font.pixelSize: Appearance?.font.pixelSize.smaller ?? 14 + font.pixelSize: Appearance?.font.pixelSize.textSmall ?? 14 font.hintingPreference: Font.PreferNoHinting // Prevent shaky text color: Appearance?.m3colors.colOnTooltip ?? "#FFFFFF" wrapMode: Text.Wrap diff --git a/config/quickshell/modules/overview/OverviewWidget.qml b/config/quickshell/modules/overview/OverviewWidget.qml index 63633602..2ea8d58a 100644 --- a/config/quickshell/modules/overview/OverviewWidget.qml +++ b/config/quickshell/modules/overview/OverviewWidget.qml @@ -25,7 +25,7 @@ Item { property var windowAddresses: HyprlandData.addresses property var monitorData: HyprlandData.monitors.find(m => m.id === root.monitor.id) property real scale: ConfigOptions.overview.scale - property color activeBorderColor: Appearance.m3colors.m3secondary + property color activeBorderColor: Appearance.m3colors.m3accentSecondary property real workspaceImplicitWidth: Math.max(100, (monitorData?.transform % 2 === 1) ? ((monitor.height - monitorData?.reserved[0] - monitorData?.reserved[2]) * root.scale / monitor.scale) : @@ -71,7 +71,7 @@ Item { property real padding: 10 anchors.fill: parent anchors.margins: Appearance.sizes.elevationMargin - border.color : ColorUtils.transparentize(Appearance.m3colors.m3outline, 0.2) + border.color : ColorUtils.transparentize(Appearance.m3colors.m3borderPrimary, 0.2) border.width : 2 implicitWidth: workspaceColumnLayout.implicitWidth + padding * 2 @@ -170,7 +170,7 @@ Item { color: "transparent" radius: parent.radius border.width: 1 - border.color: hoveredWhileDragging ? hoveredBorderColor : ColorUtils.transparentize(Appearance.m3colors.m3outline, 0.6) + border.color: hoveredWhileDragging ? hoveredBorderColor : ColorUtils.transparentize(Appearance.m3colors.m3borderPrimary, 0.6) z: 10 // Ensure it's on top } diff --git a/config/quickshell/modules/overview/OverviewWindow.qml b/config/quickshell/modules/overview/OverviewWindow.qml index 273eff7e..449a98c4 100644 --- a/config/quickshell/modules/overview/OverviewWindow.qml +++ b/config/quickshell/modules/overview/OverviewWindow.qml @@ -32,7 +32,7 @@ Rectangle { // Window property var xwaylandIndicatorToIconRatio: 0.35 property var iconToWindowRatioCompact: 0.6 property var iconPath: Quickshell.iconPath(AppSearch.guessIcon(windowData?.class), "image-missing") - property bool compactMode: Appearance.font.pixelSize.smaller * 4 > targetWindowHeight || Appearance.font.pixelSize.smaller * 4 > targetWindowWidth + property bool compactMode: Appearance.font.pixelSize.textSmall * 4 > targetWindowHeight || Appearance.font.pixelSize.textSmall * 4 > targetWindowWidth property bool indicateXWayland: (ConfigOptions.overview.showXwaylandIndicator && windowData?.xwayland) ?? false @@ -44,7 +44,7 @@ Rectangle { // Window radius: Appearance.rounding.windowRounding * root.scale color: pressed ? Appearance.colors.colLayer2Active : hovered ? Appearance.colors.colLayer2Hover : Appearance.colors.colLayer2 // border.color : ColorUtils.transparentize(Appearance.m3colors.m3outline, 0.9) - border.color : ColorUtils.transparentize(Appearance.m3colors.m3outline, 0.4) + border.color : ColorUtils.transparentize(Appearance.m3colors.m3borderPrimary, 0.4) border.pixelAligned : false border.width : 2 @@ -65,7 +65,7 @@ Rectangle { // Window anchors.verticalCenter: parent.verticalCenter anchors.left: parent.left anchors.right: parent.right - spacing: Appearance.font.pixelSize.smaller * 0.5 + spacing: Appearance.font.pixelSize.textSmall * 0.5 IconImage { id: windowIcon @@ -85,7 +85,7 @@ Rectangle { // Window Layout.fillWidth: true Layout.fillHeight: true horizontalAlignment: Text.AlignHCenter - font.pixelSize: Appearance.font.pixelSize.smaller + font.pixelSize: Appearance.font.pixelSize.textSmall font.italic: indicateXWayland ? true : false elide: Text.ElideRight text: windowData?.title ?? "" diff --git a/config/quickshell/modules/overview/SearchItem.qml b/config/quickshell/modules/overview/SearchItem.qml index 1363b88d..1357d03c 100644 --- a/config/quickshell/modules/overview/SearchItem.qml +++ b/config/quickshell/modules/overview/SearchItem.qml @@ -22,7 +22,7 @@ RippleButton { property string itemName: entry?.name property string itemIcon: entry?.icon ?? "" property var itemExecute: entry?.execute - property string fontType: entry?.fontType ?? "main" + property string fontType: entry?.fontType ?? "uiFont" property string itemClickActionName: entry?.clickActionName property string bigText: entry?.bigText ?? "" property string materialSymbol: entry?.materialSymbol ?? "" @@ -31,7 +31,7 @@ RippleButton { property string highlightPrefix: `` property string highlightSuffix: `` function highlightContent(content, query) { - if (!query || query.length === 0 || content == query || fontType === "monospace") + if (!query || query.length === 0 || content == query || fontType === "codeFont") return StringUtils.escapeHtml(content); let contentLower = content.toLowerCase(); @@ -80,7 +80,7 @@ RippleButton { buttonRadius: Appearance.rounding.normal colBackground: (root.down || root.keyboardDown) ? Appearance.colors.colLayer1Active : ((root.hovered || root.focus) ? Appearance.colors.colLayer1Hover : - ColorUtils.transparentize(Appearance.m3colors.m3surfaceContainerHigh, 1)) + ColorUtils.transparentize(Appearance.m3colors.m3layerBackground3, 1)) colBackgroundHover: Appearance.colors.colLayer1Hover colRipple: Appearance.colors.colLayer1Active @@ -140,7 +140,7 @@ RippleButton { MaterialSymbol { text: root.materialSymbol iconSize: 30 - color: Appearance.m3colors.m3onSurface + color: Appearance.m3colors.m3surfaceText } } @@ -148,8 +148,8 @@ RippleButton { id: bigTextComponent StyledText { text: root.bigText - font.pixelSize: Appearance.font.pixelSize.larger - color: Appearance.m3colors.m3onSurface + font.pixelSize: Appearance.font.pixelSize.textLarge + color: Appearance.m3colors.m3surfaceText } } @@ -160,7 +160,7 @@ RippleButton { Layout.alignment: Qt.AlignVCenter spacing: 0 StyledText { - font.pixelSize: Appearance.font.pixelSize.smaller + font.pixelSize: Appearance.font.pixelSize.textSmall color: Appearance.colors.colSubtext visible: root.itemType && root.itemType != qsTr("App") text: root.itemType @@ -178,8 +178,8 @@ RippleButton { id: activeText anchors.centerIn: parent text: "check" - font.pixelSize: Appearance.font.pixelSize.normal - color: Appearance.m3colors.m3onPrimary + font.pixelSize: Appearance.font.pixelSize.textMedium + color: Appearance.m3colors.m3accentPrimaryText } } } @@ -187,9 +187,9 @@ RippleButton { Layout.fillWidth: true id: nameText textFormat: Text.StyledText // RichText also works, but StyledText ensures elide work - font.pixelSize: Appearance.font.pixelSize.small + font.pixelSize: Appearance.font.pixelSize.textBase font.family: Appearance.font.family[root.fontType] - color: Appearance.m3colors.m3onSurface + color: Appearance.m3colors.m3surfaceText horizontalAlignment: Text.AlignLeft elide: Text.ElideRight text: `${root.displayContent}` @@ -211,7 +211,7 @@ RippleButton { Layout.fillWidth: false visible: (root.hovered || root.focus) id: clickAction - font.pixelSize: Appearance.font.pixelSize.normal + font.pixelSize: Appearance.font.pixelSize.textMedium color: Appearance.colors.colSubtext horizontalAlignment: Text.AlignRight text: root.itemClickActionName diff --git a/config/quickshell/modules/overview/SearchWidget.qml b/config/quickshell/modules/overview/SearchWidget.qml index fed710ec..f84aa558 100644 --- a/config/quickshell/modules/overview/SearchWidget.qml +++ b/config/quickshell/modules/overview/SearchWidget.qml @@ -201,8 +201,8 @@ Item { // Wrapper MaterialSymbol { id: searchIcon Layout.leftMargin: 15 - iconSize: Appearance.font.pixelSize.huge - color: Appearance.m3colors.m3onSurface + iconSize: Appearance.font.pixelSize.iconLarge + color: Appearance.m3colors.m3surfaceText text: root.searchingText.startsWith(ConfigOptions.search.prefix.clipboard) ? 'content_paste_search' : '' } TextField { // Search box @@ -213,15 +213,15 @@ Item { // Wrapper padding: 15 renderType: Text.NativeRendering font { - family: Appearance?.font.family.main ?? "sans-serif" - pixelSize: Appearance?.font.pixelSize.small ?? 15 + family: Appearance?.font.family.uiFont ?? "sans-serif" + pixelSize: Appearance?.font.pixelSize.textBase ?? 15 hintingPreference: Font.PreferFullHinting } - color: activeFocus ? Appearance.m3colors.m3onSurface : Appearance.m3colors.m3onSurfaceVariant - selectedTextColor: Appearance.m3colors.m3onSecondaryContainer - selectionColor: Appearance.m3colors.m3secondaryContainer + color: activeFocus ? Appearance.m3colors.m3surfaceText : Appearance.m3colors.m3secondaryText + selectedTextColor: Appearance.m3colors.m3selectionText + selectionColor: Appearance.m3colors.m3selectionBackground placeholderText: qsTr("Search, calculate or run") - placeholderTextColor: Appearance.m3colors.m3outline + placeholderTextColor: Appearance.m3colors.m3borderPrimary implicitWidth: root.searchingText == "" ? Appearance.sizes.searchWidthCollapsed : Appearance.sizes.searchWidth Behavior on implicitWidth { @@ -260,7 +260,7 @@ Item { // Wrapper visible: root.showResults Layout.fillWidth: true height: 1 - color: Appearance.m3colors.m3outlineVariant + color: Appearance.m3colors.m3borderSecondary } ListView { // App results diff --git a/config/quickshell/qml_color.json b/config/quickshell/qml_color.json new file mode 100644 index 00000000..888ba3e6 --- /dev/null +++ b/config/quickshell/qml_color.json @@ -0,0 +1,17 @@ +{ + "windowBackground": "#0f0f15", + "primaryText": "#bac2de", + "layerBackground1": "#1F1A1F", + "layerBackground2": "#231E23", + "layerBackground3": "#2D282E", + "surfaceText": "#EAE0E7", + "secondaryText": "#CFC3CD", + "borderPrimary": "#cba6f7", + "shadowColor": "#000000", + "accentPrimary": "#6750A4", + "accentSecondary": "#D5C0D7", + "selectionBackground": "#534457", + "accentPrimaryText": "#FFFFFF", + "selectionText": "#F2DCF3", + "borderSecondary": "#4C444D" +} \ No newline at end of file diff --git a/config/quickshell/services/ConfigLoader.qml b/config/quickshell/services/ConfigLoader.qml index 5f16bf55..d3fb4e26 100644 --- a/config/quickshell/services/ConfigLoader.qml +++ b/config/quickshell/services/ConfigLoader.qml @@ -72,7 +72,7 @@ Singleton { let targetObject = ConfigOptions; // Check if this is a font-related configuration - if (keys[0] === "font" && typeof Appearance !== 'undefined') { + if (keys[0] === "font") { targetObject = Appearance; } @@ -101,13 +101,12 @@ Singleton { } } - console.log(parents.join(".")); console.log(`[ConfigLoader] Setting live config value: ${nestedKey} = ${convertedValue}`); obj[keys[keys.length - 1]] = convertedValue; } function saveConfig() { - const plainConfig = ObjectUtils.toPlainObject(ConfigOptions) + const plainConfig = ObjectUtils.toPlainObject(ConfigOptions); Hyprland.dispatch(`exec echo '${StringUtils.shellSingleQuoteEscape(JSON.stringify(plainConfig, null, 2))}' > '${root.filePath}'`) } diff --git a/config/quickshell/services/MaterialThemeLoader.qml b/config/quickshell/services/MaterialThemeLoader.qml index cd4eb686..2d67ad5b 100644 --- a/config/quickshell/services/MaterialThemeLoader.qml +++ b/config/quickshell/services/MaterialThemeLoader.qml @@ -29,7 +29,7 @@ Singleton { } } - Appearance.m3colors.darkmode = (Appearance.m3colors.m3background.hslLightness < 0.5) + Appearance.m3colors.darkmode = (Appearance.m3colors.m3windowBackground.hslLightness < 0.5) } Timer { diff --git a/config/quickshell/shell.qml b/config/quickshell/shell.qml index 36842c2b..b14c8773 100644 --- a/config/quickshell/shell.qml +++ b/config/quickshell/shell.qml @@ -7,7 +7,6 @@ import "./modules/overview/" import QtQuick import QtQuick.Controls import QtQuick.Layouts -import QtQuick.Window import Quickshell import "./services/" diff --git a/config/wallust/templates/qml_color.json b/config/wallust/templates/qml_color.json index 70283080..03565181 100644 --- a/config/wallust/templates/qml_color.json +++ b/config/wallust/templates/qml_color.json @@ -1,21 +1,17 @@ { - "background": "#1e1e2e", - "onBackground": "#bac2de", - "surfaceContainerLow": "{{color4}}", - "surfaceContainer": "{{color6}}", - "surfaceContainerHigh": "{{color3}}", - "surfaceContainerHighest": "{{color2}}", - "onSurface": "#EAE0E7", - "onSurfaceVariant": "#CFC3CD", - "outline": "{{color7}}", - "scrim": "#000000", - "shadow": "#000000", - "primary": "{{color7}}", - "primaryContainer": "{{color7}}", - "secondary": "#D5C0D7", - "secondaryContainer": "{{color5}}", - "onPrimary": "#FFFFFF", - "onPrimaryContainer": "#21005D", - "onSecondaryContainer": "#F2DCF3", - "outlineVariant": "{{color5}}" + "windowBackground": "#0f0f15", + "primaryText": "#bac2de", + "layerBackground1": "{{color7}}", + "layerBackground2": "{{color6}}", + "layerBackground3": "#2D282E", + "surfaceText": "#EAE0E7", + "secondaryText": "#CFC3CD", + "borderPrimary": "{{color7}}", + "shadowColor": "#000000", + "accentPrimary": "{{color7}}", + "accentSecondary": "#{{color7}}", + "selectionBackground": "{{color7}}", + "accentPrimaryText": "#FFFFFF", + "selectionText": "#F2DCF3", + "borderSecondary": "#{{color5}}" } \ No newline at end of file -- cgit v1.2.3