Krolyxon
Commit fb9b856

Branches

Refs →

aurora.configquickshellauroraHyprConf.qml

183 lines · 8.3 KB · QML

1pragma Singleton
2import QtQuick
3import Quickshell
4import Quickshell.Io
5
6// Hyprland options edited from the settings app. Only what the user changed is stored, as overrides:
7// ~/.config/aurora/hypr.json {"general:gaps_in": 5, ...} (state, read back here)
8// ~/.config/aurora/hypr.lua return { general = { gaps_in = 5 } } (dofile'd at the end of hyprland.lua)
9// A change is applied live with `hyprctl eval 'hl.config({...})'` and survives reloads/logins through hypr.lua.
10// reset(opt) drops the override and reloads Hyprland's config, so the repo value comes back.
11Singleton {
12 id: root
13 readonly property string dir: Quickshell.env("HOME") + "/.config/aurora"
14
15 // every option the settings pages show: path -> type (int | float | bool | str)
16 readonly property var types: ({
17 "general:gaps_in": "int", "general:gaps_out": "int", "general:border_size": "int",
18 "general:layout": "str", "general:resize_on_border": "bool",
19 "decoration:rounding": "int", "decoration:active_opacity": "float", "decoration:inactive_opacity": "float",
20 "decoration:dim_inactive": "bool", "decoration:shadow:enabled": "bool",
21 "decoration:blur:enabled": "bool", "decoration:blur:size": "int", "decoration:blur:passes": "int",
22 "decoration:blur:xray": "bool",
23 "animations:enabled": "bool",
24 "master:mfact": "float", "master:new_status": "str", "master:new_on_top": "bool", "master:orientation": "str",
25 "dwindle:preserve_split": "bool", "dwindle:smart_split": "bool",
26 "misc:on_focus_under_fullscreen": "int", "misc:exit_window_retains_fullscreen": "bool",
27 "misc:focus_on_activate": "bool",
28 "input:kb_layout": "str", "input:kb_variant": "str", "input:kb_options": "str",
29 "input:repeat_rate": "int", "input:repeat_delay": "int", "input:sensitivity": "float",
30 "input:accel_profile": "str", "input:follow_mouse": "int", "input:natural_scroll": "bool",
31 "input:left_handed": "bool",
32 "input:touchpad:natural_scroll": "bool", "input:touchpad:tap_to_click": "bool",
33 "input:touchpad:disable_while_typing": "bool", "input:touchpad:scroll_factor": "float",
34 "gestures:workspace_swipe_invert": "bool", "gestures:workspace_swipe_distance": "int",
35 "cursor:inactive_timeout": "int", "cursor:hide_on_key_press": "bool"
36 })
37
38 property var values: ({}) // current value of every option in `types` (from getoption)
39 property var overrides: ({}) // only the changed ones
40 property string message: "" // last hyprctl error, shown by the pages
41 property bool loaded: false
42
43 function get(opt) { return values[opt] }
44 function isSet(opt) { return overrides.hasOwnProperty(opt) }
45
46 function set(opt, v) {
47 if (!types.hasOwnProperty(opt)) return
48 var o = Object.assign({}, overrides), c = Object.assign({}, values)
49 o[opt] = v
50 c[opt] = v
51 overrides = o
52 values = c
53 pending[opt] = v
54 applyTimer.restart() // coalesces slider drags into one hyprctl call
55 saveTimer.restart()
56 }
57 function reset(opt) { resetAll([opt]) }
58 // e.g. ["input:", "cursor:"]: drop every override under those sections, then reload Hyprland's config so the
59 // repo values come back. The reload waits for hypr.lua to be written (luaFile.onSaved): reloading right after
60 // setText() raced the async write and Hyprland re-read the old overrides.
61 function resetAll(prefixes) {
62 var o = Object.assign({}, overrides), n = 0
63 for (var k in o)
64 for (var i = 0; i < prefixes.length; i++)
65 if (k.indexOf(prefixes[i]) === 0) { delete o[k]; n++; break }
66 pending = {} // a slider drag still waiting to be applied would undo the reset
67 applyTimer.stop()
68 saveTimer.stop()
69 if (n === 0) { message = ""; refresh(); return } // nothing overridden: already the config values
70 overrides = o
71 reloadAfterSave = true
72 save()
73 }
74 property bool reloadAfterSave: false
75 function refresh() {
76 readProc.command = ["sh", "-c", 'for o; do hyprctl -j getoption "$o" | tr -d "\\n"; echo; done', "sh"]
77 .concat(Object.keys(types))
78 readProc.running = true
79 }
80
81 // ---- lua generation: keys only ever come from `types`, values are typed + escaped ----
82 function luaValue(opt, v) {
83 var t = types[opt]
84 if (t === "bool") return v ? "true" : "false"
85 if (t === "int") return String(Math.round(Number(v)) || 0)
86 if (t === "float") { var n = Number(v); return String(isFinite(n) ? n : 0) }
87 return '"' + String(v).replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/[\r\n]/g, " ") + '"'
88 }
89 function luaTable(map) { // {"a:b:c": v} -> "{ a = { b = { c = v } } }"
90 var tree = {}
91 for (var opt in map) {
92 if (!types.hasOwnProperty(opt)) continue
93 var parts = opt.split(":"), node = tree
94 for (var i = 0; i < parts.length - 1; i++) {
95 if (typeof node[parts[i]] !== "object") node[parts[i]] = {}
96 node = node[parts[i]]
97 }
98 node[parts[parts.length - 1]] = { lua: luaValue(opt, map[opt]) }
99 }
100 function emit(n, ind) {
101 var keys = Object.keys(n).sort(), out = []
102 for (var j = 0; j < keys.length; j++) {
103 var x = n[keys[j]]
104 out.push(ind + " " + keys[j] + " = " + (x.hasOwnProperty("lua") ? x.lua : emit(x, ind + " ")))
105 }
106 return out.length ? "{\n" + out.join(",\n") + "\n" + ind + "}" : "{}"
107 }
108 return emit(tree, "")
109 }
110
111 function save() {
112 jsonFile.setText(JSON.stringify(overrides, null, 2) + "\n")
113 luaFile.setText("-- generated by aurora's settings app (HyprConf.qml); edit there, not here\n"
114 + "return " + luaTable(overrides) + "\n")
115 }
116
117 property var pending: ({})
118 Timer {
119 id: applyTimer
120 interval: 60
121 onTriggered: {
122 var code = "hl.config(" + root.luaTable(root.pending) + ")"
123 root.pending = {}
124 applyProc.command = ["hyprctl", "eval", code]
125 applyProc.running = true
126 }
127 }
128 Timer { id: saveTimer; interval: 400; onTriggered: root.save() }
129
130 Process {
131 id: applyProc
132 stdout: StdioCollector {
133 onStreamFinished: root.message = text.trim() === "ok" || text.trim() === "" ? "" : text.trim()
134 }
135 }
136 Process {
137 id: reloadProc
138 command: ["hyprctl", "reload", "config-only"]
139 onExited: root.refresh()
140 }
141 Process {
142 id: readProc
143 stdout: StdioCollector {
144 onStreamFinished: {
145 var c = {}, lines = text.split("\n")
146 for (var i = 0; i < lines.length; i++) {
147 if (lines[i].trim() === "") continue
148 try {
149 var j = JSON.parse(lines[i]), v
150 if (j.hasOwnProperty("css")) v = parseInt(String(j.css).split(" ")[0]) // gaps: "2 2 2 2"
151 else if (j.hasOwnProperty("int")) v = j.int
152 else if (j.hasOwnProperty("float")) v = j.float
153 else if (j.hasOwnProperty("bool")) v = j.bool
154 else if (j.hasOwnProperty("str")) v = j.str === "[[EMPTY]]" ? "" : j.str // unset string option
155 if (v !== undefined) c[j.option] = root.types[j.option] === "bool" ? !!v : v
156 } catch (e) {}
157 }
158 root.values = c
159 root.loaded = true
160 }
161 }
162 }
163
164 FileView {
165 id: jsonFile
166 path: root.dir + "/hypr.json"
167 blockLoading: true
168 printErrors: false // no file until the first change
169 onLoaded: {
170 try { root.overrides = JSON.parse(text()) || {} } catch (e) { root.overrides = {} }
171 }
172 }
173 FileView { // write-only
174 id: luaFile
175 path: root.dir + "/hypr.lua"
176 preload: false
177 printErrors: false
178 onSaved: if (root.reloadAfterSave) { root.reloadAfterSave = false; reloadProc.running = true }
179 onSaveFailed: err => { root.reloadAfterSave = false; root.message = "could not write hypr.lua (error " + err + ")" }
180 }
181
182 Component.onCompleted: refresh()
183}