From be25a16e3c8f7e95e831dd1efee7619fcb2bcd1b Mon Sep 17 00:00:00 2001 From: Rene Kievits Date: Mon, 28 Nov 2022 05:43:47 +0100 Subject: [PATCH] Finished work on application launcher and fixed some bugs + cleanup and documentation --- .../application_launcher/application.lua | 173 ++++++---- .../src/modules/application_launcher/init.lua | 317 +++++++++--------- 2 files changed, 254 insertions(+), 236 deletions(-) diff --git a/awesome/src/modules/application_launcher/application.lua b/awesome/src/modules/application_launcher/application.lua index 550f82e..47368be 100644 --- a/awesome/src/modules/application_launcher/application.lua +++ b/awesome/src/modules/application_launcher/application.lua @@ -2,16 +2,18 @@ -- This is the application launcher -- -------------------------------------- --- Awesome Libs -local awful = require("awful") -local Gio = require("lgi").Gio -local gfilesystem = require("gears").filesystem -local dpi = require("beautiful").xresources.apply_dpi -local gears = require("gears") -local wibox = require("wibox") +-- Awesome libs +local abutton = require("awful.button") +local aspawn = require("awful.spawn") local base = require("wibox.widget.base") +local dpi = require("beautiful").xresources.apply_dpi +local gcolor = require("gears.color") +local gfilesystem = require("gears").filesystem +local Gio = require("lgi").Gio local gtable = require("gears.table") +local wibox = require("wibox") +-- Third party libs local json = require("src.lib.json-lua.json-lua") local cm = require("src.modules.context_menu.init") @@ -20,10 +22,24 @@ local capi = { mouse = mouse, } -local icondir = gears.filesystem.get_configuration_dir() .. "src/assets/icons/context_menu/" +local icondir = gfilesystem.get_configuration_dir() .. "src/assets/icons/context_menu/" local application_grid = { mt = {} } +--[[ + Make sure that the config folder exists and the applications.json + This is done here once because it would be unnecessary to do it for every instance +]] +do + local dir = gfilesystem.get_configuration_dir() .. "src/config" + gfilesystem.make_directories(dir) + dir = dir .. "/applications.json" + if not gfilesystem.file_readable(dir) then + aspawn("touch " .. dir) + end +end + +--#region wibox.widget.base boilerplate function application_grid:layout(_, width, height) if self._private.widget then return { base.place_widget_at(self._private.widget, 0, 0, width, height) } @@ -44,6 +60,12 @@ function application_grid:get_widget() return self._private.widget end +--#endregion + +--[[ + Calculate the levenshtein distance between two strings to determine how similar they are + I stole this from a random github gist +]] local function levenshtein_distance(str1, str2) local len1 = string.len(str1) local len2 = string.len(str2) @@ -85,12 +107,17 @@ local function levenshtein_distance(str1, str2) return matrix[len1][len2] end -function application_grid:get_applications_from_file() +---Gets all .desktop files found and filters them based on their visibility +---It used Gio.AppInfo and Gio.DesktopAppInfo to get the information +---@return table +local function get_applications_from_file() local list = {} local app_info = Gio.AppInfo + --Get all .desktop files local apps = app_info.get_all() for _, app in ipairs(apps) do if app.should_show(app) then -- check no display + --Create a new .desktop object local desktop_app_info = Gio.DesktopAppInfo.new(app_info.get_id(app)) local app_widget = wibox.widget { { @@ -100,7 +127,9 @@ function application_grid:get_applications_from_file() { -- Icon valign = "center", halign = "center", - image = Get_gicon_path(app_info.get_icon(app)), + image = Get_gicon_path(app_info.get_icon(app)) or + Get_gicon_path(app_info.get_icon(app), + Gio.DesktopAppInfo.get_string(desktop_app_info, "X-AppImage-Old-Icon")) or "", resize = true, widget = wibox.widget.imagebox }, @@ -138,17 +167,14 @@ function application_grid:get_applications_from_file() categories = Gio.DesktopAppInfo.get_categories(desktop_app_info) or "", terminal = Gio.DesktopAppInfo.get_string(desktop_app_info, "Terminal") == "true", actions = Gio.DesktopAppInfo.list_actions(desktop_app_info), + desktop_file = Gio.DesktopAppInfo.get_filename(desktop_app_info) or "", border_color = Theme_config.application_launcher.application.border_color, border_width = Theme_config.application_launcher.application.border_width, bg = Theme_config.application_launcher.application.bg, fg = Theme_config.application_launcher.application.fg, - desktop_file = Gio.DesktopAppInfo.get_filename(desktop_app_info) or "", - shape = function(cr, width, height) - gears.shape.rounded_rect(cr, width, height, dpi(8)) - end, + shape = Theme_config.application_launcher.application.shape, widget = wibox.container.background } - local context_menu = cm { widget_template = wibox.widget { { @@ -183,34 +209,33 @@ function application_grid:get_applications_from_file() entries = { { name = "Execute as sudo", - icon = gears.color.recolor_image(icondir .. "launch.svg", + icon = gcolor.recolor_image(icondir .. "launch.svg", Theme_config.application_launcher.application.cm_icon_color), callback = function() capi.awesome.emit_signal("application_launcher::show") - awful.spawn("/home/crylia/.config/awesome/src/scripts/start_as_admin.sh " .. app_widget.exec) + aspawn("/home/crylia/.config/awesome/src/scripts/start_as_admin.sh " .. app_widget.exec) end }, { name = "Pin to dock", - icon = gears.color.recolor_image(icondir .. "pin.svg", + icon = gcolor.recolor_image(icondir .. "pin.svg", Theme_config.application_launcher.application.cm_icon_color), callback = function() - local dir = gears.filesystem.get_configuration_dir() .. "src/config" - gfilesystem.make_directories(dir) - if not gfilesystem.file_readable(dir) then - os.execute("touch " .. dir .. "/dock.json") - end - local handler = io.open(dir .. "/dock.json", "r") - if not handler then - return - end - local dock_table = json:decode(handler:read("a")) or {} - handler:close() + -- Open dock.js and read all its content into a table, add the new app into the table and write it back + local file_path = gfilesystem.get_configuration_dir() .. "src/config/dock.json" + local handler = io.open(file_path, "r") + if not handler then return end + + local dock_table = json:decode(handler:read("a")) or {} + + handler:close() + assert(type(dock_table) == "table", "dock_table is not a table") - ---@diagnostic disable-next-line: param-type-mismatch table.insert(dock_table, { name = app_widget.name or "", - icon = Get_gicon_path(app_info.get_icon(app)) or "", + icon = Get_gicon_path(app_info.get_icon(app)) or + Get_gicon_path(app_info.get_icon(app), + Gio.DesktopAppInfo.get_string(desktop_app_info, "X-AppImage-Old-Icon")) or "", comment = app_widget.comment or "", exec = app_widget.exec or "", keywords = app_widget.keywords or "", @@ -219,11 +244,12 @@ function application_grid:get_applications_from_file() actions = app_widget.actions or "", desktop_file = Gio.DesktopAppInfo.get_filename(desktop_app_info) or "" }) + local dock_encoded = json:encode(dock_table) - handler = io.open("/home/crylia/.config/awesome/src/config/dock.json", "w") - if not handler then - return - end + handler = io.open(file_path, "w") + + if not handler then return end + handler:write(dock_encoded) handler:close() capi.awesome.emit_signal("dock::changed") @@ -231,7 +257,7 @@ function application_grid:get_applications_from_file() }, { name = "Add to desktop", - icon = gears.color.recolor_image(icondir .. "desktop.svg", + icon = grecolor_image(icondir .. "desktop.svg", Theme_config.application_launcher.application.cm_icon_color), callback = function() capi.awesome.emit_signal("application_launcher::show") @@ -246,23 +272,23 @@ function application_grid:get_applications_from_file() } } + -- Hide context menu when the mouse leaves it context_menu:connect_signal("mouse::leave", function() context_menu.visible = false end) - -- Execute command on left click and hide launcher + -- Execute command on left click and hide launcher, right click to show context menu app_widget:buttons( - gears.table.join( - awful.button({ + gtable.join( + abutton({ modifiers = {}, button = 1, on_release = function() Gio.AppInfo.launch_uris_async(app) - --!Change! capi.awesome.emit_signal("application_launcher::show") end }), - awful.button({ + abutton({ modifiers = {}, button = 3, on_release = function() @@ -271,13 +297,16 @@ function application_grid:get_applications_from_file() }) ) ) + Hover_signal(app_widget) table.insert(list, app_widget) end end - self.app_list = list + return list end +---Takes the search filter and returns a list of applications in the correct order +---@param search_filter any function application_grid:set_applications(search_filter) local filter = search_filter or self.filter or "" -- Reset to first position @@ -292,30 +321,34 @@ function application_grid:set_applications(search_filter) spacing = dpi(10), id = "grid", -- 200 is the application element width + 10 spacing - forced_num_cols = math.floor((capi.mouse.screen.geometry.width / 100 * 60) / (200)), + forced_num_cols = math.floor((capi.mouse.screen.geometry.width / 100 * 60) / 200), forced_num_rows = 7, orientation = "vertical", layout = wibox.layout.grid } - local dir = gfilesystem.get_configuration_dir() .. "src/config/applications.json" - if not gfilesystem.file_readable(dir) then return end - - local handler = io.open(dir, "r") + -- Read the dock.json file and get all apps, these are needed to read/write the launch count + local handler = io.open(gfilesystem.get_configuration_dir() .. "src/config/applications.json", "r") if not handler then return end + local dock_encoded = handler:read("a") or "{}" local dock = json:decode(dock_encoded) - if type(dock) ~= "table" then return end + + assert(type(dock) == "table", "dock is not a table") + local mylist = {} + for _, application in ipairs(self.app_list) do - -- Match the filter + -- Match the filter for the name, categories and keywords if string.match(string.lower(application.name or ""), string.lower(filter)) or string.match(string.lower(application.categories or ""), string.lower(filter)) or string.match(string.lower(application.keywords or ""), string.lower(filter)) then + -- If there are no elements in the table, set everything to 0 if #dock == 0 then application.counter = 0 end + -- Read the counter for the matching app for _, app in ipairs(dock) do if app.desktop_file == application.desktop_file then application.counter = app.counter or 0 @@ -329,14 +362,16 @@ function application_grid:set_applications(search_filter) end end + -- Sort the applications using the levenshtein algorithm table.sort(mylist, function(a, b) return levenshtein_distance(filter, a.name) < levenshtein_distance(filter, b.name) end) - --sort mytable by counter + --Sort the applications using the counter table.sort(mylist, function(a, b) return a.counter > b.counter end) + -- Add the apps one by one into the grid and read its position for _, app in ipairs(mylist) do grid:add(app) @@ -360,6 +395,7 @@ function application_grid:set_applications(search_filter) self:set_widget(grid) end +-- Move the curser up by one, making sure it doesn't go out of bounds function application_grid:move_up() self._private.curser.y = self._private.curser.y - 1 if self._private.curser.y < 1 then @@ -368,6 +404,7 @@ function application_grid:move_up() capi.awesome.emit_signal("update::selected") end +-- Move the curser down by one, making sure it doesn't go out of bounds function application_grid:move_down() self._private.curser.y = self._private.curser.y + 1 local grid_rows, _ = self:get_widget():get_dimension() @@ -377,6 +414,7 @@ function application_grid:move_down() capi.awesome.emit_signal("update::selected") end +-- Move the curser left by one, making sure it doesn't go out of bounds function application_grid:move_left() self._private.curser.x = self._private.curser.x - 1 if self._private.curser.x < 1 then @@ -385,6 +423,7 @@ function application_grid:move_left() capi.awesome.emit_signal("update::selected") end +-- Move the curser right by one, making sure it doesn't go out of bounds function application_grid:move_right() self._private.curser.x = self._private.curser.x + 1 local _, grid_cols = self:get_widget():get_dimension() @@ -394,22 +433,28 @@ function application_grid:move_right() capi.awesome.emit_signal("update::selected") end +--- Execute the currently selected app and add to the launch count function application_grid:execute() + -- Get the app at the current x,y local selected_widget = self:get_widget():get_widgets_at(self._private.curser.y, self._private.curser.x)[1] + -- Launch the application async Gio.AppInfo.launch_uris_async(Gio.AppInfo.create_from_commandline(selected_widget.exec, nil, 0)) - local dir = gfilesystem.get_configuration_dir() .. "src/config/applications.json" - if not gfilesystem.file_readable(dir) then return end - - local handler = io.open(dir, "r") + local file_path = gfilesystem.get_configuration_dir() .. "src/config/applications.json" + local handler = io.open(file_path, "r") if not handler then return end + local dock_encoded = handler:read("a") or "{}" local dock = json:decode(dock_encoded) - if type(dock) ~= "table" then return end + + assert(type(dock) == "table", "dock is not a table") + + -- Increase the counter by one then rewrite to the file, its a bit hacky but it works for _, prog in ipairs(dock) do if prog.desktop_file:match(selected_widget.desktop_file) then prog.counter = prog.counter + 1 + -- I don't like goto's, but its the easiest way here(PR is welcome). goto continue end end @@ -423,12 +468,13 @@ function application_grid:execute() end ::continue:: handler:close() - handler = io.open(dir, "w") + handler = io.open(file_path, "w") if not handler then return end handler:write(json:encode_pretty(dock)) handler:close() end +-- Reset the grid cursor function application_grid:reset() self._private.curser = { x = 1, @@ -444,20 +490,7 @@ function application_grid.new(args) gtable.crush(w, application_grid, true) - w._private.curser = { - x = 1, - y = 1 - } - - -- Create folder and file if it doesn't exist - local dir = gfilesystem.get_configuration_dir() .. "src/config" - gfilesystem.make_directories(dir) - dir = dir .. "/applications.json" - if not gfilesystem.file_readable(dir) then - os.execute("touch " .. dir) - end - - w:get_applications_from_file() + w.app_list = get_applications_from_file() w:set_applications() return w diff --git a/awesome/src/modules/application_launcher/init.lua b/awesome/src/modules/application_launcher/init.lua index 436425a..eb3f0c7 100644 --- a/awesome/src/modules/application_launcher/init.lua +++ b/awesome/src/modules/application_launcher/init.lua @@ -3,47 +3,98 @@ -------------------------------------- -- Awesome Libs -local awful = require("awful") -local dpi = require("beautiful").xresources.apply_dpi -local gears = require("gears") -local wibox = require("wibox") -local gshape = require("gears.shape") -local gtable = require("gears.table") -local gobject = require("gears.object") local abutton = require("awful.button") +local akeygrabber = require("awful.keygrabber") +local aplacement = require("awful.placement") +local apopup = require("awful.popup") +local awidget = require("awful.widget") +local dpi = require("beautiful").xresources.apply_dpi +local gtable = require("gears.table") +local wibox = require("wibox") + +-- Own libs +local app_grid = require("src.modules.application_launcher.application") local capi = { awesome = awesome, mouse = mouse, } +-- This grid object is shared to avoid having multipe unnecessary instances +local application_grid = app_grid {} + local application_launcher = { mt = {} } - -application_launcher.application_grid = require("src.modules.application_launcher.application") {} - - function application_launcher.new(args) args = args or {} - local ret = gobject { enable_properties = true } - - gtable.crush(ret, application_launcher, true) - - local searchbar = awful.widget.inputbox { + -- Create a new inputbox + local searchbar = awidget.inputbox { hint_text = "Search...", valign = "center", halign = "left", } - searchbar:buttons( - gtable.join { - abutton({}, 1, function() - searchbar:focus() - end) - } - ) + -- Application launcher popup + local application_container = apopup { + widget = { + { + { + { + { + { + searchbar, + widget = wibox.container.margin, + margins = 5, + }, + widget = wibox.container.constraint, + strategy = "exact", + height = dpi(50), + }, + widget = wibox.container.background, + bg = Theme_config.application_launcher.searchbar.bg, + fg = Theme_config.application_launcher.searchbar.fg, + border_color = Theme_config.application_launcher.searchbar.border_color, + border_width = Theme_config.application_launcher.searchbar.border_width, + shape = Theme_config.application_launcher.searchbar.shape, + id = "searchbar_bg" + }, + { + application_grid, + layout = require("src.lib.overflow_widget.overflow").vertical, + scrollbar_width = 0, + step = dpi(100), + }, + spacing = dpi(10), + layout = wibox.layout.fixed.vertical + }, + margins = dpi(20), + widget = wibox.container.margin + }, + height = args.screen.geometry.height / 100 * 60, + strategy = "exact", + widget = wibox.container.constraint, + }, + ontop = true, + visible = false, + stretch = false, + screen = args.screen, + placement = aplacement.centered, + bg = Theme_config.application_launcher.bg, + border_color = Theme_config.application_launcher.border_color, + border_width = Theme_config.application_launcher.border_width + } + gtable.crush(application_container, application_launcher, true) + + -- Focus the searchbar when its left clicked + searchbar:buttons(gtable.join { + abutton({}, 1, function() + searchbar:focus() + end) + }) + + --#region Hover signals to change the cursor to a text cursor local old_cursor, old_wibox searchbar:connect_signal("mouse::enter", function() local wid = capi.mouse.current_wibox @@ -56,168 +107,102 @@ function application_launcher.new(args) old_wibox.cursor = old_cursor old_wibox = nil end) + --#endregion - local applicaton_launcher = wibox.widget { - { - { - { - { - { - searchbar, - widget = wibox.container.margin, - margins = 5, - id = "marg" - }, - widget = wibox.container.constraint, - strategy = "exact", - width = 400, - height = 50, - id = "const" - }, - widget = wibox.container.background, - bg = Theme_config.application_launcher.searchbar.bg, - fg = Theme_config.application_launcher.searchbar.fg, - border_color = Theme_config.application_launcher.searchbar.border_color, - border_width = Theme_config.application_launcher.searchbar.border_width, - shape = gshape.rounded_rect, - id = "searchbar_bg" - }, - { - ret.application_grid, - spacing = dpi(10), - layout = require("src.lib.overflow_widget.overflow").vertical, - scrollbar_width = 0, - step = dpi(100), - id = "scroll_bar", - }, - spacing = dpi(10), - layout = wibox.layout.fixed.vertical - }, - margins = dpi(20), - widget = wibox.container.margin - }, - height = args.screen.geometry.height / 100 * 60, - --width = s.geometry.width / 100 * 60, - strategy = "exact", - widget = wibox.container.constraint - } + -- Get a reference to the searchbar background value + local searchbar_bg = application_container.widget:get_children_by_id("searchbar_bg")[1] - ret.application_container = awful.popup { - widget = applicaton_launcher, - ontop = true, - visible = false, - stretch = false, - screen = args.screen, - shape = function(cr, width, height) - gears.shape.rounded_rect(cr, width, height, dpi(12)) - end, - placement = awful.placement.centered, - bg = Theme_config.application_launcher.bg, - border_color = Theme_config.application_launcher.border_color, - border_width = Theme_config.application_launcher.border_width - } - - local searchbar_bg = applicaton_launcher:get_children_by_id("searchbar_bg")[1] - - capi.awesome.connect_signal( - "application_launcher::show", - function() + -- Toggle visible for the application launcher and init the searchbar + capi.awesome.connect_signal("application_launcher::show", function() + if capi.mouse.screen == args.screen then capi.awesome.emit_signal("update::selected") if capi.mouse.screen == args.screen then - ret.application_container.visible = not ret.application_container.visible + application_container.visible = not application_container.visible end - if ret.application_container.visible then + if application_container.visible then searchbar_bg.border_color = Theme_config.application_launcher.searchbar.border_active searchbar:focus() else searchbar:set_text("") - awful.keygrabber.stop() + akeygrabber.stop() end end - ) + end) - searchbar:connect_signal( - "submit", - function(_, text) - ret.application_grid:execute() + -- Execute the currently selected application, reset the searchbar and hide the launcher + searchbar:connect_signal("submit", function(_, text) + application_grid:execute() + capi.awesome.emit_signal("application_launcher::show") + searchbar:set_text("") + application_grid:set_applications(searchbar:get_text()) + searchbar_bg.border_color = Theme_config.application_launcher.searchbar.border_color + end) + + -- Hide the application launcher when the keygrabber stops and reset the searchbar + searchbar:connect_signal("stopped", function(_, stop_key) + if stop_key == "Escape" then capi.awesome.emit_signal("application_launcher::show") - searchbar:set_text("") - ret.application_grid:set_applications(searchbar:get_text()) - searchbar_bg.border_color = Theme_config.application_launcher.searchbar.border_color end - ) + searchbar:set_text("") + application_grid:set_applications(searchbar:get_text()) + searchbar_bg.border_color = Theme_config.application_launcher.searchbar.border_color + end) - searchbar:connect_signal( - "stopped", - function(_, stop_key) - if stop_key == "Escape" then - capi.awesome.emit_signal("application_launcher::show") + -- When started change the background for the searchbar + searchbar:connect_signal("started", function() + searchbar_bg.border_color = Theme_config.application_launcher.searchbar.border_active + end) + + -- On every keypress in the searchbar check for certain inputs + searchbar:connect_signal("inputbox::key_pressed", function(_, modkey, key) + if key == "Escape" then -- Escape to stop the keygrabber, hide the launcher and reset the searchbar + searchbar:stop() + capi.awesome.emit_signal("application_launcher::show") + application_grid:reset() + searchbar:set_text("") + elseif key == "Down" or key == "Right" then --If down or right is pressed initiate the grid navigation + if key == "Down" then + application_grid:move_down() + elseif key == "Right" then + application_grid:move_right() end - searchbar:set_text("") - ret.application_grid:set_applications(searchbar:get_text()) - searchbar_bg.border_color = Theme_config.application_launcher.searchbar.border_color - end - ) - - searchbar:connect_signal( - "started", - function() - searchbar_bg.border_color = Theme_config.application_launcher.searchbar.border_active - end - ) - - searchbar:connect_signal( - "inputbox::key_pressed", - function(_, modkey, key) - if key == "Escape" then - searchbar:stop() - capi.awesome.emit_signal("application_launcher::show") - ret.application_grid:reset() - searchbar:set_text("") - elseif key == "Down" or key == "Right" then - if key == "Down" then - ret.application_grid:move_down() - elseif key == "Right" then - ret.application_grid:move_right() - end - searchbar:stop() - awful.keygrabber.run(function(mod, key2, event) - if event == "press" then - if key2 == "Down" then - ret.application_grid:move_down() - elseif key2 == "Up" then - local old_y = ret.application_grid._private.curser.y - ret.application_grid:move_up() - if old_y - ret.application_grid._private.curser.y == 0 then - searchbar:focus() - end - elseif key2 == "Left" then - ret.application_grid:move_left() - elseif key2 == "Right" then - ret.application_grid:move_right() - elseif key2 == "Return" then - awful.keygrabber.stop() - ret.application_grid:execute() - capi.awesome.emit_signal("application_launcher::show") - ret.application_grid:reset() - searchbar:set_text("") - ret.application_grid:set_applications(searchbar:get_text()) - elseif key2 == "Escape" then - capi.awesome.emit_signal("application_launcher::show") - ret.application_grid:reset() - searchbar:set_text("") - ret.application_grid:set_applications(searchbar:get_text()) - awful.keygrabber.stop() + searchbar:stop() + --New keygrabber to allow for key navigation + akeygrabber.run(function(mod, key2, event) + if event == "press" then + if key2 == "Down" then + application_grid:move_down() + elseif key2 == "Up" then + local old_y = application_grid._private.curser.y + application_grid:move_up() + if old_y - application_grid._private.curser.y == 0 then + searchbar:focus() end + elseif key2 == "Left" then + application_grid:move_left() + elseif key2 == "Right" then + application_grid:move_right() + elseif key2 == "Return" then + akeygrabber.stop() + application_grid:execute() + capi.awesome.emit_signal("application_launcher::show") + application_grid:reset() + searchbar:set_text("") + application_grid:set_applications(searchbar:get_text()) + elseif key2 == "Escape" then + capi.awesome.emit_signal("application_launcher::show") + application_grid:reset() + searchbar:set_text("") + application_grid:set_applications(searchbar:get_text()) + akeygrabber.stop() end - end) - searchbar_bg.border_color = Theme_config.application_launcher.searchbar.border_color - end - ret.application_grid:set_applications(searchbar:get_text()) + end + end) + searchbar_bg.border_color = Theme_config.application_launcher.searchbar.border_color end - ) - - return ret + -- Update the applications in the grid + application_grid:set_applications(searchbar:get_text()) + end) end function application_launcher.mt:__call(...)