476 lines
16 KiB
Lua
476 lines
16 KiB
Lua
--------------------------------------
|
|
-- 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 })
|