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 WIDTH local MAX_ENTRIES local FlowBox = astalify(Gtk.FlowBox) local FlowBoxChild = astalify(Gtk.FlowBoxChild) local function exit() App:quit() end local selection = Variable(0) local entry = Variable(nil) function Application(app, selected) return FlowBoxChild({ Widget.Button({ class_name = selected(function(bool) if bool then return "app selected" else return "app" end end), on_clicked = function() app:launch() exit() 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 local astal_apps = Apps.Apps() local apps = {} local function get_app(key) local tbl = apps[key] if tbl.elm == nil then local selected = Variable(false) tbl.elm = Application(tbl.app, selected) tbl.selected = selected apps[entry] = tbl end return tbl end for _,app in pairs(astal_apps.list) do apps[app.entry] = { app = app, } end local text = Variable("") local list = text(function(query) return lib.slice(astal_apps:fuzzy_query(query), 0, MAX_ENTRIES) end) local function on_enter() local app = list:get()[selection:get() + 1] app:launch() exit() end local function update_pos(change_x, change_y) local old_pos = selection:get() local pos = old_pos local pos_x = (pos % WIDTH) + change_x local pos_y = math.floor(pos / WIDTH) + change_y local count = math.min(MAX_ENTRIES, lib.count(list:get())) pos_x = pos_x % WIDTH pos_y = pos_y % (MAX_ENTRIES/WIDTH) pos = lib.clamp(pos_y * WIDTH + pos_x, 0, count - 1) selection:set(pos) local old_app = list:get()[old_pos + 1] if old_app then get_app(old_app.entry).selected:set(false) end local app = list:get()[pos + 1] if app then get_app(app.entry).selected:set(true) end end local function on_key_press(_, event) if event.keyval == Gdk.KEY_Escape then exit() 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 Applications() return FlowBox({ hexpand = true, homogeneous = true, class_name = "apps", list:as(function(l) -- remove all old elements for _,tbl in pairs(apps) do local elm = tbl.elm if elm then local parent = elm.parent if parent ~= nil then parent:remove(elm) end end end -- map list to elements return lib.map(l, function(app) local tbl = get_app(app.entry) return tbl.elm end) end), }) 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", Applications(), }), }) end local function calc_bounds(gdkmonitor) -- constants local font_size = 14 local outer_gap = 12 -- get screen dimensions local screen_width = gdkmonitor.geometry.width local screen_height = gdkmonitor.geometry.height -- calculate size of a cell local cell_min_width = 15 * font_size local cell_padding = outer_gap*4 local cell_size = cell_min_width + cell_padding -- caculate usuable bounds local used_width = 2*5*font_size local used_height = 15*font_size local free_width = screen_width - used_width local free_height = screen_height - used_height -- caculate counts local width = math.floor(free_width / cell_size) local height = math.floor(free_height / cell_size) WIDTH = width MAX_ENTRIES = width*height end return function(gdkmonitor) local Anchor = astal.require('Astal').WindowAnchor calc_bounds(gdkmonitor) local function on_show() text:set("") selection:set(0) entry:get():grab_focus() end return Widget.Window({ class_name = "launcher", gdkmonitor = gdkmonitor, 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, Launcher(), }) end