aboutsummaryrefslogtreecommitdiffstats
path: root/config/quickshell/services
diff options
context:
space:
mode:
authorKiran George <kirangeorge1995@gmail.com>2025-06-09 11:30:08 +0530
committerKiran George <kirangeorge1995@gmail.com>2025-06-09 11:30:08 +0530
commit952aa63147c9fb28f6ace6f0bc7ccf45ced1299a (patch)
tree306e6d86603a162c00bc5113b56baac0fe7bec7c /config/quickshell/services
parent4cf0d0bd5930da76e60f6770de3ee97c10ca7024 (diff)
Overview v2
Diffstat (limited to 'config/quickshell/services')
-rw-r--r--config/quickshell/services/AppSearch.qml116
-rw-r--r--config/quickshell/services/ConfigLoader.qml116
-rw-r--r--config/quickshell/services/HyprlandData.qml69
-rw-r--r--config/quickshell/services/HyprlandKeybinds.qml73
-rw-r--r--config/quickshell/services/MaterialThemeLoader.qml58
5 files changed, 432 insertions, 0 deletions
diff --git a/config/quickshell/services/AppSearch.qml b/config/quickshell/services/AppSearch.qml
new file mode 100644
index 00000000..876df183
--- /dev/null
+++ b/config/quickshell/services/AppSearch.qml
@@ -0,0 +1,116 @@
+pragma Singleton
+
+import "root:/modules/common"
+import "root:/modules/common/functions/fuzzysort.js" as Fuzzy
+import "root:/modules/common/functions/levendist.js" as Levendist
+import Quickshell
+import Quickshell.Io
+
+/**
+ * - Eases fuzzy searching for applications by name
+ * - Guesses icon name for window class name
+ */
+Singleton {
+ id: root
+ property bool sloppySearch: ConfigOptions?.search.sloppy ?? false
+ property real scoreThreshold: 0.2
+ 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",
+ "zen": "zen-browser",
+ })
+ property var regexSubstitutions: [
+ {
+ "regex": /^steam_app_(\\d+)$/,
+ "replace": "steam_icon_$1"
+ },
+ {
+ "regex": /Minecraft.*/,
+ "replace": "minecraft"
+ },
+ {
+ "regex": /.*polkit.*/,
+ "replace": "system-lock-screen"
+ },
+ {
+ "regex": /gcr.prompter/,
+ "replace": "system-lock-screen"
+ }
+ ]
+
+ readonly property list<DesktopEntry> list: Array.from(DesktopEntries.applications.values)
+ .sort((a, b) => a.name.localeCompare(b.name))
+
+ readonly property var preppedNames: list.map(a => ({
+ name: Fuzzy.prepare(`${a.name} `),
+ entry: a
+ }))
+
+ function fuzzyQuery(search: string): var { // Idk why list<DesktopEntry> doesn't work
+ if (root.sloppySearch) {
+ const results = list.map(obj => ({
+ entry: obj,
+ score: Levendist.computeScore(obj.name.toLowerCase(), search.toLowerCase())
+ })).filter(item => item.score > root.scoreThreshold)
+ .sort((a, b) => b.score - a.score)
+ return results
+ .map(item => item.entry)
+ }
+
+ return Fuzzy.go(search, preppedNames, {
+ all: true,
+ key: "name"
+ }).map(r => {
+ return r.obj.entry
+ });
+ }
+
+ function iconExists(iconName) {
+ return (Quickshell.iconPath(iconName, true).length > 0)
+ && !iconName.includes("image-missing");
+ }
+
+ function guessIcon(str) {
+ if (!str || str.length == 0) return "image-missing";
+
+ // Normal substitutions
+ if (substitutions[str])
+ return substitutions[str];
+
+ // Regex substitutions
+ for (let i = 0; i < regexSubstitutions.length; i++) {
+ const substitution = regexSubstitutions[i];
+ const replacedName = str.replace(
+ substitution.regex,
+ substitution.replace,
+ );
+ if (replacedName != str) return replacedName;
+ }
+
+ // If it gets detected normally, no need to guess
+ if (iconExists(str)) return str;
+
+ let guessStr = str;
+ // Guess: Take only app name of reverse domain name notation
+ guessStr = str.split('.').slice(-1)[0].toLowerCase();
+ if (iconExists(guessStr)) return guessStr;
+ // Guess: normalize to kebab case
+ guessStr = str.toLowerCase().replace(/\s+/g, "-");
+ if (iconExists(guessStr)) return guessStr;
+ // Guess: First fuzze desktop entry match
+ const searchResults = root.fuzzyQuery(str);
+ if (searchResults.length > 0) {
+ const firstEntry = searchResults[0];
+ guessStr = firstEntry.icon
+ if (iconExists(guessStr)) return guessStr;
+ }
+
+ // Give up
+ return str;
+ }
+}
diff --git a/config/quickshell/services/ConfigLoader.qml b/config/quickshell/services/ConfigLoader.qml
new file mode 100644
index 00000000..051162d1
--- /dev/null
+++ b/config/quickshell/services/ConfigLoader.qml
@@ -0,0 +1,116 @@
+pragma Singleton
+pragma ComponentBehavior: Bound
+
+import "root:/modules/common"
+import "root:/modules/common/functions/file_utils.js" as FileUtils
+import "root:/modules/common/functions/string_utils.js" as StringUtils
+import "root:/modules/common/functions/object_utils.js" as ObjectUtils
+import QtQuick
+import Quickshell
+import Quickshell.Io
+import Quickshell.Hyprland
+import Qt.labs.platform
+
+/**
+ * Loads and manages the shell configuration file.
+ * The config file is by default at XDG_CONFIG_HOME/quickshell/config.json.
+ * Automatically reloaded when the file changes, but does not provide a way to save changes.
+ */
+Singleton {
+ id: root
+ property string filePath: Directories.shellConfigPath
+ property bool firstLoad: true
+
+ function loadConfig() {
+ configFileView.reload()
+ }
+
+ function applyConfig(fileContent) {
+ try {
+ const json = JSON.parse(fileContent);
+
+ ObjectUtils.applyToQtObject(ConfigOptions, json);
+ if (root.firstLoad) {
+ root.firstLoad = false;
+ } else {
+ Hyprland.dispatch(`exec notify-send "${qsTr("Shell configuration reloaded")}" "${root.filePath}"`)
+ }
+ } catch (e) {
+ console.error("[ConfigLoader] Error reading file:", e);
+ Hyprland.dispatch(`exec notify-send "${qsTr("Shell configuration failed to load")}" "${root.filePath}"`)
+ return;
+
+ }
+ }
+
+ function setLiveConfigValue(nestedKey, value) {
+ let keys = nestedKey.split(".");
+ let obj = ConfigOptions;
+ let parents = [obj];
+
+ // Traverse and collect parent objects
+ for (let i = 0; i < keys.length - 1; ++i) {
+ if (!obj[keys[i]] || typeof obj[keys[i]] !== "object") {
+ obj[keys[i]] = {};
+ }
+ obj = obj[keys[i]];
+ parents.push(obj);
+ }
+
+ // Convert value to correct type using JSON.parse when safe
+ let convertedValue = value;
+ if (typeof value === "string") {
+ let trimmed = value.trim();
+ if (trimmed === "true" || trimmed === "false" || !isNaN(Number(trimmed))) {
+ try {
+ convertedValue = JSON.parse(trimmed);
+ } catch (e) {
+ convertedValue = value;
+ }
+ }
+ }
+
+ 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)
+ Hyprland.dispatch(`exec echo '${StringUtils.shellSingleQuoteEscape(JSON.stringify(plainConfig, null, 2))}' > '${root.filePath}'`)
+ }
+
+ Timer {
+ id: delayedFileRead
+ interval: ConfigOptions.hacks.arbitraryRaceConditionDelay
+ repeat: false
+ running: false
+ onTriggered: {
+ root.applyConfig(configFileView.text())
+ }
+ }
+
+ FileView {
+ id: configFileView
+ path: Qt.resolvedUrl(root.filePath)
+ watchChanges: true
+ onFileChanged: {
+ console.log("[ConfigLoader] File changed, reloading...")
+ this.reload()
+ delayedFileRead.start()
+ }
+ onLoadedChanged: {
+ const fileContent = configFileView.text()
+ root.applyConfig(fileContent)
+ }
+ onLoadFailed: (error) => {
+ if(error == FileViewError.FileNotFound) {
+ console.log("[ConfigLoader] File not found, creating new file.")
+ root.saveConfig()
+ Hyprland.dispatch(`exec notify-send "${qsTr("Shell configuration created")}" "${root.filePath}"`)
+ } else {
+ Hyprland.dispatch(`exec notify-send "${qsTr("Shell configuration failed to load")}" "${root.filePath}"`)
+ }
+ }
+ }
+}
diff --git a/config/quickshell/services/HyprlandData.qml b/config/quickshell/services/HyprlandData.qml
new file mode 100644
index 00000000..2b88ad9c
--- /dev/null
+++ b/config/quickshell/services/HyprlandData.qml
@@ -0,0 +1,69 @@
+pragma Singleton
+pragma ComponentBehavior: Bound
+
+import QtQuick
+import Quickshell
+import Quickshell.Io
+import Quickshell.Wayland
+import Quickshell.Hyprland
+
+/**
+ * Provides access to some Hyprland data not available in Quickshell.Hyprland.
+ */
+Singleton {
+ id: root
+ property var windowList: []
+ property var addresses: []
+ property var windowByAddress: ({})
+ property var monitors: []
+
+ function updateWindowList() {
+ getClients.running = true
+ getMonitors.running = true
+ }
+
+ Component.onCompleted: {
+ updateWindowList()
+ }
+
+ Connections {
+ target: Hyprland
+
+ function onRawEvent(event) {
+ // Filter out redundant old v1 events for the same thing
+ if(event.name in [
+ "activewindow", "focusedmon", "monitoradded",
+ "createworkspace", "destroyworkspace", "moveworkspace",
+ "activespecial", "movewindow", "windowtitle"
+ ]) return ;
+ updateWindowList()
+ }
+ }
+
+ Process {
+ id: getClients
+ command: ["bash", "-c", "hyprctl clients -j | jq -c"]
+ stdout: SplitParser {
+ onRead: (data) => {
+ root.windowList = JSON.parse(data)
+ let tempWinByAddress = {}
+ for (var i = 0; i < root.windowList.length; ++i) {
+ var win = root.windowList[i]
+ tempWinByAddress[win.address] = win
+ }
+ root.windowByAddress = tempWinByAddress
+ root.addresses = root.windowList.map((win) => win.address)
+ }
+ }
+ }
+ Process {
+ id: getMonitors
+ command: ["bash", "-c", "hyprctl monitors -j | jq -c"]
+ stdout: SplitParser {
+ onRead: (data) => {
+ root.monitors = JSON.parse(data)
+ }
+ }
+ }
+}
+
diff --git a/config/quickshell/services/HyprlandKeybinds.qml b/config/quickshell/services/HyprlandKeybinds.qml
new file mode 100644
index 00000000..189ba76d
--- /dev/null
+++ b/config/quickshell/services/HyprlandKeybinds.qml
@@ -0,0 +1,73 @@
+pragma Singleton
+pragma ComponentBehavior: Bound
+
+import "root:/modules/common"
+import "root:/modules/common/functions/file_utils.js" as FileUtils
+import QtQuick
+import Quickshell
+import Quickshell.Io
+import Quickshell.Wayland
+import Quickshell.Hyprland
+
+/**
+ * A service that provides access to Hyprland keybinds.
+ * Uses the `get_keybinds.py` script to parse comments in config files in a certain format and convert to JSON.
+ */
+Singleton {
+ id: root
+ property string keybindParserPath: FileUtils.trimFileProtocol(`${Directories.config}/quickshell/scripts/hyprland/get_keybinds.py`)
+ property string defaultKeybindConfigPath: FileUtils.trimFileProtocol(`${Directories.config}/hypr/hyprland/keybinds.conf`)
+ property string userKeybindConfigPath: FileUtils.trimFileProtocol(`${Directories.config}/hypr/custom/keybinds.conf`)
+ property var defaultKeybinds: {"children": []}
+ property var userKeybinds: {"children": []}
+ property var keybinds: ({
+ children: [
+ ...(defaultKeybinds.children ?? []),
+ ...(userKeybinds.children ?? []),
+ ]
+ })
+
+ Connections {
+ target: Hyprland
+
+ function onRawEvent(event) {
+ if (event.name == "configreloaded") {
+ getDefaultKeybinds.running = true
+ getUserKeybinds.running = true
+ }
+ }
+ }
+
+ Process {
+ id: getDefaultKeybinds
+ running: true
+ command: [root.keybindParserPath, "--path", root.defaultKeybindConfigPath,]
+
+ stdout: SplitParser {
+ onRead: data => {
+ try {
+ root.defaultKeybinds = JSON.parse(data)
+ } catch (e) {
+ console.error("[CheatsheetKeybinds] Error parsing keybinds:", e)
+ }
+ }
+ }
+ }
+
+ Process {
+ id: getUserKeybinds
+ running: true
+ command: [root.keybindParserPath, "--path", root.userKeybindConfigPath]
+
+ stdout: SplitParser {
+ onRead: data => {
+ try {
+ root.userKeybinds = JSON.parse(data)
+ } catch (e) {
+ console.error("[CheatsheetKeybinds] Error parsing keybinds:", e)
+ }
+ }
+ }
+ }
+}
+
diff --git a/config/quickshell/services/MaterialThemeLoader.qml b/config/quickshell/services/MaterialThemeLoader.qml
new file mode 100644
index 00000000..cd4eb686
--- /dev/null
+++ b/config/quickshell/services/MaterialThemeLoader.qml
@@ -0,0 +1,58 @@
+pragma Singleton
+pragma ComponentBehavior: Bound
+
+import "root:/modules/common"
+import QtQuick
+import Quickshell
+import Quickshell.Io
+
+/**
+ * Automatically reloads generated material colors.
+ * It is necessary to run reapplyTheme() on startup because Singletons are lazily loaded.
+ */
+Singleton {
+ id: root
+ property string filePath: Directories.generatedMaterialThemePath
+
+ function reapplyTheme() {
+ themeFileView.reload()
+ }
+
+ function applyColors(fileContent) {
+ const json = JSON.parse(fileContent)
+ for (const key in json) {
+ if (json.hasOwnProperty(key)) {
+ // Convert snake_case to CamelCase
+ const camelCaseKey = key.replace(/_([a-z])/g, (g) => g[1].toUpperCase())
+ const m3Key = `m3${camelCaseKey}`
+ Appearance.m3colors[m3Key] = json[key]
+ }
+ }
+
+ Appearance.m3colors.darkmode = (Appearance.m3colors.m3background.hslLightness < 0.5)
+ }
+
+ Timer {
+ id: delayedFileRead
+ interval: ConfigOptions?.hacks?.arbitraryRaceConditionDelay ?? 100
+ repeat: false
+ running: false
+ onTriggered: {
+ root.applyColors(themeFileView.text())
+ }
+ }
+
+ FileView {
+ id: themeFileView
+ path: Qt.resolvedUrl(root.filePath)
+ watchChanges: true
+ onFileChanged: {
+ this.reload()
+ delayedFileRead.start()
+ }
+ onLoadedChanged: {
+ const fileContent = themeFileView.text()
+ root.applyColors(fileContent)
+ }
+ }
+}
send patches to the email below
yukais@pinapelz.com
include the subject [PATCH repo_name]
pinapelz.com
homepage