dotfiles

My PC Dotfiles & Website

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