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 }