diff --git a/helpers/color.lua b/helpers/color.lua
index 22d6501..4042360 100644
--- a/helpers/color.lua
+++ b/helpers/color.lua
@@ -1,3 +1,12 @@
+local tonumber = tonumber
+local string = string
+local math = math
+local floor = math.floor
+local max = math.max
+local min = math.min
+local abs = math.abs
+local format = string.format
+
local _color = {}
--- Try to guess if a color is dark or light.
@@ -13,6 +22,14 @@ function _color.is_dark(color)
return (numeric_value < 383)
end
+function _color.is_opaque(color)
+ if type(color) == "string" then
+ color = _color.hex_to_rgba(color)
+ end
+
+ return color.a < 0.01
+end
+
--- Lighten a color.
--
-- @string color The color to lighten with hexadecimal HTML format `"#RRGGBB"`.
@@ -49,4 +66,93 @@ function _color.darken(color, amount)
return _color.lighten(color, -amount)
end
+-- Returns a value that is clipped to interval edges if it falls outside the interval
+function _color.clip(num, min_num, max_num)
+ return max(min(num, max_num), min_num)
+end
+
+-- Converts the given hex color to rgba
+function _color.hex_to_rgba(color)
+ color = color:gsub("#", "")
+ return { r = tonumber("0x" .. color:sub(1, 2)),
+ g = tonumber("0x" .. color:sub(3, 4)),
+ b = tonumber("0x" .. color:sub(5, 6)),
+ a = #color == 8 and tonumber("0x" .. color:sub(7, 8)) or 255 }
+end
+
+-- Converts the given rgba color to hex
+function _color.rgba_to_hex(color)
+ local r = _color.clip(color.r or color[1], 0, 255)
+ local g = _color.clip(color.g or color[2], 0, 255)
+ local b = _color.clip(color.b or color[3], 0, 255)
+ local a = _color.clip(color.a or color[4] or 255, 0, 255)
+ return "#" .. format("%02x%02x%02x%02x",
+ floor(r),
+ floor(g),
+ floor(b),
+ floor(a))
+end
+
+-- Converts the given hex color to hsv
+function _color.hex_to_hsv(color)
+ local color = _color.hex2rgb(color)
+ local C_max = max(color.r, color.g, color.b)
+ local C_min = min(color.r, color.g, color.b)
+ local delta = C_max - C_min
+ local H, S, V
+ if delta == 0 then
+ H = 0
+ elseif C_max == color.r then
+ H = 60 * (((color.g - color.b) / delta) % 6)
+ elseif C_max == color.g then
+ H = 60 * (((color.b - color.r) / delta) + 2)
+ elseif C_max == color.b then
+ H = 60 * (((color.r - color.g) / delta) + 4)
+ end
+ if C_max == 0 then
+ S = 0
+ else
+ S = delta / C_max
+ end
+ V = C_max
+
+ return { h = H,
+ s = S * 100,
+ v = V * 100 }
+end
+
+-- Converts the given hsv color to hex
+function _color.hsv_to_hex(H, S, V)
+ S = S / 100
+ V = V / 100
+ if H > 360 then H = 360 end
+ if H < 0 then H = 0 end
+ local C = V * S
+ local X = C * (1 - abs(((H / 60) % 2) - 1))
+ local m = V - C
+ local r_, g_, b_ = 0, 0, 0
+ if H >= 0 and H < 60 then
+ r_, g_, b_ = C, X, 0
+ elseif H >= 60 and H < 120 then
+ r_, g_, b_ = X, C, 0
+ elseif H >= 120 and H < 180 then
+ r_, g_, b_ = 0, C, X
+ elseif H >= 180 and H < 240 then
+ r_, g_, b_ = 0, X, C
+ elseif H >= 240 and H < 300 then
+ r_, g_, b_ = X, 0, C
+ elseif H >= 300 and H < 360 then
+ r_, g_, b_ = C, 0, X
+ end
+ local r, g, b = (r_ + m) * 255, (g_ + m) * 255, (b_ + m) * 255
+ return ("#%02x%02x%02x"):format(floor(r), floor(g), floor(b))
+end
+
+function _color.multiply(color, amount)
+ return { _color.clip(color.r * amount, 0, 255),
+ _color.clip(color.g * amount, 0, 255),
+ _color.clip(color.b * amount, 0, 255),
+ 255 }
+end
+
return _color
diff --git a/helpers/icon_theme.lua b/helpers/icon_theme.lua
index 492d81e..4a1db92 100644
--- a/helpers/icon_theme.lua
+++ b/helpers/icon_theme.lua
@@ -55,6 +55,19 @@ function icon_theme:get_client_icon_path(client)
return icon
end
+function icon_theme:choose_icon(icons_names)
+ local icon_info = Gtk.IconTheme.choose_icon(self.gtk_theme, icons_names, self.icon_size, 0);
+ if icon_info then
+ local icon_path = Gtk.IconInfo.get_filename(icon_info)
+ if icon_path then
+ return icon_path
+ end
+ end
+
+ return ""
+end
+
+
function icon_theme:get_gicon_path(gicon)
if gicon == nil then
return ""
diff --git a/widget/app_launcher.lua b/widget/app_launcher.lua
index 8ef7d92..6f3f335 100644
--- a/widget/app_launcher.lua
+++ b/widget/app_launcher.lua
@@ -5,105 +5,66 @@ local gtable = require("gears.table")
local gtimer = require("gears.timer")
local wibox = require("wibox")
local beautiful = require("beautiful")
-local icon_theme = require(tostring(...):match(".*bling") .. ".helpers.icon_theme")()
+local color = require(tostring(...):match(".*bling") .. ".helpers.color")
local dpi = beautiful.xresources.apply_dpi
-
local string = string
local table = table
local math = math
+local ipairs = ipairs
local pairs = pairs
local root = root
+local capi = { screen = screen, mouse = mouse }
+local path = ...
local app_launcher = { mt = {} }
-local function mark_app(self, index)
- local app = self._private.grid.children[index]
- if app ~= nil then
- app:get_children_by_id("background")[1].bg = self.app_selected_color
- local text_widget = app:get_children_by_id("text")[1]
- if text_widget ~= nil then
- text_widget.markup = "" .. text_widget.text .. ""
- end
- end
-end
+local terminal_commands_lookup =
+{
+ alacritty = "alacritty -e",
+ termite = "termite -e",
+ rxvt = "rxvt -e",
+ terminator = "terminator -e"
+}
-local function unmark_app(self, index)
- local app = self._private.grid.children[index]
- if app ~= nil then
- app:get_children_by_id("background")[1].bg = self.app_normal_color
- local text_widget = app:get_children_by_id("text")[1]
- if text_widget ~= nil then
- text_widget.markup = "" .. text_widget.text .. ""
- end
- end
-end
+local function string_levenshtein(str1, str2)
+ local len1 = string.len(str1)
+ local len2 = string.len(str2)
+ local matrix = {}
+ local cost = 0
-local function create_app_widget(self, name, cmdline, icon, index)
- local icon = self.app_show_icon == true
- and
- {
- widget = wibox.container.place,
- halign = self.app_name_halign,
- {
- widget = wibox.widget.imagebox,
- forced_width = self.app_icon_width,
- forced_height = self.app_icon_height,
- image = icon
- }
- }
- or nil
- local name = self.app_show_name == true
- and
- {
- widget = wibox.container.place,
- halign = self.app_icon_halign,
- {
- widget = wibox.widget.textbox,
- id = "text",
- align = "center",
- font = self.app_name_font,
- markup = name
- }
- }
- or nil
+ -- quick cut-offs to save time
+ if (len1 == 0) then
+ return len2
+ elseif (len2 == 0) then
+ return len1
+ elseif (str1 == str2) then
+ return 0
+ end
- return wibox.widget
- {
- widget = wibox.container.background,
- id = "background",
- forced_width = self.app_width,
- forced_height = self.app_height,
- shape = self.app_shape,
- bg = self.app_normal_color,
- spawn = function() awful.spawn(cmdline) end,
- buttons =
- {
- awful.button({}, 1, function()
- if index == self._private.current_index or not self.select_before_spawn then
- awful.spawn(cmdline)
- self:hide()
- else
- -- Unmark the previous app
- unmark_app(self, self._private.current_index)
+ -- initialise the base matrix values
+ for i = 0, len1, 1 do
+ matrix[i] = {}
+ matrix[i][0] = i
+ end
+ for j = 0, len2, 1 do
+ matrix[0][j] = j
+ end
- self._private.current_index = index
+ -- actual Levenshtein algorithm
+ 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
- -- Mark this app
- mark_app(self, self._private.current_index)
- end
- end),
- },
- {
- widget = wibox.container.place,
- valign = self.app_content_valign,
- {
- layout = wibox.layout.fixed.vertical,
- spacing = self.app_content_spacing,
- icon,
- name
- }
- }
- }
+ matrix[i][j] = math.min(matrix[i-1][j] + 1, matrix[i][j-1] + 1, matrix[i-1][j-1] + cost)
+ end
+ end
+
+ -- return the last value - this is the Levenshtein distance
+ return matrix[len1][len2]
end
local function has_value(tab, val)
@@ -130,26 +91,184 @@ local function case_insensitive_pattern(pattern)
return p
end
+local function select_app(self, x, y)
+ local widgets = self._private.grid:get_widgets_at(x, y)
+ if widgets then
+ self._private.active_widget = widgets[1]
+ if self._private.active_widget ~= nil then
+ self._private.active_widget.selected = true
+ self._private.active_widget:get_children_by_id("background")[1].bg = self.app_selected_color
+ local text_widget = self._private.active_widget:get_children_by_id("text")[1]
+ text_widget.markup = "" .. text_widget.text .. ""
+ end
+ end
+end
+
+local function unselect_app(self)
+ if self._private.active_widget ~= nil then
+ self._private.active_widget.selected = false
+ self._private.active_widget:get_children_by_id("background")[1].bg = self.app_normal_color
+ local text_widget = self._private.active_widget:get_children_by_id("text")[1]
+ text_widget.markup = "" .. text_widget.text .. ""
+ self._private.active_widget = nil
+ end
+end
+
+local function create_app_widget(self, entry)
+ local icon = self.app_show_icon == true
+ and
+ {
+ widget = wibox.container.place,
+ halign = self.app_name_halign,
+ {
+ widget = wibox.widget.imagebox,
+ forced_width = self.app_icon_width,
+ forced_height = self.app_icon_height,
+ image = entry.icon
+ }
+ }
+ or nil
+ local name = self.app_show_name == true
+ and
+ {
+ widget = wibox.container.place,
+ halign = self.app_icon_halign,
+ {
+ widget = wibox.widget.textbox,
+ id = "text",
+ align = "center",
+ font = self.app_name_font,
+ markup = entry.name
+ }
+ }
+ or nil
+
+ local app = wibox.widget
+ {
+ widget = wibox.container.background,
+ id = "background",
+ forced_width = self.app_width,
+ forced_height = self.app_height,
+ shape = self.app_shape,
+ bg = self.app_normal_color,
+ {
+ widget = wibox.container.place,
+ valign = self.app_content_valign,
+ {
+ layout = wibox.layout.fixed.vertical,
+ spacing = self.app_content_spacing,
+ icon,
+ name
+ }
+ }
+ }
+
+ function app.spawn()
+ if entry.terminal == true then
+ if self.terminal ~= nil then
+ local terminal_command = terminal_commands_lookup[self.terminal] or self.terminal
+ awful.spawn(terminal_command .. " " .. entry.executable)
+ else
+ awful.spawn.easy_async("gtk-launch " .. entry.executable, function(stdout, stderr)
+ if stderr then
+ awful.spawn(entry.executable)
+ end
+ end)
+ end
+ else
+ awful.spawn(entry.executable)
+ end
+
+ self:hide()
+ end
+
+ app:connect_signal("mouse::enter", function(_self)
+ local widget = capi.mouse.current_wibox
+ if widget then
+ widget.cursor = "hand2"
+ end
+
+ local app = _self
+ if app.selected then
+ app:get_children_by_id("background")[1].bg = self.app_selected_hover_color
+ else
+ local is_opaque = color.is_opaque(self.app_normal_color)
+ local is_dark = color.is_dark(self.app_normal_color)
+ local app_normal_color = color.hex_to_rgba(self.app_normal_color)
+ local hover_color = (is_dark or is_opaque) and
+ color.rgba_to_hex(color.multiply(app_normal_color, 2.5)) or
+ color.rgba_to_hex(color.multiply(app_normal_color, 0.5))
+ app:get_children_by_id("background")[1].bg = self.app_normal_hover_color
+ end
+ end)
+
+ app:connect_signal("mouse::leave", function(_self)
+ local widget = capi.mouse.current_wibox
+ if widget then
+ widget.cursor = "left_ptr"
+ end
+
+ local app = _self
+ if app.selected then
+ app:get_children_by_id("background")[1].bg = self.app_selected_color
+ else
+ app:get_children_by_id("background")[1].bg = self.app_normal_color
+ end
+ end)
+
+ app:connect_signal("button::press", function(_self, lx, ly, button, mods, find_widgets_result)
+ if button == 1 then
+ local app = _self
+ if self._private.active_widget == app or not self.select_before_spawn then
+ app.spawn()
+ else
+ -- Unmark the previous app
+ unselect_app(self)
+
+ -- Mark this app
+ local pos = self._private.grid:get_widget_position(app)
+ select_app(self, pos.row, pos.col)
+ end
+ end
+ end)
+
+ return app
+end
+
local function search(self, text)
+ unselect_app(self)
+
+ local pos = self._private.grid:get_widget_position(self._private.active_widget)
+
-- Reset all the matched entries
self._private.matched_entries = {}
-- Remove all the grid widgets
self._private.grid:reset()
- for index, entry in pairs(self._private.all_entries) do
- text = text:gsub( "%W", "" )
+ if text == "" then
+ self._private.matched_entries = self._private.all_entries
+ else
+ for index, entry in pairs(self._private.all_entries) do
+ text = text:gsub( "%W", "" )
- -- Check if there's a match by the app name or app command
- if string.find(entry.name, case_insensitive_pattern(text)) ~= nil or
- self.search_commands and string.find(entry.cmdline, case_insensitive_pattern(text)) ~= nil
- then
- table.insert(self._private.matched_entries, { name = entry.name, cmdline = entry.cmdline, icon = entry.icon })
-
- -- Only add the widgets for apps that are part of the first page
- if #self._private.grid.children + 1 <= self._private.max_apps_per_page then
- self._private.grid:add(create_app_widget(self, entry.name, entry.cmdline, entry.icon, #self._private.grid.children + 1))
+ -- Check if there's a match by the app name or app command
+ if string.find(entry.name, case_insensitive_pattern(text)) ~= nil or
+ self.search_commands and string.find(entry.commandline, case_insensitive_pattern(text)) ~= nil
+ then
+ table.insert(self._private.matched_entries, { name = entry.name, commandline = entry.commandline, executable = entry.executable, terminal = entry.terminal, icon = entry.icon })
end
end
+
+ -- Sort by string similarity
+ table.sort(self._private.matched_entries, function(a, b)
+ return string_levenshtein(text, a.name) < string_levenshtein(text, b.name)
+ end)
+ end
+ for index, entry in pairs(self._private.matched_entries) do
+ -- Only add the widgets for apps that are part of the first page
+ if #self._private.grid.children + 1 <= self._private.max_apps_per_page then
+ self._private.grid:add(create_app_widget(self, entry))
+ end
end
-- Recalculate the apps per page based on the current matched entries
@@ -158,190 +277,315 @@ local function search(self, text)
-- Recalculate the pages count based on the current apps per page
self._private.pages_count = math.ceil(math.max(1, #self._private.matched_entries) / math.max(1, self._private.apps_per_page))
+ -- Page should be 1 after a search
+ self._private.current_page = 1
+
-- This is an option to mimic rofi behaviour where after a search
-- it will reselect the app whose index is the same as the app index that was previously selected
-- and if matched_entries.length < current_index it will instead select the app with the greatest index
if self.try_to_keep_index_after_searching then
- self._private.current_index = math.max(math.min(self._private.current_index, #self._private.matched_entries), 1)
-
+ if self._private.grid:get_widgets_at(pos.row, pos.col) == nil then
+ local app = self._private.grid.children[#self._private.grid.children]
+ pos = self._private.grid:get_widget_position(app)
+ end
+ select_app(self, pos.row, pos.col)
-- Otherwise select the first app on the list
else
- self._private.current_index = 1
+ select_app(self, 1, 1)
end
- self._private.current_page = 1
-
- mark_app(self, self._private.current_index)
end
local function scroll_up(self)
+ if #self._private.grid.children < 1 then
+ self._private.active_widget = nil
+ return
+ end
+
+ local rows, columns = self._private.grid:get_dimension()
+ local pos = self._private.grid:get_widget_position(self._private.active_widget)
+ local is_bigger_than_first_app = pos.col > 1 or pos.row > 1
+
-- Check if the current marked app is not the first
- if self._private.current_index > 1 then
- unmark_app(self, self._private.current_index)
-
- -- Current index should be decremented
- self._private.current_index = self._private.current_index - 1
-
- -- Mark the new app
- mark_app(self, self._private.current_index)
-
+ if is_bigger_than_first_app then
+ unselect_app(self)
+ if pos.row == 1 then
+ select_app(self, rows, pos.col - 1)
+ else
+ select_app(self, pos.row - 1, pos.col)
+ end
-- Check if the current page is not the first
elseif self._private.current_page > 1 then
- -- Remove the current page apps from the grid
- self._private.grid:reset()
+ -- Remove the current page apps from the grid
+ self._private.grid:reset()
- local max_app_index_to_include = (self._private.current_page - 1) * self._private.apps_per_page
- local min_app_index_to_include = max_app_index_to_include - self._private.apps_per_page
+ local max_app_index_to_include = (self._private.current_page - 1) * self._private.apps_per_page
+ local min_app_index_to_include = max_app_index_to_include - self._private.apps_per_page
+ for index, entry in pairs(self._private.matched_entries) do
+ -- Only add widgets that are between this range (part of the current page)
+ if index > min_app_index_to_include and index <= max_app_index_to_include then
+ self._private.grid:add(create_app_widget(self, entry))
+ end
+ end
- for index, entry in pairs(self._private.matched_entries) do
- -- Only add widgets that are between this range (part of the current page)
- if index > min_app_index_to_include and index <= max_app_index_to_include then
- self._private.grid:add(create_app_widget(self, entry.name, entry.cmdline, entry.icon, #self._private.grid.children + 1))
- end
- end
+ -- If we scrolled up a page, selected app should be the last one
+ rows, columns = self._private.grid:get_dimension()
+ select_app(self, rows, columns)
- -- If we scrolled up a page, selected app should be the last one
- self._private.current_index = self._private.apps_per_page
- mark_app(self, self._private.current_index)
-
- -- Current page should be decremented
- self._private.current_page = self._private.current_page - 1
+ -- Current page should be decremented
+ self._private.current_page = self._private.current_page - 1
end
end
local function scroll_down(self)
- local is_less_than_max_app = self._private.current_index < #self._private.grid.children
+ if #self._private.grid.children < 1 then
+ self._private.active_widget = nil
+ return
+ end
+
+ local rows, columns = self._private.grid:get_dimension()
+ local pos = self._private.grid:get_widget_position(self._private.active_widget)
+ local is_less_than_max_app = self._private.grid:index(self._private.active_widget) < #self._private.grid.children
local is_less_than_max_page = self._private.current_page < self._private.pages_count
-- Check if we can scroll down the app list
if is_less_than_max_app then
-- Unmark the previous app
- unmark_app(self, self._private.current_index)
-
- -- Current index should be incremented
- self._private.current_index = self._private.current_index + 1
-
- -- Mark the new app
- mark_app(self, self._private.current_index)
-
+ unselect_app(self)
+ if pos.row == rows then
+ select_app(self, 1, pos.col + 1)
+ else
+ select_app(self, pos.row + 1, pos.col)
+ end
-- If we can't scroll down the app list, check if we can scroll down a page
elseif is_less_than_max_page then
-- Remove the current page apps from the grid
self._private.grid:reset()
- local min_app_index_to_include = self._private.current_index * self._private.current_page
+ local min_app_index_to_include = self._private.apps_per_page * self._private.current_page
local max_app_index_to_include = min_app_index_to_include + self._private.apps_per_page
for index, entry in pairs(self._private.matched_entries) do
-- Only add widgets that are between this range (part of the current page)
if index > min_app_index_to_include and index <= max_app_index_to_include then
- self._private.grid:add(create_app_widget(self, entry.name, entry.cmdline, entry.icon, #self._private.grid.children + 1))
+ self._private.grid:add(create_app_widget(self, entry))
end
end
- -- Current app is 1 if we scroll to the next page
- self._private.current_index = 1
- mark_app(self, self._private.current_index)
+ -- Select app 1 when scrolling to the next page
+ select_app(self, 1, 1)
-- Current page should be incremented
self._private.current_page = self._private.current_page + 1
end
end
---- Shows the app launcher
-function app_launcher:show(args)
- local args = args or {}
+local function scroll_left(self)
+ if #self._private.grid.children < 1 then
+ self._private.active_widget = nil
+ return
+ end
- self.screen = args.screen or self.screen
- self.screen.app_launcher = self._private.widget
- self.screen.app_launcher.screen = self.screen
- self.screen.app_launcher.visible = true
+ local pos = self._private.grid:get_widget_position(self._private.active_widget)
+ local is_bigger_than_first_column = pos.col > 1
+ local is_not_first_page = self._private.current_page > 1
+
+ -- Check if the current marked app is not the first
+ if is_bigger_than_first_column then
+ unselect_app(self)
+ select_app(self, pos.row, pos.col - 1)
+ -- Check if the current page is not the first
+ elseif is_not_first_page then
+ -- Remove the current page apps from the grid
+ self._private.grid:reset()
+
+ local max_app_index_to_include = (self._private.current_page - 1) * self._private.apps_per_page
+ local min_app_index_to_include = max_app_index_to_include - self._private.apps_per_page
+
+ for index, entry in pairs(self._private.matched_entries) do
+ -- Only add widgets that are between this range (part of the current page)
+ if index > min_app_index_to_include and index <= max_app_index_to_include then
+ self._private.grid:add(create_app_widget(self, entry))
+ end
+ end
+
+ -- Keep the same row from last page
+ local rows, columns = self._private.grid:get_dimension()
+ select_app(self, pos.row, columns)
+
+ -- Current page should be decremented
+ self._private.current_page = self._private.current_page - 1
+ end
+end
+
+local function scroll_right(self)
+ if #self._private.grid.children < 1 then
+ self._private.active_widget = nil
+ return
+ end
+
+ local rows, columns = self._private.grid:get_dimension()
+ local pos = self._private.grid:get_widget_position(self._private.active_widget)
+ local is_less_than_max_column = pos.col < columns
+ local is_less_than_max_page = self._private.current_page < self._private.pages_count
+
+ -- Check if we can scroll down the app list
+ if is_less_than_max_column then
+ -- Unmark the previous app
+ unselect_app(self)
+
+ -- Scroll up to the max app if there are directly to the right of previous app
+ if self._private.grid:get_widgets_at(pos.row, pos.col + 1) == nil then
+ local app = self._private.grid.children[#self._private.grid.children]
+ pos = self._private.grid:get_widget_position(app)
+ select_app(self, pos.row, pos.col)
+ else
+ select_app(self, pos.row, pos.col + 1)
+ end
+
+ -- If we can't scroll down the app list, check if we can scroll down a page
+ elseif is_less_than_max_page then
+ -- Remove the current page apps from the grid
+ self._private.grid:reset()
+
+ local min_app_index_to_include = self._private.apps_per_page * self._private.current_page
+ local max_app_index_to_include = min_app_index_to_include + self._private.apps_per_page
+
+ for index, entry in pairs(self._private.matched_entries) do
+ -- Only add widgets that are between this range (part of the current page)
+ if index > min_app_index_to_include and index <= max_app_index_to_include then
+ self._private.grid:add(create_app_widget(self, entry))
+ end
+ end
+
+ -- Keep the last row
+ select_app(self, math.min(pos.row, #self._private.grid.children), 1)
+
+ -- Current page should be incremented
+ self._private.current_page = self._private.current_page + 1
+ end
+end
+
+local function init(self)
+ self._private.grid:reset()
+ self._private.matched_entries = self._private.all_entries
+ self._private.apps_per_page = self._private.max_apps_per_page
+ self._private.pages_count = math.ceil(#self._private.all_entries / self._private.apps_per_page)
+ self._private.current_page = 1
+
+ for index, entry in pairs(self._private.all_entries) do
+ -- Only add the apps that are part of the first page
+ if index <= self._private.apps_per_page then
+ self._private.grid:add(create_app_widget(self, entry))
+ else
+ break
+ end
+ end
+
+ select_app(self, 1, 1)
+end
+
+--- Shows the app launcher
+function app_launcher:show()
+ local screen = self.screen
+ if self.show_on_focused_screen then
+ screen = awful.screen.focused()
+ end
+
+ screen.app_launcher = self._private.widget
+ screen.app_launcher.screen = screen
+ screen.app_launcher.visible = true
self._private.prompt:run()
- local x = args.x or self.x or nil
- if self.rubato and self.rubato.x and x then
- self.rubato.x:set(x)
- elseif x then
- self.screen.app_launcher.x = x
- end
-
- local y = args.y or self.y or nil
- if self.rubato and self.rubato.y and y then
- self.rubato.y:set(y)
- elseif y then
- self.screen.app_launcher.y = y
- end
-
- local placement = args.placement or self.placement or nil
+ local placement = self.placement
if placement then
- self.screen.app_launcher.placement = placement
+ local pos = placement(self.screen.app_launcher, {pretend = true})
+ local animation = self.rubato
+ if animation ~= nil then
+ if animation.x then
+ animation.x.ended:unsubscribe()
+ animation.x:set(pos.x)
+ else
+ self._private.widget.x = pos.x
+ end
+ if animation.y then
+ animation.y.ended:unsubscribe()
+ animation.y:set(pos.y)
+ else
+ self._private.widget.y = pos.y
+ end
+ else
+ self._private.widget.x = pos.x
+ self._private.widget.y = pos.y
+ end
end
self:emit_signal("bling::app_launcher::visibility", true)
end
--- Hides the app launcher
-function app_launcher:hide(args)
- local args = args or {}
+function app_launcher:hide()
+ local screen = self.screen
+ if self.show_on_focused_screen then
+ screen = awful.screen.focused()
+ end
+
+ if screen.app_launcher == nil or screen.app_launcher.visible == false then
+ return
+ end
-- There's no other way to stop the prompt?
- root.fake_input('key_press', "Escape")
- root.fake_input('key_release', "Escape")
+ root.fake_input("key_press", "Escape")
+ root.fake_input("key_release", "Escape")
- if self.rubato and self.rubato.x then
- self.rubato.x:set(self.rubato.x:initial())
- self.rubato.x.ended:subscribe(function()
- self.screen.app_launcher.visible = false
- end)
- end
-
- if self.rubato and self.rubato.y then
- self.rubato.y:set(self.rubato.y:initial())
- self.rubato.y.ended:subscribe(function()
- self.screen.app_launcher.visible = false
- end)
- end
-
- if not self.rubato then
- self.screen.app_launcher.visible = false
- end
-
- self.screen = args.screen or self.screen
- self.screen.app_launcher = {}
-
- -- Reset back to initial values
- self._private.apps_per_page = self._private.max_apps_per_page
- self._private.pages_count = math.ceil(#self._private.all_entries / self._private.apps_per_page)
- self._private.matched_entries = self._private.all_entries
- self._private.current_index = 1
- self._private.current_page = 1
- self._private.grid:reset()
-
- -- Add the app widgets for the next time
- for index, entry in pairs(self._private.all_entries) do
- -- Only add the apps that are part of the first page
- if index <= self._private.apps_per_page then
- self._private.grid:add(create_app_widget(self, entry.name, entry.cmdline, entry.icon, index))
- else
- break
+ local animation = self.rubato
+ if animation ~= nil then
+ if animation.x then
+ animation.x:set(animation.x:initial())
+ end
+ if animation.y then
+ animation.y:set(animation.y:initial())
end
- end
- -- Select the first app for the next time
- mark_app(self, self._private.current_index)
+ local anim_x_duration = (animation.x and animation.x.duration) or 0
+ local anim_y_duration = (animation.y and animation.y.duration) or 0
+ local turn_off_on_anim_x_end = (anim_x_duration >= anim_y_duration) and true or false
+
+ if turn_off_on_anim_x_end then
+ animation.x.ended:subscribe(function()
+ init(self)
+ screen.app_launcher.visible = false
+ screen.app_launcher = nil
+ animation.x.ended:unsubscribe()
+ end)
+ else
+ animation.y.ended:subscribe(function()
+ init(self)
+ screen.app_launcher.visible = false
+ screen.app_launcher = nil
+ animation.y.ended:unsubscribe()
+ end)
+ end
+ else
+ init(self)
+ screen.app_launcher.visible = false
+ screen.app_launcher = nil
+ end
self:emit_signal("bling::app_launcher::visibility", false)
end
--- Toggles the app launcher
-function app_launcher:toggle(args)
- local args = args or {}
+function app_launcher:toggle()
+ local screen = self.screen
+ if self.show_on_focused_screen then
+ screen = awful.screen.focused()
+ end
- self.screen = args.screen or self.screen
- if self.screen.app_launcher and self.screen.app_launcher.visible then
- self:hide(self.screen)
+ if screen.app_launcher and screen.app_launcher.visible then
+ self:hide()
else
- self:show(self.screen)
+ self:show()
end
end
@@ -349,24 +593,28 @@ end
local function new(args)
args = args or {}
+ args.terminal = args.terminal or nil
args.search_commands = args.search_commands or true
args.skip_names = args.skip_names or {}
args.skip_commands = args.skip_commands or {}
args.skip_empty_icons = args.skip_empty_icons or false
args.sort_alphabetically = args.sort_alphabetically or true
args.select_before_spawn = args.select_before_spawn or true
+ args.hide_on_clicked_outside = args.hide_on_clicked_outside or true
args.try_to_keep_index_after_searching = args.try_to_keep_index_after_searching or false
+
args.default_app_icon_name = args.default_app_icon_name or nil
args.default_app_icon_path = args.default_app_icon_path or nil
+ args.icon_theme = args.icon_theme or nil
+ args.icons_size = args.icons_size or nil
+ args.show_on_focused_screen = args.show_on_focused_screen or true
+ args.screen = args.screen or capi.screen.primary
+ args.placement = args.placement or awful.placement.centered
args.rubato = args.rubato or nil
args.shirnk_width = args.shirnk_width or false
args.shrink_height = args.shrink_height or false
args.background = args.background or "#000000"
- args.screen = args.screen or screen.primary
- args.x = args.x or nil
- args.y = args.y or nil
- args.placement = args.placement or (args.x == nil and args.y == nil) and awful.placement.centered or nil
args.shape = args.shape or nil
args.prompt_height = args.prompt_height or dpi(100)
@@ -400,7 +648,13 @@ local function new(args)
args.app_height = args.app_height or dpi(100)
args.app_shape = args.app_shape or nil
args.app_normal_color = args.app_normal_color or beautiful.bg_normal or "#000000"
+ args.app_normal_hover_color = args.app_normal_hover_color or (color.is_dark(args.app_normal_color) or color.is_opaque(args.app_normal_color)) and
+ color.rgba_to_hex(color.multiply(color.hex_to_rgba(args.app_normal_color), 2.5)) or
+ color.rgba_to_hex(color.multiply(color.hex_to_rgba(args.app_normal_color), 0.5))
args.app_selected_color = args.app_selected_color or beautiful.fg_normal or "#FFFFFF"
+ args.app_selected_hover_color = args.app_selected_hover_color or (color.is_dark(args.app_normal_color) or color.is_opaque(args.app_normal_color)) and
+ color.rgba_to_hex(color.multiply(color.hex_to_rgba(args.app_selected_color), 2.5)) or
+ color.rgba_to_hex(color.multiply(color.hex_to_rgba(args.app_selected_color), 0.5))
args.app_content_valign = args.app_content_valign or "center"
args.app_content_spacing = args.app_content_spacing or dpi(10)
args.app_show_icon = args.app_show_icon == nil and true or args.app_show_icon
@@ -415,12 +669,13 @@ local function new(args)
local ret = gobject({})
ret._private = {}
+ ret._private.text = ""
gtable.crush(ret, app_launcher)
gtable.crush(ret, args)
-- Determines the grid width
- local grid_width = ret.shirnk_width == false
+ local grid_width = ret.shirnk_width == false
and dpi((ret.app_width * ret.apps_per_column) + ((ret.apps_per_column - 1) * ret.apps_spacing))
or nil
local grid_height = ret.shrink_height == false
@@ -436,7 +691,19 @@ local function new(args)
bg = ret.prompt_color,
fg = ret.prompt_text_color,
bg_cursor = ret.prompt_cursor_color,
+ hooks =
+ {
+ -- Disable historyu scrolling with arrow keys
+ -- TODO: implement this as other keybind? tab?
+ {{}, "Up", function(command) return true, false end},
+ {{}, "Down", function(command) return true, false end},
+ {{}, "Return", function(command) return true, false end},
+ },
changed_callback = function(text)
+ if text == ret._private.text then
+ return
+ end
+
if ret._private.search_timer ~= nil and ret._private.search_timer.started then
ret._private.search_timer:stop()
end
@@ -449,17 +716,30 @@ local function new(args)
search(ret, text)
end
}
+
+ ret._private.text = text
end,
keypressed_callback = function(mod, key, cmd)
+ if key == "Escape" then
+ ret:hide()
+ end
if key == "Return" then
- if ret._private.grid.children[ret._private.current_index] ~= nil then
- ret._private.grid.children[ret._private.current_index].spawn()
+ if ret._private.active_widget ~= nil then
+ ret._private.active_widget.spawn()
end
end
- print(key)
- end,
- done_callback = function()
- ret:hide()
+ if key == "Up" then
+ scroll_up(ret)
+ end
+ if key == "Down" then
+ scroll_down(ret)
+ end
+ if key == "Left" then
+ scroll_left(ret)
+ end
+ if key == "Right" then
+ scroll_right(ret)
+ end
end
}
ret._private.grid = wibox.widget
@@ -530,55 +810,50 @@ local function new(args)
-- Private variables to be used to be used by the scrolling and searching functions
ret._private.all_entries = {}
ret._private.matched_entries = {}
- ret._private.apps_per_page = ret.apps_per_column * ret.apps_per_row
- ret._private.max_apps_per_page = ret._private.apps_per_page
+ ret._private.max_apps_per_page = ret.apps_per_column * ret.apps_per_row
+ ret._private.apps_per_page = ret._private.max_apps_per_page
ret._private.pages_count = 0
- ret._private.current_index = 1
ret._private.current_page = 1
local app_info = Gio.AppInfo
local apps = app_info.get_all()
if ret.sort_alphabetically then
- table.sort(apps, function(a, b) return app_info.get_name(a):lower() < app_info.get_name(b):lower() end)
- end
+ table.sort(apps, function(a, b) return app_info.get_name(a):lower() < app_info.get_name(b):lower() end)
+ end
+
+ local icon_theme = require(tostring(path):match(".*bling") .. ".helpers.icon_theme")(ret.icon_theme, ret.icon_size)
for _, app in ipairs(apps) do
if app.should_show(app) then
- -- Check if this app should be skipped, depanding on the skip_names / skip_commands table
local name = app_info.get_name(app)
local commandline = app_info.get_commandline(app)
+ local executable = app_info.get_executable(app)
local icon = icon_theme:get_gicon_path(app_info.get_icon(app))
+ -- Check if this app should be skipped, depanding on the skip_names / skip_commands table
if not has_value(ret.skip_names, name) and not has_value(ret.skip_commands, commandline) then
-- Check if this app should be skipped becuase it's iconless depanding on skip_empty_icons
- if icon ~= "" or ret.skip_empty_icons == false then
- if icon == "" then
- if ret.default_app_icon_name ~= nil then
- icon = icon_theme:get_icon_path("app")
- elseif ret.default_app_icon_path ~= nil then
- icon = ret.default_app_icon_path
- end
- end
-
- -- Insert a table containing the name, command and icon of the app into the all_entries table
- table.insert(ret._private.all_entries, { name = name, cmdline = commandline, icon = icon })
-
- -- Only add the app widgets that are part of the first page
- if #ret._private.all_entries <= ret._private.apps_per_page then
- ret._private.grid:add(create_app_widget(ret, name, commandline, icon, #ret._private.all_entries))
+ if icon ~= "" or ret.skip_empty_icons == false then
+ if icon == "" then
+ if ret.default_app_icon_name ~= nil then
+ icon = icon_theme:get_icon_path(ret.default_app_icon_name)
+ elseif ret.default_app_icon_path ~= nil then
+ icon = ret.default_app_icon_path
+ else
+ icon = icon_theme:choose_icon({ "application-all", "application", "application-default-icon", "app" })
end
end
+
+ local desktop_app_info = Gio.DesktopAppInfo.new(app_info.get_id(app))
+ local terminal = Gio.DesktopAppInfo.get_string(desktop_app_info, "Terminal") == "true" and true or false
+ table.insert(ret._private.all_entries, { name = name, commandline = commandline, executable = executable, terminal = terminal, icon = icon })
+ end
end
-
- -- Matched entries contains all the apps initially
- ret._private.matched_entries = ret._private.all_entries
- ret._private.pages_count = math.ceil(#ret._private.all_entries / ret._private.apps_per_page)
-
- -- Mark the first app on startup
- mark_app(ret, 1)
end
end
+ init(ret)
+
if ret.rubato and ret.rubato.x then
ret.rubato.x:subscribe(function(pos)
ret._private.widget.x = pos
@@ -590,6 +865,32 @@ local function new(args)
end)
end
+ if ret.hide_on_clicked_outside then
+ awful.mouse.append_client_mousebinding(
+ awful.button({ }, 1, function (c)
+ ret:hide()
+ end)
+ )
+
+ awful.mouse.append_global_mousebinding(
+ awful.button({ }, 1, function (c)
+ ret:hide()
+ end)
+ )
+
+ awful.mouse.append_client_mousebinding(
+ awful.button({ }, 3, function (c)
+ ret:hide()
+ end)
+ )
+
+ awful.mouse.append_global_mousebinding(
+ awful.button({ }, 3, function (c)
+ ret:hide()
+ end)
+ )
+ end
+
return ret
end