local astal = require("astal") local Widget = require("astal.gtk3.widget") local App = require("astal.gtk3.app") local Gdk = require("astal.gtk3").Gdk local Gtk = require("astal.gtk3").Gtk local astalify = require("astal.gtk3").astalify local Apps = astal.require("AstalApps") local Variable = astal.Variable local lib = require("lib") local MAX_ENTRIES = 20 local WIDTH = 7 local FlowBox = astalify(Gtk.FlowBox) local FlowBoxChild = astalify(Gtk.FlowBoxChild) local apps = Apps.Apps() local text = Variable("") local visible = Variable(false) local selection = Variable(1) local entry = Variable(nil) local list = text(function(text) return lib.slice(apps:exact_query(text), 0, MAX_ENTRIES) end) local function on_show() text:set("") selection:set(0) entry:get():grab_focus() end local function hide() visible:set(false) end local function on_enter() local found = apps:exact_query(text:get())[selection:get() + 1] if found then found:launch() hide() end end local function update_pos(change_x, change_y) local pos = selection:get() local pos_x = (pos % WIDTH) + change_x local pos_y = math.floor(pos / WIDTH) + change_y local count = lib.count(list:get()) local height = math.floor((count + WIDTH - 1) / WIDTH) pos_x = pos_x % WIDTH pos_y = pos_y % height pos = lib.clamp(pos_y * WIDTH + pos_x, 0, count - 1) selection:set(pos) end local function on_key_press(_, event) if event.keyval == Gdk.KEY_Escape then hide() elseif event.keyval == Gdk.KEY_Return then on_enter() elseif event.keyval == Gdk.KEY_Left then update_pos(-1, 0) elseif event.keyval == Gdk.KEY_Right then update_pos(1, 0) elseif event.keyval == Gdk.KEY_Up then update_pos(0, -1) elseif event.keyval == Gdk.KEY_Down then update_pos(0, 1) end end function Application(app, idx) return FlowBoxChild({ Widget.Button({ class_name = selection():as(function(c) if (c + 1) == idx then return "app selected" else return "app" end end), on_clicked = function() app:launch() hide() end, Widget.Box({ halign = "CENTER", valign = "CENTER", vertical = true, Widget.Icon({ icon = app.icon_name, }), Widget.Label({ class_name = "name", label = app.name, valign = "CENTER", ellipsize = "END", max_width_chars = 16, }), }), }), }) end function Applications(apps) return FlowBox({ hexpand = true, homogeneous = true, class_name = "apps", lib.map(apps, Application) }) end function Launcher() return Widget.Box({ vertical = true, Widget.Entry({ class_name = "search", placeholder_text = "Search", halign = "CENTER", text = text(), setup = function(self) entry:set(self) end, on_changed = function(self) text:set(self.text) selection:set(0) end, }), Widget.Box({ class_name = "apps", list:as(Applications), }), }) end return function() local Anchor = astal.require('Astal').WindowAnchor Widget.Window({ class_name = "launcher", anchor = Anchor.TOP + Anchor.BOTTOM + Anchor.LEFT + Anchor.RIGHT, exclusivity = "EXCLUSIVE", keymode = "ON_DEMAND", application = App, on_show = on_show, on_key_press_event = on_key_press, visible = visible(), Launcher(), }) return visible end