diff --git a/docs/widgets/app_launcher.md b/docs/widgets/app_launcher.md
index b0c1ba40..446e691c 100644
--- a/docs/widgets/app_launcher.md
+++ b/docs/widgets/app_launcher.md
@@ -35,7 +35,6 @@ local app_launcher = bling.widget.app_launcher(args)
```lua
local args = {
- terminal = "alacritty" -- Set default terminal
favorites = { "firefox", "wezterm" } -- Favorites are given priority and are bubbled to top of the list
search_commands = true -- Search by app name AND commandline command
skip_names = { "Discord" } -- List of apps to omit from launcher
@@ -59,71 +58,35 @@ local args = {
icon_size = 24 -- Set icon size
type = "dock" -- awful.popup type ("dock", "desktop", "normal"...). See awesomewm docs for more detail
- show_on_focused_screen = true -- Should app launcher show on currently focused screen
- screen = awful.screen -- Screen you want the launcher to launch to
- placement = awful.placement.top_left -- Where launcher should be placed ("awful.placement.centered").
- rubato = { x = rubato_animation_x, y = rubato_animation_y } -- Rubato animation to apply to launcher
- shrink_width = true -- Automatically shrink width of launcher to fit varying numbers of apps in list (works on apps_per_column)
- shrink_height = true -- Automatically shrink height of launcher to fit varying numbers of apps in list (works on apps_per_row)
- background = "#FFFFFF" -- Set bg color
- border_width = dpi(0) -- Set border width of popup
- border_color = "#FFFFFF" -- Set border color of popup
- shape = function(cr, width, height)
+ show_on_focused_screen = true -- Should the app launcher popup show on currently focused screen
+ screen = awful.screen -- Screen you want the launcher popup to open on
+ placement = awful.placement.top_left -- Where launcher popup should be placed
+ bg = "#FFFFFF" -- Set launcher popup bg color
+ border_width = dpi(0) -- Set launcher popup border width of popup
+ border_color = "#FFFFFF" -- Set launcher popup border color of popup
+ shape = function(cr, width, height) -- Set launcher popup shape
gears.shape.rectangle(cr, width, height)
- end -- Set shape for launcher
- prompt_height = dpi(50) -- Prompt height
- prompt_margins = dpi(30) -- Prompt margins
- prompt_paddings = dpi(15) -- Prompt padding
- shape = function(cr, width, height)
- gears.shape.rectangle(cr, width, height)
- end -- Set shape for prompt
- prompt_color = "#000000" -- Prompt background color
- prompt_border_width = dpi(0) -- Prompt border width
- prompt_border_color = "#000000" -- Prompt border color
- prompt_text_halign = "center" -- Prompt text horizontal alignment
- prompt_text_valign = "center" -- Prompt text vertical alignment
- prompt_icon_text_spacing = dpi(10) -- Prompt icon text spacing
- prompt_show_icon = true -- Should prompt show icon (?)
+ end
+
+ prompt_bg_color = "#000000" -- Prompt background color
prompt_icon_font = "Comic Sans" -- Prompt icon font
- prompt_icon_color = "#000000" -- Prompt icon color
+ prompt_icon_size = 15 -- Prompt icon size
+ prompt_icon_color = "#FFFFFF" -- Prompt icon color
prompt_icon = "" -- Prompt icon
- prompt_icon_markup = string.format(
- "%s",
- args.prompt_icon_color, args.prompt_icon
- ) -- Prompt icon markup
- prompt_text = "Search:" -- Prompt text
- prompt_start_text = "manager" -- Set string for prompt to start with
- prompt_font = "Comic Sans" -- Prompt font
- prompt_text_color = "#FFFFFF" -- Prompt text color
- prompt_cursor_color = "#000000" -- Prompt cursor color
+ prompt_label_font = "Comic Sans" -- Prompt labe font
+ prompt_label_size = 15 -- Prompt labe font
+ prompt_label_color = "#FFFFFF" -- Prompt labe font
+ prompt_label = "Search" -- Prompt labe font
+ prompt_text_font = "Comic Sans" -- Prompt text font
+ prompt_text_size = 15 -- Prompt text font
+ prompt_text_color = "#FFFFFF" -- Prompt text font
apps_per_row = 3 -- Set how many apps should appear in each row
apps_per_column = 3 -- Set how many apps should appear in each column
- apps_margin = {left = dpi(40), right = dpi(40), bottom = dpi(30)} -- Margin between apps
- apps_spacing = dpi(10) -- Spacing between apps
- expand_apps = true -- Should apps expand to fill width of launcher
- app_width = dpi(400) -- Width of each app
- app_height = dpi(40) -- Height of each app
- app_shape = function(cr, width, height)
- gears.shape.rectangle(cr, width, height)
- end -- Shape of each app
app_normal_color = "#000000" -- App normal color
- app_normal_hover_color = "#111111" -- App normal hover color
app_selected_color = "#FFFFFF" -- App selected color
- app_selected_hover_color = "#EEEEEE" -- App selected hover color
- app_content_padding = dpi(10) -- App content padding
- app_content_spacing = dpi(10) -- App content spacing
- app_show_icon = true -- Should show icon?
- app_icon_halign = "center" -- App icon horizontal alignment
- app_icon_width = dpi(70) -- App icon wigth
- app_icon_height = dpi(70) -- App icon height
- app_show_name = true -- Should show app name?
- app_name_generic_name_spacing = dpi(0) -- Generic name spacing (If show_generic_name)
- app_name_halign = "center" -- App name horizontal alignment
- app_name_font = "Comic Sans" -- App name font
app_name_normal_color = "#FFFFFF" -- App name normal color
app_name_selected_color = "#000000" -- App name selected color
- app_show_generic_name = true -- Should show generic app name?
}
```
diff --git a/helpers/fzy.lua b/helpers/fzy.lua
new file mode 100644
index 00000000..d80b543f
--- /dev/null
+++ b/helpers/fzy.lua
@@ -0,0 +1,275 @@
+-- The lua implementation of the fzy string matching algorithm
+
+local SCORE_GAP_LEADING = -0.005
+local SCORE_GAP_TRAILING = -0.005
+local SCORE_GAP_INNER = -0.01
+local SCORE_MATCH_CONSECUTIVE = 1.0
+local SCORE_MATCH_SLASH = 0.9
+local SCORE_MATCH_WORD = 0.8
+local SCORE_MATCH_CAPITAL = 0.7
+local SCORE_MATCH_DOT = 0.6
+local SCORE_MAX = math.huge
+local SCORE_MIN = -math.huge
+local MATCH_MAX_LENGTH = 1024
+
+local fzy = {}
+
+-- Check if `needle` is a subsequence of the `haystack`.
+--
+-- Usually called before `score` or `positions`.
+--
+-- Args:
+-- needle (string)
+-- haystack (string)
+-- case_sensitive (bool, optional): defaults to false
+--
+-- Returns:
+-- bool
+function fzy.has_match(needle, haystack, case_sensitive)
+ if not case_sensitive then
+ needle = string.lower(needle)
+ haystack = string.lower(haystack)
+ end
+
+ local j = 1
+ for i = 1, string.len(needle) do
+ j = string.find(haystack, needle:sub(i, i), j, true)
+ if not j then
+ return false
+ else
+ j = j + 1
+ end
+ end
+
+ return true
+end
+
+local function is_lower(c)
+ return c:match("%l")
+end
+
+local function is_upper(c)
+ return c:match("%u")
+end
+
+local function precompute_bonus(haystack)
+ local match_bonus = {}
+
+ local last_char = "/"
+ for i = 1, string.len(haystack) do
+ local this_char = haystack:sub(i, i)
+ if last_char == "/" or last_char == "\\" then
+ match_bonus[i] = SCORE_MATCH_SLASH
+ elseif last_char == "-" or last_char == "_" or last_char == " " then
+ match_bonus[i] = SCORE_MATCH_WORD
+ elseif last_char == "." then
+ match_bonus[i] = SCORE_MATCH_DOT
+ elseif is_lower(last_char) and is_upper(this_char) then
+ match_bonus[i] = SCORE_MATCH_CAPITAL
+ else
+ match_bonus[i] = 0
+ end
+
+ last_char = this_char
+ end
+
+ return match_bonus
+end
+
+local function compute(needle, haystack, D, M, case_sensitive)
+ -- Note that the match bonuses must be computed before the arguments are
+ -- converted to lowercase, since there are bonuses for camelCase.
+ local match_bonus = precompute_bonus(haystack)
+ local n = string.len(needle)
+ local m = string.len(haystack)
+
+ if not case_sensitive then
+ needle = string.lower(needle)
+ haystack = string.lower(haystack)
+ end
+
+ -- Because lua only grants access to chars through substring extraction,
+ -- get all the characters from the haystack once now, to reuse below.
+ local haystack_chars = {}
+ for i = 1, m do
+ haystack_chars[i] = haystack:sub(i, i)
+ end
+
+ for i = 1, n do
+ D[i] = {}
+ M[i] = {}
+
+ local prev_score = SCORE_MIN
+ local gap_score = i == n and SCORE_GAP_TRAILING or SCORE_GAP_INNER
+ local needle_char = needle:sub(i, i)
+
+ for j = 1, m do
+ if needle_char == haystack_chars[j] then
+ local score = SCORE_MIN
+ if i == 1 then
+ score = ((j - 1) * SCORE_GAP_LEADING) + match_bonus[j]
+ elseif j > 1 then
+ local a = M[i - 1][j - 1] + match_bonus[j]
+ local b = D[i - 1][j - 1] + SCORE_MATCH_CONSECUTIVE
+ score = math.max(a, b)
+ end
+ D[i][j] = score
+ prev_score = math.max(score, prev_score + gap_score)
+ M[i][j] = prev_score
+ else
+ D[i][j] = SCORE_MIN
+ prev_score = prev_score + gap_score
+ M[i][j] = prev_score
+ end
+ end
+ end
+end
+
+-- Compute a matching score.
+--
+-- Args:
+-- needle (string): must be a subequence of `haystack`, or the result is
+-- undefined.
+-- haystack (string)
+-- case_sensitive (bool, optional): defaults to false
+--
+-- Returns:
+-- number: higher scores indicate better matches. See also `get_score_min`
+-- and `get_score_max`.
+function fzy.score(needle, haystack, case_sensitive)
+ local n = string.len(needle)
+ local m = string.len(haystack)
+
+ if n == 0 or m == 0 or m > MATCH_MAX_LENGTH or n > m then
+ return SCORE_MIN
+ elseif n == m then
+ return SCORE_MAX
+ else
+ local D = {}
+ local M = {}
+ compute(needle, haystack, D, M, case_sensitive)
+ return M[n][m]
+ end
+end
+
+-- Compute the locations where fzy matches a string.
+--
+-- Determine where each character of the `needle` is matched to the `haystack`
+-- in the optimal match.
+--
+-- Args:
+-- needle (string): must be a subequence of `haystack`, or the result is
+-- undefined.
+-- haystack (string)
+-- case_sensitive (bool, optional): defaults to false
+--
+-- Returns:
+-- {int,...}: indices, where `indices[n]` is the location of the `n`th
+-- character of `needle` in `haystack`.
+-- number: the same matching score returned by `score`
+function fzy.positions(needle, haystack, case_sensitive)
+ local n = string.len(needle)
+ local m = string.len(haystack)
+
+ if n == 0 or m == 0 or m > MATCH_MAX_LENGTH or n > m then
+ return {}, SCORE_MIN
+ elseif n == m then
+ local consecutive = {}
+ for i = 1, n do
+ consecutive[i] = i
+ end
+ return consecutive, SCORE_MAX
+ end
+
+ local D = {}
+ local M = {}
+ compute(needle, haystack, D, M, case_sensitive)
+
+ local positions = {}
+ local match_required = false
+ local j = m
+ for i = n, 1, -1 do
+ while j >= 1 do
+ if D[i][j] ~= SCORE_MIN and (match_required or D[i][j] == M[i][j]) then
+ match_required = (i ~= 1) and (j ~= 1) and (
+ M[i][j] == D[i - 1][j - 1] + SCORE_MATCH_CONSECUTIVE)
+ positions[i] = j
+ j = j - 1
+ break
+ else
+ j = j - 1
+ end
+ end
+ end
+
+ return positions, M[n][m]
+end
+
+-- Apply `has_match` and `positions` to an array of haystacks.
+--
+-- Args:
+-- needle (string)
+-- haystack ({string, ...})
+-- case_sensitive (bool, optional): defaults to false
+--
+-- Returns:
+-- {{idx, positions, score}, ...}: an array with one entry per matching line
+-- in `haystacks`, each entry giving the index of the line in `haystacks`
+-- as well as the equivalent to the return value of `positions` for that
+-- line.
+function fzy.filter(needle, haystacks, case_sensitive)
+ local result = {}
+
+ for i, line in ipairs(haystacks) do
+ if fzy.has_match(needle, line, case_sensitive) then
+ local p, s = fzy.positions(needle, line, case_sensitive)
+ table.insert(result, {i, p, s})
+ end
+ end
+
+ return result
+end
+
+-- The lowest value returned by `score`.
+--
+-- In two special cases:
+-- - an empty `needle`, or
+-- - a `needle` or `haystack` larger than than `get_max_length`,
+-- the `score` function will return this exact value, which can be used as a
+-- sentinel. This is the lowest possible score.
+function fzy.get_score_min()
+ return SCORE_MIN
+end
+
+-- The score returned for exact matches. This is the highest possible score.
+function fzy.get_score_max()
+ return SCORE_MAX
+end
+
+-- The maximum size for which `fzy` will evaluate scores.
+function fzy.get_max_length()
+ return MATCH_MAX_LENGTH
+end
+
+-- The minimum score returned for normal matches.
+--
+-- For matches that don't return `get_score_min`, their score will be greater
+-- than than this value.
+function fzy.get_score_floor()
+ return MATCH_MAX_LENGTH * SCORE_GAP_INNER
+end
+
+-- The maximum score for non-exact matches.
+--
+-- For matches that don't return `get_score_max`, their score will be less than
+-- this value.
+function fzy.get_score_ceiling()
+ return MATCH_MAX_LENGTH * SCORE_MATCH_CONSECUTIVE
+end
+
+-- The name of the currently-running implmenetation, "lua" or "native".
+function fzy.get_implementation_name()
+ return "lua"
+end
+
+return fzy
\ No newline at end of file
diff --git a/helpers/icon_theme.lua b/helpers/icon_theme.lua
index c4d45832..5491504d 100644
--- a/helpers/icon_theme.lua
+++ b/helpers/icon_theme.lua
@@ -1,85 +1,30 @@
+-------------------------------------------
+-- @author https://github.com/Kasper24
+-- @copyright 2021-2022 Kasper24
+-------------------------------------------
local lgi = require("lgi")
local Gio = lgi.Gio
+local DesktopAppInfo = Gio.DesktopAppInfo
+local AppInfo = Gio.DesktopAppInfo
local Gtk = lgi.require("Gtk", "3.0")
-local gobject = require("gears.object")
-local gtable = require("gears.table")
-local setmetatable = setmetatable
+local string = string
local ipairs = ipairs
-local icon_theme = { mt = {} }
+local ICON_SIZE = 48
+local GTK_THEME = Gtk.IconTheme.get_default()
-local name_lookup =
-{
- ["jetbrains-studio"] = "android-studio"
-}
+local _icon_theme = {}
-local function get_icon_by_pid_command(self, client, apps)
- local pid = client.pid
- if pid ~= nil then
- local handle = io.popen(string.format("ps -p %d -o comm=", pid))
- local pid_command = handle:read("*a"):gsub("^%s*(.-)%s*$", "%1")
- handle:close()
-
- for _, app in ipairs(apps) do
- local executable = app:get_executable()
- if executable and executable:find(pid_command, 1, true) then
- return self:get_gicon_path(app:get_icon())
- end
- end
- end
-end
-
-local function get_icon_by_icon_name(self, client, apps)
- local icon_name = client.icon_name and client.icon_name:lower() or nil
- if icon_name ~= nil then
- for _, app in ipairs(apps) do
- local name = app:get_name():lower()
- if name and name:find(icon_name, 1, true) then
- return self:get_gicon_path(app:get_icon())
- end
- end
+function _icon_theme.choose_icon(icons_names, icon_theme, icon_size)
+ if icon_theme then
+ GTK_THEME = Gtk.IconTheme.new()
+ Gtk.IconTheme.set_custom_theme(GTK_THEME, icon_theme);
end
-end
-
-local function get_icon_by_class(self, client, apps)
- if client.class ~= nil then
- local class = name_lookup[client.class] or client.class:lower()
-
- -- Try to remove dashes
- local class_1 = class:gsub("[%-]", "")
-
- -- Try to replace dashes with dot
- local class_2 = class:gsub("[%-]", ".")
-
- -- Try to match only the first word
- local class_3 = class:match("(.-)-") or class
- class_3 = class_3:match("(.-)%.") or class_3
- class_3 = class_3:match("(.-)%s+") or class_3
-
- local possible_icon_names = { class, class_3, class_2, class_1 }
- for _, app in ipairs(apps) do
- local id = app:get_id():lower()
- for _, possible_icon_name in ipairs(possible_icon_names) do
- if id and id:find(possible_icon_name, 1, true) then
- return self:get_gicon_path(app:get_icon())
- end
- end
- end
+ if icon_size then
+ ICON_SIZE = icon_size
end
-end
-function icon_theme:get_client_icon_path(client)
- local apps = Gio.AppInfo.get_all()
-
- return get_icon_by_pid_command(self, client, apps) or
- get_icon_by_icon_name(self, client, apps) or
- get_icon_by_class(self, client, apps) or
- client.icon or
- self:choose_icon({"window", "window-manager", "xfwm4-default", "window_list" })
-end
-
-function icon_theme:choose_icon(icons_names)
- local icon_info = self.gtk_theme:choose_icon(icons_names, self.icon_size, 0);
+ local icon_info = GTK_THEME:choose_icon(icons_names, ICON_SIZE, 0);
if icon_info then
local icon_path = icon_info:get_filename()
if icon_path then
@@ -90,12 +35,20 @@ function icon_theme:choose_icon(icons_names)
return ""
end
-function icon_theme:get_gicon_path(gicon)
+function _icon_theme.get_gicon_path(gicon, icon_theme, icon_size)
if gicon == nil then
return ""
end
- local icon_info = self.gtk_theme:lookup_by_gicon(gicon, self.icon_size, 0);
+ if icon_theme then
+ GTK_THEME = Gtk.IconTheme.new()
+ Gtk.IconTheme.set_custom_theme(GTK_THEME, icon_theme);
+ end
+ if icon_size then
+ ICON_SIZE = icon_size
+ end
+
+ local icon_info = GTK_THEME:lookup_by_gicon(gicon, ICON_SIZE, 0);
if icon_info then
local icon_path = icon_info:get_filename()
if icon_path then
@@ -106,8 +59,16 @@ function icon_theme:get_gicon_path(gicon)
return ""
end
-function icon_theme:get_icon_path(icon_name)
- local icon_info = self.gtk_theme:lookup_icon(icon_name, self.icon_size, 0)
+function _icon_theme.get_icon_path(icon_name, icon_theme, icon_size)
+ if icon_theme then
+ GTK_THEME = Gtk.IconTheme.new()
+ Gtk.IconTheme.set_custom_theme(GTK_THEME, icon_theme);
+ end
+ if icon_size then
+ ICON_SIZE = icon_size
+ end
+
+ local icon_info = GTK_THEME:lookup_icon(icon_name, ICON_SIZE, 0)
if icon_info then
local icon_path = icon_info:get_filename()
if icon_path then
@@ -118,25 +79,4 @@ function icon_theme:get_icon_path(icon_name)
return ""
end
-local function new(theme_name, icon_size)
- local ret = gobject{}
- gtable.crush(ret, icon_theme, true)
-
- ret.name = theme_name or nil
- ret.icon_size = icon_size or 48
-
- if theme_name then
- ret.gtk_theme = Gtk.IconTheme.new()
- Gtk.IconTheme.set_custom_theme(ret.gtk_theme, theme_name);
- else
- ret.gtk_theme = Gtk.IconTheme.get_default()
- end
-
- return ret
-end
-
-function icon_theme.mt:__call(...)
- return new(...)
-end
-
-return setmetatable(icon_theme, icon_theme.mt)
\ No newline at end of file
+return _icon_theme
\ No newline at end of file
diff --git a/helpers/init.lua b/helpers/init.lua
index f2c898e7..2bed9b3f 100644
--- a/helpers/init.lua
+++ b/helpers/init.lua
@@ -2,6 +2,8 @@ return {
client = require(... .. ".client"),
color = require(... .. ".color"),
filesystem = require(... .. ".filesystem"),
+ fzy = require(... .. ".fzy"),
+ icon_theme = require(... .. ".icon_theme"),
shape = require(... .. ".shape"),
time = require(... .. ".time"),
}
diff --git a/widget/app_launcher/awesome-sensible-terminal b/widget/app_launcher/awesome-sensible-terminal
new file mode 100755
index 00000000..4e47296c
--- /dev/null
+++ b/widget/app_launcher/awesome-sensible-terminal
@@ -0,0 +1,25 @@
+#!/bin/sh
+# Based on i3-sensible-terminal
+
+#
+# This code is released in public domain by Han Boetes
+#
+# This script tries to exec a terminal emulator by trying some known terminal
+# emulators.
+#
+# We welcome patches that add distribution-specific mechanisms to find the
+# preferred terminal emulator. On Debian, there is the x-terminal-emulator
+# symlink for example.
+#
+# Invariants:
+# 1. $TERMINAL must come first
+# 2. Distribution-specific mechanisms come next, e.g. x-terminal-emulator
+# 3. The terminal emulator with best accessibility comes first.
+# 4. No order is guaranteed/desired for the remaining terminal emulators.
+for terminal in "$TERMINAL" termite hyper wezterm alacritty kitty x-terminal-emulator mate-terminal gnome-terminal terminator xfce4-terminal urxvt rxvt termit Eterm aterm uxterm xterm roxterm lxterminal terminology st qterminal lilyterm tilix terminix konsole guake tilda; do
+ if command -v "$terminal" > /dev/null 2>&1; then
+ exec "$terminal" "$@"
+ fi
+done
+
+awesome-client 'local naughty = require("naughty"); naughty.notification { message = "awesome-sensible-terminal could not find a terminal emulator. Please install one." }'
diff --git a/widget/app_launcher/init.lua b/widget/app_launcher/init.lua
index 4cc6a557..806ff022 100644
--- a/widget/app_launcher/init.lua
+++ b/widget/app_launcher/init.lua
@@ -3,994 +3,433 @@ local awful = require("awful")
local gobject = require("gears.object")
local gtable = require("gears.table")
local gtimer = require("gears.timer")
-local gfilesystem = require("gears.filesystem")
local wibox = require("wibox")
local beautiful = require("beautiful")
-local color = require(tostring(...):match(".*bling") .. ".helpers.color")
-local prompt = require(... .. ".prompt")
+local text_input_widget = require(... .. ".text_input")
+local rofi_grid_widget = require(... .. ".rofi_grid")
+local helpers = require(tostring(...):match(".*bling") .. ".helpers")
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 terminal_commands_lookup =
-{
- alacritty = "alacritty -e",
- termite = "termite -e",
- rxvt = "rxvt -e",
- terminator = "terminator -e"
-}
+local AWESOME_SENSIBLE_TERMINAL_SCRIPT_PATH = debug.getinfo(1).source:match("@?(.*/)") .. "awesome-sensible-terminal"
+local RUN_AS_ROOT_SCRIPT_PATH = debug.getinfo(1).source:match("@?(.*/)") .. "run-as-root.sh"
-local function string_levenshtein(str1, str2)
- local len1 = string.len(str1)
- local len2 = string.len(str2)
- local matrix = {}
- local cost = 0
-
- -- 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
-
- -- 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
-
- -- 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
-
- 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 case_insensitive_pattern(pattern)
- -- find an optional '%' (group 1) followed by any character (group 2)
- local p = pattern:gsub("(%%?)(.)", function(percent, letter)
- if percent ~= "" or not letter:match("%a") then
- -- if the '%' matched, or `letter` is not a letter, return "as is"
- return percent .. letter
- else
- -- else, return a case-insensitive character class of the matched letter
- return string.format("[%s%s]", letter:lower(), letter:upper())
- end
- end)
-
- return p
+local function default_value(value, default)
+ if value == nil then
+ return default
+ else
+ return value
+ end
end
local function has_value(tab, val)
- for index, value in pairs(tab) do
- if val:find(case_insensitive_pattern(value)) then
+ for _, value in ipairs(tab) do
+ if val:lower():find(value:lower(), 1, true) then
return true
end
end
return false
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 name_widget = self._private.active_widget:get_children_by_id("name")[1]
- if name_widget then
- name_widget.markup = string.format("%s", self.app_name_selected_color, name_widget.text)
- end
- local generic_name_widget = self._private.active_widget:get_children_by_id("generic_name")[1]
- if generic_name_widget then
- generic_name_widget.markup = string.format("%s", self.app_name_selected_color, generic_name_widget.text)
- end
- 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 name_widget = self._private.active_widget:get_children_by_id("name")[1]
- if name_widget then
- name_widget.markup = string.format("%s", self.app_name_normal_color, name_widget.text)
- end
- local generic_name_widget = self._private.active_widget:get_children_by_id("generic_name")[1]
- if generic_name_widget then
- generic_name_widget.markup = string.format("%s", self.app_name_normal_color, generic_name_widget.text)
- end
- self._private.active_widget = nil
- end
-end
-
-local function create_app_widget(self, entry)
- local icon = self.app_show_icon == true and
- {
- widget = wibox.widget.imagebox,
- halign = self.app_icon_halign,
- 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.widget.textbox,
- id = "name",
- font = self.app_name_font,
- markup = string.format("%s", self.app_name_normal_color, entry.name)
- } or nil
-
- local generic_name = entry.generic_name ~= nil and self.app_show_generic_name == true and
- {
- widget = wibox.widget.textbox,
- id = "generic_name",
- font = self.app_name_font,
- markup = entry.generic_name ~= "" and " (" .. entry.generic_name .. ")" or ""
- } 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,
+local function build_widget(self)
+ local widget_template = self.widget_template
+ if widget_template == nil then
+ widget_template = wibox.widget
{
- widget = wibox.container.margin,
- margins = self.app_content_padding,
- {
- -- Using this hack instead of container.place because that will fuck with the name/icon halign
- layout = wibox.layout.align.vertical,
- expand = "outside",
- nil,
+ layout = rofi_grid_widget,
+ widget_template = wibox.widget {
+ widget = wibox.container.margin,
+ margins = dpi(15),
{
layout = wibox.layout.fixed.vertical,
- spacing = self.app_content_spacing,
- icon,
+ spacing = dpi(15),
{
- widget = wibox.container.place,
- halign = self.app_name_halign,
+ widget = text_input_widget,
+ id = "text_input_role",
+ forced_width = dpi(650),
+ forced_height = dpi(60),
+ text_color = self.text_input_text_color,
+ selection_bg = self.text_input_selection_bg,
+ reset_on_stop = self.reset_on_hide,
+ placeholder = self.text_input_placeholder,
+ widget_template = wibox.widget {
+ widget = wibox.container.background,
+ bg = self.text_input_bg_color,
+ {
+ widget = wibox.container.margin,
+ margins = dpi(15),
+ {
+ layout = wibox.layout.stack,
+ {
+ widget = wibox.widget.textbox,
+ id = "placeholder_role",
+ text = "Search: "
+ },
+ {
+ widget = wibox.widget.textbox,
+ id = "text_role"
+ },
+ }
+ }
+ }
+ },
+ {
+ layout = wibox.layout.fixed.horizontal,
+ spacing = dpi(10),
{
- layout = wibox.layout.fixed.horizontal,
- spacing = self.app_name_generic_name_spacing,
- name,
- generic_name
+ layout = wibox.layout.grid,
+ id = "grid_role",
+ orientation = "horizontal",
+ homogeneous = true,
+ spacing = dpi(15),
+ column_count = self.apps_per_column,
+ row_count = self.apps_per_row,
+ },
+ {
+ layout = wibox.container.rotate,
+ direction = 'west',
+ {
+ widget = wibox.widget.slider,
+ id = "scrollbar_role",
+ forced_width = dpi(5),
+ forced_height = dpi(10),
+ minimum = 1,
+ value = 1,
+ bar_height= 3,
+ bar_color = "#00000000",
+ bar_active_color = "#00000000",
+ handle_width = dpi(50),
+ handle_color = beautiful.bg_normal,
+ handle_color = beautiful.fg_normal
+ }
}
}
- },
- nil
- }
- }
- }
+ }
+ },
+ entry_template = function(app)
+ local widget = wibox.widget
+ {
+ widget = wibox.container.background,
+ forced_width = dpi(300),
+ forced_height = dpi(120),
+ bg = self.app_normal_color,
+ {
+ widget = wibox.container.margin,
+ margins = dpi(10),
+ {
+ layout = wibox.layout.fixed.vertical,
+ spacing = dpi(10),
+ {
+ widget = wibox.container.place,
+ halign = "center",
+ valign = "center",
+ {
+ widget = wibox.widget.imagebox,
+ id = "icon_role",
+ forced_width = dpi(70),
+ forced_height = dpi(70),
+ image = app.icon
+ },
+ },
+ {
+ widget = wibox.container.place,
+ halign = "center",
+ valign = "center",
+ {
+ widget = wibox.widget.textbox,
+ id = "name_role",
+ markup = string.format("%s", self.app_name_normal_color, app.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)
+ widget:connect_signal("mouse::enter", function()
+ local widget = capi.mouse.current_wibox
+ if widget then
+ widget.cursor = "hand2"
end
end)
- end
- else
- awful.spawn(entry.executable)
- end
- if self.hide_on_launch then
- self:hide()
- end
- end
-
- app:connect_signal("mouse::enter", function(_self)
- local widget = capi.mouse.current_wibox
- if widget then
- widget.cursor = "hand2"
- end
+ widget:connect_signal("mouse::leave", function()
+ local widget = capi.mouse.current_wibox
+ if widget then
+ widget.cursor = "left_ptr"
+ end
+ 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)
+ widget:connect_signal("button::press", function(_, __, __, button)
+ if button == 1 then
+ widget:select_or_exec()
+ end
+ end)
- app:connect_signal("mouse::leave", function(_self)
- local widget = capi.mouse.current_wibox
- if widget then
- widget.cursor = "left_ptr"
- end
+ widget:connect_signal("select", function()
+ widget.bg = self.app_selected_color
+ local name_widget = widget:get_children_by_id("name_role")[1]
+ name_widget.markup = string.format("%s", self.app_name_selected_color, name_widget.text)
+ 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)
+ widget:connect_signal("unselect", function()
+ widget.bg = self.app_normal_color
+ local name_widget = widget:get_children_by_id("name_role")[1]
+ name_widget.markup = string.format("%s", self.app_name_normal_color, name_widget.text)
+ 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)
+ return widget
+ end
+ }
+ end
+ self._private.widget = awful.popup
+ {
+ screen = self.screen,
+ type = self.type,
+ visible = false,
+ ontop = true,
+ placement = self.placement,
+ border_width = self.border_width,
+ border_color = self.border_color,
+ shape = self.shape,
+ bg = self.bg,
+ widget = widget_template
+ }
- -- Mark this app
- local pos = self._private.grid:get_widget_position(app)
- select_app(self, pos.row, pos.col)
+ widget_template:set_search_fn(function(text, app)
+ local matched_groups = Gio.DesktopAppInfo.search(text:lower())
+ for _, matched_group in ipairs(matched_groups) do
+ for _, app_id in ipairs(matched_group) do
+ if app.id == app_id then
+ return true
+ end
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()
-
- 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:lower(), text:lower(), 1, true) ~= nil or
- self.search_commands and string.find(entry.commandline, text:lower(), 1, true) ~= nil
- then
- table.insert(self._private.matched_entries, {
- name = entry.name,
- generic_name = entry.generic_name,
- commandline = entry.commandline,
- executable = entry.executable,
- terminal = entry.terminal,
- icon = entry.icon
- })
+ local app_launcher = self
+ widget_template:connect_signal("entry_widget::add", function(_, widget, app)
+ function widget:exec()
+ if app.terminal == true then
+ local pid = awful.spawn.with_shell(AWESOME_SENSIBLE_TERMINAL_SCRIPT_PATH .. " -e " .. app.exec)
+ local class = app.startup_wm_class or app.name
+ awful.spawn.with_shell(string.format(
+ [[xdotool search --sync --all --pid %s --name '.*' set_window --classname "%s" set_window --class "%s"]],
+ pid,
+ class,
+ class
+ ))
+ else
+ app:launch()
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
- self._private.apps_per_page = math.min(#self._private.matched_entries, self._private.max_apps_per_page)
-
- -- 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
- 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
- select_app(self, 1, 1)
- end
-end
-
-local function page_backward(self, direction)
- if self._private.current_page > 1 then
- self._private.current_page = self._private.current_page - 1
- elseif self.wrap_page_scrolling and #self._private.matched_entries >= self._private.max_apps_per_page then
- self._private.current_page = self._private.pages_count
- elseif self.wrap_app_scrolling then
- local rows, columns = self._private.grid:get_dimension()
- unselect_app(self)
- select_app(self, math.min(rows, #self._private.grid.children % self.apps_per_row), columns)
- return
- else
- return
- end
-
- local pos = self._private.grid:get_widget_position(self._private.active_widget)
-
- -- Remove the current page apps from the grid
- self._private.grid:reset()
-
- local max_app_index_to_include = self._private.apps_per_page * self._private.current_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
-
- local rows, columns = self._private.grid:get_dimension()
- if self._private.current_page < self._private.pages_count then
- if direction == "up" then
- select_app(self, rows, columns)
- else
- -- Keep the same row from last page
- select_app(self, pos.row, columns)
- end
- elseif self.wrap_page_scrolling then
- if direction == "up" then
- select_app(self, math.min(rows, #self._private.grid.children % self.apps_per_row), columns)
- else
- -- Keep the same row from last page
- select_app(self, math.min(pos.row, #self._private.grid.children % self.apps_per_row), columns)
- end
- end
-end
-
-local function page_forward(self, direction)
- local min_app_index_to_include = 0
- local max_app_index_to_include = self._private.apps_per_page
-
- if self._private.current_page < self._private.pages_count then
- min_app_index_to_include = self._private.apps_per_page * self._private.current_page
- self._private.current_page = self._private.current_page + 1
- max_app_index_to_include = self._private.apps_per_page * self._private.current_page
- elseif self.wrap_page_scrolling and #self._private.matched_entries >= self._private.max_apps_per_page then
- self._private.current_page = 1
- min_app_index_to_include = 0
- max_app_index_to_include = self._private.apps_per_page
- elseif self.wrap_app_scrolling then
- unselect_app(self)
- select_app(self, 1, 1)
- return
- else
- return
- end
- local pos = self._private.grid:get_widget_position(self._private.active_widget)
-
- -- Remove the current page apps from the grid
- self._private.grid:reset()
-
- 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))
+ if app_launcher.hide_on_launch then
+ app_launcher:hide()
+ end
end
- end
- if self._private.current_page > 1 or self.wrap_page_scrolling then
- if direction == "down" then
- select_app(self, 1, 1)
- else
- local last_col_max_row = math.min(pos.row, #self._private.grid.children % self.apps_per_row)
- if last_col_max_row ~= 0 then
- select_app(self, last_col_max_row, 1)
+ function widget:exec_as_root()
+ if app.terminal == true then
+ local pid = awful.spawn.with_shell(
+ AWESOME_SENSIBLE_TERMINAL_SCRIPT_PATH .. " -e " ..
+ RUN_AS_ROOT_SCRIPT_PATH .. " " ..
+ app.exec
+ )
+ local class = app.startup_wm_class or app.name
+ awful.spawn.with_shell(string.format(
+ [[xdotool search --sync --all --pid %s --name '.*' set_window --classname "%s" set_window --class "%s"]],
+ pid,
+ class,
+ class
+ ))
else
- select_app(self, pos.row, 1)
+ awful.spawn(RUN_AS_ROOT_SCRIPT_PATH .. " " .. app.exec)
end
- end
- end
-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 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
- else
- page_backward(self, "up")
- end
-end
-
-local function scroll_down(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_app = self._private.grid:index(self._private.active_widget) < #self._private.grid.children
-
- -- Check if we can scroll down the app list
- if is_less_than_max_app then
- -- Unmark the previous app
- unselect_app(self)
- if pos.row == rows then
- select_app(self, 1, pos.col + 1)
- else
- select_app(self, pos.row + 1, pos.col)
+ if app_launcher.hide_on_launch then
+ app_launcher:hide()
+ end
end
- else
- page_forward(self, "down")
- end
-end
-
-local function scroll_left(self)
- if #self._private.grid.children < 1 then
- self._private.active_widget = nil
- return
- end
-
- local pos = self._private.grid:get_widget_position(self._private.active_widget)
- local is_bigger_than_first_column = pos.col > 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)
- else
- page_backward(self, "left")
- 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
-
- -- Check if we can scroll down the app list
- if is_less_than_max_column then
- -- Unmark the previous app
- unselect_app(self)
+ end)
- -- 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)
+ self:get_text_input():connect_signal("key::press", function(_, mod, key, cmd)
+ if key == "Escape" then
+ app_launcher:hide()
end
+ end)
- else
- page_forward(self, "right")
- end
-end
-
-local function reset(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
+ self:get_text_input():connect_signal("key::release", function(_, mod, key, cmd)
+ if key == "Return" then
+ if app_launcher:get_rofi_grid():get_selected_widget() ~= nil then
+ app_launcher:get_rofi_grid():get_selected_widget():exec()
+ end
end
- end
-
- select_app(self, 1, 1)
+ end)
end
-local function generate_apps(self)
- self._private.all_entries = {}
- self._private.matched_entries = {}
+function app_launcher:refresh_app_list()
+ local entries = {}
local app_info = Gio.AppInfo
local apps = app_info.get_all()
- if self.sort_alphabetically then
- table.sort(apps, function(a, b)
- local app_a_score = app_info.get_name(a):lower()
- if has_value(self.favorites, app_info.get_name(a)) then
- app_a_score = "aaaaaaaaaaa" .. app_a_score
- end
- local app_b_score = app_info.get_name(b):lower()
- if has_value(self.favorites, app_info.get_name(b)) then
- app_b_score = "aaaaaaaaaaa" .. app_b_score
- end
-
- return app_a_score < app_b_score
- end)
- elseif self.reverse_sort_alphabetically then
- table.sort(apps, function(a, b)
- local app_a_score = app_info.get_name(a):lower()
- if has_value(self.favorites, app_info.get_name(a)) then
- app_a_score = "zzzzzzzzzzz" .. app_a_score
- end
- local app_b_score = app_info.get_name(b):lower()
- if has_value(self.favorites, app_info.get_name(b)) then
- app_b_score = "zzzzzzzzzzz" .. app_b_score
- end
-
- return app_a_score > app_b_score
- end)
- else
- table.sort(apps, function(a, b)
- local app_a_favorite = has_value(self.favorites, app_info.get_name(a))
- local app_b_favorite = has_value(self.favorites, app_info.get_name(b))
-
- if app_a_favorite and not app_b_favorite then
- return true
- elseif app_b_favorite and not app_a_favorite then
- return false
- elseif app_a_favorite and app_b_favorite then
- return app_info.get_name(a):lower() < app_info.get_name(b):lower()
- else
- return false
- end
- end)
- end
-
- local icon_theme = require(tostring(path):match(".*bling") .. ".helpers.icon_theme")(self.icon_theme, self.icon_size)
-
for _, app in ipairs(apps) do
- if app.should_show(app) then
- 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))
+ if app:should_show() then
+ local id = app:get_id()
+ local desktop_app_info = Gio.DesktopAppInfo.new(id)
+ local name = desktop_app_info:get_string("Name")
+ local exec = desktop_app_info:get_string("Exec")
-- Check if this app should be skipped, depanding on the skip_names / skip_commands table
- if not has_value(self.skip_names, name) and not has_value(self.skip_commands, commandline) then
+ if not has_value(self.skip_names, name) and not has_value(self.skip_commands, exec) then
-- Check if this app should be skipped becuase it's iconless depanding on skip_empty_icons
+ local icon = helpers.icon_theme.get_gicon_path(app_info.get_icon(app), self.icon_theme, self.icon_size)
if icon ~= "" or self.skip_empty_icons == false then
if icon == "" then
if self.default_app_icon_name ~= nil then
- icon = icon_theme:get_icon_path(self.default_app_icon_name)
+ icon = helpers.icon_theme.get_icon_path(self.default_app_icon_name, self.icon_theme, self.icon_size)
elseif self.default_app_icon_path ~= nil then
icon = self.default_app_icon_path
else
- icon = icon_theme:choose_icon({"application-all", "application", "application-default-icon", "app"})
+ icon = helpers.icon_theme.choose_icon(
+ {"application-all", "application", "application-default-icon", "app"},
+ self.icon_theme, self.icon_size)
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
- local generic_name = Gio.DesktopAppInfo.get_string(desktop_app_info, "GenericName") or nil
-
- table.insert(self._private.all_entries, {
+ local app = {
+ uid = desktop_app_info:get_filename(),
+ desktop_app_info = desktop_app_info,
+ path = desktop_app_info:get_filename(),
+ id = id,
name = name,
- generic_name = generic_name,
- commandline = commandline,
- executable = executable,
- terminal = terminal,
- icon = icon
- })
+ generic_name = desktop_app_info:get_string("GenericName"),
+ startup_wm_class = desktop_app_info:get_startup_wm_class(),
+ keywords = desktop_app_info:get_string("Keywords"),
+ icon = icon,
+ icon_name = desktop_app_info:get_string("Icon"),
+ terminal = desktop_app_info:get_string("Terminal") == "true" and true or false,
+ exec = exec,
+ launch = function()
+ app:launch()
+ end
+ }
+
+ table.insert(entries, app)
end
end
end
end
+
+ self:get_rofi_grid():set_entries(entries)
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
- self._private.prompt:start()
-
- local animation = self.rubato
- if animation ~= nil then
- if self._private.widget.goal_x == nil then
- self._private.widget.goal_x = self._private.widget.x
- end
- if self._private.widget.goal_y == nil then
- self._private.widget.goal_y = self._private.widget.y
- self._private.widget.placement = nil
- end
+ self:refresh_app_list()
- if animation.x then
- animation.x.ended:unsubscribe()
- animation.x:set(self._private.widget.goal_x)
- gtimer {
- timeout = 0.01,
- call_now = false,
- autostart = true,
- single_shot = true,
- callback = function()
- screen.app_launcher.visible = true
- end
- }
- end
- if animation.y then
- animation.y.ended:unsubscribe()
- animation.y:set(self._private.widget.goal_y)
- gtimer {
- timeout = 0.01,
- call_now = false,
- autostart = true,
- single_shot = true,
- callback = function()
- screen.app_launcher.visible = true
- end
- }
- end
- else
- screen.app_launcher.visible = true
+ if self.show_on_focused_screen then
+ self:get_widget().screen = awful.screen.focused()
end
- self:emit_signal("bling::app_launcher::visibility", true)
+ self:get_widget().visible = true
+ self:get_text_input():focus()
+ self:emit_signal("visibility", true)
end
---- Hides the app launcher
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
+ if self:get_widget().visible == false then
return
end
- self._private.prompt:stop()
-
- 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
-
- 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()
- if self.reset_on_hide == true then reset(self) end
- screen.app_launcher.visible = false
- screen.app_launcher = nil
- animation.x.ended:unsubscribe()
- end)
- else
- animation.y.ended:subscribe(function()
- if self.reset_on_hide == true then reset(self) end
- screen.app_launcher.visible = false
- screen.app_launcher = nil
- animation.y.ended:unsubscribe()
- end)
- end
- else
- if self.reset_on_hide == true then reset(self) end
- screen.app_launcher.visible = false
- screen.app_launcher = nil
+ if self.reset_on_hide == true then
+ self:get_rofi_grid():reset()
end
- self:emit_signal("bling::app_launcher::visibility", false)
+ self:get_widget().visible = false
+ self:get_text_input():unfocus()
+ self:emit_signal("visibility", false)
end
---- Toggles the app launcher
function app_launcher:toggle()
- local screen = self.screen
- if self.show_on_focused_screen then
- screen = awful.screen.focused()
- end
-
- if screen.app_launcher and screen.app_launcher.visible then
+ if self:get_widget().visible then
self:hide()
else
self:show()
end
end
--- Returns a new app launcher
-local function new(args)
- args = args or {}
-
- args.terminal = args.terminal or nil
- args.favorites = args.favorites or {}
- args.search_commands = args.search_commands == nil and true or args.search_commands
- args.skip_names = args.skip_names or {}
- args.skip_commands = args.skip_commands or {}
- args.skip_empty_icons = args.skip_empty_icons ~= nil and args.skip_empty_icons or false
- args.sort_alphabetically = args.sort_alphabetically == nil and true or args.sort_alphabetically
- args.reverse_sort_alphabetically = args.reverse_sort_alphabetically ~= nil and args.reverse_sort_alphabetically or false
- args.select_before_spawn = args.select_before_spawn == nil and true or args.select_before_spawn
- args.hide_on_left_clicked_outside = args.hide_on_left_clicked_outside == nil and true or args.hide_on_left_clicked_outside
- args.hide_on_right_clicked_outside = args.hide_on_right_clicked_outside == nil and true or args.hide_on_right_clicked_outside
- args.hide_on_launch = args.hide_on_launch == nil and true or args.hide_on_launch
- args.try_to_keep_index_after_searching = args.try_to_keep_index_after_searching ~= nil and args.try_to_keep_index_after_searching or false
- args.reset_on_hide = args.reset_on_hide == nil and true or args.reset_on_hide
- args.save_history = args.save_history == nil and true or args.save_history
- args.wrap_page_scrolling = args.wrap_page_scrolling == nil and true or args.wrap_page_scrolling
- args.wrap_app_scrolling = args.wrap_app_scrolling == nil and true or args.wrap_app_scrolling
-
- 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.icon_size = args.icon_size or nil
+function app_launcher:get_widget()
+ return self._private.widget
+end
- args.type = args.type or "dock"
- args.show_on_focused_screen = args.show_on_focused_screen == nil and true or args.show_on_focused_screen
- args.screen = args.screen or capi.screen.primary
- args.placement = args.placement or awful.placement.centered
- args.rubato = args.rubato or nil
- args.shrink_width = args.shrink_width ~= nil and args.shrink_width or false
- args.shrink_height = args.shrink_height ~= nil and args.shrink_height or false
- args.background = args.background or "#000000"
- args.border_width = args.border_width or beautiful.border_width or dpi(0)
- args.border_color = args.border_color or beautiful.border_color or "#FFFFFF"
- args.shape = args.shape or nil
+function app_launcher:get_rofi_grid()
+ return self:get_widget().widget
+end
- args.prompt_height = args.prompt_height or dpi(100)
- args.prompt_margins = args.prompt_margins or dpi(0)
- args.prompt_paddings = args.prompt_paddings or dpi(30)
- args.prompt_shape = args.prompt_shape or nil
- args.prompt_color = args.prompt_color or beautiful.fg_normal or "#FFFFFF"
- args.prompt_border_width = args.prompt_border_width or beautiful.border_width or dpi(0)
- args.prompt_border_color = args.prompt_border_color or beautiful.border_color or args.prompt_color
- args.prompt_text_halign = args.prompt_text_halign or "left"
- args.prompt_text_valign = args.prompt_text_valign or "center"
- args.prompt_icon_text_spacing = args.prompt_icon_text_spacing or dpi(10)
- args.prompt_show_icon = args.prompt_show_icon == nil and true or args.prompt_show_icon
- args.prompt_icon_font = args.prompt_icon_font or beautiful.font
- args.prompt_icon_color = args.prompt_icon_color or beautiful.bg_normal or "#000000"
- args.prompt_icon = args.prompt_icon or ""
- args.prompt_icon_markup = args.prompt_icon_markup or string.format("%s", args.prompt_icon_color, args.prompt_icon)
- args.prompt_text = args.prompt_text or "Search: "
- args.prompt_start_text = args.prompt_start_text or ""
- args.prompt_font = args.prompt_font or beautiful.font
- args.prompt_text_color = args.prompt_text_color or beautiful.bg_normal or "#000000"
- args.prompt_cursor_color = args.prompt_cursor_color or beautiful.bg_normal or "#000000"
+function app_launcher:get_text_input()
+ return self:get_rofi_grid():get_text_input()
+end
- args.apps_per_row = args.apps_per_row or 5
- args.apps_per_column = args.apps_per_column or 3
- args.apps_margin = args.apps_margin or dpi(30)
- args.apps_spacing = args.apps_spacing or dpi(30)
+local function new(args)
+ args = args or {}
- args.expand_apps = args.expand_apps == nil and true or args.expand_apps
- args.app_width = args.app_width or dpi(300)
- args.app_height = args.app_height or dpi(120)
- 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_padding = args.app_content_padding or dpi(10)
- 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
- args.app_icon_halign = args.app_icon_halign or "center"
- args.app_icon_width = args.app_icon_width or dpi(70)
- args.app_icon_height = args.app_icon_height or dpi(70)
- args.app_show_name = args.app_show_name == nil and true or args.app_show_name
- args.app_name_generic_name_spacing = args.app_name_generic_name_spacing or dpi(0)
- args.app_name_halign = args.app_name_halign or "center"
- args.app_name_font = args.app_name_font or beautiful.font
- args.app_name_normal_color = args.app_name_normal_color or beautiful.fg_normal or "#FFFFFF"
- args.app_name_selected_color = args.app_name_selected_color or beautiful.bg_normal or "#000000"
- args.app_show_generic_name = args.app_show_generic_name ~= nil and args.app_show_generic_name or false
+ local ret = gobject {}
+
+ args.skip_names = default_value(args.skip_names, {})
+ args.skip_commands = default_value(args.skip_commands, {})
+ args.skip_empty_icons = default_value(args.skip_empty_icons, false)
+ args.hide_on_left_clicked_outside = default_value(args.hide_on_left_clicked_outside, true)
+ args.hide_on_right_clicked_outside = default_value(args.hide_on_right_clicked_outside, true)
+ args.hide_on_launch = default_value(args.hide_on_launch, true)
+ args.reset_on_hide = default_value(args.reset_on_hide, true)
+
+ args.type = default_value(args.type, "dock")
+ args.show_on_focused_screen = default_value(args.show_on_focused_screen, true)
+ args.screen = default_value(args.screen, capi.screen.primary)
+ args.placement = default_value(args.placement, awful.placement.centered)
+ args.bg = default_value(args.bg, "#000000")
+ args.border_width = default_value(args.border_width, beautiful.border_width or dpi(0))
+ args.border_color = default_value(args.border_color, beautiful.border_color or "#FFFFFF")
+ args.shape = default_value(args.shape, nil)
+
+ args.default_app_icon_name = default_value(args.default_app_icon_name, nil)
+ args.default_app_icon_path = default_value(args.default_app_icon_path, nil)
+ args.icon_theme = default_value(args.icon_theme, nil)
+ args.icon_size = default_value(args.icon_size, nil)
+
+ args.apps_per_row = default_value(args.apps_per_row, 5)
+ args.apps_per_column = default_value(args.apps_per_column, 3)
+
+ args.text_input_bg_color = default_value(args.text_input_bg_color, "#000000")
+ args.text_input_text_color = default_value(args.text_input_text_color, "#FFFFFF")
+ args.text_input_selection_bg = default_value(args.text_input_selection_bg, "#FF0000")
+ args.text_input_placeholder = default_value(args.text_input_placeholder, "Search:")
+
+ args.app_normal_color = default_value(args.app_normal_color, "#000000")
+ args.app_selected_color = default_value(args.app_selected_color, "#FFFFFF")
+ args.app_name_normal_color = default_value(args.app_name_normal_color, "#FFFFFF")
+ args.app_name_selected_color = default_value(args.app_name_selected_color, "#000000")
+
+ gtable.crush(ret, app_launcher, true)
+ gtable.crush(ret, args, true)
- local ret = gobject({})
ret._private = {}
ret._private.text = ""
-
- gtable.crush(ret, app_launcher)
- gtable.crush(ret, args)
-
- -- Calculate the grid width and height
- local grid_width = ret.shrink_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
- and dpi((ret.app_height * ret.apps_per_row) + ((ret.apps_per_row - 1) * ret.apps_spacing))
- or nil
-
- -- These widgets need to be later accessed
- ret._private.prompt = prompt
- {
- prompt = ret.prompt_text,
- text = ret.prompt_start_text,
- font = ret.prompt_font,
- reset_on_stop = ret.reset_on_hide,
- bg_cursor = ret.prompt_cursor_color,
- history_path = ret.save_history == true and gfilesystem.get_cache_dir() .. "/history" or nil,
- 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
-
- ret._private.search_timer = gtimer {
- timeout = 0.05,
- autostart = true,
- single_shot = true,
- callback = function()
- 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.active_widget ~= nil then
- ret._private.active_widget.spawn()
- end
- end
- 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
- {
- layout = wibox.layout.grid,
- forced_width = grid_width,
- forced_height = grid_height,
- orientation = "horizontal",
- homogeneous = true,
- expand = ret.expand_apps,
- spacing = ret.apps_spacing,
- forced_num_rows = ret.apps_per_row,
- buttons =
- {
- awful.button({}, 4, function() scroll_up(ret) end),
- awful.button({}, 5, function() scroll_down(ret) end)
- }
- }
- ret._private.widget = awful.popup
- {
- type = args.type,
- visible = false,
- ontop = true,
- placement = ret.placement,
- border_width = ret.border_width,
- border_color = ret.border_color,
- shape = ret.shape,
- bg = ret.background,
- widget =
- {
- layout = wibox.layout.fixed.vertical,
- {
- widget = wibox.container.margin,
- margins = ret.prompt_margins,
- {
- widget = wibox.container.background,
- forced_height = ret.prompt_height,
- shape = ret.prompt_shape,
- bg = ret.prompt_color,
- fg = ret.prompt_text_color,
- border_width = ret.prompt_border_width,
- border_color = ret.prompt_border_color,
- {
- widget = wibox.container.margin,
- margins = ret.prompt_paddings,
- {
- widget = wibox.container.place,
- halign = ret.prompt_text_halign,
- valign = ret.prompt_text_valign,
- {
- layout = wibox.layout.fixed.horizontal,
- spacing = ret.prompt_icon_text_spacing,
- {
- widget = wibox.widget.textbox,
- font = ret.prompt_icon_font,
- markup = ret.prompt_icon_markup
- },
- ret._private.prompt.textbox
- }
- }
- }
- }
- },
- {
- widget = wibox.container.margin,
- margins = ret.apps_margin,
- ret._private.grid
- }
- }
- }
-
- -- Private variables to be used to be used by the scrolling and searching functions
- 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_page = 1
-
- generate_apps(ret)
- reset(ret)
-
- if ret.rubato and ret.rubato.x then
- ret.rubato.x:subscribe(function(pos)
- ret._private.widget.x = pos
- end)
- end
- if ret.rubato and ret.rubato.y then
- ret.rubato.y:subscribe(function(pos)
- ret._private.widget.y = pos
- end)
- end
+ ret._private.search_timer = gtimer {
+ timeout = 0.05,
+ call_now = false,
+ autostart = false,
+ single_shot = true,
+ callback = function()
+ ret:search()
+ end
+ }
if ret.hide_on_left_clicked_outside then
awful.mouse.append_client_mousebinding(
@@ -1019,37 +458,12 @@ local function new(args)
)
end
- local kill_old_inotify_process_script = [[ ps x | grep "inotifywait -e modify /usr/share/applications" | grep -v grep | awk '{print $1}' | xargs kill ]]
- local subscribe_script = [[ bash -c "while (inotifywait -e modify /usr/share/applications -qq) do echo; done" ]]
-
- awful.spawn.easy_async_with_shell(kill_old_inotify_process_script, function()
- awful.spawn.with_line_callback(subscribe_script, {stdout = function(_)
- generate_apps(ret)
- end})
- end)
+ build_widget(ret)
+ ret:refresh_app_list()
return ret
end
-function app_launcher.text(args)
- args = args or {}
-
- args.prompt_height = args.prompt_height or dpi(50)
- args.prompt_margins = args.prompt_margins or dpi(30)
- args.prompt_paddings = args.prompt_paddings or dpi(15)
- args.app_width = args.app_width or dpi(400)
- args.app_height = args.app_height or dpi(40)
- args.apps_spacing = args.apps_spacing or dpi(10)
- args.apps_per_row = args.apps_per_row or 15
- args.apps_per_column = args.apps_per_column or 1
- args.app_name_halign = args.app_name_halign or "left"
- args.app_show_icon = args.app_show_icon ~= nil and args.app_show_icon or false
- args.app_show_generic_name = args.app_show_generic_name == nil and true or args.app_show_generic_name
- args.apps_margin = args.apps_margin or { left = dpi(40), right = dpi(40), bottom = dpi(30) }
-
- return new(args)
-end
-
function app_launcher.mt:__call(...)
return new(...)
end
diff --git a/widget/app_launcher/prompt.lua b/widget/app_launcher/prompt.lua
deleted file mode 100644
index fae3b86c..00000000
--- a/widget/app_launcher/prompt.lua
+++ /dev/null
@@ -1,656 +0,0 @@
----------------------------------------------------------------------------
---- Modified Prompt module.
--- @author Julien Danjou <julien@danjou.info>
--- @copyright 2008 Julien Danjou
----------------------------------------------------------------------------
-
-local akey = require("awful.key")
-local keygrabber = require("awful.keygrabber")
-local gobject = require("gears.object")
-local gdebug = require('gears.debug')
-local gtable = require("gears.table")
-local gcolor = require("gears.color")
-local gstring = require("gears.string")
-local gfs = require("gears.filesystem")
-local wibox = require("wibox")
-local beautiful = require("beautiful")
-local io = io
-local table = table
-local math = math
-local ipairs = ipairs
-local unpack = unpack or table.unpack -- luacheck: globals unpack (compatibility with Lua 5.1)
-local capi = { selection = selection }
-
-local prompt = { mt = {} }
-
---- Private data
-local data = {}
-data.history = {}
-
-local function itera(inc,a, i)
- i = i + inc
- local v = a[i]
- if v then return i,v end
-end
-
-local function history_check_load(id, max)
- if id and id ~= "" and not data.history[id] then
- data.history[id] = { max = 50, table = {} }
-
- if max then
- data.history[id].max = max
- end
-
- local f = io.open(id, "r")
- if not f then return end
-
- -- Read history file
- for line in f:lines() do
- if gtable.hasitem(data.history[id].table, line) == nil then
- table.insert(data.history[id].table, line)
- if #data.history[id].table >= data.history[id].max then
- break
- end
- end
- end
- f:close()
- end
-end
-
-local function is_word_char(c)
- if string.find(c, "[{[(,.:;_-+=@/ ]") then
- return false
- else
- return true
- end
-end
-
-local function cword_start(s, pos)
- local i = pos
- if i > 1 then
- i = i - 1
- end
- while i >= 1 and not is_word_char(s:sub(i, i)) do
- i = i - 1
- end
- while i >= 1 and is_word_char(s:sub(i, i)) do
- i = i - 1
- end
- if i <= #s then
- i = i + 1
- end
- return i
-end
-
-local function cword_end(s, pos)
- local i = pos
- while i <= #s and not is_word_char(s:sub(i, i)) do
- i = i + 1
- end
- while i <= #s and is_word_char(s:sub(i, i)) do
- i = i + 1
- end
- return i
-end
-
-local function history_save(id)
- if data.history[id] then
- gfs.make_parent_directories(id)
- local f = io.open(id, "w")
- if not f then
- gdebug.print_warning("Failed to write the history to "..id)
- return
- end
- for i = 1, math.min(#data.history[id].table, data.history[id].max) do
- f:write(data.history[id].table[i] .. "\n")
- end
- f:close()
- end
-end
-
-local function history_items(id)
- if data.history[id] then
- return #data.history[id].table
- else
- return -1
- end
-end
-
-local function history_add(id, command)
- if data.history[id] and command ~= "" then
- local index = gtable.hasitem(data.history[id].table, command)
- if index == nil then
- table.insert(data.history[id].table, command)
-
- -- Do not exceed our max_cmd
- if #data.history[id].table > data.history[id].max then
- table.remove(data.history[id].table, 1)
- end
-
- history_save(id)
- else
- -- Bump this command to the end of history
- table.remove(data.history[id].table, index)
- table.insert(data.history[id].table, command)
- history_save(id)
- end
- end
-end
-
-local function have_multibyte_char_at(text, position)
- return text:sub(position, position):wlen() == -1
-end
-
-local function prompt_text_with_cursor(args)
- local char, spacer, text_start, text_end, ret
- local text = args.text or ""
- local _prompt = args.prompt or ""
- local underline = args.cursor_ul or "none"
-
- if args.select_all then
- if #text == 0 then char = " " else char = gstring.xml_escape(text) end
- spacer = " "
- text_start = ""
- text_end = ""
- elseif #text < args.cursor_pos then
- char = " "
- spacer = ""
- text_start = gstring.xml_escape(text)
- text_end = ""
- else
- local offset = 0
- if have_multibyte_char_at(text, args.cursor_pos) then
- offset = 1
- end
- char = gstring.xml_escape(text:sub(args.cursor_pos, args.cursor_pos + offset))
- spacer = " "
- text_start = gstring.xml_escape(text:sub(1, args.cursor_pos - 1))
- text_end = gstring.xml_escape(text:sub(args.cursor_pos + 1 + offset))
- end
-
- local cursor_color = gcolor.ensure_pango_color(args.cursor_color)
- local text_color = gcolor.ensure_pango_color(args.text_color)
-
- if args.highlighter then
- text_start, text_end = args.highlighter(text_start, text_end)
- end
-
- ret = _prompt .. text_start .. "" .. char .. "" .. text_end .. spacer
-
- return ret
-end
-
-local function update(self)
- self.textbox:set_font(self.font)
- self.textbox:set_markup(prompt_text_with_cursor{
- text = self.command, text_color = self.fg_cursor, cursor_color = self.bg_cursor,
- cursor_pos = self._private_cur_pos, cursor_ul = self.ul_cursor, select_all = self.select_all,
- prompt = self.prompt, highlighter = self.highlighter })
-end
-
-local function exec(self, cb, command_to_history)
- self.textbox:set_markup("")
- history_add(self.history_path, command_to_history)
- keygrabber.stop(self._private.grabber)
- if cb then cb(self.command) end
- if self.done_callback then
- self.done_callback()
- end
-end
-
-function prompt:start()
- -- The cursor position
- if self.reset_on_stop == true or self._private_cur_pos == nil then
- self._private_cur_pos = (self.select_all and 1) or self.text:wlen() + 1
- end
- if self.reset_on_stop == true then self.text = "" self.command = "" end
-
- self.textbox:set_font(self.font)
- self.textbox:set_markup(prompt_text_with_cursor{
- text = self.reset_on_stop and self.text or self.command, text_color = self.fg_cursor, cursor_color = self.bg_cursor,
- cursor_pos = self._private_cur_pos, cursor_ul = self.ul_cursor, select_all = self.select_all,
- prompt = self.prompt, highlighter = self.highlighter})
-
- self._private.search_term = nil
-
- history_check_load(self.history_path, self.history_max)
- local history_index = history_items(self.history_path) + 1
-
- -- The completion element to use on completion request.
- local ncomp = 1
-
- local command_before_comp
- local cur_pos_before_comp
-
- self._private.grabber = keygrabber.run(function(modifiers, key, event)
- -- Convert index array to hash table
- local mod = {}
- for _, v in ipairs(modifiers) do mod[v] = true end
-
- if event ~= "press" then
- if self.keyreleased_callback then
- self.keyreleased_callback(mod, key, self.command)
- end
- return
- end
-
- -- Call the user specified callback. If it returns true as
- -- the first result then return from the function. Treat the
- -- second and third results as a new command and new prompt
- -- to be set (if provided)
- if self.keypressed_callback then
- local user_catched, new_command, new_prompt =
- self.keypressed_callback(mod, key, self.command)
- if new_command or new_prompt then
- if new_command then
- self.command = new_command
- end
- if new_prompt then
- self.prompt = new_prompt
- end
- update(self)
- end
- if user_catched then
- if self.changed_callback then
- self.changed_callback(self.command)
- end
- return
- end
- end
-
- local filtered_modifiers = {}
-
- -- User defined cases
- if self.hooks[key] then
- -- Remove caps and num lock
- for _, m in ipairs(modifiers) do
- if not gtable.hasitem(akey.ignore_modifiers, m) then
- table.insert(filtered_modifiers, m)
- end
- end
-
- for _,v in ipairs(self.hooks[key]) do
- if #filtered_modifiers == #v[1] then
- local match = true
- for _,v2 in ipairs(v[1]) do
- match = match and mod[v2]
- end
- if match then
- local cb
- local ret, quit = v[3](self.command)
- local original_command = self.command
-
- -- Support both a "simple" and a "complex" way to
- -- control if the prompt should quit.
- quit = quit == nil and (ret ~= true) or (quit~=false)
-
- -- Allow the callback to change the command
- self.command = (ret ~= true) and ret or self.command
-
- -- Quit by default, but allow it to be disabled
- if ret and type(ret) ~= "boolean" then
- cb = self.exe_callback
- if not quit then
- self._private_cur_pos = ret:wlen() + 1
- update(self)
- end
- elseif quit then
- -- No callback.
- cb = function() end
- end
-
- -- Execute the callback
- if cb then
- exec(self, cb, original_command)
- end
-
- return
- end
- end
- end
- end
-
- -- Get out cases
- if (mod.Control and (key == "c" or key == "g"))
- or (not mod.Control and key == "Escape") then
- self:stop()
- return false
- elseif (mod.Control and (key == "j" or key == "m"))
- -- or (not mod.Control and key == "Return")
- -- or (not mod.Control and key == "KP_Enter")
- then
- exec(self, self.exe_callback, self.command)
- -- We already unregistered ourselves so we don't want to return
- -- true, otherwise we may unregister someone else.
- return
- end
-
- -- Control cases
- if mod.Control then
- self.select_all = nil
- if key == "v" then
- local selection = capi.selection()
- if selection then
- -- Remove \n
- local n = selection:find("\n")
- if n then
- selection = selection:sub(1, n - 1)
- end
- self.command = self.command:sub(1, self._private_cur_pos - 1) .. selection .. self.command:sub(self._private_cur_pos)
- self._private_cur_pos = self._private_cur_pos + #selection
- end
- elseif key == "a" then
- self._private_cur_pos = 1
- elseif key == "b" then
- if self._private_cur_pos > 1 then
- self._private_cur_pos = self._private_cur_pos - 1
- if have_multibyte_char_at(self.command, self._private_cur_pos) then
- self._private_cur_pos = self._private_cur_pos - 1
- end
- end
- elseif key == "d" then
- if self._private_cur_pos <= #self.command then
- self.command = self.command:sub(1, self._private_cur_pos - 1) .. self.command:sub(self._private_cur_pos + 1)
- end
- elseif key == "p" then
- if history_index > 1 then
- history_index = history_index - 1
-
- self.command = data.history[self.history_path].table[history_index]
- self._private_cur_pos = #self.command + 2
- end
- elseif key == "n" then
- if history_index < history_items(self.history_path) then
- history_index = history_index + 1
-
- self.command = data.history[self.history_path].table[history_index]
- self._private_cur_pos = #self.command + 2
- elseif history_index == history_items(self.history_path) then
- history_index = history_index + 1
-
- self.command = ""
- self._private_cur_pos = 1
- end
- elseif key == "e" then
- self._private_cur_pos = #self.command + 1
- elseif key == "r" then
- self._private.search_term = self._private.search_term or self.command:sub(1, self._private_cur_pos - 1)
- for i,v in (function(a,i) return itera(-1,a,i) end), data.history[self.history_path].table, history_index do
- if v:find(self._private.search_term,1,true) ~= nil then
- self.command=v
- history_index=i
- self._private_cur_pos=#self.command+1
- break
- end
- end
- elseif key == "s" then
- self._private.search_term = self._private.search_term or self.command:sub(1, self._private_cur_pos - 1)
- for i,v in (function(a,i) return itera(1,a,i) end), data.history[self.history_path].table, history_index do
- if v:find(self._private.search_term,1,true) ~= nil then
- self.command=v
- history_index=i
- self._private_cur_pos=#self.command+1
- break
- end
- end
- elseif key == "f" then
- if self._private_cur_pos <= #self.command then
- if have_multibyte_char_at(self.command, self._private_cur_pos) then
- self._private_cur_pos = self._private_cur_pos + 2
- else
- self._private_cur_pos = self._private_cur_pos + 1
- end
- end
- elseif key == "h" then
- if self._private_cur_pos > 1 then
- local offset = 0
- if have_multibyte_char_at(self.command, self._private_cur_pos - 1) then
- offset = 1
- end
- self.command = self.command:sub(1, self._private_cur_pos - 2 - offset) .. self.command:sub(self._private_cur_pos)
- self._private_cur_pos = self._private_cur_pos - 1 - offset
- end
- elseif key == "k" then
- self.command = self.command:sub(1, self._private_cur_pos - 1)
- elseif key == "u" then
- self.command = self.command:sub(self._private_cur_pos, #self.command)
- self._private_cur_pos = 1
- elseif key == "Prior" then
- self._private.search_term = self.command:sub(1, self._private_cur_pos - 1) or ""
- for i,v in (function(a,i) return itera(-1,a,i) end), data.history[self.history_path].table, history_index do
- if v:find(self._private.search_term,1,true) == 1 then
- self.command=v
- history_index=i
- break
- end
- end
- elseif key == "Next" then
- self._private.search_term = self.command:sub(1, self._private_cur_pos - 1) or ""
- for i,v in (function(a,i) return itera(1,a,i) end), data.history[self.history_path].table, history_index do
- if v:find(self._private.search_term,1,true) == 1 then
- self.command=v
- history_index=i
- break
- end
- end
- elseif key == "w" or key == "BackSpace" then
- local wstart = 1
- local wend = 1
- local cword_start_pos = 1
- local cword_end_pos = 1
- while wend < self._private_cur_pos do
- wend = self.command:find("[{[(,.:;_-+=@/ ]", wstart)
- if not wend then wend = #self.command + 1 end
- if self._private_cur_pos >= wstart and self._private_cur_pos <= wend + 1 then
- cword_start_pos = wstart
- cword_end_pos = self._private_cur_pos - 1
- break
- end
- wstart = wend + 1
- end
- self.command = self.command:sub(1, cword_start_pos - 1) .. self.command:sub(cword_end_pos + 1)
- self._private_cur_pos = cword_start_pos
- elseif key == "Delete" then
- -- delete from history only if:
- -- we are not dealing with a new command
- -- the user has not edited an existing entry
- if self.command == data.history[self.history_path].table[history_index] then
- table.remove(data.history[self.history_path].table, history_index)
- if history_index <= history_items(self.history_path) then
- self.command = data.history[self.history_path].table[history_index]
- self._private_cur_pos = #self.command + 2
- elseif history_index > 1 then
- history_index = history_index - 1
-
- self.command = data.history[self.history_path].table[history_index]
- self._private_cur_pos = #self.command + 2
- else
- self.command = ""
- self._private_cur_pos = 1
- end
- end
- end
- elseif mod.Mod1 or mod.Mod3 then
- if key == "b" then
- self._private_cur_pos = cword_start(self.command, self._private_cur_pos)
- elseif key == "f" then
- self._private_cur_pos = cword_end(self.command, self._private_cur_pos)
- elseif key == "d" then
- self.command = self.command:sub(1, self._private_cur_pos - 1) .. self.command:sub(cword_end(self.command, self._private_cur_pos))
- elseif key == "BackSpace" then
- local wstart = cword_start(self.command, self._private_cur_pos)
- self.command = self.command:sub(1, wstart - 1) .. self.command:sub(self._private_cur_pos)
- self._private_cur_pos = wstart
- end
- else
- if self.completion_callback then
- if key == "Tab" or key == "ISO_Left_Tab" then
- if key == "ISO_Left_Tab" or mod.Shift then
- if ncomp == 1 then return end
- if ncomp == 2 then
- self.command = command_before_comp
- self.textbox:set_font(self.font)
- self.textbox:set_markup(prompt_text_with_cursor{
- text = command_before_comp, text_color = self.fg_cursor, cursor_color = self.bg_cursor,
- cursor_pos = self._private_cur_pos, cursor_ul = self.ul_cursor, select_all = self.select_all,
- prompt = self.prompt })
- self._private_cur_pos = cur_pos_before_comp
- ncomp = 1
- return
- end
-
- ncomp = ncomp - 2
- elseif ncomp == 1 then
- command_before_comp = self.command
- cur_pos_before_comp = self._private_cur_pos
- end
- local matches
- self.command, self._private_cur_pos, matches = self.completion_callback(command_before_comp, cur_pos_before_comp, ncomp)
- ncomp = ncomp + 1
- key = ""
- -- execute if only one match found and autoexec flag set
- if matches and #matches == 1 and args.autoexec then
- exec(self, self.exe_callback)
- return
- end
- elseif key ~= "Shift_L" and key ~= "Shift_R" then
- ncomp = 1
- end
- end
-
- -- Typin cases
- if mod.Shift and key == "Insert" then
- local selection = capi.selection()
- if selection then
- -- Remove \n
- local n = selection:find("\n")
- if n then
- selection = selection:sub(1, n - 1)
- end
- self.command = self.command:sub(1, self._private_cur_pos - 1) .. selection .. self.command:sub(self._private_cur_pos)
- self._private_cur_pos = self._private_cur_pos + #selection
- end
- elseif key == "Home" then
- self._private_cur_pos = 1
- elseif key == "End" then
- self._private_cur_pos = #self.command + 1
- elseif key == "BackSpace" then
- if self._private_cur_pos > 1 then
- local offset = 0
- if have_multibyte_char_at(self.command, self._private_cur_pos - 1) then
- offset = 1
- end
- self.command = self.command:sub(1, self._private_cur_pos - 2 - offset) .. self.command:sub(self._private_cur_pos)
- self._private_cur_pos = self._private_cur_pos - 1 - offset
- end
- elseif key == "Delete" then
- self.command = self.command:sub(1, self._private_cur_pos - 1) .. self.command:sub(self._private_cur_pos + 1)
- elseif key == "Left" then
- self._private_cur_pos = self._private_cur_pos - 1
- elseif key == "Right" then
- self._private_cur_pos = self._private_cur_pos + 1
- elseif key == "Prior" then
- if history_index > 1 then
- history_index = history_index - 1
-
- self.command = data.history[self.history_path].table[history_index]
- self._private_cur_pos = #self.command + 2
- end
- elseif key == "Next" then
- if history_index < history_items(self.history_path) then
- history_index = history_index + 1
-
- self.command = data.history[self.history_path].table[history_index]
- self._private_cur_pos = #self.command + 2
- elseif history_index == history_items(self.history_path) then
- history_index = history_index + 1
-
- self.command = ""
- self._private_cur_pos = 1
- end
- else
- -- wlen() is UTF-8 aware but #key is not,
- -- so check that we have one UTF-8 char but advance the cursor of # position
- if key:wlen() == 1 then
- if self.select_all then self.command = "" end
- self.command = self.command:sub(1, self._private_cur_pos - 1) .. key .. self.command:sub(self._private_cur_pos)
- self._private_cur_pos = self._private_cur_pos + #key
- end
- end
- if self._private_cur_pos < 1 then
- self._private_cur_pos = 1
- elseif self._private_cur_pos > #self.command + 1 then
- self._private_cur_pos = #self.command + 1
- end
- self.select_all = nil
- end
-
- update(self)
- if self.changed_callback then
- self.changed_callback(self.command)
- end
- end)
-end
-
-function prompt:stop()
- keygrabber.stop(self._private.grabber)
- history_save(self.history_path)
- if self.done_callback then self.done_callback() end
- return false
-end
-
-local function new(args)
- args = args or {}
-
- args.command = args.text or ""
- args.prompt = args.prompt or ""
- args.text = args.text or ""
- args.font = args.font or beautiful.prompt_font or beautiful.font
- args.bg_cursor = args.bg_cursor or beautiful.prompt_bg_cursor or beautiful.bg_focus or "white"
- args.fg_cursor = args.fg_cursor or beautiful.prompt_fg_cursor or beautiful.fg_focus or "black"
- args.ul_cursor = args.ul_cursor or nil
- args.reset_on_stop = args.reset_on_stop == nil and true or args.reset_on_stop
- args.select_all = args.select_all or nil
- args.highlighter = args.highlighter or nil
- args.hooks = args.hooks or {}
- args.keypressed_callback = args.keypressed_callback or nil
- args.changed_callback = args.changed_callback or nil
- args.done_callback = args.done_callback or nil
- args.history_max = args.history_max or nil
- args.history_path = args.history_path or nil
- args.completion_callback = args.completion_callback or nil
- args.exe_callback = args.exe_callback or nil
- args.textbox = args.textbox or wibox.widget.textbox()
-
- -- Build the hook map
- local hooks = {}
- for _,v in ipairs(args.hooks) do
- if #v == 3 then
- local _,key,callback = unpack(v)
- if type(callback) == "function" then
- hooks[key] = hooks[key] or {}
- hooks[key][#hooks[key]+1] = v
- else
- gdebug.print_warning("The hook's 3rd parameter has to be a function.")
- end
- else
- gdebug.print_warning("The hook has to have 3 parameters.")
- end
- end
- args.hooks = hooks
-
- local ret = gobject({})
- ret._private = {}
- gtable.crush(ret, prompt)
- gtable.crush(ret, args)
-
- return ret
-end
-
-function prompt.mt:__call(...)
- return new(...)
-end
-
-return setmetatable(prompt, prompt.mt)
\ No newline at end of file
diff --git a/widget/app_launcher/rofi_grid.lua b/widget/app_launcher/rofi_grid.lua
new file mode 100644
index 00000000..147c0edd
--- /dev/null
+++ b/widget/app_launcher/rofi_grid.lua
@@ -0,0 +1,722 @@
+local gtable = require("gears.table")
+local gtimer = require("gears.timer")
+local helpers = require(tostring(...):match(".*bling") .. ".helpers")
+local fzy_has_match = helpers.fzy.has_match
+local fzy_score = helpers.fzy.score
+local wibox = require("wibox")
+local ipairs = ipairs
+local pairs = pairs
+local table = table
+local math = math
+
+local rofi_grid = { mt = {} }
+
+local properties = {
+ "entries", "favorites", "page", "lazy_load_widgets",
+ "widget_template", "entry_template",
+ "sort_fn", "search_fn", "search_sort_fn",
+ "sort_alphabetically","reverse_sort_alphabetically,",
+ "wrap_page_scrolling", "wrap_entry_scrolling"
+}
+
+local function build_properties(prototype, prop_names)
+ for _, prop in ipairs(prop_names) do
+ if not prototype["set_" .. prop] then
+ prototype["set_" .. prop] = function(self, value)
+ if self._private[prop] ~= value then
+ self._private[prop] = value
+ self:emit_signal("widget::redraw_needed")
+ self:emit_signal("property::" .. prop, value)
+ end
+ return self
+ end
+ end
+ if not prototype["get_" .. prop] then
+ prototype["get_" .. prop] = function(self)
+ return self._private[prop]
+ end
+ end
+ end
+end
+
+local function has_value(tab, val)
+ for _, value in ipairs(tab) do
+ if val:lower():find(value:lower(), 1, true) then
+ return true
+ end
+ end
+ return false
+end
+
+local function has_entry(entries, entry)
+ for _, looped_entry in ipairs(entries) do
+ if looped_entry.uid == entry.uid then
+ return true
+ end
+ end
+
+ return false
+end
+
+local function scroll(self, dir, page_dir)
+ local grid = self:get_grid()
+ if #grid.children < 1 then
+ self._private.selected_widget = nil
+ self._private.selected_entry = nil
+ return
+ end
+
+ local next_widget_index = nil
+ local grid_orientation = grid:get_orientation()
+
+ if dir == "up" then
+ if grid_orientation == "horizontal" then
+ next_widget_index = grid:index(self:get_selected_widget()) - 1
+ elseif grid_orientation == "vertical" then
+ next_widget_index = grid:index(self:get_selected_widget()) - grid.column_count
+ end
+ elseif dir == "down" then
+ if grid_orientation == "horizontal" then
+ next_widget_index = grid:index(self:get_selected_widget()) + 1
+ elseif grid_orientation == "vertical" then
+ next_widget_index = grid:index(self:get_selected_widget()) + grid.column_count
+ end
+ elseif dir == "left" then
+ if grid_orientation == "horizontal" then
+ next_widget_index = grid:index(self:get_selected_widget()) - grid.row_count
+ elseif grid_orientation == "vertical" then
+ next_widget_index = grid:index(self:get_selected_widget()) - 1
+ end
+ elseif dir == "right" then
+ if grid_orientation == "horizontal" then
+ next_widget_index = grid:index(self:get_selected_widget()) + grid.row_count
+ elseif grid_orientation == "vertical" then
+ next_widget_index = grid:index(self:get_selected_widget()) + 1
+ end
+ end
+
+ local next_widget = grid.children[next_widget_index]
+ if next_widget then
+ next_widget:select("scroll")
+ self:emit_signal("scroll", self:get_index_of_entry(self:get_selected_entry()))
+ else
+ if dir == "up" or dir == "left" then
+ self:page_backward(page_dir or dir)
+ elseif dir == "down" or dir == "right" then
+ self:page_forward(page_dir or dir)
+ end
+ end
+end
+
+local function entry_widget(rofi_grid, entry)
+ if rofi_grid._private.entries_widgets_cache[entry.uid] then
+ return rofi_grid._private.entries_widgets_cache[entry.uid]
+ end
+ local widget = rofi_grid._private.entry_template(entry, rofi_grid)
+
+ function widget:select(context)
+ if rofi_grid:get_selected_widget() then
+ rofi_grid:get_selected_widget():unselect()
+ end
+
+ rofi_grid._private.selected_widget = self
+ rofi_grid._private.selected_entry = entry
+
+ local index = rofi_grid:get_index_of_entry(entry)
+ self:emit_signal("select", index, context)
+ rofi_grid:emit_signal("select", index, context)
+ end
+
+ function widget:unselect()
+ rofi_grid._private.selected_widget = nil
+ rofi_grid._private.selected_entry = nil
+
+ widget:emit_signal("unselect")
+ rofi_grid:emit_signal("unselect")
+ end
+
+ function widget:is_selected()
+ return rofi_grid._private.selected_widget == self
+ end
+
+ -- Should be overriden
+ function widget:exec() end
+
+ function widget:select_or_exec(context)
+ if self:is_selected() then
+ self:exec()
+ else
+ self:select(context)
+ end
+ end
+
+ rofi_grid:emit_signal("entry_widget::add", widget, entry)
+
+ rofi_grid._private.entries_widgets_cache[entry.uid] = widget
+ return rofi_grid._private.entries_widgets_cache[entry.uid]
+end
+
+local function default_search_sort_fn(text, a, b)
+ return fzy_score(text, a.name) > fzy_score(text, b.name)
+end
+
+local function default_search_fn(text, entry)
+ if fzy_has_match(text, entry.name) then
+ return true
+ end
+ return false
+end
+
+local function default_sort_fn(self, a, b)
+ local is_a_favorite = has_value(self.favorites, a.id)
+ local is_b_favorite = has_value(self.favorites, b.id)
+
+ -- Sort the favorite apps first
+ if is_a_favorite and not is_b_favorite then
+ return true
+ elseif not is_a_favorite and is_b_favorite then
+ return false
+ end
+
+ -- Sort alphabetically if specified
+ if self.sort_alphabetically then
+ return a.name:lower() < b.name:lower()
+ elseif self.reverse_sort_alphabetically then
+ return b.name:lower() > a.name:lower()
+ else
+ return true
+ end
+end
+
+function rofi_grid:set_widget_template(widget_template)
+ self._private.text_input = widget_template:get_children_by_id("text_input_role")[1]
+ self._private.grid = widget_template:get_children_by_id("grid_role")[1]
+ self._private.scrollbar = widget_template:get_children_by_id("scrollbar_role")
+ if self._private.scrollbar then
+ self._private.scrollbar = self._private.scrollbar[1]
+ end
+
+ widget_template:connect_signal("button::press", function(_, lx, ly, button, mods, find_widgets_result)
+ if button == 4 then
+ if self:get_grid():get_orientation() == "horizontal" then
+ self:scroll_up()
+ else
+ self:scroll_left("up")
+ end
+ elseif button == 5 then
+ if self:get_grid():get_orientation() == "horizontal" then
+ self:scroll_down()
+ else
+ self:scroll_right("down")
+ end
+ end
+ end)
+
+ self:get_text_input():connect_signal("property::text", function(_, text)
+ if text == self:get_text() then
+ return
+ end
+
+ self._private.text = text
+ self._private.search_timer:again()
+ end)
+
+ self:get_text_input():connect_signal("key::release", function(_, mod, key, cmd)
+ if key == "Up" then
+ self:scroll_up()
+ end
+ if key == "Down" then
+ self:scroll_down()
+ end
+ if key == "Left" then
+ self:scroll_left()
+ end
+ if key == "Right" then
+ self:scroll_right()
+ end
+ end)
+
+ local scrollbar = self:get_scrollbar()
+ if scrollbar then
+ function scrollbar:set_value(value, instant)
+ value = math.min(value, self:get_maximum())
+ value = math.max(value, self:get_minimum())
+ local changed = self._private.value ~= value
+
+ self._private.value = value
+
+ if changed then
+ self:emit_signal( "property::value", value, instant)
+ self:emit_signal( "widget::redraw_needed" )
+ end
+ end
+
+ self:connect_signal("scroll", function(self, new_index)
+ scrollbar:set_value(new_index, true)
+ end)
+
+ self:connect_signal("page::forward", function(self, new_index)
+ scrollbar:set_value(new_index, true)
+ end)
+
+ self:connect_signal("page::backward", function(self, new_index)
+ scrollbar:set_value(new_index, true)
+ end)
+
+ self:connect_signal("search", function(self, text, new_index)
+ scrollbar:set_maximum(math.max(2, #self:get_matched_entries()))
+ if new_index then
+ scrollbar:set_value(new_index, true)
+ end
+ end)
+
+ self:connect_signal("select", function(self, new_index)
+ scrollbar:set_value(new_index, true)
+ end)
+
+ scrollbar:connect_signal("property::value", function(_, value, instant)
+ if instant ~= true then
+ self:scroll_to_index(value)
+ end
+ end)
+ end
+
+ self._private.max_entries_per_page = self:get_grid().column_count * self:get_grid().row_count
+ self._private.entries_per_page = self._private.max_entries_per_page
+
+ self:set_widget(widget_template)
+end
+
+function rofi_grid:add_entry(entry)
+ table.insert(self._private.entries, entry)
+ self:set_sort_fn()
+ self:reset()
+end
+
+function rofi_grid:set_entries(new_entries, sort_fn)
+ -- Remove old entries that are not in the new entries table
+ for index, entry in pairs(self:get_entries()) do
+ if has_entry(new_entries, entry) == false then
+ table.remove(self._private.entries, index)
+
+ if self._private.entries_widgets_cache[entry.uid] then
+ self._private.entries_widgets_cache[entry.uid]:emit_signal("removed")
+ self._private.entries_widgets_cache[entry.uid] = nil
+ end
+ end
+ end
+
+ -- Add new entries that are not in the old entries table
+ for _, entry in ipairs(new_entries) do
+ if has_entry(self:get_entries(), entry) == false then
+ table.insert(self._private.entries, entry)
+
+ if self:get_lazy_load_widgets() == false then
+ self._private.entries_widgets_cache[entry.uid] = entry_widget(self, entry)
+ end
+ end
+ end
+
+ self:set_sort_fn(sort_fn)
+ self:reset()
+end
+
+function rofi_grid:set_favorites(favorites)
+ self._private.favorites = favorites
+ if self:get_entries() and #self:get_entries() > 1 then
+ self:set_sort_fn()
+ self:refresh()
+ end
+end
+
+function rofi_grid:refresh()
+ local max_entry_index_to_include = self._private.entries_per_page * self:get_current_page()
+ local min_entry_index_to_include = max_entry_index_to_include - self._private.entries_per_page
+
+ self:get_grid():reset()
+
+ for index, entry in ipairs(self:get_matched_entries()) do
+ -- Only add widgets that are between this range (part of the current page)
+ if index > min_entry_index_to_include and index <= max_entry_index_to_include then
+ self:get_grid():add(entry_widget(self, entry))
+ end
+ end
+end
+
+function rofi_grid:reset()
+ self:get_grid():reset()
+ self._private.matched_entries = self:get_entries()
+ self._private.entries_per_page = self._private.max_entries_per_page
+ self._private.pages_count = math.ceil(#self:get_entries() / self._private.entries_per_page)
+ self._private.current_page = 1
+
+ for index, entry in ipairs(self:get_entries()) do
+ -- Only add the entrys that are part of the first page
+ if index <= self._private.entries_per_page then
+ self:get_grid():add(entry_widget(self, entry))
+ else
+ break
+ end
+ end
+
+ local widget = self:get_grid():get_widgets_at(1, 1)
+ if widget then
+ widget = widget[1]
+ if widget then
+ widget:select("new_page")
+ end
+ end
+
+ local scrollbar = self:get_scrollbar()
+ if scrollbar then
+ if #self:get_grid().children <= 0 then
+ self:get_scrollbar():set_visible(false)
+ else
+ self:get_scrollbar():set_visible(true)
+ scrollbar:set_maximum(#self:get_entries())
+ scrollbar:set_value(1)
+ end
+ end
+
+ self:get_text_input():set_text("")
+end
+
+function rofi_grid:set_sort_fn(sort_fn)
+ if sort_fn ~= nil then
+ self._private.sort_fn = sort_fn
+ end
+ if self._private.sort_fn ~= nil then
+ table.sort(self._private.entries, self._private.sort_fn)
+ end
+end
+
+function rofi_grid:search()
+ local text = self:get_text()
+ local old_pos = self:get_grid():get_widget_position(self:get_selected_widget())
+
+ -- Reset all the matched entrys
+ self._private.matched_entries = {}
+ -- Remove all the grid widgets
+ self:get_grid():reset()
+
+ if text == "" then
+ self._private.matched_entries = self:get_entries()
+ else
+ for _, entry in ipairs(self:get_entries()) do
+ text = text:gsub( "%W", "" )
+ if self._private.search_fn(text:lower(), entry) then
+ table.insert(self:get_matched_entries(), entry)
+ end
+ end
+
+ if self:get_search_sort_fn() then
+ table.sort(self:get_matched_entries(), function(a, b)
+ return self._private.search_sort_fn(text, a, b)
+ end)
+ end
+ end
+ for _, entry in ipairs(self._private.matched_entries) do
+ -- Only add the widgets for entrys that are part of the first page
+ if #self:get_grid().children + 1 <= self._private.max_entries_per_page then
+ self:get_grid():add(entry_widget(self, entry))
+ end
+ end
+
+ -- Recalculate the entrys per page based on the current matched entrys
+ self._private.entries_per_page = math.min(#self:get_matched_entries(), self._private.max_entries_per_page)
+
+ -- Recalculate the pages count based on the current entrys per page
+ self._private.pages_count = math.ceil(math.max(1, #self:get_matched_entries()) / math.max(1, self._private.entries_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 entry whose index is the same as the entry index that was previously selected
+ -- and if matched_entries.length < current_index it will instead select the entry with the greatest index
+ if self._private.try_to_keep_index_after_searching then
+ local widget_at_old_pos = self:get_grid():get_widgets_at(old_pos.row, old_pos.col)
+ if widget_at_old_pos and widget_at_old_pos[1] then
+ widget_at_old_pos[1]:select("search")
+ else
+ local widget = self:get_grid().children[#self:get_grid().children]
+ widget:select("search")
+ end
+ -- Otherwise select the first entry on the list
+ elseif self:get_grid().children[1] then
+ local widget = self:get_grid().children[1]
+ widget:select("search")
+ end
+
+ if #self:get_grid().children <= 0 then
+ self:get_scrollbar():set_visible(false)
+ else
+ self:get_scrollbar():set_visible(true)
+ end
+
+ self:emit_signal("search", self:get_text(), self:get_index_of_entry(self:get_selected_entry()))
+end
+
+function rofi_grid:scroll_to_index(index)
+ local selected_widget_index = self:get_grid():index(self:get_selected_widget())
+ if index == selected_widget_index then
+ return
+ end
+
+ local page = self:get_page_of_index(index)
+ if self:get_current_page() ~= page then
+ self:set_page(page)
+ end
+
+ local index_within_page = index - (page - 1) * self._private.entries_per_page
+ self:get_grid().children[index_within_page]:select("scroll")
+end
+
+function rofi_grid:scroll_up(page_dir)
+ scroll(self, "up", page_dir)
+end
+
+function rofi_grid:scroll_down(page_dir)
+ scroll(self, "down", page_dir)
+end
+
+function rofi_grid:scroll_left(page_dir)
+ scroll(self, "left", page_dir)
+end
+
+function rofi_grid:scroll_right(page_dir)
+ scroll(self, "right", page_dir)
+end
+
+function rofi_grid:page_forward(dir)
+ local min_entry_index_to_include = 0
+ local max_entry_index_to_include = self._private.entries_per_page
+
+ if self:get_current_page() < self:get_pages_count() then
+ min_entry_index_to_include = self._private.entries_per_page * self:get_current_page()
+ self._private.current_page = self:get_current_page() + 1
+ max_entry_index_to_include = self._private.entries_per_page * self:get_current_page()
+ elseif self._private.wrap_page_scrolling and #self:get_matched_entries() >= self._private.max_entries_per_page then
+ self._private.current_page = 1
+ min_entry_index_to_include = 0
+ max_entry_index_to_include = self._private.entries_per_page
+ elseif self._private.wrap_entry_scrolling then
+ local widget = self:get_grid():get_widgets_at(1, 1)[1]
+ widget:select("new_page")
+ self:emit_signal("scroll", self:get_index_of_entry(self:get_selected_entry()))
+ return
+ else
+ return
+ end
+
+ local pos = self:get_grid():get_widget_position(self:get_selected_widget())
+
+ -- Remove the current page entrys from the grid
+ self:get_grid():reset()
+
+ for index, entry in ipairs(self:get_matched_entries()) do
+ -- Only add widgets that are between this range (part of the current page)
+ if index > min_entry_index_to_include and index <= max_entry_index_to_include then
+ self:get_grid():add(entry_widget(self, entry))
+ end
+ end
+
+ if self:get_current_page() > 1 or self._private.wrap_page_scrolling then
+ local widget = nil
+ if dir == "down" then
+ widget = self:get_grid():get_widgets_at(1, 1)[1]
+ elseif dir == "right" then
+ widget = self:get_grid():get_widgets_at(pos.row, 1)
+ if widget then
+ widget = widget[1]
+ end
+ if widget == nil then
+ widget = self:get_grid().children[#self:get_grid().children]
+ end
+ end
+ widget:select("new_page")
+ end
+
+ self:emit_signal("page::forward", self:get_index_of_entry(self:get_selected_entry()))
+end
+
+function rofi_grid:page_backward(dir)
+ if self:get_current_page() > 1 then
+ self._private.current_page = self:get_current_page() - 1
+ elseif self._private.wrap_page_scrolling and #self:get_matched_entries() >= self._private.max_entries_per_page then
+ self._private.current_page = self:get_pages_count()
+ elseif self._private.wrap_entry_scrolling then
+ local widget = self:get_grid().children[#self:get_grid().children]
+ widget:select("new_page")
+ self:emit_signal("scroll", self:get_index_of_entry(self:get_selected_entry()))
+ return
+ else
+ return
+ end
+
+ local pos = self:get_grid():get_widget_position(self:get_selected_widget())
+
+ -- Remove the current page entrys from the grid
+ self:get_grid():reset()
+
+ local max_entry_index_to_include = self._private.entries_per_page * self:get_current_page()
+ local min_entry_index_to_include = max_entry_index_to_include - self._private.entries_per_page
+
+ for index, entry in ipairs(self:get_matched_entries()) do
+ -- Only add widgets that are between this range (part of the current page)
+ if index > min_entry_index_to_include and index <= max_entry_index_to_include then
+ self:get_grid():add(entry_widget(self, entry))
+ end
+ end
+
+ local widget = nil
+ if self:get_current_page() < self:get_pages_count() then
+ if dir == "up" then
+ widget = self:get_grid().children[#self:get_grid().children]
+ else
+ -- Keep the same row from last page
+ local _, columns = self:get_grid():get_dimension()
+ widget = self:get_grid():get_widgets_at(pos.row, columns)[1]
+ end
+ elseif self._private.wrap_page_scrolling then
+ widget = self:get_grid().children[#self:get_grid().children]
+ end
+ widget:select("new_page")
+
+ self:emit_signal("page::backward", self:get_index_of_entry(self:get_selected_entry()))
+end
+
+function rofi_grid:set_page(page)
+ self:get_grid():reset()
+ self._private.matched_entries = self:get_entries()
+ self._private.entries_per_page = self._private.max_entries_per_page
+ self._private.pages_count = math.ceil(#self:get_entries() / self._private.entries_per_page)
+ self._private.current_page = page
+
+ local max_entry_index_to_include = self._private.entries_per_page * self:get_current_page()
+ local min_entry_index_to_include = max_entry_index_to_include - self._private.entries_per_page
+
+ for index, entry in ipairs(self:get_matched_entries()) do
+ -- Only add widgets that are between this range (part of the current page)
+ if index > min_entry_index_to_include and index <= max_entry_index_to_include then
+ self:get_grid():add(entry_widget(self, entry))
+ end
+ end
+
+ local widget = self:get_grid():get_widgets_at(1, 1)
+ if widget then
+ widget = widget[1]
+ if widget then
+ widget:select("new_page")
+ end
+ end
+end
+
+function rofi_grid:get_scrollbar()
+ return self._private.scrollbar
+end
+
+function rofi_grid:get_text_input()
+ return self._private.text_input
+end
+
+function rofi_grid:get_grid()
+ return self._private.grid
+end
+
+function rofi_grid:get_entries_per_page()
+ return self._private.entries_per_page
+end
+
+function rofi_grid:get_pages_count()
+ return self._private.pages_count
+end
+
+function rofi_grid:get_current_page()
+ return self._private.current_page
+end
+
+function rofi_grid:get_matched_entries()
+ return self._private.matched_entries
+end
+
+function rofi_grid:get_text()
+ return self._private.text
+end
+
+function rofi_grid:get_selected_widget()
+ return self._private.selected_widget
+end
+
+function rofi_grid:get_selected_entry()
+ return self._private.selected_entry
+end
+
+function rofi_grid:get_page_of_entry(entry)
+ return math.floor((self:get_index_of_entry(entry) - 1) / self._private.entries_per_page) + 1
+end
+
+function rofi_grid:get_page_of_index(index)
+ return math.floor((index - 1) / self._private.entries_per_page) + 1
+end
+
+function rofi_grid:get_index_of_entry(entry)
+ for index, matched_entry in ipairs(self:get_matched_entries()) do
+ if matched_entry == entry then
+ return index
+ end
+ end
+end
+
+function rofi_grid:get_entry_of_index(index)
+ return self:get_matched_entries()[index]
+end
+
+local function new()
+ local widget = wibox.container.background()
+ gtable.crush(widget, rofi_grid, true)
+
+ local wp = widget._private
+ wp.entries_widgets_cache = setmetatable({}, { __mode = "v" })
+
+ wp.entries = {}
+ wp.favorites = {}
+ wp.sort_alphabetically = true
+ wp.reverse_sort_alphabetically = false
+ wp.sort_fn = function(a, b)
+ return default_sort_fn(widget, a, b)
+ end
+ wp.search_fn = function(text, entry)
+ return default_search_fn(text, entry)
+ end
+ wp.search_sort_fn = function(text, a, b)
+ return default_search_sort_fn(text, a, b)
+ end
+ wp.try_to_keep_index_after_searching = false
+ wp.wrap_page_scrolling = true
+ wp.wrap_entry_scrolling = true
+ wp.lazy_load_widgets = false
+
+ wp.text = ""
+ wp.pages_count = 0
+ wp.current_page = 1
+ wp.search_timer = gtimer {
+ timeout = 0.05,
+ call_now = false,
+ autostart = false,
+ single_shot = true,
+ callback = function()
+ widget:search()
+ end
+ }
+
+ return widget
+end
+
+function rofi_grid.mt:__call(...)
+ return new(...)
+end
+
+build_properties(rofi_grid, properties)
+
+return setmetatable(rofi_grid, rofi_grid.mt)
diff --git a/widget/app_launcher/run-as-root.sh b/widget/app_launcher/run-as-root.sh
new file mode 100755
index 00000000..61b72fc8
--- /dev/null
+++ b/widget/app_launcher/run-as-root.sh
@@ -0,0 +1,5 @@
+#!/bin/bash
+
+PROGRAM=$1
+
+pkexec env DISPLAY=$DISPLAY XAUTHORITY=$XAUTHORITY $PROGRAM
diff --git a/widget/app_launcher/text_input.lua b/widget/app_launcher/text_input.lua
new file mode 100644
index 00000000..7b60fe86
--- /dev/null
+++ b/widget/app_launcher/text_input.lua
@@ -0,0 +1,930 @@
+-------------------------------------------
+-- @author https://github.com/Kasper24
+-- @copyright 2021-2022 Kasper24
+-------------------------------------------
+local lgi = require('lgi')
+local Gtk = lgi.require('Gtk', '3.0')
+local Gdk = lgi.require('Gdk', '3.0')
+local Pango = lgi.Pango
+local awful = require("awful")
+local gtable = require("gears.table")
+local gtimer = require("gears.timer")
+local gcolor = require("gears.color")
+local wibox = require("wibox")
+local beautiful = require("beautiful")
+local abs = math.abs
+local ipairs = ipairs
+local string = string
+local capi = {
+ awesome = awesome,
+ root = root,
+ tag = tag,
+ client = client,
+ mouse = mouse,
+ mousegrabber = mousegrabber
+}
+
+local text_input = {
+ mt = {}
+}
+
+local properties = {
+ "unfocus_keys",
+ "unfocus_on_root_clicked", "unfocus_on_client_clicked", "unfocus_on_client_focus",
+ "unfocus_on_mouse_leave", "unfocus_on_tag_change",
+ "focus_on_subject_mouse_enter", "unfocus_on_subject_mouse_leave",
+ "click_timeout",
+ "reset_on_unfocus",
+ "text_color",
+ "placeholder", "initial",
+ "pattern", "obscure",
+ "cursor_blink", "cursor_blink_rate","cursor_size", "cursor_bg",
+ "selection_bg"
+}
+
+text_input.patterns = {
+ numbers = "[%d.]*",
+ numbers_one_decimal = "%d*%.?%d*",
+ round_numbers = "[0-9]*",
+ email = "%S+@%S+%.%S+",
+ time = "%d%d?:%d%d:%d%d?|%d%d?:%d%d",
+ date = "%d%d%d%d%-%d%d%-%d%d|%d%d?/%d%d?/%d%d%d%d|%d%d?%.%d%d?%.%d%d%d%d",
+ phone = "%+?%d[%d%-%s]+%d",
+ url = "https?://[%w-_%.]+%.[%w]+/?[%w-_%.?=%+]*",
+ email = "[%w._%-%+]+@[%w._%-]+%.%w+",
+ alphanumeric = "%w+",
+ letters = "[a-zA-Z]+"
+}
+
+local function build_properties(prototype, prop_names)
+ for _, prop in ipairs(prop_names) do
+ if not prototype["set_" .. prop] then
+ prototype["set_" .. prop] = function(self, value)
+ if self._private[prop] ~= value then
+ self._private[prop] = value
+ self:emit_signal("widget::redraw_needed")
+ self:emit_signal("property::" .. prop, value)
+ end
+ return self
+ end
+ end
+ if not prototype["get_" .. prop] then
+ prototype["get_" .. prop] = function(self)
+ return self._private[prop]
+ end
+ end
+ end
+end
+
+local function has_value(tab, val)
+ for _, value in ipairs(tab) do
+ if val:lower():find(value:lower(), 1, true) then
+ return true
+ end
+ end
+ return false
+end
+
+local function is_word_char(c)
+ if string.find(c, "[{[(,.:;_-+=@/ ]") then
+ return false
+ else
+ return true
+ end
+end
+
+local function cword_start(s, pos)
+ local i = pos
+ if i > 1 then
+ i = i - 1
+ end
+ while i >= 1 and not is_word_char(s:sub(i, i)) do
+ i = i - 1
+ end
+ while i >= 1 and is_word_char(s:sub(i, i)) do
+ i = i - 1
+ end
+ if i <= #s then
+ i = i + 1
+ end
+ return i
+end
+
+local function cword_end(s, pos)
+ local i = pos
+ while i <= #s and not is_word_char(s:sub(i, i)) do
+ i = i + 1
+ end
+ while i <= #s and is_word_char(s:sub(i, i)) do
+ i = i + 1
+ end
+ return i
+end
+
+local function set_mouse_cursor(cursor)
+ capi.root.cursor(cursor)
+ local wibox = capi.mouse.current_wibox
+ if wibox then
+ wibox.cursor = cursor
+ end
+end
+
+local function single_double_triple_tap(self, args)
+ local wp = self._private
+
+ if wp.click_timer == nil then
+ wp.click_timer = gtimer {
+ timeout = wp.click_timeout,
+ autostart = false,
+ call_now = false,
+ single_shot = true,
+ callback = function()
+ wp.click_count = 0
+ end
+ }
+ end
+
+ wp.click_timer:again()
+ wp.click_count = wp.click_count + 1
+ if wp.click_count == 1 then
+ args.on_single_click()
+ elseif wp.click_count == 2 then
+ args.on_double_click()
+ elseif wp.click_count == 3 then
+ args.on_triple_click()
+ wp.click_count = 0
+ end
+end
+
+local function run_keygrabber(self)
+ local wp = self._private
+ wp.keygrabber = awful.keygrabber.run(function(modifiers, key, event)
+ if event ~= "press" then
+ self:emit_signal("key::release", modifiers, key, event)
+ return
+ end
+ self:emit_signal("key::press", modifiers, key, event)
+
+ -- Convert index array to hash table
+ local mod = {}
+ for _, v in ipairs(modifiers) do
+ mod[v] = true
+ end
+
+ if mod.Control then
+ if key == "a" then
+ self:select_all()
+ elseif key == "c" then
+ self:copy()
+ elseif key == "v" then
+ self:paste()
+ elseif key == "b" or key == "Left" then
+ self:set_cursor_index_to_word_start()
+ elseif key == "f" or key == "Right" then
+ self:set_cursor_index_to_word_end()
+ elseif key == "d" then
+ self:delete_next_word()
+ elseif key == "BackSpace" then
+ self:delete_previous_word()
+ end
+ elseif mod.Shift and key:wlen() ~= 1 then
+ if key =="Left" then
+ self:decremeant_selection_end_index()
+ elseif key == "Right" then
+ self:increamant_selection_end_index()
+ end
+ else
+ if has_value(wp.unfocus_keys, key) then
+ self:unfocus()
+ end
+
+ if mod.Shift and key == "Insert" then
+ self:paste()
+ elseif key == "Home" then
+ self:set_cursor_index(0)
+ elseif key == "End" then
+ self:set_cursor_index_to_end()
+ elseif key == "BackSpace" then
+ self:delete_text()
+ elseif key == "Delete" then
+ self:delete_text_after_cursor()
+ elseif key == "Left" then
+ self:decremeant_cursor_index()
+ elseif key == "Right" then
+ self:increamant_cursor_index()
+ elseif key:wlen() == 1 then
+ self:update_text(key)
+ end
+ end
+ end)
+end
+
+function text_input:set_widget_template(widget_template)
+ local wp = self._private
+
+ wp.text_widget = widget_template:get_children_by_id("text_role")[1]
+ wp.text_widget.forced_width = math.huge
+ local text_draw = wp.text_widget.draw
+ if self:get_initial() then
+ self:replace_text(self:get_initial())
+ end
+
+ local placeholder_widget = widget_template:get_children_by_id("placeholder_role")
+ if placeholder_widget then
+ placeholder_widget = placeholder_widget[1]
+ end
+
+ function wp.text_widget:draw(context, cr, width, height)
+ local _, logical_rect = self._private.layout:get_pixel_extents()
+
+ -- Selection bg
+ cr:set_source(gcolor.change_opacity(wp.selection_bg, wp.selection_opacity))
+ cr:rectangle(
+ wp.selection_start_x,
+ logical_rect.y - 3,
+ wp.selection_end_x - wp.selection_start_x,
+ logical_rect.y + logical_rect.height + 6
+ )
+ cr:fill()
+
+ -- Cursor
+ cr:set_source(gcolor.change_opacity(wp.cursor_bg, wp.cursor_opacity))
+ cr:set_line_width(wp.cursor_width)
+ cr:move_to(wp.cursor_x, logical_rect.y - 3)
+ cr:line_to(wp.cursor_x, logical_rect.y + logical_rect.height + 6)
+ cr:stroke()
+
+ cr:set_source(gcolor(wp.text_color))
+ text_draw(self, context, cr, width, height)
+
+ if self:get_text() == "" and placeholder_widget then
+ placeholder_widget.visible = true
+ elseif placeholder_widget then
+ placeholder_widget.visible = false
+ end
+ end
+
+ local function on_drag(_, lx, ly)
+ lx, ly = wp.hierarchy:get_matrix_from_device():transform_point(lx, ly)
+ if abs(lx - wp.press_pos.lx) > 2 or abs(ly - wp.press_pos.ly) > 2 then
+ if self:get_mode() ~= "overwrite" then
+ self:set_selection_start_index_from_x_y(wp.press_pos.lx, wp.press_pos.ly)
+ end
+ self:set_selection_end_index_from_x_y(lx, ly)
+ end
+ end
+
+ wp.text_widget:connect_signal("button::press", function(_, lx, ly, button, mods, find_widgets_result)
+ if button == 1 then
+ single_double_triple_tap(self, {
+ on_single_click = function()
+ self:focus()
+ self:set_cursor_index_from_x_y(lx, ly)
+ end,
+ on_double_click = function()
+ self:set_selection_to_word()
+ end,
+ on_triple_click = function()
+ self:select_all()
+ end
+ })
+
+ wp.press_pos = { lx = lx, ly = ly }
+ wp.hierarchy = find_widgets_result.hierarchy
+ find_widgets_result.drawable:connect_signal("mouse::move", on_drag)
+ end
+ end)
+
+ wp.text_widget:connect_signal("button::release", function(_, lx, ly, button, mods, find_widgets_result)
+ if button == 1 then
+ find_widgets_result.drawable:disconnect_signal("mouse::move", on_drag)
+ end
+ end)
+
+ wp.text_widget:connect_signal("mouse::enter", function()
+ set_mouse_cursor("xterm")
+ end)
+
+ wp.text_widget:connect_signal("mouse::leave", function(_, find_widgets_result)
+ if self:get_focused() == false then
+ set_mouse_cursor("left_ptr")
+ end
+
+ find_widgets_result.drawable:disconnect_signal("mouse::move", on_drag)
+ if wp.unfocus_on_mouse_leave then
+ self:unfocus()
+ end
+ end)
+
+ self:set_widget(widget_template)
+end
+
+function text_input:get_mode()
+ return self._private.mode
+end
+
+function text_input:set_focused(focused)
+ if focused == true then
+ self:focus()
+ else
+ self:unfocus()
+ end
+end
+
+function text_input:set_pattern(pattern)
+ self._private.pattern = text_input.patterns[pattern]
+end
+
+function text_input:set_obscure(obscure)
+ self._private.obscure = obscure
+ self:set_text(self:get_text())
+end
+
+function text_input:toggle_obscure()
+ self:set_obscure(not self._private.obscure)
+end
+
+function text_input:set_initial(initial)
+ self._private.initial = initial
+ self:replace_text(initial)
+end
+
+function text_input:update_text(text)
+ if self:get_mode() == "insert" then
+ self:insert_text(text)
+ else
+ self:overwrite_text(text)
+ end
+end
+
+function text_input:set_text(text)
+ self._private.text_buffer = text
+
+ if self:get_obscure() then
+ self:get_text_widget():set_text(string.rep("*", #text))
+ else
+ self:get_text_widget():set_text(text)
+ end
+end
+
+function text_input:replace_text(text)
+ self._private.text_buffer = text
+
+ if self:get_obscure() then
+ self:set_text(string.rep("*", #text))
+ else
+ self:set_text(text)
+ end
+
+ if self:get_text() == "" then
+ self:set_cursor_index(0)
+ else
+ self:set_cursor_index(#text)
+ end
+end
+
+function text_input:insert_text(text)
+ local wp = self._private
+
+ local old_text = self:get_text()
+ local cursor_index = self:get_cursor_index()
+ local left_text = old_text:sub(1, cursor_index) .. text
+ local right_text = old_text:sub(cursor_index + 1)
+ local new_text = left_text .. right_text
+ if wp.pattern then
+ new_text = new_text:match(wp.pattern)
+ if new_text then
+ self:set_text(new_text)
+ self:set_cursor_index(self:get_cursor_index() + #text)
+ self:emit_signal("property::text", self:get_text())
+ end
+ else
+ self:set_text(new_text)
+ self:set_cursor_index(self:get_cursor_index() + #text)
+ self:emit_signal("property::text", self:get_text())
+ end
+end
+
+function text_input:overwrite_text(text)
+ local wp = self._private
+
+ local start_pos = wp.selection_start
+ local end_pos = wp.selection_end
+ if start_pos > end_pos then
+ start_pos, end_pos = end_pos, start_pos
+ end
+
+ local old_text = self:get_text()
+ local left_text = old_text:sub(1, start_pos)
+ local right_text = old_text:sub(end_pos + 1)
+ local new_text = left_text .. text .. right_text
+
+ if wp.pattern then
+ new_text = new_text:match(wp.pattern)
+ if new_text then
+ self:set_text(new_text)
+ self:set_cursor_index(#left_text + 1)
+ self:emit_signal("property::text", self:get_text())
+ end
+ else
+ self:set_text(new_text)
+ self:set_cursor_index(#left_text + 1)
+ self:emit_signal("property::text", self:get_text())
+ end
+end
+
+function text_input:copy()
+ local wp = self._private
+ if self:get_mode() == "overwrite" then
+ local text = self:get_text()
+ local start_pos = self._private.selection_start
+ local end_pos = self._private.selection_end
+ if start_pos > end_pos then
+ start_pos, end_pos = end_pos + 1, start_pos
+ end
+ text = text:sub(start_pos, end_pos)
+ wp.clipboard:set_text(text, -1)
+ end
+end
+
+function text_input:paste()
+ local wp = self._private
+
+ wp.clipboard:request_text(function(clipboard, text)
+ if text then
+ self:update_text(text)
+ end
+ end)
+end
+
+function text_input:delete_next_word()
+ local old_text = self:get_text()
+ local cursor_index = self:get_cursor_index()
+
+ local left_text = old_text:sub(1, cursor_index)
+ local right_text = old_text:sub(cword_end(old_text, cursor_index + 1))
+ self:set_text(left_text .. right_text)
+ self:emit_signal("property::text", self:get_text())
+end
+
+function text_input:delete_previous_word()
+ local old_text = self:get_text()
+ local cursor_index = self:get_cursor_index()
+ local wstart = cword_start(old_text, cursor_index + 1) - 1
+ local left_text = old_text:sub(1, wstart)
+ local right_text = old_text:sub(cursor_index + 1)
+ self:set_text(left_text .. right_text)
+ self:set_cursor_index(wstart)
+ self:emit_signal("property::text", self:get_text())
+end
+
+function text_input:delete_text()
+ if self:get_mode() == "insert" then
+ self:delete_text_before_cursor()
+ else
+ self:overwrite_text("")
+ end
+end
+
+function text_input:delete_text_before_cursor()
+ local cursor_index = self:get_cursor_index()
+ if cursor_index > 0 then
+ local old_text = self:get_text()
+ local left_text = old_text:sub(1, cursor_index - 1)
+ local right_text = old_text:sub(cursor_index + 1)
+ self:set_text(left_text .. right_text)
+ self:set_cursor_index(cursor_index - 1)
+ self:emit_signal("property::text", self:get_text())
+ end
+end
+
+function text_input:delete_text_after_cursor()
+ local cursor_index = self:get_cursor_index()
+ if cursor_index < #self:get_text() then
+ local old_text = self:get_text()
+ local left_text = old_text:sub(1, cursor_index)
+ local right_text = old_text:sub(cursor_index + 2)
+ self:set_text(left_text .. right_text)
+ self:emit_signal("property::text", self:get_text())
+ end
+end
+
+function text_input:get_text()
+ return self._private.text_buffer or ""
+end
+
+function text_input:get_text_widget()
+ return self._private.text_widget
+end
+
+function text_input:show_selection()
+ self._private.selection_opacity = 1
+ self:get_text_widget():emit_signal("widget::redraw_needed")
+end
+
+function text_input:hide_selection()
+ self._private.selection_opacity = 0
+ self:get_text_widget():emit_signal("widget::redraw_needed")
+end
+
+function text_input:select_all()
+ if self:get_text() == "" then
+ return
+ end
+
+ self:set_selection_start_index(0)
+ self:set_selection_end_index(#self:get_text())
+end
+
+function text_input:set_selection_to_word()
+ if self:get_text() == "" then
+ return
+ end
+
+ local word_start_index = cword_start(self:get_text(), self:get_cursor_index() + 1) - 1
+ local word_end_index = cword_end(self:get_text(), self:get_cursor_index() + 1) - 1
+
+ self:set_selection_start_index(word_start_index)
+ self:set_selection_end_index(word_end_index)
+end
+
+function text_input:set_selection_start_index(index)
+ if #self:get_text() == 0 then
+ return
+ end
+
+ index = math.max(math.min(index, #self:get_text()), 0)
+
+ local layout = self:get_text_widget()._private.layout
+ local strong_pos, weak_pos = layout:get_caret_pos(index)
+ if strong_pos then
+ self._private.selection_start = index
+ self._private.selection_start_x = strong_pos.x / Pango.SCALE
+ self._private.selection_start_y = strong_pos.y / Pango.SCALE
+ self:get_text_widget():emit_signal("widget::redraw_needed")
+ end
+end
+
+function text_input:set_selection_end_index(index)
+ if #self:get_text() == 0 then
+ return
+ end
+
+ index = math.max(math.min(index, #self:get_text()), 0)
+
+ local layout = self:get_text_widget()._private.layout
+ local strong_pos, weak_pos = layout:get_caret_pos(index)
+ if strong_pos then
+ if self:get_mode() ~= "overwrite" and index ~= self._private.selection_start then
+ self._private.mode = "overwrite"
+ self:show_selection()
+ self:hide_cursor()
+ end
+
+ self._private.selection_end = index
+ self._private.selection_end_x = strong_pos.x / Pango.SCALE
+ self._private.selection_end_y = strong_pos.y / Pango.SCALE
+ self:get_text_widget():emit_signal("widget::redraw_needed")
+ end
+end
+
+function text_input:increamant_selection_end_index()
+ if self:get_mode() == "insert" then
+ self:set_selection_start_index(self:get_cursor_index())
+ self:set_selection_end_index(self:get_cursor_index() + 1)
+ else
+ self:set_selection_end_index(self._private.selection_end + 1)
+ end
+end
+
+function text_input:decremeant_selection_end_index()
+ if self:get_mode() == "insert" then
+ self:set_selection_start_index(self:get_cursor_index())
+ self:set_selection_end_index(self:get_cursor_index() - 1)
+ else
+ self:set_selection_end_index(self._private.selection_end - 1)
+ end
+end
+
+function text_input:set_selection_start_index_from_x_y(x, y)
+ local layout = self:get_text_widget()._private.layout
+ local index, trailing = layout:xy_to_index(x * Pango.SCALE, y * Pango.SCALE)
+ if index then
+ self:set_selection_start_index(index)
+ else
+ local pixel_rect, logical_rect = self:get_text_widget()._private.layout:get_pixel_extents()
+ if x < logical_rect.x + logical_rect.width then
+ self:set_selection_start_index(0)
+ else
+ self:set_selection_start_index(#self:get_text())
+ end
+ end
+end
+
+function text_input:set_selection_end_index_from_x_y(x, y)
+ local layout = self:get_text_widget()._private.layout
+ local index, trailing = layout:xy_to_index(x * Pango.SCALE, y * Pango.SCALE)
+ if index then
+ self:set_selection_end_index(index + trailing)
+ else
+ local pixel_rect, logical_rect = self:get_text_widget()._private.layout:get_pixel_extents()
+ if x < logical_rect.x + logical_rect.width then
+ self:set_selection_end_index(0)
+ else
+ self:set_selection_end_index(#self:get_text())
+ end
+ end
+end
+
+function text_input:show_cursor()
+ self._private.cursor_opacity = 1
+ self:get_text_widget():emit_signal("widget::redraw_needed")
+end
+
+function text_input:hide_cursor()
+ self._private.cursor_opacity = 0
+ self:get_text_widget():emit_signal("widget::redraw_needed")
+end
+
+function text_input:set_cursor_index(index)
+ index = math.max(math.min(index, #self:get_text()), 0)
+
+ local layout = self:get_text_widget()._private.layout
+ local strong_pos, weak_pos = layout:get_cursor_pos(index)
+ if strong_pos then
+ if strong_pos == self._private.cursor_index and self._private.mode == "insert" then
+ return
+ end
+
+ if self:get_focused() and self:get_mode() ~= "insert" then
+ self:show_cursor()
+ end
+
+ self._private.cursor_index = index
+ self._private.mode = "insert"
+
+ self._private.cursor_x = strong_pos.x / Pango.SCALE
+ self._private.cursor_y = strong_pos.y / Pango.SCALE
+
+ self:hide_selection()
+
+ self:get_text_widget():emit_signal("widget::redraw_needed")
+ end
+end
+
+function text_input:set_cursor_index_from_x_y(x, y)
+ local layout = self:get_text_widget()._private.layout
+ local index, trailing = layout:xy_to_index(x * Pango.SCALE, y * Pango.SCALE)
+
+ if index then
+ self:set_cursor_index(index)
+ else
+ local pixel_rect, logical_rect = self:get_text_widget()._private.layout:get_pixel_extents()
+ if x < logical_rect.x + logical_rect.width then
+ self:set_cursor_index(0)
+ else
+ self:set_cursor_index(#self:get_text())
+ end
+ end
+end
+
+function text_input:set_cursor_index_to_word_start()
+ self:set_cursor_index(cword_start(self:get_text(), self:get_cursor_index() + 1) - 1)
+end
+
+function text_input:set_cursor_index_to_word_end()
+ self:set_cursor_index(cword_end(self:get_text(), self:get_cursor_index() + 1) - 1)
+end
+
+function text_input:set_cursor_index_to_end()
+ self:set_cursor_index(#self:get_text())
+end
+
+function text_input:increamant_cursor_index()
+ if self:get_mode() == "insert" then
+ self:set_cursor_index(self:get_cursor_index() + 1)
+ else
+ local start_pos = self._private.selection_start
+ local end_pos = self._private.selection_end
+ if start_pos > end_pos then
+ start_pos, end_pos = end_pos, start_pos
+ end
+ self:set_cursor_index(end_pos)
+ end
+end
+
+function text_input:decremeant_cursor_index()
+ if self:get_mode() == "insert" then
+ self:set_cursor_index(self:get_cursor_index() - 1)
+ else
+ local start_pos = self._private.selection_start
+ local end_pos = self._private.selection_end
+ if start_pos > end_pos then
+ start_pos, end_pos = end_pos, start_pos
+ end
+ self:set_cursor_index(start_pos)
+ end
+end
+
+function text_input:get_cursor_index()
+ return self._private.cursor_index
+end
+
+function text_input:set_focus_on_subject_mouse_enter(subject)
+ subject:connect_signal("mouse::enter", function()
+ self:focus()
+ end)
+end
+
+function text_input:set_unfocus_on_subject_mouse_leave(subject)
+ subject:connect_signal("mouse::leave", function()
+ self:unfocus()
+ end)
+end
+
+function text_input:get_focused()
+ return self._private.focused
+end
+
+function text_input:focus()
+ local wp = self._private
+
+ if self:get_focused() == true then
+ return
+ end
+
+ -- Do it first, so the cursor won't change back when unfocus was called on the focused text input
+ capi.awesome.emit_signal("text_input::focus", self)
+
+ set_mouse_cursor("xterm")
+
+ if self:get_mode() == "insert" then
+ self:show_cursor()
+ end
+
+ run_keygrabber(self)
+
+ if wp.cursor_blink then
+ if wp.cursor_blink_timer == nil then
+ wp.cursor_blink_timer = gtimer {
+ timeout = wp.cursor_blink_rate,
+ autostart = false,
+ call_now = false,
+ single_shot = false,
+ callback = function()
+ if self._private.cursor_opacity == 1 then
+ self:hide_cursor()
+ elseif self:get_mode() == "insert" then
+ self:show_cursor()
+ end
+ end
+ }
+ end
+ wp.cursor_blink_timer:start()
+ end
+
+
+ wp.focused = true
+ self:emit_signal("focus")
+end
+
+function text_input:unfocus(context)
+ local wp = self._private
+ if self:get_focused() == false then
+ return
+ end
+
+ set_mouse_cursor("left_ptr")
+ self:hide_cursor()
+ wp.cursor_blink_timer:stop()
+ self:hide_selection()
+ if self.reset_on_unfocus == true then
+ self:replace_text("")
+ end
+
+ awful.keygrabber.stop(wp.keygrabber)
+ wp.focused = false
+ self:emit_signal("unfocus", context or "normal", self:get_text())
+end
+
+function text_input:toggle()
+ local wp = self._private
+
+ if self:get_focused() == false then
+ self:focus()
+ else
+ self:unfocus()
+ end
+end
+
+local function new()
+ local widget = wibox.container.background()
+ gtable.crush(widget, text_input, true)
+
+ local wp = widget._private
+
+ wp.focused = false
+ wp.clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
+ wp.cursor_index = 0
+ wp.mode = "insert"
+ wp.click_count = 0
+
+ wp.cursor_x = 0
+ wp.cursor_y = 0
+ wp.cursor_opacity = 0
+ wp.selection_start_x = 0
+ wp.selection_end_x = 0
+ wp.selection_start_y = 0
+ wp.selection_end_y = 0
+ wp.selection_opacity = 0
+
+ wp.click_timeout = 0.2
+
+ wp.unfocus_keys = { }
+ wp.unfocus_on_root_clicked = false
+ wp.unfocus_on_client_clicked = false
+ wp.unfocus_on_mouse_leave = false
+ wp.unfocus_on_tag_change = false
+ wp.unfocus_on_other_text_input_focus = false
+ wp.unfocus_on_client_focus = false
+
+ wp.focus_on_subject_mouse_enter = nil
+ wp.unfocus_on_subject_mouse_leave = nil
+
+ wp.reset_on_unfocus = true
+
+ wp.pattern = nil
+ wp.obscure = false
+
+ wp.placeholder = ""
+ wp.text_color = beautiful.fg_normal
+ wp.text = ""
+
+ wp.cursor_width = 2
+ wp.cursor_bg = beautiful.fg_normal
+ wp.cursor_blink = true
+ wp.cursor_blink_rate = 0.6
+
+ wp.selection_bg = beautiful.bg_normal
+
+ widget:set_widget_template(wibox.widget {
+ layout = wibox.layout.stack,
+ {
+ widget = wibox.widget.textbox,
+ id = "placeholder_role",
+ text = wp.placeholder
+ },
+ {
+ widget = wibox.widget.textbox,
+ id = "text_role",
+ text = wp.text
+ }
+ })
+
+ capi.tag.connect_signal("property::selected", function()
+ if wp.unfocus_on_tag_change then
+ widget:unfocus()
+ end
+ end)
+
+ capi.awesome.connect_signal("text_input::focus", function(text_input)
+ if wp.unfocus_on_other_text_input_focus and text_input ~= widget then
+ widget:unfocus()
+ end
+ end)
+
+ capi.client.connect_signal("focus", function()
+ if wp.unfocus_on_client_focus then
+ widget:unfocus()
+ end
+ end)
+
+ awful.mouse.append_global_mousebindings({
+ awful.button({"Any"}, 1, function()
+ if wp.unfocus_on_root_clicked then
+ widget:unfocus()
+ end
+ end),
+ awful.button({"Any"}, 3, function()
+ if wp.unfocus_on_root_clicked then
+ widget:unfocus()
+ end
+ end)
+ })
+
+ capi.client.connect_signal("button::press", function()
+ if wp.unfocus_on_client_clicked then
+ widget:unfocus()
+ end
+ end)
+
+ return widget
+end
+
+function text_input.mt:__call(...)
+ return new(...)
+end
+
+build_properties(text_input, properties)
+
+return setmetatable(text_input, text_input.mt)
diff --git a/widget/tabbed_misc/custom_tasklist.lua b/widget/tabbed_misc/custom_tasklist.lua
index ac223771..bfa93e9d 100644
--- a/widget/tabbed_misc/custom_tasklist.lua
+++ b/widget/tabbed_misc/custom_tasklist.lua
@@ -11,14 +11,14 @@ local function tabobj_support(self, c, index, clients)
end
local group = c.bling_tabbed
-
+
-- TODO: Allow customization here
local layout_v = wibox.widget {
vertical_spacing = dpi(2),
horizontal_spacing = dpi(2),
layout = wibox.layout.grid.horizontal,
- forced_num_rows = 2,
- forced_num_cols = 2,
+ row_count = 2,
+ column_count = 2,
homogeneous = true
}