dotfiles
My PC Dotfiles & Websitegit clone https://github.com/maydayv7/dotfiles
files/noctalia/keyhelp/panel.luau (7252B)
1local model = require("./lib/model.luau")
2local pages, selected, query = {}, "", ""
3local pageNumber, pageCount, inputRevision, resultsRevision = 1, 1, 0, 0
4local generation, opened, loading, errorMessage = 0, false, false, nil
5local context = {}
6local render, load
7local tr = noctalia.tr
8
9local function resetResults()
10 pageNumber = 1
11 resultsRevision = resultsRevision + 1
12end
13
14local function choose(id)
15 selected, query = id, ""
16 inputRevision = inputRevision + 1
17 resetResults()
18 render()
19end
20
21local function changePage(delta)
22 local nextPage = math.max(1, math.min(pageNumber + delta, pageCount))
23 if nextPage == pageNumber then
24 return
25 end
26 pageNumber = nextPage
27 resultsRevision = resultsRevision + 1
28 render()
29end
30
31local function renderResults(rows)
32 local content, previousGroup = {}, nil
33 for _, row in ipairs(rows) do
34 local group = row.page.id .. "/" .. row.group.id
35 if previousGroup ~= group then
36 table.insert(
37 content,
38 ui.label({
39 key = "heading-" .. group,
40 text = row.page.name .. " · " .. row.group.name,
41 color = "primary",
42 fontWeight = "bold",
43 fontSize = 14,
44 })
45 )
46 previousGroup = group
47 elseif row.entry.gap_before then
48 table.insert(content, ui.separator({ key = "gap-" .. row.key, color = "outline", spacing = 4 }))
49 end
50 table.insert(
51 content,
52 ui.row(
53 { key = row.key, gap = 16, paddingH = 12, paddingV = 8, fill = "surface_variant", radius = 8, align = "center" },
54 {
55 ui.label({ text = row.entry.description, flexGrow = 1, maxWidth = 460, maxLines = 5, fontSize = 14 }),
56 ui.label({
57 text = row.entry.keys,
58 width = 260,
59 maxWidth = 260,
60 maxLines = 5,
61 textAlign = "end",
62 fontSize = 13,
63 fontWeight = "bold",
64 color = "primary",
65 }),
66 }
67 )
68 )
69 end
70 if #rows == 0 then
71 table.insert(content, ui.label({ text = tr("no_results"), color = "on_surface_variant" }))
72 end
73 -- A new key resets scrolling when the query, app, or page changes
74 return ui.scroll({ key = "results-" .. resultsRevision, flexGrow = 1, gap = 8 }, content)
75end
76
77render = function()
78 if not opened then
79 return
80 end
81 local children = {
82 ui.row({ gap = 12, align = "center" }, {
83 ui.glyph({ name = "keyboard", color = "primary", size = 24 }),
84 ui.label({ text = tr("title"), fontSize = 20, fontWeight = "bold", flexGrow = 1 }),
85 ui.button({
86 glyph = "x",
87 variant = "ghost",
88 controlSize = "sm",
89 tooltip = tr("close"),
90 onClick = function()
91 panel.close()
92 end,
93 }),
94 }),
95 }
96 if loading or errorMessage then
97 table.insert(
98 children,
99 ui.label({ text = loading and tr("loading") or tr("load_error"), color = "on_surface_variant" })
100 )
101 if errorMessage then
102 table.insert(children, ui.label({ text = errorMessage, maxLines = 6 }))
103 table.insert(
104 children,
105 ui.button({
106 text = tr("retry"),
107 onClick = function()
108 load()
109 end,
110 })
111 )
112 end
113 panel.render(ui.column({ gap = 16, padding = 20 }, children))
114 return
115 end
116
117 local selectedName = ""
118 for _, page in ipairs(pages) do
119 if page.id == selected then
120 selectedName = page.name
121 break
122 end
123 end
124
125 table.insert(
126 children,
127 ui.input({
128 key = "search-" .. inputRevision,
129 value = query,
130 focus = true,
131 placeholder = tr("search", { app = selectedName }),
132 onChange = function(value)
133 query = value
134 resetResults()
135 render()
136 end,
137 })
138 )
139 local tabs = {}
140 for _, page in ipairs(pages) do
141 table.insert(
142 tabs,
143 ui.button({
144 key = page.id,
145 text = page.name,
146 controlSize = "sm",
147 flexGrow = 1,
148 variant = "ghost",
149 selected = selected == page.id,
150 onClick = function()
151 choose(page.id)
152 end,
153 })
154 )
155 end
156 table.insert(children, ui.row({ gap = 8 }, tabs))
157
158 local results = model.search(pages, selected, query)
159 local rows
160 rows, pageNumber, pageCount = model.paginate(results, pageNumber)
161 table.insert(children, renderResults(rows))
162 table.insert(
163 children,
164 ui.row({ gap = 8, align = "center" }, {
165 ui.label({ text = tr("count", { count = #results }), flexGrow = 1, color = "on_surface_variant", fontSize = 12 }),
166 ui.button({
167 key = "previous",
168 text = tr("previous"),
169 controlSize = "sm",
170 variant = "ghost",
171 enabled = pageNumber > 1,
172 onClick = function()
173 changePage(-1)
174 end,
175 }),
176 ui.label({ text = tr("page", { current = pageNumber, total = pageCount }), fontSize = 12 }),
177 ui.button({
178 key = "next",
179 text = tr("next"),
180 controlSize = "sm",
181 variant = "ghost",
182 enabled = pageNumber < pageCount,
183 onClick = function()
184 changePage(1)
185 end,
186 }),
187 })
188 )
189 table.insert(children, ui.label({ text = tr("hints"), fontSize = 11, color = "on_surface_variant", maxLines = 2 }))
190 panel.render(ui.column({ gap = 12, padding = 20, flexGrow = 1 }, children))
191end
192
193load = function()
194 generation = generation + 1
195 local request = generation
196 loading, errorMessage = true, nil
197 render()
198 local accepted = noctalia.readFileAsync("shortcuts.json", function(raw, err)
199 if not opened or generation ~= request then
200 return
201 end
202 loading = false
203 local data
204 if raw then
205 data, err = noctalia.json.decode(raw)
206 if data then
207 data, err = model.prepare(data)
208 end
209 end
210 if not data then
211 errorMessage = err or tr("invalid_data")
212 noctalia.log("Keyhelp: " .. errorMessage)
213 else
214 pages = model.pages(data, context.compositor)
215 selected = model.selectPage(pages, context.appIds)
216 resetResults()
217 end
218 render()
219 end)
220 if not accepted then
221 loading, errorMessage = false, tr("read_busy")
222 render()
223 end
224end
225
226function onOpen()
227 opened = true
228 context = noctalia.state.get("context") or {}
229 if context.compositor ~= "hyprland" and context.compositor ~= "niri" then
230 context = { compositor = noctalia.getenv("NIRI_SOCKET") and "niri" or "hyprland", appIds = {} }
231 end
232 noctalia.state.set("panel_open", true)
233 query, pages, selected = "", {}, ""
234 inputRevision = inputRevision + 1
235 load()
236end
237
238function onClose()
239 opened = false
240 generation = generation + 1
241 noctalia.state.set("panel_open", false)
242end
243
244function onExit()
245 onClose()
246end
247
248function onKey(chord, pressed)
249 if not pressed or loading or errorMessage then
250 return
251 end
252 if chord == "Page_Down" then
253 changePage(1)
254 elseif chord == "Page_Up" then
255 changePage(-1)
256 elseif chord == "ctrl+f" then
257 inputRevision = inputRevision + 1
258 render()
259 elseif chord == "ctrl+Tab" or chord == "ctrl+shift+Tab" then
260 local delta = chord == "ctrl+Tab" and 1 or -1
261 for index, page in ipairs(pages) do
262 if page.id == selected then
263 choose(pages[(index - 1 + delta) % #pages + 1].id)
264 break
265 end
266 end
267 end
268end