rewritten app launcher

This commit is contained in:
Rene Kievits
2023-04-24 22:12:18 +02:00
parent ea77c5f1a1
commit 24bce4c810
3 changed files with 552 additions and 688 deletions

View File

@@ -0,0 +1,552 @@
local abutton = require('awful.button')
local akey = require('awful.key')
local akeygrabber = require('awful.keygrabber')
local aplacement = require('awful.placement')
local apopup = require('awful.popup')
local beautiful = require('beautiful')
local dpi = beautiful.xresources.apply_dpi
local gtable = require('gears.table')
local gtimer = require('gears.timer')
local wibox = require('wibox')
local gobject = require('gears.object')
local Gio = require('lgi').Gio
local gfilesystem = require('gears.filesystem')
local gcolor = require('gears.color')
local fzy = require('fzy')
local hover = require('src.tools.hover')
local inputbox = require('src.modules.inputbox')
local context_menu = require('src.modules.context_menu')
local config = require('src.tools.config')
local icon_lookup = require('src.tools.gio_icon_lookup')
local dock = require('src.modules.crylia_bar.dock')
local icondir = gfilesystem.get_configuration_dir() .. 'src/assets/icons/context_menu/'
local capi = {
awesome = awesome,
mouse = mouse,
}
local launcher = gobject {}
function launcher:fetch_apps()
for _, app in ipairs(Gio.AppInfo.get_all()) do
local app_id = app:get_id()
if app:should_show() and not self.app_table[app_id] then
local GDesktopAppInfo = Gio.DesktopAppInfo.new(app_id)
local app_icon = app:get_icon()
local app_name = app:get_name()
local w = wibox.widget {
{
{
{
{
{ -- Icon
valign = 'center',
halign = 'center',
image = icon_lookup:get_gicon_path(app_icon)
or icon_lookup:get_gicon_path(app_icon, GDesktopAppInfo:get_string('X-AppImage-Old-Icon'))
or '',
resize = true,
widget = wibox.widget.imagebox,
},
height = dpi(64),
width = dpi(64),
strategy = 'exact',
widget = wibox.container.constraint,
},
{
{ -- Name
text = app_name,
halign = 'center',
valign = 'center',
widget = wibox.widget.textbox,
},
strategy = 'exact',
width = dpi(170),
-- Prevents widget from overflowing
height = dpi(40),
widget = wibox.container.constraint,
},
layout = wibox.layout.fixed.vertical,
},
widget = wibox.container.place,
},
margins = dpi(10),
widget = wibox.container.margin,
},
widget = wibox.container.background,
shape = beautiful.shape[4],
fg = beautiful.colorscheme.fg,
bg = beautiful.colorscheme.bg1,
border_color = beautiful.colorscheme.border_color,
border_width = dpi(2),
name = app_name,
keywords = GDesktopAppInfo:get_string('Keywords'),
categories = GDesktopAppInfo:get_categories(),
execute = function()
app:launch_uris_async()
end,
}
hover.bg_hover { widget = w }
local cm = context_menu {
widget_template = wibox.widget {
{
{
{
{
widget = wibox.widget.imagebox,
resize = true,
valign = 'center',
halign = 'center',
id = 'icon_role',
},
widget = wibox.container.constraint,
stragety = 'exact',
width = dpi(24),
height = dpi(24),
id = 'const',
},
{
widget = wibox.widget.textbox,
valign = 'center',
halign = 'left',
id = 'text_role',
},
layout = wibox.layout.fixed.horizontal,
},
widget = wibox.container.margin,
},
fg = beautiful.colorscheme.fg,
widget = wibox.container.background,
},
spacing = dpi(10),
entries = {
{
name = 'Launch',
icon = gcolor.recolor_image(icondir .. 'launch.svg', beautiful.colorscheme.bg_purple),
callback = function()
self:toggle(capi.mouse.screen)
app:launch_uris_async()
end,
},
{
name = 'Add to Desktop',
icon = gcolor.recolor_image(icondir .. 'desktop.svg', beautiful.colorscheme.bg_purple),
callback = function()
--TODO: Replace desktop:add_to_desktop() once rewritten
capi.awesome.emit_signal('desktop::add_to_desktop', {
label = app_name,
icon = icon_lookup:get_gicon_path(app:get_icon()) or '',
exec = GDesktopAppInfo:get_string('Exec'),
desktop_file = GDesktopAppInfo:get_filename() or '',
})
end,
},
{
name = 'Pin to Dock',
icon = gcolor.recolor_image(icondir .. 'pin.svg', beautiful.colorscheme.bg_purple),
callback = function()
dock:get_dock_for_screen(capi.mouse.screen):pin_element {
desktop_file = GDesktopAppInfo:get_filename(),
}
end,
},
},
}
cm:connect_signal('mouse::leave', function()
cm.visible = false
end)
w:buttons(gtable.join {
abutton {
modifiers = {},
button = 1,
on_release = function()
app.launch_uris_async(app)
akeygrabber.stop()
self.searchbar:set_text('')
self:filter_apps('')
self:toggle(capi.mouse.screen)
end,
},
abutton {
modifiers = {},
button = 3,
on_release = function()
cm:toggle()
end,
},
})
self.app_table[app_id] = w
end
end
end
local function levenshtein_distance(str1, str2)
local len1 = string.len(str1)
local len2 = string.len(str2)
local matrix = {}
local cost = 0
if (len1 == 0) then
return len2
elseif (len2 == 0) then
return len1
elseif (str1 == str2) then
return 0
end
for i = 0, len1, 1 do
matrix[i] = {}
matrix[i][0] = i
end
for j = 0, len2, 1 do
matrix[0][j] = j
end
for i = 1, len1, 1 do
for j = 1, len2, 1 do
if str1:byte(i) == str2:byte(j) then
cost = 0
else
cost = 1
end
matrix[i][j] = math.min(
matrix[i - 1][j] + 1,
matrix[i][j - 1] + 1,
matrix[i - 1][j - 1] + cost
)
end
end
return matrix[len1][len2]
end
function launcher:filter_apps(filter)
filter = filter or ''
self.grid:reset()
local app_list = {}
for _, app in pairs(self.app_table) do
if filter == ''
or fzy.has_match(filter, app.name)
or fzy.has_match(filter, app.category or '')
or fzy.has_match(filter, app.keywords or '')
then
table.insert(app_list, app)
end
end
table.sort(app_list, function(a, b)
return a.name < b.name
end)
--sort by lowest levenshtein distance
table.sort(app_list, function(a, b)
return levenshtein_distance(filter, a.name) < levenshtein_distance(filter, b.name)
end)
for _, app in ipairs(app_list) do
self.grid:add(app)
end
end
function launcher:toggle(screen)
if not self.popup.visible then
self.popup.screen = screen
self.grid.forced_num_cols = math.floor((screen.geometry.width / 100 * 60) / 200)
self:focus_searchbar()
self.popup.widget:get_children_by_id('overflow')[1]:set_scroll_factor(0)
self.popup.visible = true
else
self.cursor = {
x = 1, y = 1,
}
self.popup.visible = false
end
end
function launcher:selection_remove(x, y)
self.grid:get_widgets_at(y, x)[1].border_color = beautiful.colorscheme.border_color
end
function launcher:selection_update(x, y)
local w_old = self.grid:get_widgets_at(y, x)[1]
local w_new = self.grid:get_widgets_at(self.cursor.y, self.cursor.x)[1]
w_old.border_color = beautiful.colorscheme.border_color
w_new.border_color = beautiful.colorscheme.bg_teal
end
local up_offset = 0
function launcher:move_down()
local row, _ = self.grid:get_dimension()
if self.cursor.y < row then
if not self.grid:get_widgets_at(self.cursor.y + 1, self.cursor.x) then return end
self.cursor.y = self.cursor.y + 1
self:selection_update(self.cursor.x, self.cursor.y - 1)
up_offset = up_offset - 1
if up_offset < 0 then
up_offset = 0
end
end
if up_offset == 0 then
if math.floor((capi.mouse.screen.geometry.width / 100 * 60) / 200) < self.cursor.y then
local overflow = self.popup.widget:get_children_by_id('overflow')[1]
overflow:set_scroll_factor(overflow:get_scroll_factor() + (1 / 24 * 127 / 100))
end
end
end
function launcher:move_up()
local row, _ = self.grid:get_dimension()
if self.cursor.y > 1 then
self.cursor.y = self.cursor.y - 1
self:selection_update(self.cursor.x, self.cursor.y + 1)
up_offset = up_offset + 1
if up_offset > 5 then
up_offset = 5
end
end
if up_offset == 5 then
local overflow = self.popup.widget:get_children_by_id('overflow')[1]
overflow:set_scroll_factor(overflow:get_scroll_factor() - (1 / 24 * 127 / 100))
end
end
function launcher:move_left()
if self.cursor.x > 1 then
self.cursor.x = self.cursor.x - 1
self:selection_update(self.cursor.x + 1, self.cursor.y)
end
end
function launcher:move_right()
local _, col = self.grid:get_dimension()
if self.cursor.x < col then
if not self.grid:get_widgets_at(self.cursor.y, self.cursor.x + 1) then return end
self.cursor.x = self.cursor.x + 1
self:selection_update(self.cursor.x - 1, self.cursor.y)
end
end
function launcher:focus_searchbar()
self.searchbar:focus()
self.popup.widget:get_children_by_id('searchbar_bg')[1].border_color = beautiful.colorscheme.bg_teal
end
function launcher:unfocus_searchbar()
self.searchbar:unfocus()
self.popup.widget:get_children_by_id('searchbar_bg')[1].border_color = beautiful.colorscheme.border_color
end
local instance = nil
if not instance then
instance = setmetatable(launcher, {
__call = function(self, screen)
self.app_table = {}
self.cursor = {
x = 1,
y = 1,
}
self:fetch_apps()
self.grid = wibox.widget {
homogenous = true,
expand = false,
spacing = dpi(10),
-- 190 is the application element width + 10 spacing
forced_num_cols = math.floor((capi.mouse.screen.geometry.width / 100 * 50) / 190),
orientation = 'vertical',
layout = wibox.layout.grid,
}
self.keygrabber = akeygrabber {
autostart = false,
stop_key = { 'Escape', 'Return' },
mask_event_callback = false,
stop_callback = function(_, k)
if (k == 'Return') or (k == 'Escape') then
if k == 'Return' then
self.grid:get_widgets_at(self.cursor.y, self.cursor.x)[1].execute()
end
self:selection_remove(self.cursor.x, self.cursor.y)
self:toggle(capi.mouse.screen)
self:filter_apps('')
self.searchbar:set_text('')
self.keygrabber:stop()
end
self.cursor = {
x = 1,
y = 1,
}
end,
keybindings = {
akey {
modifiers = {},
key = 'Down',
on_press = function()
self:move_down()
end,
},
akey {
modifiers = {},
key = 'Up',
on_press = function()
local y = self.cursor.y
self:move_up()
-- If it didn't move we want to reenter the searchbar
if y - self.cursor.y == 0 then
self:selection_remove(self.cursor.x, self.cursor.y)
self.keygrabber:stop()
self:focus_searchbar()
end
end,
},
akey {
modifiers = {},
key = 'Left',
on_press = function()
self:move_left()
end,
},
akey {
modifiers = {},
key = 'Right',
on_press = function()
self:move_right()
end,
},
},
}
self.searchbar = inputbox {
text_hint = 'Search...',
mouse_focus = false,
font = beautiful.font .. ' regular 12',
fg = beautiful.colorscheme.fg,
}
self.searchbar:connect_signal('button::press', function()
self:selection_remove(self.cursor.x, self.cursor.y)
akeygrabber.stop()
self:focus_searchbar()
end)
self.searchbar:connect_signal('inputbox::keypressed', function(_, _, key)
if key == 'Escape' then
self:unfocus_searchbar()
self:toggle(capi.mouse.screen)
self.searchbar:set_text('')
self:filter_apps('')
elseif key == 'Return' then
self:unfocus_searchbar()
self:toggle(capi.mouse.screen)
self.grid:get_widgets_at(self.cursor.x, self.cursor.y)[1].execute()
self:filter_apps('')
elseif key == 'Down' then
if not (self.keygrabber.running == akeygrabber.current_instance) then
self:selection_update(1, 1)
self:unfocus_searchbar()
self.keygrabber:start()
end
else
self:filter_apps(self.searchbar:get_text())
end
end)
--#region Hover signals to change the cursor to a text cursor
local old_cursor, old_wibox
self.searchbar:connect_signal('mouse::enter', function()
local wid = capi.mouse.current_wibox
if wid then
old_cursor, old_wibox = wid.cursor, wid
wid.cursor = 'xterm'
end
end)
self.searchbar:connect_signal('mouse::leave', function()
old_wibox.cursor = old_cursor
old_wibox = nil
end)
--#endregion
self:filter_apps('')
self.popup = apopup {
widget = {
{
{
{
{
{
self.searchbar.widget,
halign = 'left',
valign = 'center',
id = 'searchbar',
buttons = { gtable.join(
abutton {
modifiers = {},
button = 1,
on_press = function()
self:focus_searchbar()
end,
}
), },
widget = wibox.container.place,
},
widget = wibox.container.margin,
margins = 5,
},
widget = wibox.container.constraint,
strategy = 'exact',
height = dpi(50),
},
widget = wibox.container.background,
bg = beautiful.colorscheme.bg,
fg = beautiful.colorscheme.fg,
border_color = beautiful.colorscheme.border_color,
border_width = dpi(2),
shape = beautiful.shape[4],
id = 'searchbar_bg',
},
{
{
self.grid,
layout = require('src.lib.overflow_widget.overflow').vertical,
scrollbar_width = 0,
id = 'overflow',
step = dpi(100),
},
height = dpi((122 * 5) + 10 * 4),
strategy = 'exact',
widget = wibox.container.constraint,
},
spacing = dpi(10),
layout = wibox.layout.fixed.vertical,
},
margins = dpi(20),
widget = wibox.container.margin,
},
ontop = true,
visible = true,
stretch = false,
screen = screen,
placement = aplacement.centered,
bg = beautiful.colorscheme.bg,
border_color = beautiful.colorscheme.border_color,
border_width = dpi(2),
}
-- Let the popup render once
gtimer.delayed_call(function()
self.popup.visible = false
end)
end,
})
end
return instance

View File

@@ -1,475 +0,0 @@
--------------------------------------
-- This is the application launcher --
--------------------------------------
-- Awesome libs
local abutton = require('awful.button')
local aspawn = require('awful.spawn')
local base = require('wibox.widget.base')
local beautiful = require('beautiful')
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')
-- Local libs
local config = require('src.tools.config')
local hover = require('src.tools.hover')
local cm = require('src.modules.context_menu.init')
local dock = require('src.modules.crylia_bar.dock')
local icon_lookup = require('src.tools.gio_icon_lookup')()
local capi = {
awesome = awesome,
mouse = mouse,
}
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) }
end
end
function application_grid:fit(context, width, height)
local w, h = 0, 0
if self._private.widget then
w, h = base.fit_widget(self, context, self._private.widget, width, height)
end
return w, h
end
application_grid.set_widget = base.set_widget_common
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)
local matrix = {}
local cost = 0
if (len1 == 0) then
return len2
elseif (len2 == 0) then
return len1
elseif (str1 == str2) then
return 0
end
for i = 0, len1, 1 do
matrix[i] = {}
matrix[i][0] = i
end
for j = 0, len2, 1 do
matrix[0][j] = j
end
for i = 1, len1, 1 do
for j = 1, len2, 1 do
if str1:byte(i) == str2:byte(j) then
cost = 0
else
cost = 1
end
matrix[i][j] = math.min(
matrix[i - 1][j] + 1,
matrix[i][j - 1] + 1,
matrix[i - 1][j - 1] + cost
)
end
end
return matrix[len1][len2]
end
---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 {
{
{
{
{
{ -- Icon
valign = 'center',
halign = 'center',
image = icon_lookup:get_gicon_path(app_info.get_icon(app)) or
icon_lookup: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,
},
height = dpi(64),
width = dpi(64),
strategy = 'exact',
widget = wibox.container.constraint,
},
{
{ -- Name
text = app_info.get_name(app),
align = 'center',
valign = 'center',
widget = wibox.widget.textbox,
},
strategy = 'exact',
width = dpi(170),
-- Prevents widget from overflowing
height = dpi(40),
widget = wibox.container.constraint,
},
layout = wibox.layout.fixed.vertical,
},
halign = 'center',
valign = 'center',
widget = wibox.container.place,
},
margins = dpi(10),
widget = wibox.container.margin,
},
name = app_info.get_name(app),
comment = Gio.DesktopAppInfo.get_string(desktop_app_info, 'Comment') or '',
exec = Gio.DesktopAppInfo.get_string(desktop_app_info, 'Exec'),
keywords = Gio.DesktopAppInfo.get_string(desktop_app_info, 'Keywords') or '',
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 = beautiful.colorscheme.border_color,
border_width = dpi(2),
bg = beautiful.colorscheme.bg1,
fg = beautiful.colorscheme.fg,
shape = beautiful.shape[4],
widget = wibox.container.background,
}
local context_menu = cm {
widget_template = wibox.widget {
{
{
{
{
widget = wibox.widget.imagebox,
resize = true,
valign = 'center',
halign = 'center',
id = 'icon_role',
},
widget = wibox.container.constraint,
stragety = 'exact',
width = dpi(24),
height = dpi(24),
id = 'const',
},
{
widget = wibox.widget.textbox,
valign = 'center',
halign = 'left',
id = 'text_role',
},
layout = wibox.layout.fixed.horizontal,
},
widget = wibox.container.margin,
},
widget = wibox.container.background,
},
spacing = dpi(10),
entries = {
{
name = 'Execute as sudo',
icon = gcolor.recolor_image(icondir .. 'launch.svg',
beautiful.colorscheme.bg_purple),
callback = function()
capi.awesome.emit_signal('application_launcher::show')
aspawn('/home/crylia/.config/awesome/src/scripts/start_as_admin.sh ' .. app_widget.exec)
end,
},
{
name = 'Pin to dock',
icon = gcolor.recolor_image(icondir .. 'pin.svg',
beautiful.colorscheme.bg_purple),
callback = function()
-- Open dock.js and read all its content into a table, add the new app into the table and write it back
--[[ async.read_json(gfilesystem.get_configuration_dir() .. 'src/config/dock.json', function(data)
table.insert(data, {
name = app_widget.name or '',
icon = icon_lookup.get_gicon_path(app_info.get_icon(app)) or
icon_lookup.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 '',
categories = app_widget.categories or '',
terminal = app_widget.terminal or '',
actions = app_widget.actions or '',
desktop_file = Gio.DesktopAppInfo.get_filename(desktop_app_info) or ''
})
async.write_json(gfilesystem.get_configuration_dir() .. 'src/config/dock.json', data, function()
capi.awesome.emit_signal('dock::changed')
end)
end) ]]
dock:get_dock_for_screen(capi.mouse.screen):pin_element {
desktop_file = Gio.DesktopAppInfo.get_filename(desktop_app_info) or '',
}
end,
},
{
name = 'Add to desktop',
icon = gcolor.recolor_image(icondir .. 'desktop.svg',
beautiful.colorscheme.bg_purple),
callback = function()
capi.awesome.emit_signal('application_launcher::show')
capi.awesome.emit_signal('desktop::add_to_desktop', {
label = app_info.get_name(app),
icon = icon_lookup:get_gicon_path(app_info.get_icon(app)) or '',
exec = Gio.DesktopAppInfo.get_string(desktop_app_info, 'Exec'),
desktop_file = Gio.DesktopAppInfo.get_filename(desktop_app_info) or '',
})
end,
},
},
}
-- 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, right click to show context menu
app_widget:buttons(
gtable.join(
abutton {
modifiers = {},
button = 1,
on_release = function()
Gio.AppInfo.launch_uris_async(app)
capi.awesome.emit_signal('application_launcher::show')
end,
},
abutton {
modifiers = {},
button = 3,
on_release = function()
context_menu:toggle()
end,
}
)
)
hover.bg_hover { widget = app_widget }
table.insert(list, app_widget)
end
end
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
self._private.curser = {
x = 1,
y = 1,
}
local grid = wibox.widget {
homogenous = true,
expand = false,
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_rows = 7,
orientation = 'vertical',
layout = wibox.layout.grid,
}
-- Read the dock.json file and get all apps, these are needed to read/write the launch count
local data = config.read_json(gfilesystem.get_configuration_dir() .. 'src/config/applications.json')
local mylist = {}
for _, application in ipairs(self.app_list) do
-- 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 #data == 0 then
application.counter = 0
end
-- Read the counter for the matching app
for _, app in ipairs(data) do
if app.desktop_file == application.desktop_file then
application.counter = app.counter or 0
break;
else
application.counter = 0
end
end
table.insert(mylist, application)
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 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)
-- Get the current position in the grid of the app as a table
local pos = grid:get_widget_position(app)
-- Check if the curser is currently at the same position as the app
capi.awesome.connect_signal(
'update::selected',
function()
if self._private.curser.y == pos.row and self._private.curser.x == pos.col then
app.border_color = beautiful.colorscheme.bg_purple
else
app.border_color = beautiful.colorscheme.bg1
end
end
)
end
capi.awesome.emit_signal('update::selected')
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
self._private.curser.y = 1
end
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()
if self._private.curser.y > grid_rows then
self._private.curser.y = grid_rows
end
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
self._private.curser.x = 1
end
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()
if self._private.curser.x > grid_cols then
self._private.curser.x = grid_cols
end
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 data = config.read_json(gfilesystem.get_configuration_dir() .. 'src/config/applications.json')
-- Increase the counter by one then rewrite to the file, its a bit hacky but it works
for _, prog in ipairs(data) 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
do
local prog = {
name = selected_widget.name,
desktop_file = selected_widget.desktop_file,
counter = 1,
}
table.insert(data, prog)
end
::continue::
config.write_json(gfilesystem.get_configuration_dir() .. 'src/config/applications.json', data)
end
-- Reset the grid cursor
function application_grid:reset()
self._private.curser = {
x = 1,
y = 1,
}
capi.awesome.emit_signal('update::selected')
end
function application_grid.new(args)
args = args or {}
local w = base.make_widget(nil, nil, { enable_properties = true })
gtable.crush(w, application_grid, true)
w.app_list = get_applications_from_file()
w:set_applications()
return w
end
return setmetatable(application_grid, { __call = function(...) return application_grid.new(...) end })

View File

@@ -1,213 +0,0 @@
--------------------------------------
-- This is the application launcher --
--------------------------------------
-- Awesome Libs
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 beautiful = require('beautiful')
local dpi = require('beautiful').xresources.apply_dpi
local gtable = require('gears.table')
local wibox = require('wibox')
local gtimer = require('gears.timer')
-- Own libs
local app_grid = require('src.modules.application_launcher.application')
local input = require('src.modules.inputbox')
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 = {}
function application_launcher.new(args)
args = args or {}
-- Create a new inputbox
local searchbar = input {
text_hint = 'Search...',
mouse_focus = true,
fg = beautiful.colorscheme.fg,
password_mode = true,
}
-- Application launcher popup
local application_container = apopup {
widget = {
{
{
{
{
{
{
searchbar.widget,
halign = 'left',
valign = 'center',
widget = wibox.container.place,
},
widget = wibox.container.margin,
margins = 5,
},
widget = wibox.container.constraint,
strategy = 'exact',
height = dpi(50),
},
widget = wibox.container.background,
bg = beautiful.colorscheme.bg,
fg = beautiful.colorscheme.fg,
border_color = beautiful.colorscheme.border_color,
border_width = dpi(2),
shape = beautiful.shape[4],
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 = beautiful.colorscheme.bg,
border_color = beautiful.colorscheme.border_color,
border_width = dpi(2),
}
-- Delayed call to give the popup some time to evaluate its width
gtimer.delayed_call(function()
if application_container.width then
application_container.widget.width = application_container.width
end
end)
gtable.crush(application_container, application_launcher, true)
--#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
if wid then
old_cursor, old_wibox = wid.cursor, wid
wid.cursor = 'xterm'
end
end)
searchbar:connect_signal('mouse::leave', function()
old_wibox.cursor = old_cursor
old_wibox = nil
end)
--#endregion
-- Get a reference to the searchbar background value
local searchbar_bg = application_container.widget:get_children_by_id('searchbar_bg')[1]
-- 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
application_container.visible = not application_container.visible
end
if application_container.visible then
searchbar_bg.border_color = beautiful.colorscheme.bg_blue
searchbar:focus()
else
searchbar:set_text('')
akeygrabber.stop()
end
end
end)
-- Hide the application launcher when the keygrabber stops and reset the searchbar
searchbar:connect_signal('inputbox::stop', function(_, stop_key)
if stop_key == 'Escape' then
capi.awesome.emit_signal('application_launcher::show')
end
searchbar:set_text('')
application_grid:set_applications(searchbar:get_text())
searchbar_bg.border_color = beautiful.colorscheme.border_color
end)
-- When started change the background for the searchbar
searchbar:connect_signal('inputbox::start', function()
searchbar_bg.border_color = beautiful.colorscheme.bg_blue
end)
-- On every keypress in the searchbar check for certain inputs
searchbar:connect_signal('inputbox::keypressed', function(_, modkey, key)
if key == 'Escape' then -- Escape to stop the keygrabber, hide the launcher and reset the searchbar
searchbar:unfocus()
capi.awesome.emit_signal('application_launcher::show')
application_grid:reset()
searchbar:set_text('')
elseif key == 'Return' then
application_grid:execute()
capi.awesome.emit_signal('application_launcher::show')
searchbar:set_text('')
application_grid:set_applications(searchbar:get_text())
searchbar_bg.border_color = beautiful.colorscheme.border_color
elseif key == 'Down' 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:unfocus()
--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
end)
searchbar_bg.border_color = beautiful.colorscheme.border_color
end
-- Update the applications in the grid
application_grid:set_applications(searchbar:get_text())
end)
end
return setmetatable(application_launcher, { __call = function(_, ...) return application_launcher.new(...) end })