From 639c5667343cc62b2ca07082fcd8d50892d5cbc0 Mon Sep 17 00:00:00 2001 From: Nooo37 <70270606+Nooo37@users.noreply.github.com> Date: Sun, 26 Sep 2021 12:37:11 +0200 Subject: [PATCH 01/21] Window switcher docs (#111); closes #106 * add all window switcher theme vars * add window switcher docs --- docs/_sidebar.md | 1 + docs/widgets/window_switcher.md | 36 +++++++++++++++++++++++++++++++++ theme-var-template.lua | 21 +++++++++++++++++++ 3 files changed, 58 insertions(+) create mode 100644 docs/widgets/window_switcher.md diff --git a/docs/_sidebar.md b/docs/_sidebar.md index c7fc2b1..9216d34 100644 --- a/docs/_sidebar.md +++ b/docs/_sidebar.md @@ -17,6 +17,7 @@ - [Tag Preview](widgets/tag_preview.md) - [Task Preview](widgets/task_preview.md) - [Tabbed Misc](widgets/tabbed_misc.md) + - [Window Switcher](widgets/window_switcher.md) - Extra - [Theme Variable Template](theme.md) diff --git a/docs/widgets/window_switcher.md b/docs/widgets/window_switcher.md new file mode 100644 index 0000000..c14a355 --- /dev/null +++ b/docs/widgets/window_switcher.md @@ -0,0 +1,36 @@ +## 🎨 Window Switcher + +A popup that allows you to switch clients similar to what is known from MacOS or GNOME. + +![](https://user-images.githubusercontent.com/70270606/133311802-8aef1012-346f-4f4c-843d-10d9de54ffeb.png) + +*image by [No37](https://github.com/Nooo37)* + +### Usage + +To enable: + +```lua +bling.widget.window_switcher.enable { + type = "thumbnail", -- set to anything other than "thumbnail" to disable client previews + + -- keybindings (the examples provided are also the default if kept unset) + hide_window_switcher_key = "Escape", -- The key on which to close the popup + minimize_key = "n", -- The key on which to minimize the selected client + unminimize_key = "N", -- The key on which to unminimize all clients + kill_client_key = "q", -- The key on which to close the selected client + cycle_key = "Tab", -- The key on which to cycle through all clients + previous_key = "Left", -- The key on which to select the previous client + next_key = "Right", -- The key on which to select the next client + vim_previous_key = "h", -- Alternative key on which to select the previous client + vim_next_key = "l", -- Alternative key on which to select the next client +} +``` + +To run the window swicher you have to run: + +```lua +awesome.emit_signal("bling::window_switcher::turn_on") +``` +from within your configuration (usually using a keybind). + diff --git a/theme-var-template.lua b/theme-var-template.lua index ad65463..fc545c1 100644 --- a/theme-var-template.lua +++ b/theme-var-template.lua @@ -76,4 +76,25 @@ theme.task_preview_widget_border_color = "#ffffff" -- The border color of the wi theme.task_preview_widget_border_width = 3 -- The border width of the widget theme.task_preview_widget_margin = 0 -- The margin of the widget +-- window switcher +theme.window_switcher_widget_bg = "#000000" -- The bg color of the widget +theme.window_switcher_widget_border_width = 3 -- The border width of the widget +theme.window_switcher_widget_border_radius = 0 -- The border radius of the widget +theme.window_switcher_widget_border_color = "#ffffff" -- The border color of the widget +theme.window_switcher_clients_spacing = 20 -- The space between each client item +theme.window_switcher_client_icon_horizontal_spacing = 5 -- The space between client icon and text +theme.window_switcher_client_width = 150 -- The width of one client widget +theme.window_switcher_client_height = 250 -- The height of one client widget +theme.window_switcher_client_margins = 10 -- The margin between the content and the border of the widget +theme.window_switcher_thumbnail_margins = 10 -- The margin between one client thumbnail and the rest of the widget +theme.thumbnail_scale = false -- If set to true, the thumbnails fit policy will be set to "fit" instead of "auto" +theme.window_switcher_name_margins = 10 -- The margin of one clients title to the rest of the widget +theme.window_switcher_name_valign = "center" -- How to vertically align one clients title +theme.window_switcher_name_forced_width = 200 -- The width of one title +theme.window_switcher_name_font = "Sans 11" -- The font of all titles +theme.window_switcher_name_normal_color = "#ffffff" -- The color of one title if the client is unfocused +theme.window_switcher_name_focus_color = "#ff0000" -- The color of one title if the client is focused +theme.window_switcher_icon_valign = "center" -- How to vertially align the one icon +theme.window_switcher_icon_width = 40 -- Thw width of one icon + -- LuaFormatter on From 7f17e7bb8b133e8a83696669bc4ab9717fa6d047 Mon Sep 17 00:00:00 2001 From: gokul <33443763+JavaCafe01@users.noreply.github.com> Date: Thu, 30 Sep 2021 12:21:21 -0700 Subject: [PATCH 02/21] Update Docs (#118) Clarified a few things in the window switcher documentation and added a keybind example. --- docs/widgets/window_switcher.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/docs/widgets/window_switcher.md b/docs/widgets/window_switcher.md index c14a355..28ed0a4 100644 --- a/docs/widgets/window_switcher.md +++ b/docs/widgets/window_switcher.md @@ -1,6 +1,6 @@ ## 🎨 Window Switcher -A popup that allows you to switch clients similar to what is known from MacOS or GNOME. +A popup with client previews that allows you to switch clients similar to the alt-tab menu in MacOS, GNOME, and Windows. ![](https://user-images.githubusercontent.com/70270606/133311802-8aef1012-346f-4f4c-843d-10d9de54ffeb.png) @@ -27,10 +27,15 @@ bling.widget.window_switcher.enable { } ``` -To run the window swicher you have to run: +To run the window swicher you have to emit this signal from within your configuration (usually using a keybind). ```lua awesome.emit_signal("bling::window_switcher::turn_on") ``` -from within your configuration (usually using a keybind). +For example: +```lua + awful.key({altkey}, "Tab", function() + awesome.emit_signal("bling::window_switcher::turn_on") + end, {description = "Window Switcher", group = "client"}) +``` From 394c3ceeb72647ee4afee64d762c90bc0c884a49 Mon Sep 17 00:00:00 2001 From: Kasper Date: Sun, 3 Oct 2021 13:19:48 +0300 Subject: [PATCH 03/21] Free ram for widgets with clients previews (#117) --- widget/tag_preview.lua | 39 ++++++++++++++++++++------------------ widget/task_preview.lua | 3 +++ widget/window_switcher.lua | 2 ++ 3 files changed, 26 insertions(+), 18 deletions(-) diff --git a/widget/tag_preview.lua b/widget/tag_preview.lua index f2ef42b..f663be4 100644 --- a/widget/tag_preview.lua +++ b/widget/tag_preview.lua @@ -107,7 +107,7 @@ local function draw_widget( end end - return { + return wibox.widget { { { { @@ -182,23 +182,21 @@ local enable = function(opts) tag_preview_box.maximum_width = scale * geo.width + margin * 2 tag_preview_box.maximum_height = scale * geo.height + margin * 2 - tag_preview_box:setup( - draw_widget( - t, - tag_preview_image, - scale, - screen_radius, - client_radius, - client_opacity, - client_bg, - client_border_color, - client_border_width, - widget_bg, - widget_border_color, - widget_border_width, - geo, - margin - ) + tag_preview_box.widget = draw_widget( + t, + tag_preview_image, + scale, + screen_radius, + client_radius, + client_opacity, + client_bg, + client_border_color, + client_border_width, + widget_bg, + widget_border_color, + widget_border_width, + geo, + margin ) end) @@ -208,6 +206,11 @@ local enable = function(opts) tag_preview_box.y = s.geometry.y + widget_y end + if v == false then + tag_preview_box.widget = nil + collectgarbage("collect") + end + tag_preview_box.visible = v end) end diff --git a/widget/task_preview.lua b/widget/task_preview.lua index 4712ebd..b16a845 100644 --- a/widget/task_preview.lua +++ b/widget/task_preview.lua @@ -153,6 +153,9 @@ local enable = function(opts) widget_width, widget_height ) + else + task_preview_box.widget = nil + collectgarbage("collect") end if not placement_fn then diff --git a/widget/window_switcher.lua b/widget/window_switcher.lua index 46a303b..ac835a5 100644 --- a/widget/window_switcher.lua +++ b/widget/window_switcher.lua @@ -59,6 +59,8 @@ local window_switcher_hide = function(window_switcher_box) -- Stop and hide window_switcher awful.keygrabber.stop(window_switcher_grabber) window_switcher_box.visible = false + window_switcher_box.widget = nil + collectgarbage("collect") end local function draw_widget( From abfd5b8ddd15f98e90f2e7d4646a66dd521da24f Mon Sep 17 00:00:00 2001 From: Nooo37 <70270606+Nooo37@users.noreply.github.com> Date: Sun, 3 Oct 2021 16:19:17 +0200 Subject: [PATCH 04/21] Add tabbar_{fg,bg}_{normal, focus}_inactive theme vars (#114); closes #112 --- module/tabbed.lua | 19 ++++++++++++++++++- theme-var-template.lua | 4 ++++ widget/tabbar/boxes.lua | 14 +++++++++----- widget/tabbar/default.lua | 14 +++++++++----- widget/tabbar/modern.lua | 14 +++++++++----- widget/tabbar/pure.lua | 14 +++++++++++--- 6 files changed, 60 insertions(+), 19 deletions(-) diff --git a/module/tabbed.lua b/module/tabbed.lua index b7ae67d..c53ec03 100644 --- a/module/tabbed.lua +++ b/module/tabbed.lua @@ -23,6 +23,14 @@ local bar = require( tabbed = {} +-- helper function to connect to the (un)focus signals +local function update_tabbar_from(c) + if not c or not c.bling_tabbed then + return + end + tabbed.update_tabbar(c.bling_tabbed) +end + -- used to change focused tab relative to the currently focused one tabbed.iter = function(idx) if not idx then @@ -50,6 +58,8 @@ tabbed.remove = function(c) awful.titlebar.hide(c, bar.position) end c.bling_tabbed = nil + c:disconnect_signal("focus", update_tabbar_from) + c:disconnect_signal("unfocus", update_tabbar_from) awesome.emit_signal("bling::tabbed::client_removed", tabobj, c) tabbed.switch_to(tabobj, 1) end @@ -67,6 +77,8 @@ tabbed.add = function(c, tabobj) if c.bling_tabbed then tabbed.remove(c) end + c:connect_signal("focus", update_tabbar_from) + c:connect_signal("unfocus", update_tabbar_from) helpers.client.sync(c, tabobj.clients[tabobj.focused_idx]) tabobj.clients[#tabobj.clients + 1] = c tabobj.focused_idx = #tabobj.clients @@ -218,12 +230,15 @@ end tabbed.update_tabbar = function(tabobj) local flexlist = bar.layout() + local tabobj_focused_client = tabobj.clients[tabobj.focused_idx] + local tabobj_is_focused = (client.focus == tabobj_focused_client) -- itearte over all tabbed clients to create the widget tabbed list for idx, c in ipairs(tabobj.clients) do local buttons = gears.table.join(awful.button({}, 1, function() tabbed.switch_to(tabobj, idx) end)) - wid_temp = bar.create(c, (idx == tabobj.focused_idx), buttons) + local wid_temp = bar.create(c, (idx == tabobj.focused_idx), buttons, + not tabobj_is_focused) flexlist:add(wid_temp) end -- add tabbar to each tabbed client (clients will be hided anyway) @@ -240,6 +255,8 @@ end tabbed.init = function(c) local tabobj = {} tabobj.clients = { c } + c:connect_signal("focus", update_tabbar_from) + c:connect_signal("unfocus", update_tabbar_from) tabobj.focused_idx = 1 tabbed.update(tabobj) end diff --git a/theme-var-template.lua b/theme-var-template.lua index fc545c1..4fc1c8e 100644 --- a/theme-var-template.lua +++ b/theme-var-template.lua @@ -36,6 +36,10 @@ theme.tabbar_bg_normal = "#000000" -- background color of the focused client on theme.tabbar_fg_normal = "#ffffff" -- foreground color of the focused client on the tabbar theme.tabbar_bg_focus = "#1A2026" -- background color of unfocused clients on the tabbar theme.tabbar_fg_focus = "#ff0000" -- foreground color of unfocused clients on the tabbar +theme.tabbar_bg_focus_inactive = nil -- background color of the focused client on the tabbar when inactive +theme.tabbar_fg_focus_inactive = nil -- foreground color of the focused client on the tabbar when inactive +theme.tabbar_bg_normal_inactive = nil -- background color of unfocused clients on the tabbar when inactive +theme.tabbar_fg_normal_inactive = nil -- foreground color of unfocused clients on the tabbar when inactive -- mstab theme.mstab_bar_ontop = false -- whether you want to allow the bar to be ontop of clients diff --git a/widget/tabbar/boxes.lua b/widget/tabbar/boxes.lua index d0619da..720f420 100644 --- a/widget/tabbar/boxes.lua +++ b/widget/tabbar/boxes.lua @@ -8,16 +8,20 @@ local bg_normal = beautiful.tabbar_bg_normal or beautiful.bg_normal or "#ffffff" local fg_normal = beautiful.tabbar_fg_normal or beautiful.fg_normal or "#000000" local bg_focus = beautiful.tabbar_bg_focus or beautiful.bg_focus or "#000000" local fg_focus = beautiful.tabbar_fg_focus or beautiful.fg_focus or "#ffffff" +local bg_focus_inactive = beautiful.tabbar_bg_focus_inactive or bg_focus +local fg_focus_inactive = beautiful.tabbar_fg_focus_inactive or fg_focus +local bg_normal_inactive = beautiful.tabbar_bg_normal_inactive or bg_normal +local fg_normal_inactive = beautiful.tabbar_fg_normal_inactive or fg_normal local font = beautiful.tabbar_font or beautiful.font or "Hack 15" local size = beautiful.tabbar_size or 40 local position = beautiful.tabbar_position or "bottom" -local function create(c, focused_bool, buttons) - local bg_temp = bg_normal - local fg_temp = fg_normal +local function create(c, focused_bool, buttons, inactive_bool) + local bg_temp = inactive_bool and bg_normal_inactive or bg_normal + local fg_temp = inactive_bool and fg_normal_inactive or fg_normal if focused_bool then - bg_temp = bg_focus - fg_temp = fg_focus + bg_temp = inactive_bool and bg_focus_inactive or bg_focus + fg_temp = inactive_bool and fg_focus_inactive or fg_focus end local wid_temp = wibox.widget({ { diff --git a/widget/tabbar/default.lua b/widget/tabbar/default.lua index 2774d6f..ad6b0b1 100644 --- a/widget/tabbar/default.lua +++ b/widget/tabbar/default.lua @@ -7,18 +7,22 @@ local bg_normal = beautiful.tabbar_bg_normal or beautiful.bg_normal or "#ffffff" local fg_normal = beautiful.tabbar_fg_normal or beautiful.fg_normal or "#000000" local bg_focus = beautiful.tabbar_bg_focus or beautiful.bg_focus or "#000000" local fg_focus = beautiful.tabbar_fg_focus or beautiful.fg_focus or "#ffffff" +local bg_focus_inactive = beautiful.tabbar_bg_focus_inactive or bg_focus +local fg_focus_inactive = beautiful.tabbar_fg_focus_inactive or fg_focus +local bg_normal_inactive = beautiful.tabbar_bg_normal_inactive or bg_normal +local fg_normal_inactive = beautiful.tabbar_fg_normal_inactive or fg_normal local font = beautiful.tabbar_font or beautiful.font or "Hack 15" local size = beautiful.tabbar_size or 20 local position = beautiful.tabbar_position or "top" -local function create(c, focused_bool, buttons) +local function create(c, focused_bool, buttons, inactive_bool) local flexlist = wibox.layout.flex.horizontal() local title_temp = c.name or c.class or "-" - local bg_temp = bg_normal - local fg_temp = fg_normal + local bg_temp = inactive_bool and bg_normal_inactive or bg_normal + local fg_temp = inactive_bool and fg_normal_inactive or fg_normal if focused_bool then - bg_temp = bg_focus - fg_temp = fg_focus + bg_temp = inactive_bool and bg_focus_inactive or bg_focus + fg_temp = inactive_bool and fg_focus_inactive or fg_focus end local text_temp = wibox.widget.textbox() text_temp.align = "center" diff --git a/widget/tabbar/modern.lua b/widget/tabbar/modern.lua index 11f5e44..5f48066 100644 --- a/widget/tabbar/modern.lua +++ b/widget/tabbar/modern.lua @@ -10,6 +10,10 @@ local bg_normal = beautiful.tabbar_bg_normal or beautiful.bg_normal or "#ffffff" local fg_normal = beautiful.tabbar_fg_normal or beautiful.fg_normal or "#000000" local bg_focus = beautiful.tabbar_bg_focus or beautiful.bg_focus or "#000000" local fg_focus = beautiful.tabbar_fg_focus or beautiful.fg_focus or "#ffffff" +local bg_focus_inactive = beautiful.tabbar_bg_focus_inactive or bg_focus +local fg_focus_inactive = beautiful.tabbar_fg_focus_inactive or fg_focus +local bg_normal_inactive = beautiful.tabbar_bg_normal_inactive or bg_normal +local fg_normal_inactive = beautiful.tabbar_fg_normal_inactive or fg_normal local font = beautiful.tabbar_font or beautiful.font or "Hack 15" local size = beautiful.tabbar_size or dpi(40) local border_radius = beautiful.mstab_border_radius @@ -66,14 +70,14 @@ local function create_title_button(c, color_focus, color_unfocus) return tb end -local function create(c, focused_bool, buttons) +local function create(c, focused_bool, buttons, inactive_bool) -- local flexlist = wibox.layout.flex.horizontal() local title_temp = c.name or c.class or "-" - local bg_temp = bg_normal - local fg_temp = fg_normal + local bg_temp = inactive_bool and bg_normal_inactive or bg_normal + local fg_temp = inactive_bool and fg_normal_inactive or fg_normal if focused_bool then - bg_temp = bg_focus - fg_temp = fg_focus + bg_temp = inactive_bool and bg_focus_inactive or bg_focus + fg_temp = inactive_bool and fg_focus_inactive or fg_focus end local text_temp = wibox.widget.textbox() text_temp.align = "center" diff --git a/widget/tabbar/pure.lua b/widget/tabbar/pure.lua index 588e59f..5be82e5 100644 --- a/widget/tabbar/pure.lua +++ b/widget/tabbar/pure.lua @@ -8,13 +8,21 @@ local bg_normal = beautiful.tabbar_bg_normal or beautiful.bg_normal or "#ffffff" local fg_normal = beautiful.tabbar_fg_normal or beautiful.fg_normal or "#000000" local bg_focus = beautiful.tabbar_bg_focus or beautiful.bg_focus or "#000000" local fg_focus = beautiful.tabbar_fg_focus or beautiful.fg_focus or "#ffffff" +local bg_focus_inactive = beautiful.tabbar_bg_focus_inactive or bg_focus +local fg_focus_inactive = beautiful.tabbar_fg_focus_inactive or fg_focus +local bg_normal_inactive = beautiful.tabbar_bg_normal_inactive or bg_normal +local fg_normal_inactive = beautiful.tabbar_fg_normal_inactive or fg_normal local font = beautiful.tabbar_font or beautiful.font or "Hack 15" local size = beautiful.tabbar_size or 20 local position = beautiful.tabbar_position or "top" -local function create(c, focused_bool, buttons) - local bg_temp = focused_bool and bg_focus or bg_normal - local fg_temp = focused_bool and fg_focus or fg_normal +local function create(c, focused_bool, buttons, inactive_bool) + local bg_temp = inactive_bool and bg_normal_inactive or bg_normal + local fg_temp = inactive_bool and fg_normal_inactive or fg_normal + if focused_bool then + bg_temp = inactive_bool and bg_focus_inactive or bg_focus + fg_temp = inactive_bool and fg_focus_inactive or fg_focus + end local wid_temp = wibox.widget({ { From 536009e58ef9a465298558f0f12d6d9dfc1be740 Mon Sep 17 00:00:00 2001 From: nuxsh Date: Mon, 11 Oct 2021 17:47:58 +0530 Subject: [PATCH 05/21] fix(docs): missing commas in task preview example (#121) --- docs/widgets/task_preview.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/widgets/task_preview.md b/docs/widgets/task_preview.md index 136a207..4195dbf 100644 --- a/docs/widgets/task_preview.md +++ b/docs/widgets/task_preview.md @@ -49,9 +49,9 @@ bling.widget.task_preview.enable { widget = awful.widget.clienticon, -- The client icon }, { - id = 'name_role' -- The client name / title + id = 'name_role', -- The client name / title widget = wibox.widget.textbox, - } + }, layout = wibox.layout.flex.horizontal }, widget = wibox.container.margin, From 19fa3a2d10b128b5fb01c2fb18e8c7ad3e99c30a Mon Sep 17 00:00:00 2001 From: Bran <71175207+HeavyRain266@users.noreply.github.com> Date: Wed, 13 Oct 2021 16:26:04 +0100 Subject: [PATCH 06/21] Update docs (#120) Formatted markdown and lua snippets, removed trailing spaces and made example keybinds compilant with default awesome config Co-authored-by: Nooo37 --- docs/layouts/layout.md | 39 ++++++++++++--------- docs/module/flash.md | 4 +-- docs/module/scratch.md | 8 ++--- docs/module/swal.md | 10 +++--- docs/module/tabbed.md | 50 +++++++++++++------------- docs/module/twall.md | 21 +++++------ docs/module/wall.md | 18 +++++----- docs/signals/pctl.md | 32 ++++++++--------- docs/widgets/tabbed_misc.md | 62 ++++++++++++++++----------------- docs/widgets/tag_preview.md | 41 +++++++++++----------- docs/widgets/task_preview.md | 48 ++++++++++++------------- docs/widgets/window_switcher.md | 22 ++++++------ 12 files changed, 181 insertions(+), 174 deletions(-) diff --git a/docs/layouts/layout.md b/docs/layouts/layout.md index a3a697d..5a9bcd6 100644 --- a/docs/layouts/layout.md +++ b/docs/layouts/layout.md @@ -6,7 +6,7 @@ Everyone of them supports multiple master clients and master width factor making The mstab layout uses the tab theme from the tabbed module. -```Lua +```lua bling.layout.mstab bling.layout.centered bling.layout.vertical @@ -16,36 +16,40 @@ bling.layout.deck ``` ### Theme Variables + ```lua -- mstab -theme.mstab_bar_ontop = false -- whether you want to allow the bar to be ontop of clients -theme.mstab_dont_resize_slaves = false -- whether the tabbed stack windows should be smaller than the - -- currently focused stack window (set it to true if you use - -- transparent terminals. False if you use shadows on solid ones -theme.mstab_bar_padding = "default" -- how much padding there should be between clients and your tabbar - -- by default it will adjust based on your useless gaps. - -- If you want a custom value. Set it to the number of pixels (int) -theme.mstab_border_radius = 0 -- border radius of the tabbar -theme.mstab_bar_height = 40 -- height of the tabbar -theme.mstab_tabbar_position = "top" -- position of the tabbar (mstab currently does not support left,right) -theme.mstab_tabbar_style = "default" -- style of the tabbar ("default", "boxes" or "modern") - -- defaults to the tabbar_style so only change if you want a - -- different style for mstab and tabbed +theme.mstab_bar_ontop = false -- whether you want to allow the bar to be ontop of clients +theme.mstab_dont_resize_slaves = false -- whether the tabbed stack windows should be smaller than the + -- currently focused stack window (set it to true if you use + -- transparent terminals. False if you use shadows on solid ones +theme.mstab_bar_padding = "default" -- how much padding there should be between clients and your tabbar + -- by default it will adjust based on your useless gaps. + -- If you want a custom value. Set it to the number of pixels (int) +theme.mstab_border_radius = 0 -- border radius of the tabbar +theme.mstab_bar_height = 40 -- height of the tabbar +theme.mstab_tabbar_position = "top" -- position of the tabbar (mstab currently does not support left,right) +theme.mstab_tabbar_style = "default" -- style of the tabbar ("default", "boxes" or "modern") + -- defaults to the tabbar_style so only change if you want a + -- different style for mstab and tabbed ``` ### Previews #### Mstab (dynamic tabbing layout) + ![](https://imgur.com/HZRgApE.png) -*screenshot by [javacafe](https://github.com/JavaCafe01)* +*screenshot by [JavaCafe01](https://github.com/JavaCafe01)* #### Centered + ![](https://media.discordapp.net/attachments/769673106842845194/780095998239834142/unknown.png) -*screenshot by [branwright](https://github.com/branwright1)* +*screenshot by [HeavyRain266](https://github.com/HeavyRain266)* #### Equal area + ![](https://imgur.com/JCFFywv.png) *screenshot by [bysmutheye](https://github.com/bysmutheye)* @@ -56,4 +60,5 @@ The left area shows the deck layout in action. In this screenshot it is used tog ![](https://cdn.discordapp.com/attachments/635625954219261982/877957824225894430/unknown.png) -*screenshot by [javacafe](https://github.com/JavaCafe01)* +*screenshot by [JavaCafe01](https://github.com/JavaCafe01)* + diff --git a/docs/module/flash.md b/docs/module/flash.md index 00f1582..a12b78d 100644 --- a/docs/module/flash.md +++ b/docs/module/flash.md @@ -22,8 +22,8 @@ awful.key({modkey}, "Up", ### Theme Variables ```lua -theme.flash_focus_start_opacity = 0.6 -- the starting opacity -theme.flash_focus_step = 0.01 -- the step of animation +theme.flash_focus_start_opacity = 0.6 -- the starting opacity +theme.flash_focus_step = 0.01 -- the step of animation ``` ### Preview diff --git a/docs/module/scratch.md b/docs/module/scratch.md index 8259870..54f8ddd 100644 --- a/docs/module/scratch.md +++ b/docs/module/scratch.md @@ -18,7 +18,7 @@ To initalize a scratchpad you can do something like the following: ```lua local bling = require("bling") -local rubato = require("rubato") -- Totally optional, only required if you are using animations. +local rubato = require("rubato") -- Totally optional, only required if you are using animations. -- These are example rubato tables. You can use one for just y, just x, or both. -- The duration and easing is up to you. Please check out the rubato docs to learn more. @@ -28,7 +28,7 @@ local anim_y = rubato.timed { easing = rubato.quadratic, intro = 0.1, duration = 0.3, - awestore_compat = true -- This option must be set to true. + awestore_compat = true -- This option must be set to true. } local anim_x = rubato.timed { @@ -37,7 +37,7 @@ local anim_x = rubato.timed { easing = rubato.quadratic, intro = 0.1, duration = 0.3, - awestore_compat = true -- This option must be set to true. + awestore_compat = true -- This option must be set to true. } local term_scratch = bling.module.scratchpad { @@ -49,7 +49,7 @@ local term_scratch = bling.module.scratchpad { geometry = {x=360, y=90, height=900, width=1200}, -- The geometry in a floating state reapply = true, -- Whether all those properties should be reapplied on every new opening of the scratchpad (MUST BE TRUE FOR ANIMATIONS) dont_focus_before_close = false, -- When set to true, the scratchpad will be closed by the toggle function regardless of whether its focused or not. When set to false, the toggle function will first bring the scratchpad into focus and only close it on a second call - rubato = {x = anim_x, y = anim_y} -- Optional. This is how you can pass in the rubato tables for animations. If you don't want animations, you can ignore this option. + rubato = {x = anim_x, y = anim_y} -- Optional. This is how you can pass in the rubato tables for animations. If you don't want animations, you can ignore this option. } ``` diff --git a/docs/module/swal.md b/docs/module/swal.md index ad40c0f..ae339fc 100644 --- a/docs/module/swal.md +++ b/docs/module/swal.md @@ -6,15 +6,15 @@ Can your window manager swallow? It probably can... To activate and deactivate window swallowing here are the following functions. If you want to activate it, just call the `start` function once in your `rc.lua`. ```lua -bling.module.window_swallowing.start() -- activates window swallowing -bling.module.window_swallowing.stop() -- deactivates window swallowing -bling.module.window_swallowing.toggle() -- toggles window swallowing +bling.module.window_swallowing.start() -- activates window swallowing +bling.module.window_swallowing.stop() -- deactivates window swallowing +bling.module.window_swallowing.toggle() -- toggles window swallowing ``` ### Theme Variables ```lua -theme.dont_swallow_classname_list = {"firefox", "Gimp"} -- list of class names that should not be swallowed -theme.dont_swallow_filter_activated = true -- whether the filter above should be active +theme.dont_swallow_classname_list = {"firefox", "Gimp"} -- list of class names that should not be swallowed +theme.dont_swallow_filter_activated = true -- whether the filter above should be active ``` ### Preview diff --git a/docs/module/tabbed.md b/docs/module/tabbed.md index 985fca3..35edb07 100644 --- a/docs/module/tabbed.md +++ b/docs/module/tabbed.md @@ -6,10 +6,10 @@ Tabbed implements a tab container. There are also different themes for the tabs. You should bind these functions to keys in order to use the tabbed module effectively: ```lua -bling.module.tabbed.pick() -- picks a client with your cursor to add to the tabbing group -bling.module.tabbed.pop() -- removes the focused client from the tabbing group -bling.module.tabbed.iter() -- iterates through the currently focused tabbing group -bling.module.tabbed.pick_with_dmenu() -- picks a client with a dmenu application (defaults to rofi, other options can be set with a string parameter like "dmenu") +bling.module.tabbed.pick() -- picks a client with your cursor to add to the tabbing group +bling.module.tabbed.pop() -- removes the focused client from the tabbing group +bling.module.tabbed.iter() -- iterates through the currently focused tabbing group +bling.module.tabbed.pick_with_dmenu() -- picks a client with a dmenu application (defaults to rofi, other options can be set with a string parameter like "dmenu") bling.module.tabbed.pick_by_direction(dir) -- picks a client based on direction ("up", "down", "left" or "right") ``` @@ -17,25 +17,25 @@ bling.module.tabbed.pick_by_direction(dir) -- picks a client based on direction ```lua -- For tabbed only -theme.tabbed_spawn_in_tab = false -- whether a new client should spawn into the focused tabbing container +theme.tabbed_spawn_in_tab = false -- whether a new client should spawn into the focused tabbing container -- For tabbar in general theme.tabbar_ontop = false -theme.tabbar_radius = 0 -- border radius of the tabbar -theme.tabbar_style = "default" -- style of the tabbar ("default", "boxes" or "modern") -theme.tabbar_font = "Sans 11" -- font of the tabbar -theme.tabbar_size = 40 -- size of the tabbar -theme.tabbar_position = "top" -- position of the tabbar -theme.tabbar_bg_normal = "#000000" -- background color of the focused client on the tabbar -theme.tabbar_fg_normal = "#ffffff" -- foreground color of the focused client on the tabbar -theme.tabbar_bg_focus = "#1A2026" -- background color of unfocused clients on the tabbar -theme.tabbar_fg_focus = "#ff0000" -- foreground color of unfocused clients on the tabbar -theme.tabbar_disable = false -- disable the tab bar entirely +theme.tabbar_radius = 0 -- border radius of the tabbar +theme.tabbar_style = "default" -- style of the tabbar ("default", "boxes" or "modern") +theme.tabbar_font = "Sans 11" -- font of the tabbar +theme.tabbar_size = 40 -- size of the tabbar +theme.tabbar_position = "top" -- position of the tabbar +theme.tabbar_bg_normal = "#000000" -- background color of the focused client on the tabbar +theme.tabbar_fg_normal = "#ffffff" -- foreground color of the focused client on the tabbar +theme.tabbar_bg_focus = "#1A2026" -- background color of unfocused clients on the tabbar +theme.tabbar_fg_focus = "#ff0000" -- foreground color of unfocused clients on the tabbar +theme.tabbar_disable = false -- disable the tab bar entirely -- the following variables are currently only for the "modern" tabbar style -theme.tabbar_color_close = "#f9929b" -- chnges the color of the close button -theme.tabbar_color_min = "#fbdf90" -- chnges the color of the minimize button -theme.tabbar_color_float = "#ccaced" -- chnges the color of the float button +theme.tabbar_color_close = "#f9929b" -- chnges the color of the close button +theme.tabbar_color_min = "#fbdf90" -- chnges the color of the minimize button +theme.tabbar_color_float = "#ccaced" -- chnges the color of the float button ``` ### Preview @@ -44,19 +44,19 @@ Modern theme: -*screenshot by [javacafe](https://github.com/JavaCafe01)* +*screenshot by [JavaCafe01](https://github.com/JavaCafe01)* ### Signals The tabbed module emits a few signals for the purpose of integration, ```lua -- bling::tabbed::update -- triggered whenever a tabbed object is updated --- tabobj -- the object that caused the update +-- tabobj -- the object that caused the update -- bling::tabbed::client_added -- triggered whenever a new client is added to a tab group --- tabobj -- the object that the client was added to --- client -- the client that added +-- tabobj -- the object that the client was added to +-- client -- the client that added -- bling::tabbed::client_removed -- triggered whenever a client is removed from a tab group --- tabobj -- the object that the client was removed from --- client -- the client that was removed +-- tabobj -- the object that the client was removed from +-- client -- the client that was removed -- bling::tabbed::changed_focus -- triggered whenever a tab group's focus is changed --- tabobj -- the modified tab group +-- tabobj -- the modified tab group ``` diff --git a/docs/module/twall.md b/docs/module/twall.md index a6d31af..69c09c3 100644 --- a/docs/module/twall.md +++ b/docs/module/twall.md @@ -4,16 +4,16 @@ The function to set an automatically created tiled wallpaper can be called the following way (you don't need to set every option in the table): ```lua -awful.screen.connect_for_each_screen(function(s) -- that way the wallpaper is applied to every screen - bling.module.tiled_wallpaper("x", s, { -- call the actual function ("x" is the string that will be tiled) - fg = "#ff0000", -- define the foreground color - bg = "#00ffff", -- define the background color - offset_y = 25, -- set a y offset - offset_x = 25, -- set a x offset - font = "Hack", -- set the font (without the size) - font_size = 14, -- set the font size - padding = 100, -- set padding (default is 100) - zickzack = true -- rectangular pattern or criss cross +awful.screen.connect_for_each_screen(function(s) -- that way the wallpaper is applied to every screen + bling.module.tiled_wallpaper("x", s, { -- call the actual function ("x" is the string that will be tiled) + fg = "#ff0000", -- define the foreground color + bg = "#00ffff", -- define the background color + offset_y = 25, -- set a y offset + offset_x = 25, -- set a x offset + font = "Hack", -- set the font (without the size) + font_size = 14, -- set the font size + padding = 100, -- set padding (default is 100) + zickzack = true -- rectangular pattern or criss cross }) end) ``` @@ -23,3 +23,4 @@ end) ![](https://media.discordapp.net/attachments/702548913999314964/773887721294135296/tiled-wallpapers.png?width=1920&height=1080) *screenshots by [Nooo37](https://github.com/Nooo37)* + diff --git a/docs/module/wall.md b/docs/module/wall.md index ea87f52..0869166 100644 --- a/docs/module/wall.md +++ b/docs/module/wall.md @@ -18,7 +18,7 @@ bling.module.wallpaper.setup { bling.module.wallpaper.setup { set_function = bling.module.wallpaper.setters.random, wallpaper = {"/path/to/a/folder", "/path/to/another/folder"}, - change_timer = 631, -- prime numbers are better for timers + change_timer = 631, -- prime numbers are better for timers position = "fit", background = "#424242" } @@ -99,14 +99,14 @@ Here are the defaults: ```lua -- Default parameters bling.module.wallpaper.setup { - screen = nil, -- the screen to apply the wallpaper, as seen in gears.wallpaper functions - change_timer = nil, -- the timer in seconds. If set, call the set_function every change_timer seconds - set_function = nil, -- the setter function + screen = nil, -- the screen to apply the wallpaper, as seen in gears.wallpaper functions + change_timer = nil, -- the timer in seconds. If set, call the set_function every change_timer seconds + set_function = nil, -- the setter function -- parameters used by bling.module.wallpaper.prepare_list - wallpaper = nil, -- the wallpaper object, see simple or simple_schedule documentation - image_formats = {"jpg", "jpeg", "png", "bmp"}, -- when searching in folder, consider these files only - recursive = true, -- when searching in folder, search also in subfolders + wallpaper = nil, -- the wallpaper object, see simple or simple_schedule documentation + image_formats = {"jpg", "jpeg", "png", "bmp"}, -- when searching in folder, consider these files only + recursive = true, -- when searching in folder, search also in subfolders -- parameters used by bling.module.wallpaper.apply position = nil, -- use a function of gears.wallpaper when applicable ("centered", "fit", "maximized", "tiled") @@ -116,8 +116,8 @@ bling.module.wallpaper.setup { scale = 1, -- see gears.wallpaper.centered -- parameters that only apply to bling.module.wallpaper.setter.awesome (as a setter or as a wallpaper function) - colors = { -- see beautiful.theme_assets.wallpaper - bg = beautiful.bg_color, -- the actual default is this color but darkened or lightned + colors = { -- see beautiful.theme_assets.wallpaper + bg = beautiful.bg_color, -- the actual default is this color but darkened or lightned fg = beautiful.fg_color, alt_fg = beautiful.fg_focus } diff --git a/docs/signals/pctl.md b/docs/signals/pctl.md index edce9b0..9fb09c6 100644 --- a/docs/signals/pctl.md +++ b/docs/signals/pctl.md @@ -24,20 +24,20 @@ To disable: `bling.signal.playerctl.disable()` Here are the signals available: ```lua --- bling::playerctl::status -- first line is the signal --- playing (boolean) -- indented lines are function parameters --- player_name (string) +-- bling::playerctl::status -- first line is the signal +-- playing (boolean) -- indented lines are function parameters +-- player_name (string) -- bling::playerctl::title_artist_album --- title (string) --- artist (string) --- album_path (string) --- player_name (string) +-- title (string) +-- artist (string) +-- album_path (string) +-- player_name (string) -- bling::playerctl::position --- interval_sec (number) --- length_sec (number) --- player_name (string) +-- interval_sec (number) +-- length_sec (number) +-- player_name (string) -- bling::playerctl::no_players --- (No parameters) +-- (No parameters) ``` ### Example Implementation @@ -122,8 +122,8 @@ By default, this module will output signals from the most recently active player These options can be set through a call to `bling.signal.playerctl.enable()` or these theme variables: ```lua theme.playerctl_backend = "playerctl_cli" -theme.playerctl_ignore = {} -theme.playerctl_player = {} +theme.playerctl_ignore = {} +theme.playerctl_player = {} theme.playerctl_update_on_activity = true theme.playerctl_position_update_interval = 1 ``` @@ -140,12 +140,12 @@ bling.signal.playerctl.enable { -- OR in your theme file: -- Same config as above but with theme variables theme.playerctl_backend = "playerctl_lib" -theme.playerctl_ignore = "firefox" -theme.playerctl_player = {"ncspot", "%any"} +theme.playerctl_ignore = "firefox" +theme.playerctl_player = {"ncspot", "%any"} -- Prioritize vlc over all other players and deprioritize spotify theme.playerctl_backend = "playerctl_lib" -theme.playerctl_player = {"vlc", "%any", "spotify"} +theme.playerctl_player = {"vlc", "%any", "spotify"} -- Disable priority of most recently active players theme.playerctl_backend = "playerctl_lib" diff --git a/docs/widgets/tabbed_misc.md b/docs/widgets/tabbed_misc.md index 2fbdb48..93aeeb5 100644 --- a/docs/widgets/tabbed_misc.md +++ b/docs/widgets/tabbed_misc.md @@ -16,11 +16,11 @@ To use the task list indicator: ```lua bling.widget.tabbed_misc.titlebar_indicator(client, { - layout_spacing = dpi(5), -- Set spacing in between items - icon_size = dpi(24), - icon_margin = 0, - bg_color_focus = "#282828", -- Color for the focused items - bg_color = "#1d2021", -- Color for normal / unfocused items + layout_spacing = dpi(5), -- Set spacing in between items + icon_size = dpi(24), -- Set icon size + icon_margin = 0, -- Set icon margin + bg_color_focus = "#282828", -- Color for the focused items + bg_color = "#1d2021", -- Color for normal / unfocused items icon_shape = gears.shape.circle -- Set icon shape, }) ``` @@ -72,32 +72,32 @@ The module exports a function that can be added to your tasklist as a `update_ca ### Usage ```lua awful.widget.tasklist({ - screen = s, - filter = awful.widget.tasklist.filter.currenttags, - layout = { - spacing = dpi(10), - layout = wibox.layout.fixed.vertical, - }, - style = { - bg_normal = "#00000000", - }, - widget_template = { - { - { - widget = wibox.widget.imagebox, - id = "icon_role", - align = "center", - valign = "center", - }, - width = dpi(24), - height = dpi(24), - widget = wibox.container.constraint, - }, - widget = wibox.container.background, -- IT MUST BE A CONTAINER WIDGET AS THAT IS WHAT THE FUNCTION EXPECTS - update_callback = require("bling.widget.tabbed_misc").custom_tasklist, - id = "background_role", - }, - }) + screen = s, + filter = awful.widget.tasklist.filter.currenttags, + layout = { + spacing = dpi(10), + layout = wibox.layout.fixed.vertical, + }, + style = { + bg_normal = "#00000000", + }, + widget_template = { + { + { + widget = wibox.widget.imagebox, + id = "icon_role", + align = "center", + valign = "center", + }, + width = dpi(24), + height = dpi(24), + widget = wibox.container.constraint, + }, + widget = wibox.container.background, -- IT MUST BE A CONTAINER WIDGET AS THAT IS WHAT THE FUNCTION EXPECTS + update_callback = require("bling.widget.tabbed_misc").custom_tasklist, + id = "background_role", + }, +}) ``` If you need to do something else, it can be used like so diff --git a/docs/widgets/tag_preview.md b/docs/widgets/tag_preview.md index 9251253..8e53373 100644 --- a/docs/widgets/tag_preview.md +++ b/docs/widgets/tag_preview.md @@ -24,19 +24,19 @@ bling.widget.tag_preview.enable { top = 30, left = 30 } - }) - end -} + }) + end +} ``` Here are the signals available: ```lua --- bling::tag_preview::update -- first line is the signal --- t (tag) -- indented lines are function parameters +-- bling::tag_preview::update -- first line is the signal +-- t (tag) -- indented lines are function parameters -- bling::tag_preview::visibility --- s (screen) --- v (boolean) +-- s (screen) +-- v (boolean) ``` By default, the widget is not visible. You must implement when it will update and when it will show. @@ -100,7 +100,7 @@ s.mytaglist = awful.widget.taglist { create_callback = function(self, c3, index, objects) --luacheck: no unused args self:get_children_by_id('index_role')[1].markup = ' '..index..' ' self:connect_signal('mouse::enter', function() - + -- BLING: Only show widget when there are clients in the tag if #c3:clients() > 0 then -- BLING: Update the widget with the new tag @@ -118,8 +118,8 @@ s.mytaglist = awful.widget.taglist { self:connect_signal('mouse::leave', function() -- BLING: Turn the widget off - awesome.emit_signal("bling::tag_preview::visibility", s, false) - + awesome.emit_signal("bling::tag_preview::visibility", s, false + if self.has_backup then self.bg = self.backup end end) end, @@ -132,17 +132,18 @@ s.mytaglist = awful.widget.taglist { ``` ### Theme Variables + ```lua -theme.tag_preview_widget_border_radius = 0 -- Border radius of the widget (With AA) -theme.tag_preview_client_border_radius = 0 -- Border radius of each client in the widget (With AA) -theme.tag_preview_client_opacity = 0.5 -- Opacity of each client -theme.tag_preview_client_bg = "#000000" -- The bg color of each client -theme.tag_preview_client_border_color = "#ffffff" -- The border color of each client -theme.tag_preview_client_border_width = 3 -- The border width of each client -theme.tag_preview_widget_bg = "#000000" -- The bg color of the widget -theme.tag_preview_widget_border_color = "#ffffff" -- The border color of the widget -theme.tag_preview_widget_border_width = 3 -- The border width of the widget -theme.tag_preview_widget_margin = 0 -- The margin of the widget +theme.tag_preview_widget_border_radius = 0 -- Border radius of the widget (With AA) +theme.tag_preview_client_border_radius = 0 -- Border radius of each client in the widget (With AA) +theme.tag_preview_client_opacity = 0.5 -- Opacity of each client +theme.tag_preview_client_bg = "#000000" -- The bg color of each client +theme.tag_preview_client_border_color = "#ffffff" -- The border color of each client +theme.tag_preview_client_border_width = 3 -- The border width of each client +theme.tag_preview_widget_bg = "#000000" -- The bg color of the widget +theme.tag_preview_widget_border_color = "#ffffff" -- The border color of the widget +theme.tag_preview_widget_border_width = 3 -- The border width of the widget +theme.tag_preview_widget_margin = 0 -- The margin of the widget ``` NOTE: I recommend to only use the widget border radius theme variable when not using shadows with a compositor, as anti-aliased rounding with the outer widgets made with AwesomeWM rely on the actual bg being transparent. If you want rounding with shadows on the widget, use a compositor like [jonaburg's fork](https://github.com/jonaburg/picom). diff --git a/docs/widgets/task_preview.md b/docs/widgets/task_preview.md index 4195dbf..da81f5e 100644 --- a/docs/widgets/task_preview.md +++ b/docs/widgets/task_preview.md @@ -12,40 +12,40 @@ To enable: ```lua bling.widget.task_preview.enable { - x = 20, -- The x-coord of the popup - y = 20, -- The y-coord of the popup - height = 200, -- The height of the popup - width = 200, -- The width of the popup - placement_fn = function(c) -- Place the widget using awful.placement (this overrides x & y) + x = 20, -- The x-coord of the popup + y = 20, -- The y-coord of the popup + height = 200, -- The height of the popup + width = 200, -- The width of the popup + placement_fn = function(c) -- Place the widget using awful.placement (this overrides x & y) awful.placement.bottom(c, { margins = { bottom = 30 } - }) + }) end -} +} ``` To allow for more customization, there is also a `widget_structure` property (as seen in some default awesome widgets) which is optional. An example is as follows - ```lua bling.widget.task_preview.enable { - x = 20, -- The x-coord of the popup - y = 20, -- The y-coord of the popup - height = 200, -- The height of the popup - width = 200, -- The width of the popup - placement_fn = function(c) -- Place the widget using awful.placement (this overrides x & y) + x = 20, -- The x-coord of the popup + y = 20, -- The y-coord of the popup + height = 200, -- The height of the popup + width = 200, -- The width of the popup + placement_fn = function(c) -- Place the widget using awful.placement (this overrides x & y) awful.placement.bottom(c, { margins = { bottom = 30 } - }) + }) end, -- Your widget will automatically conform to the given size due to a constraint container. widget_structure = { { { { - id = 'icon_role', + id = 'icon_role', widget = awful.widget.clienticon, -- The client icon }, { @@ -66,16 +66,16 @@ bling.widget.task_preview.enable { }, layout = wibox.layout.fixed.vertical } -} +} ``` Here are the signals available: ```lua -- bling::task_preview::visibility -- first line is the signal --- s (screen) -- indented lines are function parameters --- v (boolean) --- c (client) +-- s (screen) -- indented lines are function parameters +-- v (boolean) +-- c (client) ``` By default, the widget is not visible. You must implement when it will update and when it will show. @@ -124,7 +124,7 @@ s.mytasklist = awful.widget.tasklist { nil, create_callback = function(self, c, index, objects) --luacheck: no unused args self:get_children_by_id('clienticon')[1].client = c - + -- BLING: Toggle the popup on hover and disable it off hover self:connect_signal('mouse::enter', function() awesome.emit_signal("bling::task_preview::visibility", s, @@ -142,11 +142,11 @@ s.mytasklist = awful.widget.tasklist { ### Theme Variables ```lua -theme.task_preview_widget_border_radius = 0 -- Border radius of the widget (With AA) -theme.task_preview_widget_bg = "#000000" -- The bg color of the widget -theme.task_preview_widget_border_color = "#ffffff" -- The border color of the widget -theme.task_preview_widget_border_width = 3 -- The border width of the widget -theme.task_preview_widget_margin = 0 -- The margin of the widget +theme.task_preview_widget_border_radius = 0 -- Border radius of the widget (With AA) +theme.task_preview_widget_bg = "#000000" -- The bg color of the widget +theme.task_preview_widget_border_color = "#ffffff" -- The border color of the widget +theme.task_preview_widget_border_width = 3 -- The border width of the widget +theme.task_preview_widget_margin = 0 -- The margin of the widget ``` NOTE: I recommend to only use the widget border radius theme variable when not using shadows with a compositor, as anti-aliased rounding with the outer widgets made with AwesomeWM rely on the actual bg being transparent. If you want rounding with shadows on the widget, use a compositor like [jonaburg's fork](https://github.com/jonaburg/picom). diff --git a/docs/widgets/window_switcher.md b/docs/widgets/window_switcher.md index 28ed0a4..26dc7b5 100644 --- a/docs/widgets/window_switcher.md +++ b/docs/widgets/window_switcher.md @@ -16,14 +16,14 @@ bling.widget.window_switcher.enable { -- keybindings (the examples provided are also the default if kept unset) hide_window_switcher_key = "Escape", -- The key on which to close the popup - minimize_key = "n", -- The key on which to minimize the selected client - unminimize_key = "N", -- The key on which to unminimize all clients - kill_client_key = "q", -- The key on which to close the selected client - cycle_key = "Tab", -- The key on which to cycle through all clients - previous_key = "Left", -- The key on which to select the previous client - next_key = "Right", -- The key on which to select the next client - vim_previous_key = "h", -- Alternative key on which to select the previous client - vim_next_key = "l", -- Alternative key on which to select the next client + minimize_key = "n", -- The key on which to minimize the selected client + unminimize_key = "N", -- The key on which to unminimize all clients + kill_client_key = "q", -- The key on which to close the selected client + cycle_key = "Tab", -- The key on which to cycle through all clients + previous_key = "Left", -- The key on which to select the previous client + next_key = "Right", -- The key on which to select the next client + vim_previous_key = "h", -- Alternative key on which to select the previous client + vim_next_key = "l", -- Alternative key on which to select the next client } ``` @@ -35,7 +35,7 @@ awesome.emit_signal("bling::window_switcher::turn_on") For example: ```lua - awful.key({altkey}, "Tab", function() - awesome.emit_signal("bling::window_switcher::turn_on") - end, {description = "Window Switcher", group = "client"}) + awful.key({Mod1}, "Tab", function() + awesome.emit_signal("bling::window_switcher::turn_on") + end, {description = "Window Switcher", group = "bling"}) ``` From 29d4a87347d343eb8204816643cb63d2f3db1224 Mon Sep 17 00:00:00 2001 From: nuxsh Date: Wed, 13 Oct 2021 20:58:34 +0530 Subject: [PATCH 07/21] Add theme vars to window switcher docs (#122) --- docs/theme.md | 20 ++++++++++++++++++++ docs/widgets/window_switcher.md | 23 +++++++++++++++++++++++ theme-var-template.lua | 4 ++-- 3 files changed, 45 insertions(+), 2 deletions(-) diff --git a/docs/theme.md b/docs/theme.md index 592d771..8ba6694 100644 --- a/docs/theme.md +++ b/docs/theme.md @@ -75,4 +75,24 @@ theme.task_preview_widget_border_color = "#ffffff" -- The border color of the theme.task_preview_widget_border_width = 3 -- The border width of the widget theme.task_preview_widget_margin = 0 -- The margin of the widget +-- window switcher widget +theme.window_switcher_widget_bg = "#000000" -- The bg color of the widget +theme.window_switcher_widget_border_width = 3 -- The border width of the widget +theme.window_switcher_widget_border_radius = 0 -- The border radius of the widget +theme.window_switcher_widget_border_color = "#ffffff" -- The border color of the widget +theme.window_switcher_clients_spacing = 20 -- The space between each client item +theme.window_switcher_client_icon_horizontal_spacing = 5 -- The space between client icon and text +theme.window_switcher_client_width = 150 -- The width of one client widget +theme.window_switcher_client_height = 250 -- The height of one client widget +theme.window_switcher_client_margins = 10 -- The margin between the content and the border of the widget +theme.window_switcher_thumbnail_margins = 10 -- The margin between one client thumbnail and the rest of the widget +theme.thumbnail_scale = false -- If set to true, the thumbnails fit policy will be set to "fit" instead of "auto" +theme.window_switcher_name_margins = 10 -- The margin of one clients title to the rest of the widget +theme.window_switcher_name_valign = "center" -- How to vertically align one clients title +theme.window_switcher_name_forced_width = 200 -- The width of one title +theme.window_switcher_name_font = "sans 11" -- The font of all titles +theme.window_switcher_name_normal_color = "#ffffff" -- The color of one title if the client is unfocused +theme.window_switcher_name_focus_color = "#ff0000" -- The color of one title if the client is focused +theme.window_switcher_icon_valign = "center" -- How to vertically align the one icon +theme.window_switcher_icon_width = 40 -- The width of one icon ``` diff --git a/docs/widgets/window_switcher.md b/docs/widgets/window_switcher.md index 26dc7b5..46b3843 100644 --- a/docs/widgets/window_switcher.md +++ b/docs/widgets/window_switcher.md @@ -39,3 +39,26 @@ For example: awesome.emit_signal("bling::window_switcher::turn_on") end, {description = "Window Switcher", group = "bling"}) ``` + +### Theme Variables +```lua +theme.window_switcher_widget_bg = "#000000" -- The bg color of the widget +theme.window_switcher_widget_border_width = 3 -- The border width of the widget +theme.window_switcher_widget_border_radius = 0 -- The border radius of the widget +theme.window_switcher_widget_border_color = "#ffffff" -- The border color of the widget +theme.window_switcher_clients_spacing = 20 -- The space between each client item +theme.window_switcher_client_icon_horizontal_spacing = 5 -- The space between client icon and text +theme.window_switcher_client_width = 150 -- The width of one client widget +theme.window_switcher_client_height = 250 -- The height of one client widget +theme.window_switcher_client_margins = 10 -- The margin between the content and the border of the widget +theme.window_switcher_thumbnail_margins = 10 -- The margin between one client thumbnail and the rest of the widget +theme.thumbnail_scale = false -- If set to true, the thumbnails fit policy will be set to "fit" instead of "auto" +theme.window_switcher_name_margins = 10 -- The margin of one clients title to the rest of the widget +theme.window_switcher_name_valign = "center" -- How to vertically align one clients title +theme.window_switcher_name_forced_width = 200 -- The width of one title +theme.window_switcher_name_font = "sans 11" -- The font of all titles +theme.window_switcher_name_normal_color = "#ffffff" -- The color of one title if the client is unfocused +theme.window_switcher_name_focus_color = "#ff0000" -- The color of one title if the client is focused +theme.window_switcher_icon_valign = "center" -- How to vertically align the one icon +theme.window_switcher_icon_width = 40 -- The width of one icon +``` diff --git a/theme-var-template.lua b/theme-var-template.lua index 4fc1c8e..421d37c 100644 --- a/theme-var-template.lua +++ b/theme-var-template.lua @@ -98,7 +98,7 @@ theme.window_switcher_name_forced_width = 200 -- The width of one title theme.window_switcher_name_font = "Sans 11" -- The font of all titles theme.window_switcher_name_normal_color = "#ffffff" -- The color of one title if the client is unfocused theme.window_switcher_name_focus_color = "#ff0000" -- The color of one title if the client is focused -theme.window_switcher_icon_valign = "center" -- How to vertially align the one icon -theme.window_switcher_icon_width = 40 -- Thw width of one icon +theme.window_switcher_icon_valign = "center" -- How to vertically align the one icon +theme.window_switcher_icon_width = 40 -- The width of one icon -- LuaFormatter on From 2a6afd05942b0192f7e10f0478a9f87183567a2f Mon Sep 17 00:00:00 2001 From: nuxsh Date: Mon, 18 Oct 2021 13:17:25 +0530 Subject: [PATCH 08/21] Add tabbar inactive theme vars to docs (#123) --- docs/module/tabbed.md | 24 ++++++++++++++---------- docs/theme.md | 23 ++++++++++++++--------- 2 files changed, 28 insertions(+), 19 deletions(-) diff --git a/docs/module/tabbed.md b/docs/module/tabbed.md index 35edb07..e705357 100644 --- a/docs/module/tabbed.md +++ b/docs/module/tabbed.md @@ -21,16 +21,20 @@ theme.tabbed_spawn_in_tab = false -- whether a new client should spawn into the -- For tabbar in general theme.tabbar_ontop = false -theme.tabbar_radius = 0 -- border radius of the tabbar -theme.tabbar_style = "default" -- style of the tabbar ("default", "boxes" or "modern") -theme.tabbar_font = "Sans 11" -- font of the tabbar -theme.tabbar_size = 40 -- size of the tabbar -theme.tabbar_position = "top" -- position of the tabbar -theme.tabbar_bg_normal = "#000000" -- background color of the focused client on the tabbar -theme.tabbar_fg_normal = "#ffffff" -- foreground color of the focused client on the tabbar -theme.tabbar_bg_focus = "#1A2026" -- background color of unfocused clients on the tabbar -theme.tabbar_fg_focus = "#ff0000" -- foreground color of unfocused clients on the tabbar -theme.tabbar_disable = false -- disable the tab bar entirely +theme.tabbar_radius = 0 -- border radius of the tabbar +theme.tabbar_style = "default" -- style of the tabbar ("default", "boxes" or "modern") +theme.tabbar_font = "Sans 11" -- font of the tabbar +theme.tabbar_size = 40 -- size of the tabbar +theme.tabbar_position = "top" -- position of the tabbar +theme.tabbar_bg_normal = "#000000" -- background color of the focused client on the tabbar +theme.tabbar_fg_normal = "#ffffff" -- foreground color of the focused client on the tabbar +theme.tabbar_bg_focus = "#1A2026" -- background color of unfocused clients on the tabbar +theme.tabbar_fg_focus = "#ff0000" -- foreground color of unfocused clients on the tabbar +theme.tabbar_bg_focus_inactive = nil -- background color of the focused client on the tabbar when inactive +theme.tabbar_fg_focus_inactive = nil -- foreground color of the focused client on the tabbar when inactive +theme.tabbar_bg_normal_inactive = nil -- background color of unfocused clients on the tabbar when inactive +theme.tabbar_fg_normal_inactive = nil -- foreground color of unfocused clients on the tabbar when inactive +theme.tabbar_disable = false -- disable the tab bar entirely -- the following variables are currently only for the "modern" tabbar style theme.tabbar_color_close = "#f9929b" -- chnges the color of the close button diff --git a/docs/theme.md b/docs/theme.md index 8ba6694..60f9077 100644 --- a/docs/theme.md +++ b/docs/theme.md @@ -26,15 +26,20 @@ theme.tabbed_spawn_in_tab = false -- whether a new client should spawn -- tabbar general theme.tabbar_ontop = false -theme.tabbar_radius = 0 -- border radius of the tabbar -theme.tabbar_style = "default" -- style of the tabbar ("default", "boxes" or "modern") -theme.tabbar_font = "Sans 11" -- font of the tabbar -theme.tabbar_size = 40 -- size of the tabbar -theme.tabbar_position = "top" -- position of the tabbar -theme.tabbar_bg_normal = "#000000" -- background color of the focused client on the tabbar -theme.tabbar_fg_normal = "#ffffff" -- foreground color of the focused client on the tabbar -theme.tabbar_bg_focus = "#1A2026" -- background color of unfocused clients on the tabbar -theme.tabbar_fg_focus = "#ff0000" -- foreground color of unfocused clients on the tabbar +theme.tabbar_radius = 0 -- border radius of the tabbar +theme.tabbar_style = "default" -- style of the tabbar ("default", "boxes" or "modern") +theme.tabbar_font = "Sans 11" -- font of the tabbar +theme.tabbar_size = 40 -- size of the tabbar +theme.tabbar_position = "top" -- position of the tabbar +theme.tabbar_bg_normal = "#000000" -- background color of the focused client on the tabbar +theme.tabbar_fg_normal = "#ffffff" -- foreground color of the focused client on the tabbar +theme.tabbar_bg_focus = "#1A2026" -- background color of unfocused clients on the tabbar +theme.tabbar_fg_focus = "#ff0000" -- foreground color of unfocused clients on the tabbar +theme.tabbar_bg_focus_inactive = nil -- background color of the focused client on the tabbar when inactive +theme.tabbar_fg_focus_inactive = nil -- foreground color of the focused client on the tabbar when inactive +theme.tabbar_bg_normal_inactive = nil -- background color of unfocused clients on the tabbar when inactive +theme.tabbar_fg_normal_inactive = nil -- foreground color of unfocused clients on the tabbar when inactive +theme.tabbar_disable = false -- disable the tab bar entirely -- mstab theme.mstab_bar_ontop = false -- whether you want to allow the bar to be ontop of clients From 2816b2124b6073b19b3b4937df66ef5f27423b8d Mon Sep 17 00:00:00 2001 From: Nooo37 Date: Sun, 24 Oct 2021 12:42:16 +0200 Subject: [PATCH 09/21] chore: update rockspec --- bling-scm-5.rockspec => bling-dev-1.rockspec | 37 ++++++++++++-------- 1 file changed, 23 insertions(+), 14 deletions(-) rename bling-scm-5.rockspec => bling-dev-1.rockspec (66%) diff --git a/bling-scm-5.rockspec b/bling-dev-1.rockspec similarity index 66% rename from bling-scm-5.rockspec rename to bling-dev-1.rockspec index 2dbb36c..575d3f0 100644 --- a/bling-scm-5.rockspec +++ b/bling-dev-1.rockspec @@ -1,19 +1,19 @@ package = "bling" -version = "scm-5" +version = "dev-1" source = { - url = "git://github.com/Nooo37/bling", + url = "git://github.com/BlingCorp/bling", branch = "master", } description = { summary = "Utilities for the AwesomeWM", detailed = [[ - This module extends the Awesome window manager with alternative layouts, - flash focus, tabbing, a simple tiling wallpaper generator, a declarative + This module extends the Awesome window manager with alternative layouts, + flash focus, tabbing, a simple tiling wallpaper generator, a declarative wallpaper setter, window swallowing and a playerctl signal. ]], - homepage = "https://github.com/Nooo37/bling", + homepage = "https://github.com/BlingCorp/bling", license = "MIT", } @@ -23,32 +23,41 @@ dependencies = { build = { type = "builtin", - modules = { + modules = { ["bling"] = "init.lua", - ["bling.layout"] = "layout/init.lua", - ["bling.layout.centered"] = "layout/centered.lua", - ["bling.layout.equalarea"] = "layout/equalarea.lua", - ["bling.layout.horizontal"] = "layout/horizontal.lua", - ["bling.layout.mstab"] = "layout/mstab.lua", - ["bling.layout.vertical"] = "layout/vertical.lua", ["bling.helpers"] = "helpers/init.lua", ["bling.helpers.client"] = "helpers/client.lua", ["bling.helpers.color"] = "helpers/color.lua", ["bling.helpers.filesystem"] = "helpers/filesystem.lua", ["bling.helpers.shape"] = "helpers/shape.lua", ["bling.helpers.time"] = "helpers/time.lua", + ["bling.layout"] = "layout/init.lua", + ["bling.layout.centered"] = "layout/centered.lua", + ["bling.layout.deck"] = "layout/deck.lua", + ["bling.layout.equalarea"] = "layout/equalarea.lua", + ["bling.layout.horizontal"] = "layout/horizontal.lua", + ["bling.layout.mstab"] = "layout/mstab.lua", + ["bling.layout.vertical"] = "layout/vertical.lua", ["bling.module"] = "module/init.lua", ["bling.module.flash_focus"] = "module/flash_focus.lua", + ["bling.module.scratchpad"] = "module/scratchpad.lua", ["bling.module.tabbed"] = "module/tabbed.lua", ["bling.module.tiled_wallpaper"] = "module/tiled_wallpaper.lua", ["bling.module.wallpaper"] = "module/wallpaper.lua", ["bling.module.window_swallowing"] = "module/window_swallowing.lua", ["bling.signal"] = "signal/init.lua", - ["bling.signal.playerctl"] = "signal/playerctl.lua", + ["bling.signal.playerctl"] = "signal/playerctl/init.lua", + ["bling.signal.playerctl.playerctl_cli"] = "signal/playerctl/playerctl_cli.lua", + ["bling.signal.playerctl.playerctl_lib"] = "signal/playerctl/playerctl_lib.lua", + ["bling.widget"] = "widget/init.lua", ["bling.widget.tabbar.boxes"] = "widget/tabbar/boxes.lua", ["bling.widget.tabbar.default"] = "widget/tabbar/default.lua", ["bling.widget.tabbar.modern"] = "widget/tabbar/modern.lua", + ["bling.widget.tabbed_misc"] = "widget/tabbed_misc/init.lua", + ["bling.widget.tabbed_misc.custom_tasklist"] = "widget/tabbed_misc/custom_tasklist.lua", + ["bling.widget.tabbed_misc.titlebar_indicator"] = "widget/tabbed_misc/titlebar_indicator.lua", ["bling.widget.tag_preview"] = "widget/tag_preview.lua", - ["bling.widget"] = "widget/init.lua", + ["bling.widget.task_preview"] = "widget/task_preview.lua", + ["bling.widget.window_switcher"] = "widget/window_switcher.lua", }, } From 4e84a884d90b2b5714061d9c32e7635325148067 Mon Sep 17 00:00:00 2001 From: gokul <33443763+JavaCafe01@users.noreply.github.com> Date: Sun, 31 Oct 2021 00:14:29 -0700 Subject: [PATCH 10/21] Added new feature for tag_preview widget (#127) The tag preview widgets can have a wallpaper background. --- docs/widgets/tag_preview.md | 8 +++++++- widget/tag_preview.lua | 27 +++++++++++++++++---------- 2 files changed, 24 insertions(+), 11 deletions(-) diff --git a/docs/widgets/tag_preview.md b/docs/widgets/tag_preview.md index 8e53373..e9e081b 100644 --- a/docs/widgets/tag_preview.md +++ b/docs/widgets/tag_preview.md @@ -25,7 +25,13 @@ bling.widget.tag_preview.enable { left = 30 } }) - end + end, + background_image = wibox.widget { -- Set a background image (like a wallpaper) for the widget + image = beautiful.wallpaper, + horizontal_fit_policy = "fit", + vertical_fit_policy = "fit", + widget = wibox.widget.imagebox + } } ``` diff --git a/widget/tag_preview.lua b/widget/tag_preview.lua index f663be4..e6ea1be 100644 --- a/widget/tag_preview.lua +++ b/widget/tag_preview.lua @@ -28,7 +28,8 @@ local function draw_widget( widget_border_color, widget_border_width, geo, - margin + margin, + background_image ) local client_list = wibox.layout.manual() client_list.forced_height = geo.height @@ -109,20 +110,24 @@ local function draw_widget( return wibox.widget { { + background_image, { { { - client_list, - forced_height = geo.height, - forced_width = geo.width, - widget = wibox.container.place, + { + client_list, + forced_height = geo.height, + forced_width = geo.width, + widget = wibox.container.place, + }, + layout = wibox.layout.align.horizontal, }, - layout = wibox.layout.align.horizontal, + layout = wibox.layout.align.vertical, }, - layout = wibox.layout.align.vertical, + margins = margin, + widget = wibox.container.margin, }, - margins = margin, - widget = wibox.container.margin, + layout = wibox.layout.stack }, bg = widget_bg, shape_border_width = widget_border_width, @@ -142,6 +147,7 @@ local enable = function(opts) local work_area = opts.honor_workarea or false local padding = opts.honor_padding or false local placement_fn = opts.placement_fn or nil + local background_image = opts.background_image or nil local margin = beautiful.tag_preview_widget_margin or dpi(0) local screen_radius = beautiful.tag_preview_widget_border_radius or dpi(0) @@ -196,7 +202,8 @@ local enable = function(opts) widget_border_color, widget_border_width, geo, - margin + margin, + background_image ) end) From f2ec9d9920f559a33d0cdb36662316c17539f54b Mon Sep 17 00:00:00 2001 From: gokul <33443763+JavaCafe01@users.noreply.github.com> Date: Sun, 31 Oct 2021 00:16:43 -0700 Subject: [PATCH 11/21] Update tag_preview.lua --- widget/tag_preview.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/widget/tag_preview.lua b/widget/tag_preview.lua index e6ea1be..ced0f56 100644 --- a/widget/tag_preview.lua +++ b/widget/tag_preview.lua @@ -147,7 +147,7 @@ local enable = function(opts) local work_area = opts.honor_workarea or false local padding = opts.honor_padding or false local placement_fn = opts.placement_fn or nil - local background_image = opts.background_image or nil + local background_image = opts.background_widget or nil local margin = beautiful.tag_preview_widget_margin or dpi(0) local screen_radius = beautiful.tag_preview_widget_border_radius or dpi(0) From 0688673dc4744f687b9afa4d7884ba190fdc203e Mon Sep 17 00:00:00 2001 From: gokul <33443763+JavaCafe01@users.noreply.github.com> Date: Sun, 31 Oct 2021 00:18:37 -0700 Subject: [PATCH 12/21] Update variable in docs and fix syntax errors --- docs/widgets/tag_preview.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/widgets/tag_preview.md b/docs/widgets/tag_preview.md index e9e081b..159c602 100644 --- a/docs/widgets/tag_preview.md +++ b/docs/widgets/tag_preview.md @@ -26,7 +26,7 @@ bling.widget.tag_preview.enable { } }) end, - background_image = wibox.widget { -- Set a background image (like a wallpaper) for the widget + background_widget = wibox.widget { -- Set a background image (like a wallpaper) for the widget image = beautiful.wallpaper, horizontal_fit_policy = "fit", vertical_fit_policy = "fit", @@ -124,7 +124,7 @@ s.mytaglist = awful.widget.taglist { self:connect_signal('mouse::leave', function() -- BLING: Turn the widget off - awesome.emit_signal("bling::tag_preview::visibility", s, false + awesome.emit_signal("bling::tag_preview::visibility", s, false) if self.has_backup then self.bg = self.backup end end) From f3215bcaec53f83be225801bd01acc1f947dd8f0 Mon Sep 17 00:00:00 2001 From: gokul <33443763+JavaCafe01@users.noreply.github.com> Date: Sun, 31 Oct 2021 01:22:51 -0700 Subject: [PATCH 13/21] Update gif for tag_preview docs --- docs/widgets/tag_preview.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/widgets/tag_preview.md b/docs/widgets/tag_preview.md index 159c602..bdf033d 100644 --- a/docs/widgets/tag_preview.md +++ b/docs/widgets/tag_preview.md @@ -2,7 +2,7 @@ This is a popup widget that will show a preview of a specified tag that illustrates the position, size, content, and icon of all clients. -![](https://imgur.com/3nYe1e8.gif) +![](https://imgur.com/zFdvs4K.gif) *gif by [javacafe](https://github.com/JavaCafe01)* From d1277c10fc74a18388325175c73a416bfc68b28f Mon Sep 17 00:00:00 2001 From: Kasper Date: Wed, 3 Nov 2021 23:38:36 +0200 Subject: [PATCH 14/21] Add a new 'icon-theme' module (#129) * Add a new 'icon-theme' module * Chcek for nil on get_gicon_path --- module/icon_theme.lua | 122 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 module/icon_theme.lua diff --git a/module/icon_theme.lua b/module/icon_theme.lua new file mode 100644 index 0000000..efc7c30 --- /dev/null +++ b/module/icon_theme.lua @@ -0,0 +1,122 @@ +local Gio = require("lgi").Gio +local Gtk = require("lgi").Gtk +local gobject = require("gears.object") +local gtable = require("gears.table") +local beautiful = require("beautiful") +local helpers = require("helpers") +local setmetatable = setmetatable +local ipairs = ipairs + +local icon_theme = { mt = {} } + +function icon_theme:get_client_icon_path(client) + local function find_icon(class) + if self._private.client_icon_cache[class] ~= nil then + return self._private.client_icon_cache[class] + end + + for _, app in ipairs(Gio.AppInfo.get_all()) do + local id = Gio.AppInfo.get_id(app) + if id:match(helpers.misc.case_insensitive_pattern(class)) then + self._private.client_icon_cache[class] = self:get_gicon_path(Gio.AppInfo.get_icon(app)) + return self._private.client_icon_cache[class] + end + end + + return nil + end + + local class = client.class + if class == "jetbrains-studio" then + class = "android-studio" + end + + local icon = self:get_icon_path("gnome-window-manager") + + if class ~= nil then + class = class:gsub("[%-]", "%%%0") + icon = find_icon(class) or icon + + class = client.class + class = class:gsub("[%-]", "") + icon = find_icon(class) or icon + + class = client.class + class = class:gsub("[%-]", ".") + icon = find_icon(class) or icon + + class = client.class + class = class:match("(.-)-") or class + class = class:match("(.-)%.") or class + class = class:match("(.-)%s+") or class + class = class:gsub("[%-]", "%%%0") + icon = find_icon(class) or icon + end + + return icon +end + +function icon_theme:get_gicon_path(gicon) + if gicon == nil then + return "" + end + + if self._private.icon_cache[gicon] ~= nil then + return self._private.icon_cache[gicon] + end + + local icon_info = Gtk.IconTheme.lookup_by_gicon(self.gtk_theme, gicon, self.icon_size, 0); + if icon_info then + local icon_path = Gtk.IconInfo.get_filename(icon_info) + if icon_path then + self._private.icon_cache[gicon] = icon_path + return icon_path + end + end + + return "" +end + +function icon_theme:get_icon_path(icon_name) + if self._private.icon_cache[icon_name] ~= nil then + return self._private.icon_cache[icon_name] + end + + local icon_info = Gtk.IconTheme.lookup_icon(self.gtk_theme, icon_name, self.icon_size, 0); + if icon_info then + local icon_path = Gtk.IconInfo.get_filename(icon_info) + if icon_path then + self._private.icon_cache[icon_name] = icon_path + return icon_path + end + end + + return "" +end + +local function new(theme_name, icon_size) + local ret = gobject{} + gtable.crush(ret, icon_theme, true) + + ret._private = {} + ret._private.client_icon_cache = {} + ret._private.icon_cache = {} + + 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 From bebb445de6156c981a70b7636acfae3d82cd31e5 Mon Sep 17 00:00:00 2001 From: Kasper Date: Wed, 3 Nov 2021 23:38:50 +0200 Subject: [PATCH 15/21] App launcher ala rofi widget (#103) * Initial commit * Fix missing dpi variable * Add an option to search by command * Add turn_on and turn_off signals * Add options to skip apps by their names or commands * Add an option to skip apps with no icons * Fix looping over the wrong table * Refactor to make it into a proper bling like widget * Fix selecting the wrong app after a search * Why was this in a seperate check? * Fix various issues with toggle/show/hide * Stop it from complaining * Fix wrong app getting selected after scrolling up/down * Add an option to spawn the app when pressing on it regardless if it was selected or not * lol what? * Don't add widgets that won't be visible after scrolling down * Yap wasn't needed * This is a little clearer * Add an option 'try_to_keep_index_after_search' to mimic rofi behaviour * Only add widgets that are visible after a search * Fix search not adding the correct number of widgets * Add proper customization options * Add proper customizaiton options for the prompt * Simplfy scroll down logic and fix possible bugs * Add animation support * Fix app list being empty on some occasions * Default placement when x and y is nil * Free up ram * Add a default icon option * style change * Not needed and also hurts search peformance by a decent amount * Fix error when trying to spawn an app when no app is currently marked * Not needed * Add a small debounce delay for the search to prevent it from lagging * Formatting * Replace menubar with app_info * Fix the default icon option --- widget/app_launcher.lua | 600 ++++++++++++++++++++++++++++++++++++++++ widget/init.lua | 1 + 2 files changed, 601 insertions(+) create mode 100644 widget/app_launcher.lua diff --git a/widget/app_launcher.lua b/widget/app_launcher.lua new file mode 100644 index 0000000..cc13659 --- /dev/null +++ b/widget/app_launcher.lua @@ -0,0 +1,600 @@ +local Gio = require("lgi").Gio +local awful = require("awful") +local gobject = require("gears.object") +local gtable = require("gears.table") +local gtimer = require("gears.timer") +local wibox = require("wibox") +local beautiful = require("beautiful") +local icon_theme = require(tostring(...):match(".*bling") .. ".module.icon_theme")() +local dpi = beautiful.xresources.apply_dpi + +local string = string +local table = table +local math = math +local pairs = pairs +local root = root + +local app_launcher = { mt = {} } + +local function mark_app(self, index) + local app = self._private.grid.children[index] + if app ~= nil then + app:get_children_by_id("background")[1].bg = self.app_selected_color + local text_widget = app:get_children_by_id("text")[1] + if text_widget ~= nil then + text_widget.markup = "" .. text_widget.text .. "" + end + end +end + +local function unmark_app(self, index) + local app = self._private.grid.children[index] + if app ~= nil then + app:get_children_by_id("background")[1].bg = self.app_normal_color + local text_widget = app:get_children_by_id("text")[1] + if text_widget ~= nil then + text_widget.markup = "" .. text_widget.text .. "" + end + end +end + +local function create_app_widget(self, name, cmdline, icon, index) + local icon = self.app_show_icon == true + and + { + widget = wibox.container.place, + halign = self.app_name_halign, + { + widget = wibox.widget.imagebox, + forced_width = self.app_icon_width, + forced_height = self.app_icon_height, + image = icon + } + } + or nil + local name = self.app_show_name == true + and + { + widget = wibox.container.place, + halign = self.app_icon_halign, + { + widget = wibox.widget.textbox, + id = "text", + align = "center", + font = self.app_name_font, + markup = name + } + } + or nil + + return wibox.widget + { + widget = wibox.container.background, + id = "background", + forced_width = self.app_width, + forced_height = self.app_height, + shape = self.app_shape, + bg = self.app_normal_color, + spawn = function() awful.spawn(cmdline) end, + buttons = + { + awful.button({}, 1, function() + if index == self._private.current_index or not self.select_before_spawn then + awful.spawn(cmdline) + self:hide() + else + -- Unmark the previous app + unmark_app(self, self._private.current_index) + + self._private.current_index = index + + -- Mark this app + mark_app(self, self._private.current_index) + end + end), + }, + { + widget = wibox.container.place, + valign = self.app_content_valign, + { + layout = wibox.layout.fixed.vertical, + spacing = self.app_content_spacing, + icon, + name + } + } + } +end + +local function has_value(tab, val) + for index, value in pairs(tab) do + if val:find(value) then + return true + end + end + return false +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 +end + +local function search(self, text) + -- Reset all the matched entries + self._private.matched_entries = {} + -- Remove all the grid widgets + self._private.grid:reset() + + for index, entry in pairs(self._private.all_entries) do + text = text:gsub( "%W", "" ) + + -- Check if there's a match by the app name or app command + if string.find(entry.name, case_insensitive_pattern(text)) ~= nil or + self.search_commands and string.find(entry.cmdline, case_insensitive_pattern(text)) ~= nil + then + table.insert(self._private.matched_entries, { name = entry.name, cmdline = entry.cmdline, icon = entry.icon }) + + -- Only add the widgets for apps that are part of the first page + if #self._private.grid.children + 1 <= self._private.max_apps_per_page then + self._private.grid:add(create_app_widget(self, entry.name, entry.cmdline, entry.icon, #self._private.grid.children + 1)) + end + 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)) + + -- This is an option to mimic rofi behaviour where after a search + -- it will reselect the app whose index is the same as the app index that was previously selected + -- and if matched_entries.length < current_index it will instead select the app with the greatest index + if self.try_to_keep_index_after_searching then + self._private.current_index = math.max(math.min(self._private.current_index, #self._private.matched_entries), 1) + + -- Otherwise select the first app on the list + else + self._private.current_index = 1 + end + self._private.current_page = 1 + + mark_app(self, self._private.current_index) +end + +local function scroll_up(self) + -- Check if the current marked app is not the first + if self._private.current_index > 1 then + unmark_app(self, self._private.current_index) + + -- Current index should be decremented + self._private.current_index = self._private.current_index - 1 + + -- Mark the new app + mark_app(self, self._private.current_index) + + -- Check if the current page is not the first + elseif self._private.current_page > 1 then + -- Remove the current page apps from the grid + self._private.grid:reset() + + local max_app_index_to_include = (self._private.current_page - 1) * self._private.apps_per_page + local min_app_index_to_include = max_app_index_to_include - self._private.apps_per_page + + + for index, entry in pairs(self._private.matched_entries) do + -- Only add widgets that are between this range (part of the current page) + if index > min_app_index_to_include and index <= max_app_index_to_include then + self._private.grid:add(create_app_widget(self, entry.name, entry.cmdline, entry.icon, #self._private.grid.children + 1)) + end + end + + -- If we scrolled up a page, selected app should be the last one + self._private.current_index = self._private.apps_per_page + mark_app(self, self._private.current_index) + + -- Current page should be decremented + self._private.current_page = self._private.current_page - 1 + end +end + +local function scroll_down(self) + local is_less_than_max_app = self._private.current_index < #self._private.grid.children + local is_less_than_max_page = self._private.current_page < self._private.pages_count + + -- Check if we can scroll down the app list + if is_less_than_max_app then + -- Unmark the previous app + unmark_app(self, self._private.current_index) + + -- Current index should be incremented + self._private.current_index = self._private.current_index + 1 + + -- Mark the new app + mark_app(self, self._private.current_index) + + -- If we can't scroll down the app list, check if we can scroll down a page + elseif is_less_than_max_page then + -- Remove the current page apps from the grid + self._private.grid:reset() + + local min_app_index_to_include = self._private.current_index * self._private.current_page + local max_app_index_to_include = min_app_index_to_include + self._private.apps_per_page + + for index, entry in pairs(self._private.matched_entries) do + -- Only add widgets that are between this range (part of the current page) + if index > min_app_index_to_include and index <= max_app_index_to_include then + self._private.grid:add(create_app_widget(self, entry.name, entry.cmdline, entry.icon, #self._private.grid.children + 1)) + end + end + + -- Current app is 1 if we scroll to the next page + self._private.current_index = 1 + mark_app(self, self._private.current_index) + + -- Current page should be incremented + self._private.current_page = self._private.current_page + 1 + end +end + +--- Shows the app launcher +function app_launcher:show(args) + local args = args or {} + + self.screen = args.screen or self.screen + self.screen.app_launcher = self._private.widget + self.screen.app_launcher.screen = self.screen + self.screen.app_launcher.visible = true + self._private.prompt:run() + + local x = args.x or self.x or nil + if self.rubato and self.rubato.x and x then + self.rubato.x:set(x) + elseif x then + self.screen.app_launcher.x = x + end + + local y = args.y or self.y or nil + if self.rubato and self.rubato.y and y then + self.rubato.y:set(y) + elseif y then + self.screen.app_launcher.y = y + end + + local placement = args.placement or self.placement or nil + if placement then + self.screen.app_launcher.placement = placement + end + + self:emit_signal("bling::app_launcher::visibility", true) +end + +--- Hides the app launcher +function app_launcher:hide(args) + local args = args or {} + + -- There's no other way to stop the prompt? + root.fake_input('key_press', "Escape") + root.fake_input('key_release', "Escape") + + if self.rubato and self.rubato.x then + self.rubato.x:set(self.rubato.x:initial()) + self.rubato.x.ended:subscribe(function() + self.screen.app_launcher.visible = false + end) + end + + if self.rubato and self.rubato.y then + self.rubato.y:set(self.rubato.y:initial()) + self.rubato.y.ended:subscribe(function() + self.screen.app_launcher.visible = false + end) + end + + if not self.rubato then + self.screen.app_launcher.visible = false + end + + self.screen = args.screen or self.screen + self.screen.app_launcher = {} + + -- Reset back to initial values + self._private.apps_per_page = self._private.max_apps_per_page + self._private.pages_count = math.ceil(#self._private.all_entries / self._private.apps_per_page) + self._private.matched_entries = self._private.all_entries + self._private.current_index = 1 + self._private.current_page = 1 + self._private.grid:reset() + + -- Add the app widgets for the next time + for index, entry in pairs(self._private.all_entries) do + -- Only add the apps that are part of the first page + if index <= self._private.apps_per_page then + self._private.grid:add(create_app_widget(self, entry.name, entry.cmdline, entry.icon, index)) + else + break + end + end + + -- Select the first app for the next time + mark_app(self, self._private.current_index) + + self:emit_signal("bling::app_launcher::visibility", false) +end + +--- Toggles the app launcher +function app_launcher:toggle(args) + local args = args or {} + + self.screen = args.screen or self.screen + if self.screen.app_launcher and self.screen.app_launcher.visible then + self:hide(self.screen) + else + self:show(self.screen) + end +end + +-- Returns a new app launcher +local function new(args) + args = args or {} + + args.search_commands = args.search_commands or true + args.skip_names = args.skip_names or {} + args.skip_commands = args.skip_commands or {} + args.skip_empty_icons = args.skip_empty_icons or false + args.sort_alphabetically = args.sort_alphabetically or true + args.select_before_spawn = args.select_before_spawn or true + args.try_to_keep_index_after_searching = args.try_to_keep_index_after_searching or false + args.default_app_icon_name = args.default_app_icon_name or nil + args.default_app_icon_path = args.default_app_icon_path or nil + + args.rubato = args.rubato or nil + args.shirnk_width = args.shirnk_width or false + args.shrink_height = args.shrink_height or false + args.background = args.background or "#000000" + args.screen = args.screen or screen.primary + args.x = args.x or nil + args.y = args.y or nil + args.placement = args.placement or (args.x == nil and args.y == nil) and awful.placement.centered or nil + args.shape = args.shape or nil + + args.prompt_height = args.prompt_height or dpi(100) + 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" + + 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) + + args.expand_apps = args.expand_apps or true + args.app_width = args.app_width or dpi(300) + args.app_height = args.app_height or dpi(100) + args.app_shape = args.app_shape or nil + args.app_normal_color = args.app_normal_color or beautiful.bg_normal or "#000000" + args.app_selected_color = args.app_selected_color or beautiful.fg_normal or "#FFFFFF" + args.app_content_valign = args.app_content_valign or "center" + args.app_content_spacing = args.app_content_spacing or dpi(10) + args.app_show_icon = args.app_show_icon == nil and true or args.app_show_icon + 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_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" + + local ret = gobject({}) + ret._private = {} + + gtable.crush(ret, app_launcher) + gtable.crush(ret, args) + + -- Determines the grid width + local grid_width = ret.shirnk_width == false + and dpi((ret.app_width * ret.apps_per_column) + ((ret.apps_per_column - 1) * ret.apps_spacing)) + or nil + local grid_height = ret.shrink_height == false + 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 = awful.widget.prompt + { + prompt = ret.prompt_text, + text = ret.prompt_start_text, + font = ret.prompt_font, + bg = ret.prompt_color, + fg = ret.prompt_text_color, + bg_cursor = ret.prompt_cursor_color, + changed_callback = function(text) + 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 + } + end, + keypressed_callback = function(mod, key, cmd) + if key == "Return" then + if ret._private.grid.children[ret._private.current_index] ~= nil then + ret._private.grid.children[ret._private.current_index].spawn() + end + end + print(key) + end, + done_callback = function() + ret:hide() + 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 = "dock", + visible = false, + ontop = true, + 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, + 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 + } + } + } + } + }, + { + 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.all_entries = {} + ret._private.matched_entries = {} + ret._private.apps_per_page = ret.apps_per_column * ret.apps_per_row + ret._private.max_apps_per_page = ret._private.apps_per_page + ret._private.pages_count = 0 + ret._private.current_index = 1 + ret._private.current_page = 1 + + local app_info = Gio.AppInfo + local apps = app_info.get_all() + if ret.sort_alphabetically then + table.sort(apps, function(a, b) return app_info.get_name(a):lower() < app_info.get_name(b):lower() end) + end + + for _, app in ipairs(apps) do + if app.should_show(app) then + -- Check if this app should be skipped, depanding on the skip_names / skip_commands table + local name = app_info.get_name(app) + local commandline = app_info.get_commandline(app) + local icon = icon_theme:get_gicon_path(app_info.get_icon(app)) + + if not has_value(ret.skip_names, name) and not has_value(ret.skip_commands, commandline) then + -- Check if this app should be skipped becuase it's iconless depanding on skip_empty_icons + if icon ~= "" or ret.skip_empty_icons == false then + if icon == "" then + if ret.default_app_icon_name ~= nil then + icon = icon_theme:get_icon_path("app") + elseif ret.default_app_icon_path ~= nil then + icon = ret.default_app_icon_path + end + end + + -- Insert a table containing the name, command and icon of the app into the all_entries table + table.insert(ret._private.all_entries, { name = name, cmdline = commandline, icon = icon }) + + -- Only add the app widgets that are part of the first page + if #ret._private.all_entries <= ret._private.apps_per_page then + ret._private.grid:add(create_app_widget(ret, name, commandline, icon, #ret._private.all_entries)) + end + end + end + + -- Matched entries contains all the apps initially + ret._private.matched_entries = ret._private.all_entries + ret._private.pages_count = math.ceil(#ret._private.all_entries / ret._private.apps_per_page) + + -- Mark the first app on startup + mark_app(ret, 1) + end + end + + 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 + + return ret +end + +function app_launcher.mt:__call(...) + return new(...) +end + +return setmetatable(app_launcher, app_launcher.mt) diff --git a/widget/init.lua b/widget/init.lua index df8bfe9..d3c6ebd 100644 --- a/widget/init.lua +++ b/widget/init.lua @@ -3,4 +3,5 @@ return { task_preview = require(... .. ".task_preview"), window_switcher = require(... .. ".window_switcher"), tabbed_misc = require(... .. ".tabbed_misc"), + app_launcher = require(... .. ".app_launcher"), } From 274df77bba6e88563f754bf8fec18e6d6cceb61d Mon Sep 17 00:00:00 2001 From: Gokul Swaminathan Date: Wed, 3 Nov 2021 14:44:03 -0700 Subject: [PATCH 16/21] Minor edits for the icon_theme and app_launcher PR --- {module => helpers}/icon_theme.lua | 3 +-- widget/app_launcher.lua | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) rename {module => helpers}/icon_theme.lua (97%) diff --git a/module/icon_theme.lua b/helpers/icon_theme.lua similarity index 97% rename from module/icon_theme.lua rename to helpers/icon_theme.lua index efc7c30..492d81e 100644 --- a/module/icon_theme.lua +++ b/helpers/icon_theme.lua @@ -2,7 +2,6 @@ local Gio = require("lgi").Gio local Gtk = require("lgi").Gtk local gobject = require("gears.object") local gtable = require("gears.table") -local beautiful = require("beautiful") local helpers = require("helpers") local setmetatable = setmetatable local ipairs = ipairs @@ -119,4 +118,4 @@ function icon_theme.mt:__call(...) return new(...) end -return setmetatable(icon_theme, icon_theme.mt) \ No newline at end of file +return setmetatable(icon_theme, icon_theme.mt) diff --git a/widget/app_launcher.lua b/widget/app_launcher.lua index cc13659..8ef7d92 100644 --- a/widget/app_launcher.lua +++ b/widget/app_launcher.lua @@ -5,7 +5,7 @@ local gtable = require("gears.table") local gtimer = require("gears.timer") local wibox = require("wibox") local beautiful = require("beautiful") -local icon_theme = require(tostring(...):match(".*bling") .. ".module.icon_theme")() +local icon_theme = require(tostring(...):match(".*bling") .. ".helpers.icon_theme")() local dpi = beautiful.xresources.apply_dpi local string = string From ab1a25e676ef22c2e5cb3262c74fb03c09026fcf Mon Sep 17 00:00:00 2001 From: Kasper Date: Wed, 3 Nov 2021 23:59:53 +0200 Subject: [PATCH 17/21] Fix some issue with the client preview for the tag and task preview widgets (#128) --- widget/tag_preview.lua | 19 ++++++++++++++--- widget/task_preview.lua | 45 +++++++++++++++++++++++++++++++++-------- 2 files changed, 53 insertions(+), 11 deletions(-) diff --git a/widget/tag_preview.lua b/widget/tag_preview.lua index ced0f56..6974ff4 100644 --- a/widget/tag_preview.lua +++ b/widget/tag_preview.lua @@ -175,9 +175,22 @@ local enable = function(opts) }) tag.connect_signal("property::selected", function(t) - for _, c in ipairs(t:clients()) do - c.prev_content = gears.surface.duplicate_surface(c.content) - end + -- Awesome switches up tags on startup really fast it seems, probably depends on what rules you have set + -- which can cause the c.content to not show the correct image + gears.timer + { + timeout = 0.1, + call_now = false, + autostart = true, + single_shot = true, + callback = function() + if t.selected == true then + for _, c in ipairs(t:clients()) do + c.prev_content = gears.surface.duplicate_surface(c.content) + end + end + end + } end) awesome.connect_signal("bling::tag_preview::update", function(t) diff --git a/widget/task_preview.lua b/widget/task_preview.lua index b16a845..03b0565 100644 --- a/widget/task_preview.lua +++ b/widget/task_preview.lua @@ -30,14 +30,24 @@ local function draw_widget( end) then return end - local content = gears.surface(c.content) - local cr = cairo.Context(content) - local x, y, w, h = cr:clip_extents() - local img = cairo.ImageSurface.create(cairo.Format.ARGB32, w - x, h - y) - cr = cairo.Context(img) - cr:set_source_surface(content, 0, 0) - cr.operator = cairo.Operator.SOURCE - cr:paint() + + local content = nil + if c.active then + content = gears.surface(c.content) + elseif c.prev_content then + content = gears.surface(c.prev_content) + end + + local img = nil + if content ~= nil then + local cr = cairo.Context(content) + local x, y, w, h = cr:clip_extents() + img = cairo.ImageSurface.create(cairo.Format.ARGB32, w - x, h - y) + cr = cairo.Context(img) + cr:set_source_surface(content, 0, 0) + cr.operator = cairo.Operator.SOURCE + cr:paint() + end local widget = wibox.widget({ (widget_template or { @@ -139,6 +149,25 @@ local enable = function(opts) bg = "#00000000", }) + tag.connect_signal("property::selected", function(t) + -- Awesome switches up tags on startup really fast it seems, probably depends on what rules you have set + -- which can cause the c.content to not show the correct image + gears.timer + { + timeout = 0.1, + call_now = false, + autostart = true, + single_shot = true, + callback = function() + if t.selected == true then + for _, c in ipairs(t:clients()) do + c.prev_content = gears.surface.duplicate_surface(c.content) + end + end + end + } + end) + awesome.connect_signal("bling::task_preview::visibility", function(s, v, c) if v then -- Update task preview contents From 0a49f7ff43ac7d0cb592b4cb2805f64f91b61a71 Mon Sep 17 00:00:00 2001 From: undefinedDarkness Date: Mon, 27 Sep 2021 17:04:40 +0530 Subject: [PATCH 18/21] make tag preview fallback to a awesomewm icon --- widget/tag_preview.lua | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/widget/tag_preview.lua b/widget/tag_preview.lua index f2ef42b..52ee80d 100644 --- a/widget/tag_preview.lua +++ b/widget/tag_preview.lua @@ -36,14 +36,19 @@ local function draw_widget( local tag_screen = t.screen for i, c in ipairs(t:clients()) do if not c.hidden and not c.minimized then - local img_box = wibox.widget({ - image = gears.surface.load(c.icon), + + local img_box = wibox.widget ({ resize = true, forced_height = 100 * scale, forced_width = 100 * scale, widget = wibox.widget.imagebox, }) + -- If fails to set image, fallback to a awesome icon + if not pcall(function() img_box.image = gears.surface.load(c.icon) end) then + img_box.image = beautiful.theme_assets.awesome_icon (24, "#222222", "#fafafa") + end + if tag_preview_image then if c.prev_content or t.selected then local content From e4fd438e3ed75c4dc57241d546c3c27779eb4e1d Mon Sep 17 00:00:00 2001 From: Kasper Date: Fri, 5 Nov 2021 05:38:54 +0200 Subject: [PATCH 19/21] More app launcher improvements (#132) * Make sure there is always a default icon * Use get_example_icon_path to get the default icon * Refactor scrolling to add support for left and right scrolling * fixup! Make sure there is always a default icon * Fix calculation happening at the wrong time * Fix default icons again * Where did that come from? * Fix calculation happening at the wrong time for scroll_up as well * Fix error on scroll right when scrolling to page where amount of rows is smaller than the selected row * Sort search results by string similarity * Don't sort by similarity when the search string is empty * Add hover effects * try_to_keep_index_after_searching should be false by default * This should only trigger for lmb * Add an option to hide the app launcher when clicked with lmb/rmb outside of it * Use gtk-launch so terminal apps spawn correctly * Use get_executable instead of get_commandline * Add an option to set the hover colors * Further improvements for the spawn function * Fix scrolling/searching errors when there app list is empty * This should never be nil anyway * whitespace * Refactor show, hide and toggle method + remove support for manually setting x an y (use placement) * Add arguements for custom icon_theme (defaults to the selected system gtk theme) and icon_size (defaults to 48) * Pass the app table instead of individual keys to create_app_widget * Add an arguement to pass the default terminal for terminal apps as gtk-launch only uses xterm * Reformating * Rename 'mark_app' and 'unmark_app' to 'select_app' and 'unselect_app' * Call :hide() from app.spawn() to avoid calling hide() twice on some cases * Fix escape not closing the launcher after b7e44ec4 * Reduce code duplication and only reset the launcher when the animation is over (if not nil) * Set active_widget to nil when the grid is empty to prevent from spawning the wrong app * Override the default exe_callback * Override the default behaviour for 'Return' via hooks instead because overriding only exe_callback still doesn't stop it from pausing the prompt keygrabber * Set active_widget to nil on unselect * Unselect previous app on search to avoid from spawning it when the grid is empty * Use double quotes for everything --- helpers/color.lua | 106 ++++++ helpers/icon_theme.lua | 13 + widget/app_launcher.lua | 805 +++++++++++++++++++++++++++------------- 3 files changed, 672 insertions(+), 252 deletions(-) diff --git a/helpers/color.lua b/helpers/color.lua index 22d6501..4042360 100644 --- a/helpers/color.lua +++ b/helpers/color.lua @@ -1,3 +1,12 @@ +local tonumber = tonumber +local string = string +local math = math +local floor = math.floor +local max = math.max +local min = math.min +local abs = math.abs +local format = string.format + local _color = {} --- Try to guess if a color is dark or light. @@ -13,6 +22,14 @@ function _color.is_dark(color) return (numeric_value < 383) end +function _color.is_opaque(color) + if type(color) == "string" then + color = _color.hex_to_rgba(color) + end + + return color.a < 0.01 +end + --- Lighten a color. -- -- @string color The color to lighten with hexadecimal HTML format `"#RRGGBB"`. @@ -49,4 +66,93 @@ function _color.darken(color, amount) return _color.lighten(color, -amount) end +-- Returns a value that is clipped to interval edges if it falls outside the interval +function _color.clip(num, min_num, max_num) + return max(min(num, max_num), min_num) +end + +-- Converts the given hex color to rgba +function _color.hex_to_rgba(color) + color = color:gsub("#", "") + return { r = tonumber("0x" .. color:sub(1, 2)), + g = tonumber("0x" .. color:sub(3, 4)), + b = tonumber("0x" .. color:sub(5, 6)), + a = #color == 8 and tonumber("0x" .. color:sub(7, 8)) or 255 } +end + +-- Converts the given rgba color to hex +function _color.rgba_to_hex(color) + local r = _color.clip(color.r or color[1], 0, 255) + local g = _color.clip(color.g or color[2], 0, 255) + local b = _color.clip(color.b or color[3], 0, 255) + local a = _color.clip(color.a or color[4] or 255, 0, 255) + return "#" .. format("%02x%02x%02x%02x", + floor(r), + floor(g), + floor(b), + floor(a)) +end + +-- Converts the given hex color to hsv +function _color.hex_to_hsv(color) + local color = _color.hex2rgb(color) + local C_max = max(color.r, color.g, color.b) + local C_min = min(color.r, color.g, color.b) + local delta = C_max - C_min + local H, S, V + if delta == 0 then + H = 0 + elseif C_max == color.r then + H = 60 * (((color.g - color.b) / delta) % 6) + elseif C_max == color.g then + H = 60 * (((color.b - color.r) / delta) + 2) + elseif C_max == color.b then + H = 60 * (((color.r - color.g) / delta) + 4) + end + if C_max == 0 then + S = 0 + else + S = delta / C_max + end + V = C_max + + return { h = H, + s = S * 100, + v = V * 100 } +end + +-- Converts the given hsv color to hex +function _color.hsv_to_hex(H, S, V) + S = S / 100 + V = V / 100 + if H > 360 then H = 360 end + if H < 0 then H = 0 end + local C = V * S + local X = C * (1 - abs(((H / 60) % 2) - 1)) + local m = V - C + local r_, g_, b_ = 0, 0, 0 + if H >= 0 and H < 60 then + r_, g_, b_ = C, X, 0 + elseif H >= 60 and H < 120 then + r_, g_, b_ = X, C, 0 + elseif H >= 120 and H < 180 then + r_, g_, b_ = 0, C, X + elseif H >= 180 and H < 240 then + r_, g_, b_ = 0, X, C + elseif H >= 240 and H < 300 then + r_, g_, b_ = X, 0, C + elseif H >= 300 and H < 360 then + r_, g_, b_ = C, 0, X + end + local r, g, b = (r_ + m) * 255, (g_ + m) * 255, (b_ + m) * 255 + return ("#%02x%02x%02x"):format(floor(r), floor(g), floor(b)) +end + +function _color.multiply(color, amount) + return { _color.clip(color.r * amount, 0, 255), + _color.clip(color.g * amount, 0, 255), + _color.clip(color.b * amount, 0, 255), + 255 } +end + return _color diff --git a/helpers/icon_theme.lua b/helpers/icon_theme.lua index 492d81e..4a1db92 100644 --- a/helpers/icon_theme.lua +++ b/helpers/icon_theme.lua @@ -55,6 +55,19 @@ function icon_theme:get_client_icon_path(client) return icon end +function icon_theme:choose_icon(icons_names) + local icon_info = Gtk.IconTheme.choose_icon(self.gtk_theme, icons_names, self.icon_size, 0); + if icon_info then + local icon_path = Gtk.IconInfo.get_filename(icon_info) + if icon_path then + return icon_path + end + end + + return "" +end + + function icon_theme:get_gicon_path(gicon) if gicon == nil then return "" diff --git a/widget/app_launcher.lua b/widget/app_launcher.lua index 8ef7d92..6f3f335 100644 --- a/widget/app_launcher.lua +++ b/widget/app_launcher.lua @@ -5,105 +5,66 @@ local gtable = require("gears.table") local gtimer = require("gears.timer") local wibox = require("wibox") local beautiful = require("beautiful") -local icon_theme = require(tostring(...):match(".*bling") .. ".helpers.icon_theme")() +local color = require(tostring(...):match(".*bling") .. ".helpers.color") local dpi = beautiful.xresources.apply_dpi - local string = string local table = table local math = math +local ipairs = ipairs local pairs = pairs local root = root +local capi = { screen = screen, mouse = mouse } +local path = ... local app_launcher = { mt = {} } -local function mark_app(self, index) - local app = self._private.grid.children[index] - if app ~= nil then - app:get_children_by_id("background")[1].bg = self.app_selected_color - local text_widget = app:get_children_by_id("text")[1] - if text_widget ~= nil then - text_widget.markup = "" .. text_widget.text .. "" - end - end -end +local terminal_commands_lookup = +{ + alacritty = "alacritty -e", + termite = "termite -e", + rxvt = "rxvt -e", + terminator = "terminator -e" +} -local function unmark_app(self, index) - local app = self._private.grid.children[index] - if app ~= nil then - app:get_children_by_id("background")[1].bg = self.app_normal_color - local text_widget = app:get_children_by_id("text")[1] - if text_widget ~= nil then - text_widget.markup = "" .. text_widget.text .. "" - end - end -end +local function string_levenshtein(str1, str2) + local len1 = string.len(str1) + local len2 = string.len(str2) + local matrix = {} + local cost = 0 -local function create_app_widget(self, name, cmdline, icon, index) - local icon = self.app_show_icon == true - and - { - widget = wibox.container.place, - halign = self.app_name_halign, - { - widget = wibox.widget.imagebox, - forced_width = self.app_icon_width, - forced_height = self.app_icon_height, - image = icon - } - } - or nil - local name = self.app_show_name == true - and - { - widget = wibox.container.place, - halign = self.app_icon_halign, - { - widget = wibox.widget.textbox, - id = "text", - align = "center", - font = self.app_name_font, - markup = name - } - } - or nil + -- quick cut-offs to save time + if (len1 == 0) then + return len2 + elseif (len2 == 0) then + return len1 + elseif (str1 == str2) then + return 0 + end - return wibox.widget - { - widget = wibox.container.background, - id = "background", - forced_width = self.app_width, - forced_height = self.app_height, - shape = self.app_shape, - bg = self.app_normal_color, - spawn = function() awful.spawn(cmdline) end, - buttons = - { - awful.button({}, 1, function() - if index == self._private.current_index or not self.select_before_spawn then - awful.spawn(cmdline) - self:hide() - else - -- Unmark the previous app - unmark_app(self, self._private.current_index) + -- initialise the base matrix values + for i = 0, len1, 1 do + matrix[i] = {} + matrix[i][0] = i + end + for j = 0, len2, 1 do + matrix[0][j] = j + end - self._private.current_index = index + -- actual Levenshtein algorithm + for i = 1, len1, 1 do + for j = 1, len2, 1 do + if (str1:byte(i) == str2:byte(j)) then + cost = 0 + else + cost = 1 + end - -- Mark this app - mark_app(self, self._private.current_index) - end - end), - }, - { - widget = wibox.container.place, - valign = self.app_content_valign, - { - layout = wibox.layout.fixed.vertical, - spacing = self.app_content_spacing, - icon, - name - } - } - } + matrix[i][j] = math.min(matrix[i-1][j] + 1, matrix[i][j-1] + 1, matrix[i-1][j-1] + cost) + end + end + + -- return the last value - this is the Levenshtein distance + return matrix[len1][len2] end local function has_value(tab, val) @@ -130,26 +91,184 @@ local function case_insensitive_pattern(pattern) return p end +local function select_app(self, x, y) + local widgets = self._private.grid:get_widgets_at(x, y) + if widgets then + self._private.active_widget = widgets[1] + if self._private.active_widget ~= nil then + self._private.active_widget.selected = true + self._private.active_widget:get_children_by_id("background")[1].bg = self.app_selected_color + local text_widget = self._private.active_widget:get_children_by_id("text")[1] + text_widget.markup = "" .. text_widget.text .. "" + end + end +end + +local function unselect_app(self) + if self._private.active_widget ~= nil then + self._private.active_widget.selected = false + self._private.active_widget:get_children_by_id("background")[1].bg = self.app_normal_color + local text_widget = self._private.active_widget:get_children_by_id("text")[1] + text_widget.markup = "" .. text_widget.text .. "" + self._private.active_widget = nil + end +end + +local function create_app_widget(self, entry) + local icon = self.app_show_icon == true + and + { + widget = wibox.container.place, + halign = self.app_name_halign, + { + widget = wibox.widget.imagebox, + forced_width = self.app_icon_width, + forced_height = self.app_icon_height, + image = entry.icon + } + } + or nil + local name = self.app_show_name == true + and + { + widget = wibox.container.place, + halign = self.app_icon_halign, + { + widget = wibox.widget.textbox, + id = "text", + align = "center", + font = self.app_name_font, + markup = entry.name + } + } + or nil + + local app = wibox.widget + { + widget = wibox.container.background, + id = "background", + forced_width = self.app_width, + forced_height = self.app_height, + shape = self.app_shape, + bg = self.app_normal_color, + { + widget = wibox.container.place, + valign = self.app_content_valign, + { + layout = wibox.layout.fixed.vertical, + spacing = self.app_content_spacing, + icon, + name + } + } + } + + function app.spawn() + if entry.terminal == true then + if self.terminal ~= nil then + local terminal_command = terminal_commands_lookup[self.terminal] or self.terminal + awful.spawn(terminal_command .. " " .. entry.executable) + else + awful.spawn.easy_async("gtk-launch " .. entry.executable, function(stdout, stderr) + if stderr then + awful.spawn(entry.executable) + end + end) + end + else + awful.spawn(entry.executable) + end + + self:hide() + end + + app:connect_signal("mouse::enter", function(_self) + local widget = capi.mouse.current_wibox + if widget then + widget.cursor = "hand2" + end + + local app = _self + if app.selected then + app:get_children_by_id("background")[1].bg = self.app_selected_hover_color + else + local is_opaque = color.is_opaque(self.app_normal_color) + local is_dark = color.is_dark(self.app_normal_color) + local app_normal_color = color.hex_to_rgba(self.app_normal_color) + local hover_color = (is_dark or is_opaque) and + color.rgba_to_hex(color.multiply(app_normal_color, 2.5)) or + color.rgba_to_hex(color.multiply(app_normal_color, 0.5)) + app:get_children_by_id("background")[1].bg = self.app_normal_hover_color + end + end) + + app:connect_signal("mouse::leave", function(_self) + local widget = capi.mouse.current_wibox + if widget then + widget.cursor = "left_ptr" + end + + local app = _self + if app.selected then + app:get_children_by_id("background")[1].bg = self.app_selected_color + else + app:get_children_by_id("background")[1].bg = self.app_normal_color + end + end) + + app:connect_signal("button::press", function(_self, lx, ly, button, mods, find_widgets_result) + if button == 1 then + local app = _self + if self._private.active_widget == app or not self.select_before_spawn then + app.spawn() + else + -- Unmark the previous app + unselect_app(self) + + -- Mark this app + local pos = self._private.grid:get_widget_position(app) + select_app(self, pos.row, pos.col) + end + end + end) + + return app +end + local function search(self, text) + unselect_app(self) + + local pos = self._private.grid:get_widget_position(self._private.active_widget) + -- Reset all the matched entries self._private.matched_entries = {} -- Remove all the grid widgets self._private.grid:reset() - for index, entry in pairs(self._private.all_entries) do - text = text:gsub( "%W", "" ) + if text == "" then + self._private.matched_entries = self._private.all_entries + else + for index, entry in pairs(self._private.all_entries) do + text = text:gsub( "%W", "" ) - -- Check if there's a match by the app name or app command - if string.find(entry.name, case_insensitive_pattern(text)) ~= nil or - self.search_commands and string.find(entry.cmdline, case_insensitive_pattern(text)) ~= nil - then - table.insert(self._private.matched_entries, { name = entry.name, cmdline = entry.cmdline, icon = entry.icon }) - - -- Only add the widgets for apps that are part of the first page - if #self._private.grid.children + 1 <= self._private.max_apps_per_page then - self._private.grid:add(create_app_widget(self, entry.name, entry.cmdline, entry.icon, #self._private.grid.children + 1)) + -- Check if there's a match by the app name or app command + if string.find(entry.name, case_insensitive_pattern(text)) ~= nil or + self.search_commands and string.find(entry.commandline, case_insensitive_pattern(text)) ~= nil + then + table.insert(self._private.matched_entries, { name = entry.name, commandline = entry.commandline, executable = entry.executable, terminal = entry.terminal, icon = entry.icon }) end end + + -- Sort by string similarity + table.sort(self._private.matched_entries, function(a, b) + return string_levenshtein(text, a.name) < string_levenshtein(text, b.name) + end) + end + for index, entry in pairs(self._private.matched_entries) do + -- Only add the widgets for apps that are part of the first page + if #self._private.grid.children + 1 <= self._private.max_apps_per_page then + self._private.grid:add(create_app_widget(self, entry)) + end end -- Recalculate the apps per page based on the current matched entries @@ -158,190 +277,315 @@ local function search(self, text) -- Recalculate the pages count based on the current apps per page self._private.pages_count = math.ceil(math.max(1, #self._private.matched_entries) / math.max(1, self._private.apps_per_page)) + -- Page should be 1 after a search + self._private.current_page = 1 + -- This is an option to mimic rofi behaviour where after a search -- it will reselect the app whose index is the same as the app index that was previously selected -- and if matched_entries.length < current_index it will instead select the app with the greatest index if self.try_to_keep_index_after_searching then - self._private.current_index = math.max(math.min(self._private.current_index, #self._private.matched_entries), 1) - + if self._private.grid:get_widgets_at(pos.row, pos.col) == nil then + local app = self._private.grid.children[#self._private.grid.children] + pos = self._private.grid:get_widget_position(app) + end + select_app(self, pos.row, pos.col) -- Otherwise select the first app on the list else - self._private.current_index = 1 + select_app(self, 1, 1) end - self._private.current_page = 1 - - mark_app(self, self._private.current_index) end local function scroll_up(self) + if #self._private.grid.children < 1 then + self._private.active_widget = nil + return + end + + local rows, columns = self._private.grid:get_dimension() + local pos = self._private.grid:get_widget_position(self._private.active_widget) + local is_bigger_than_first_app = pos.col > 1 or pos.row > 1 + -- Check if the current marked app is not the first - if self._private.current_index > 1 then - unmark_app(self, self._private.current_index) - - -- Current index should be decremented - self._private.current_index = self._private.current_index - 1 - - -- Mark the new app - mark_app(self, self._private.current_index) - + if is_bigger_than_first_app then + unselect_app(self) + if pos.row == 1 then + select_app(self, rows, pos.col - 1) + else + select_app(self, pos.row - 1, pos.col) + end -- Check if the current page is not the first elseif self._private.current_page > 1 then - -- Remove the current page apps from the grid - self._private.grid:reset() + -- Remove the current page apps from the grid + self._private.grid:reset() - local max_app_index_to_include = (self._private.current_page - 1) * self._private.apps_per_page - local min_app_index_to_include = max_app_index_to_include - self._private.apps_per_page + local max_app_index_to_include = (self._private.current_page - 1) * self._private.apps_per_page + local min_app_index_to_include = max_app_index_to_include - self._private.apps_per_page + for index, entry in pairs(self._private.matched_entries) do + -- Only add widgets that are between this range (part of the current page) + if index > min_app_index_to_include and index <= max_app_index_to_include then + self._private.grid:add(create_app_widget(self, entry)) + end + end - for index, entry in pairs(self._private.matched_entries) do - -- Only add widgets that are between this range (part of the current page) - if index > min_app_index_to_include and index <= max_app_index_to_include then - self._private.grid:add(create_app_widget(self, entry.name, entry.cmdline, entry.icon, #self._private.grid.children + 1)) - end - end + -- If we scrolled up a page, selected app should be the last one + rows, columns = self._private.grid:get_dimension() + select_app(self, rows, columns) - -- If we scrolled up a page, selected app should be the last one - self._private.current_index = self._private.apps_per_page - mark_app(self, self._private.current_index) - - -- Current page should be decremented - self._private.current_page = self._private.current_page - 1 + -- Current page should be decremented + self._private.current_page = self._private.current_page - 1 end end local function scroll_down(self) - local is_less_than_max_app = self._private.current_index < #self._private.grid.children + if #self._private.grid.children < 1 then + self._private.active_widget = nil + return + end + + local rows, columns = self._private.grid:get_dimension() + local pos = self._private.grid:get_widget_position(self._private.active_widget) + local is_less_than_max_app = self._private.grid:index(self._private.active_widget) < #self._private.grid.children local is_less_than_max_page = self._private.current_page < self._private.pages_count -- Check if we can scroll down the app list if is_less_than_max_app then -- Unmark the previous app - unmark_app(self, self._private.current_index) - - -- Current index should be incremented - self._private.current_index = self._private.current_index + 1 - - -- Mark the new app - mark_app(self, self._private.current_index) - + unselect_app(self) + if pos.row == rows then + select_app(self, 1, pos.col + 1) + else + select_app(self, pos.row + 1, pos.col) + end -- If we can't scroll down the app list, check if we can scroll down a page elseif is_less_than_max_page then -- Remove the current page apps from the grid self._private.grid:reset() - local min_app_index_to_include = self._private.current_index * self._private.current_page + local min_app_index_to_include = self._private.apps_per_page * self._private.current_page local max_app_index_to_include = min_app_index_to_include + self._private.apps_per_page for index, entry in pairs(self._private.matched_entries) do -- Only add widgets that are between this range (part of the current page) if index > min_app_index_to_include and index <= max_app_index_to_include then - self._private.grid:add(create_app_widget(self, entry.name, entry.cmdline, entry.icon, #self._private.grid.children + 1)) + self._private.grid:add(create_app_widget(self, entry)) end end - -- Current app is 1 if we scroll to the next page - self._private.current_index = 1 - mark_app(self, self._private.current_index) + -- Select app 1 when scrolling to the next page + select_app(self, 1, 1) -- Current page should be incremented self._private.current_page = self._private.current_page + 1 end end ---- Shows the app launcher -function app_launcher:show(args) - local args = args or {} +local function scroll_left(self) + if #self._private.grid.children < 1 then + self._private.active_widget = nil + return + end - self.screen = args.screen or self.screen - self.screen.app_launcher = self._private.widget - self.screen.app_launcher.screen = self.screen - self.screen.app_launcher.visible = true + local pos = self._private.grid:get_widget_position(self._private.active_widget) + local is_bigger_than_first_column = pos.col > 1 + local is_not_first_page = self._private.current_page > 1 + + -- Check if the current marked app is not the first + if is_bigger_than_first_column then + unselect_app(self) + select_app(self, pos.row, pos.col - 1) + -- Check if the current page is not the first + elseif is_not_first_page then + -- Remove the current page apps from the grid + self._private.grid:reset() + + local max_app_index_to_include = (self._private.current_page - 1) * self._private.apps_per_page + local min_app_index_to_include = max_app_index_to_include - self._private.apps_per_page + + for index, entry in pairs(self._private.matched_entries) do + -- Only add widgets that are between this range (part of the current page) + if index > min_app_index_to_include and index <= max_app_index_to_include then + self._private.grid:add(create_app_widget(self, entry)) + end + end + + -- Keep the same row from last page + local rows, columns = self._private.grid:get_dimension() + select_app(self, pos.row, columns) + + -- Current page should be decremented + self._private.current_page = self._private.current_page - 1 + end +end + +local function scroll_right(self) + if #self._private.grid.children < 1 then + self._private.active_widget = nil + return + end + + local rows, columns = self._private.grid:get_dimension() + local pos = self._private.grid:get_widget_position(self._private.active_widget) + local is_less_than_max_column = pos.col < columns + local is_less_than_max_page = self._private.current_page < self._private.pages_count + + -- Check if we can scroll down the app list + if is_less_than_max_column then + -- Unmark the previous app + unselect_app(self) + + -- Scroll up to the max app if there are directly to the right of previous app + if self._private.grid:get_widgets_at(pos.row, pos.col + 1) == nil then + local app = self._private.grid.children[#self._private.grid.children] + pos = self._private.grid:get_widget_position(app) + select_app(self, pos.row, pos.col) + else + select_app(self, pos.row, pos.col + 1) + end + + -- If we can't scroll down the app list, check if we can scroll down a page + elseif is_less_than_max_page then + -- Remove the current page apps from the grid + self._private.grid:reset() + + local min_app_index_to_include = self._private.apps_per_page * self._private.current_page + local max_app_index_to_include = min_app_index_to_include + self._private.apps_per_page + + for index, entry in pairs(self._private.matched_entries) do + -- Only add widgets that are between this range (part of the current page) + if index > min_app_index_to_include and index <= max_app_index_to_include then + self._private.grid:add(create_app_widget(self, entry)) + end + end + + -- Keep the last row + select_app(self, math.min(pos.row, #self._private.grid.children), 1) + + -- Current page should be incremented + self._private.current_page = self._private.current_page + 1 + end +end + +local function init(self) + self._private.grid:reset() + self._private.matched_entries = self._private.all_entries + self._private.apps_per_page = self._private.max_apps_per_page + self._private.pages_count = math.ceil(#self._private.all_entries / self._private.apps_per_page) + self._private.current_page = 1 + + for index, entry in pairs(self._private.all_entries) do + -- Only add the apps that are part of the first page + if index <= self._private.apps_per_page then + self._private.grid:add(create_app_widget(self, entry)) + else + break + end + end + + select_app(self, 1, 1) +end + +--- Shows the app launcher +function app_launcher:show() + local screen = self.screen + if self.show_on_focused_screen then + screen = awful.screen.focused() + end + + screen.app_launcher = self._private.widget + screen.app_launcher.screen = screen + screen.app_launcher.visible = true self._private.prompt:run() - local x = args.x or self.x or nil - if self.rubato and self.rubato.x and x then - self.rubato.x:set(x) - elseif x then - self.screen.app_launcher.x = x - end - - local y = args.y or self.y or nil - if self.rubato and self.rubato.y and y then - self.rubato.y:set(y) - elseif y then - self.screen.app_launcher.y = y - end - - local placement = args.placement or self.placement or nil + local placement = self.placement if placement then - self.screen.app_launcher.placement = placement + local pos = placement(self.screen.app_launcher, {pretend = true}) + local animation = self.rubato + if animation ~= nil then + if animation.x then + animation.x.ended:unsubscribe() + animation.x:set(pos.x) + else + self._private.widget.x = pos.x + end + if animation.y then + animation.y.ended:unsubscribe() + animation.y:set(pos.y) + else + self._private.widget.y = pos.y + end + else + self._private.widget.x = pos.x + self._private.widget.y = pos.y + end end self:emit_signal("bling::app_launcher::visibility", true) end --- Hides the app launcher -function app_launcher:hide(args) - local args = args or {} +function app_launcher:hide() + local screen = self.screen + if self.show_on_focused_screen then + screen = awful.screen.focused() + end + + if screen.app_launcher == nil or screen.app_launcher.visible == false then + return + end -- There's no other way to stop the prompt? - root.fake_input('key_press', "Escape") - root.fake_input('key_release', "Escape") + root.fake_input("key_press", "Escape") + root.fake_input("key_release", "Escape") - if self.rubato and self.rubato.x then - self.rubato.x:set(self.rubato.x:initial()) - self.rubato.x.ended:subscribe(function() - self.screen.app_launcher.visible = false - end) - end - - if self.rubato and self.rubato.y then - self.rubato.y:set(self.rubato.y:initial()) - self.rubato.y.ended:subscribe(function() - self.screen.app_launcher.visible = false - end) - end - - if not self.rubato then - self.screen.app_launcher.visible = false - end - - self.screen = args.screen or self.screen - self.screen.app_launcher = {} - - -- Reset back to initial values - self._private.apps_per_page = self._private.max_apps_per_page - self._private.pages_count = math.ceil(#self._private.all_entries / self._private.apps_per_page) - self._private.matched_entries = self._private.all_entries - self._private.current_index = 1 - self._private.current_page = 1 - self._private.grid:reset() - - -- Add the app widgets for the next time - for index, entry in pairs(self._private.all_entries) do - -- Only add the apps that are part of the first page - if index <= self._private.apps_per_page then - self._private.grid:add(create_app_widget(self, entry.name, entry.cmdline, entry.icon, index)) - else - break + local animation = self.rubato + if animation ~= nil then + if animation.x then + animation.x:set(animation.x:initial()) + end + if animation.y then + animation.y:set(animation.y:initial()) end - end - -- Select the first app for the next time - mark_app(self, self._private.current_index) + local anim_x_duration = (animation.x and animation.x.duration) or 0 + local anim_y_duration = (animation.y and animation.y.duration) or 0 + local turn_off_on_anim_x_end = (anim_x_duration >= anim_y_duration) and true or false + + if turn_off_on_anim_x_end then + animation.x.ended:subscribe(function() + init(self) + screen.app_launcher.visible = false + screen.app_launcher = nil + animation.x.ended:unsubscribe() + end) + else + animation.y.ended:subscribe(function() + init(self) + screen.app_launcher.visible = false + screen.app_launcher = nil + animation.y.ended:unsubscribe() + end) + end + else + init(self) + screen.app_launcher.visible = false + screen.app_launcher = nil + end self:emit_signal("bling::app_launcher::visibility", false) end --- Toggles the app launcher -function app_launcher:toggle(args) - local args = args or {} +function app_launcher:toggle() + local screen = self.screen + if self.show_on_focused_screen then + screen = awful.screen.focused() + end - self.screen = args.screen or self.screen - if self.screen.app_launcher and self.screen.app_launcher.visible then - self:hide(self.screen) + if screen.app_launcher and screen.app_launcher.visible then + self:hide() else - self:show(self.screen) + self:show() end end @@ -349,24 +593,28 @@ end local function new(args) args = args or {} + args.terminal = args.terminal or nil args.search_commands = args.search_commands or true args.skip_names = args.skip_names or {} args.skip_commands = args.skip_commands or {} args.skip_empty_icons = args.skip_empty_icons or false args.sort_alphabetically = args.sort_alphabetically or true args.select_before_spawn = args.select_before_spawn or true + args.hide_on_clicked_outside = args.hide_on_clicked_outside or true args.try_to_keep_index_after_searching = args.try_to_keep_index_after_searching or false + args.default_app_icon_name = args.default_app_icon_name or nil args.default_app_icon_path = args.default_app_icon_path or nil + args.icon_theme = args.icon_theme or nil + args.icons_size = args.icons_size or nil + args.show_on_focused_screen = args.show_on_focused_screen or true + args.screen = args.screen or capi.screen.primary + args.placement = args.placement or awful.placement.centered args.rubato = args.rubato or nil args.shirnk_width = args.shirnk_width or false args.shrink_height = args.shrink_height or false args.background = args.background or "#000000" - args.screen = args.screen or screen.primary - args.x = args.x or nil - args.y = args.y or nil - args.placement = args.placement or (args.x == nil and args.y == nil) and awful.placement.centered or nil args.shape = args.shape or nil args.prompt_height = args.prompt_height or dpi(100) @@ -400,7 +648,13 @@ local function new(args) args.app_height = args.app_height or dpi(100) args.app_shape = args.app_shape or nil args.app_normal_color = args.app_normal_color or beautiful.bg_normal or "#000000" + args.app_normal_hover_color = args.app_normal_hover_color or (color.is_dark(args.app_normal_color) or color.is_opaque(args.app_normal_color)) and + color.rgba_to_hex(color.multiply(color.hex_to_rgba(args.app_normal_color), 2.5)) or + color.rgba_to_hex(color.multiply(color.hex_to_rgba(args.app_normal_color), 0.5)) args.app_selected_color = args.app_selected_color or beautiful.fg_normal or "#FFFFFF" + args.app_selected_hover_color = args.app_selected_hover_color or (color.is_dark(args.app_normal_color) or color.is_opaque(args.app_normal_color)) and + color.rgba_to_hex(color.multiply(color.hex_to_rgba(args.app_selected_color), 2.5)) or + color.rgba_to_hex(color.multiply(color.hex_to_rgba(args.app_selected_color), 0.5)) args.app_content_valign = args.app_content_valign or "center" args.app_content_spacing = args.app_content_spacing or dpi(10) args.app_show_icon = args.app_show_icon == nil and true or args.app_show_icon @@ -415,12 +669,13 @@ local function new(args) local ret = gobject({}) ret._private = {} + ret._private.text = "" gtable.crush(ret, app_launcher) gtable.crush(ret, args) -- Determines the grid width - local grid_width = ret.shirnk_width == false + local grid_width = ret.shirnk_width == false and dpi((ret.app_width * ret.apps_per_column) + ((ret.apps_per_column - 1) * ret.apps_spacing)) or nil local grid_height = ret.shrink_height == false @@ -436,7 +691,19 @@ local function new(args) bg = ret.prompt_color, fg = ret.prompt_text_color, bg_cursor = ret.prompt_cursor_color, + hooks = + { + -- Disable historyu scrolling with arrow keys + -- TODO: implement this as other keybind? tab? + {{}, "Up", function(command) return true, false end}, + {{}, "Down", function(command) return true, false end}, + {{}, "Return", function(command) return true, false end}, + }, changed_callback = function(text) + if text == ret._private.text then + return + end + if ret._private.search_timer ~= nil and ret._private.search_timer.started then ret._private.search_timer:stop() end @@ -449,17 +716,30 @@ local function new(args) search(ret, text) end } + + ret._private.text = text end, keypressed_callback = function(mod, key, cmd) + if key == "Escape" then + ret:hide() + end if key == "Return" then - if ret._private.grid.children[ret._private.current_index] ~= nil then - ret._private.grid.children[ret._private.current_index].spawn() + if ret._private.active_widget ~= nil then + ret._private.active_widget.spawn() end end - print(key) - end, - done_callback = function() - ret:hide() + if key == "Up" then + scroll_up(ret) + end + if key == "Down" then + scroll_down(ret) + end + if key == "Left" then + scroll_left(ret) + end + if key == "Right" then + scroll_right(ret) + end end } ret._private.grid = wibox.widget @@ -530,55 +810,50 @@ local function new(args) -- Private variables to be used to be used by the scrolling and searching functions ret._private.all_entries = {} ret._private.matched_entries = {} - ret._private.apps_per_page = ret.apps_per_column * ret.apps_per_row - ret._private.max_apps_per_page = ret._private.apps_per_page + ret._private.max_apps_per_page = ret.apps_per_column * ret.apps_per_row + ret._private.apps_per_page = ret._private.max_apps_per_page ret._private.pages_count = 0 - ret._private.current_index = 1 ret._private.current_page = 1 local app_info = Gio.AppInfo local apps = app_info.get_all() if ret.sort_alphabetically then - table.sort(apps, function(a, b) return app_info.get_name(a):lower() < app_info.get_name(b):lower() end) - end + table.sort(apps, function(a, b) return app_info.get_name(a):lower() < app_info.get_name(b):lower() end) + end + + local icon_theme = require(tostring(path):match(".*bling") .. ".helpers.icon_theme")(ret.icon_theme, ret.icon_size) for _, app in ipairs(apps) do if app.should_show(app) then - -- Check if this app should be skipped, depanding on the skip_names / skip_commands table local name = app_info.get_name(app) local commandline = app_info.get_commandline(app) + local executable = app_info.get_executable(app) local icon = icon_theme:get_gicon_path(app_info.get_icon(app)) + -- Check if this app should be skipped, depanding on the skip_names / skip_commands table if not has_value(ret.skip_names, name) and not has_value(ret.skip_commands, commandline) then -- Check if this app should be skipped becuase it's iconless depanding on skip_empty_icons - if icon ~= "" or ret.skip_empty_icons == false then - if icon == "" then - if ret.default_app_icon_name ~= nil then - icon = icon_theme:get_icon_path("app") - elseif ret.default_app_icon_path ~= nil then - icon = ret.default_app_icon_path - end - end - - -- Insert a table containing the name, command and icon of the app into the all_entries table - table.insert(ret._private.all_entries, { name = name, cmdline = commandline, icon = icon }) - - -- Only add the app widgets that are part of the first page - if #ret._private.all_entries <= ret._private.apps_per_page then - ret._private.grid:add(create_app_widget(ret, name, commandline, icon, #ret._private.all_entries)) + if icon ~= "" or ret.skip_empty_icons == false then + if icon == "" then + if ret.default_app_icon_name ~= nil then + icon = icon_theme:get_icon_path(ret.default_app_icon_name) + elseif ret.default_app_icon_path ~= nil then + icon = ret.default_app_icon_path + else + icon = icon_theme:choose_icon({ "application-all", "application", "application-default-icon", "app" }) end end + + local desktop_app_info = Gio.DesktopAppInfo.new(app_info.get_id(app)) + local terminal = Gio.DesktopAppInfo.get_string(desktop_app_info, "Terminal") == "true" and true or false + table.insert(ret._private.all_entries, { name = name, commandline = commandline, executable = executable, terminal = terminal, icon = icon }) + end end - - -- Matched entries contains all the apps initially - ret._private.matched_entries = ret._private.all_entries - ret._private.pages_count = math.ceil(#ret._private.all_entries / ret._private.apps_per_page) - - -- Mark the first app on startup - mark_app(ret, 1) end end + init(ret) + if ret.rubato and ret.rubato.x then ret.rubato.x:subscribe(function(pos) ret._private.widget.x = pos @@ -590,6 +865,32 @@ local function new(args) end) end + if ret.hide_on_clicked_outside then + awful.mouse.append_client_mousebinding( + awful.button({ }, 1, function (c) + ret:hide() + end) + ) + + awful.mouse.append_global_mousebinding( + awful.button({ }, 1, function (c) + ret:hide() + end) + ) + + awful.mouse.append_client_mousebinding( + awful.button({ }, 3, function (c) + ret:hide() + end) + ) + + awful.mouse.append_global_mousebinding( + awful.button({ }, 3, function (c) + ret:hide() + end) + ) + end + return ret end From 92dabc890e15397d823983b688f5beb7aae8d4c2 Mon Sep 17 00:00:00 2001 From: Nooo37 <70270606+Nooo37@users.noreply.github.com> Date: Sat, 6 Nov 2021 19:33:58 +0100 Subject: [PATCH 20/21] Refactor layout icon path search and also search in package.path (#137, closes #136) --- layout/centered.lua | 22 ++-------------- layout/deck.lua | 23 +---------------- layout/equalarea.lua | 21 +--------------- layout/horizontal.lua | 22 +--------------- layout/init.lua | 58 +++++++++++++++++++++++++++---------------- layout/mstab.lua | 15 +---------- layout/vertical.lua | 22 +--------------- 7 files changed, 43 insertions(+), 140 deletions(-) diff --git a/layout/centered.lua b/layout/centered.lua index c1f79c2..4e1ad24 100644 --- a/layout/centered.lua +++ b/layout/centered.lua @@ -1,7 +1,4 @@ local awful = require("awful") -local gears = require("gears") -local gcolor = require("gears.color") -local beautiful = require("beautiful") local math = math local mylayout = {} @@ -58,6 +55,7 @@ function mylayout.arrange(p) -- iterate through slaves for idx = 1, nslaves do -- idx=nmaster+1,#p.clients do local c = p.clients[idx + nmaster] + local g if idx % 2 == 0 then g = { x = area.x, @@ -83,20 +81,4 @@ function mylayout.arrange(p) end end -local icon_raw = gears.filesystem.get_configuration_dir() - .. tostring(...):match("^.*bling"):gsub("%.", "/") - .. "/icons/layouts/centered.png" - -local function get_icon() - if icon_raw ~= nil then - return gcolor.recolor_image(icon_raw, beautiful.fg_normal) - else - return nil - end -end - -return { - layout = mylayout, - icon_raw = icon_raw, - get_icon = get_icon, -} +return mylayout diff --git a/layout/deck.lua b/layout/deck.lua index a1e7546..e0500b9 100644 --- a/layout/deck.lua +++ b/layout/deck.lua @@ -1,8 +1,3 @@ -local awful = require("awful") -local gears = require("gears") -local gcolor = require("gears.color") -local beautiful = require("beautiful") - local mylayout = {} mylayout.name = "deck" @@ -39,20 +34,4 @@ function mylayout.arrange(p) end end -local icon_raw = gears.filesystem.get_configuration_dir() - .. tostring(...):match("^.*bling"):gsub("%.", "/") - .. "/icons/layouts/deck.png" - -local function get_icon() - if icon_raw ~= nil then - return gcolor.recolor_image(icon_raw, beautiful.fg_normal) - else - return nil - end -end - -return { - layout = mylayout, - icon_raw = icon_raw, - get_icon = get_icon, -} +return mylayout diff --git a/layout/equalarea.lua b/layout/equalarea.lua index eab2acc..37e972d 100644 --- a/layout/equalarea.lua +++ b/layout/equalarea.lua @@ -1,6 +1,3 @@ -local gears = require("gears") -local gcolor = require("gears.color") -local beautiful = require("beautiful") local math = math local screen = screen local mylayout = {} @@ -77,20 +74,4 @@ function mylayout.arrange(p) divide(p, g, 1, #cls, cls, mwfact, mcount) end -local icon_raw = gears.filesystem.get_configuration_dir() - .. tostring(...):match("^.*bling"):gsub("%.", "/") - .. "/icons/layouts/equalarea.png" - -local function get_icon() - if icon_raw ~= nil then - return gcolor.recolor_image(icon_raw, beautiful.fg_normal) - else - return nil - end -end - -return { - layout = mylayout, - icon_raw = icon_raw, - get_icon = get_icon, -} +return mylayout diff --git a/layout/horizontal.lua b/layout/horizontal.lua index 353b4a8..23f9c9e 100644 --- a/layout/horizontal.lua +++ b/layout/horizontal.lua @@ -1,7 +1,3 @@ -local awful = require("awful") -local gears = require("gears") -local gcolor = require("gears.color") -local beautiful = require("beautiful") local math = math local mylayout = {} @@ -57,20 +53,4 @@ function mylayout.arrange(p) end end -local icon_raw = gears.filesystem.get_configuration_dir() - .. tostring(...):match("^.*bling"):gsub("%.", "/") - .. "/icons/layouts/horizontal.png" - -local function get_icon() - if icon_raw ~= nil then - return gcolor.recolor_image(icon_raw, beautiful.fg_normal) - else - return nil - end -end - -return { - layout = mylayout, - icon_raw = icon_raw, - get_icon = get_icon, -} +return mylayout diff --git a/layout/init.lua b/layout/init.lua index f7ed971..223d9d4 100644 --- a/layout/init.lua +++ b/layout/init.lua @@ -1,30 +1,44 @@ local beautiful = require("beautiful") +local gears = require("gears") -local mstab = require(... .. ".mstab") -beautiful.layout_mstab = mstab.get_icon() +local M = {} +local relative_lua_path = tostring(...) -local vertical = require(... .. ".vertical") -beautiful.layout_vertical = vertical.get_icon() +local function get_layout_icon_path(name) + local relative_icon_path = relative_lua_path + :match("^.*bling"):gsub("%.", "/") + .. "/icons/layouts/" .. name .. ".png" -local horizontal = require(... .. ".horizontal") -beautiful.layout_horizontal = horizontal.get_icon() + for p in package.path:gmatch('([^;]+)') do + p = p:gsub("?.*", "") + local absolute_icon_path = p .. relative_icon_path + if gears.filesystem.file_readable(absolute_icon_path) then + return absolute_icon_path + end + end +end -local centered = require(... .. ".centered") -beautiful.layout_centered = centered.get_icon() +local function get_icon(icon_raw) + if icon_raw ~= nil then + return gears.color.recolor_image(icon_raw, beautiful.fg_normal) + else + return nil + end +end -local equalarea = require(... .. ".equalarea") -beautiful.layout_equalarea = equalarea.get_icon() - -local deck = require(... .. ".deck") -beautiful.layout_deck = deck.get_icon() - -local layout = { - mstab = mstab.layout, - centered = centered.layout, - vertical = vertical.layout, - horizontal = horizontal.layout, - equalarea = equalarea.layout, - deck = deck.layout, +local layouts = { + "mstab", + "vertical", + "horizontal", + "centered", + "equalarea", + "deck" } -return layout +for _, layout_name in ipairs(layouts) do + local icon_raw = get_layout_icon_path(layout_name) + beautiful["layout_" .. layout_name] = get_icon(icon_raw) + M[layout_name] = require(... .. "." .. layout_name) +end + +return M diff --git a/layout/mstab.lua b/layout/mstab.lua index 6cbdb84..639275f 100644 --- a/layout/mstab.lua +++ b/layout/mstab.lua @@ -1,7 +1,6 @@ local awful = require("awful") local gears = require("gears") local wibox = require("wibox") -local gcolor = require("gears.color") local beautiful = require("beautiful") local mylayout = {} @@ -242,16 +241,4 @@ function mylayout.arrange(p) ) end -local icon_raw = gears.filesystem.get_configuration_dir() - .. tostring(...):match("^.*bling"):gsub("%.", "/") - .. "/icons/layouts/mstab.png" - -local function get_icon() - if icon_raw ~= nil then - return gcolor.recolor_image(icon_raw, beautiful.fg_normal) - else - return nil - end -end - -return { layout = mylayout, icon_raw = icon_raw, get_icon = get_icon } +return mylayout diff --git a/layout/vertical.lua b/layout/vertical.lua index f0eb8aa..8b6811e 100644 --- a/layout/vertical.lua +++ b/layout/vertical.lua @@ -1,7 +1,3 @@ -local awful = require("awful") -local gears = require("gears") -local gcolor = require("gears.color") -local beautiful = require("beautiful") local math = math local mylayout = {} @@ -57,20 +53,4 @@ function mylayout.arrange(p) end end -local icon_raw = gears.filesystem.get_configuration_dir() - .. tostring(...):match("^.*bling"):gsub("%.", "/") - .. "/icons/layouts/vertical.png" - -local function get_icon() - if icon_raw ~= nil then - return gcolor.recolor_image(icon_raw, beautiful.fg_normal) - else - return nil - end -end - -return { - layout = mylayout, - icon_raw = icon_raw, - get_icon = get_icon, -} +return mylayout From 3c8b3b8cc01e398171f73601824810ee01d49f87 Mon Sep 17 00:00:00 2001 From: Kasper Date: Mon, 8 Nov 2021 22:42:38 +0200 Subject: [PATCH 21/21] Some more app launcher features (#138) * Rename init() to reset() * Regenerate app list when an app is installed/uninstalled * Fix some issue with inside app content placement * Add an option to show the generic_name of the app (used a lot in rofi text mode style) * Set default spacing to 0 * Use a modified prompt to add methods such as :start() :stop() and some uncustomizable behaviour * Move app_launcher into it's own folder * Fix default values for various bools * Why would anyone want to reverse sort alphabetically? * Maybe someone will want that? * Make has_value case insensitive * Add an option to specify favorite apps (will appear first) * Fix widget placement * Add ctrl+v paste shortcut * Add an option to not close the widget when launching an app * Refactor page scrolling code to add support for wrap scrolling options * Fix init for bool with false default values * Fix app name halign not working * This is padding and not margin * Increase the height to make space for the new padding property * Fix bg and fg setting not getting applied for the prompt * Fix some scrolling bugs * Add default template for text version * Yet another scrolling issue * This should be on by default * Fix animation flickering --- .../init.lua} | 608 ++++++++++------ widget/app_launcher/prompt.lua | 656 ++++++++++++++++++ 2 files changed, 1036 insertions(+), 228 deletions(-) rename widget/{app_launcher.lua => app_launcher/init.lua} (61%) create mode 100644 widget/app_launcher/prompt.lua diff --git a/widget/app_launcher.lua b/widget/app_launcher/init.lua similarity index 61% rename from widget/app_launcher.lua rename to widget/app_launcher/init.lua index 6f3f335..7eeaf17 100644 --- a/widget/app_launcher.lua +++ b/widget/app_launcher/init.lua @@ -3,9 +3,11 @@ 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 dpi = beautiful.xresources.apply_dpi local string = string local table = table @@ -67,15 +69,6 @@ local function string_levenshtein(str1, str2) return matrix[len1][len2] end -local function has_value(tab, val) - for index, value in pairs(tab) do - if val:find(value) then - return true - end - end - return false -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) @@ -91,6 +84,15 @@ local function case_insensitive_pattern(pattern) return p end +local function has_value(tab, val) + for index, value in pairs(tab) do + if val:find(case_insensitive_pattern(value)) 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 @@ -98,8 +100,14 @@ local function select_app(self, x, y) if self._private.active_widget ~= nil then self._private.active_widget.selected = true self._private.active_widget:get_children_by_id("background")[1].bg = self.app_selected_color - local text_widget = self._private.active_widget:get_children_by_id("text")[1] - text_widget.markup = "" .. text_widget.text .. "" + 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 @@ -108,40 +116,43 @@ local function unselect_app(self) if self._private.active_widget ~= nil then self._private.active_widget.selected = false self._private.active_widget:get_children_by_id("background")[1].bg = self.app_normal_color - local text_widget = self._private.active_widget:get_children_by_id("text")[1] - text_widget.markup = "" .. text_widget.text .. "" + 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.container.place, - halign = self.app_name_halign, - { - widget = wibox.widget.imagebox, - forced_width = self.app_icon_width, - forced_height = self.app_icon_height, - image = entry.icon - } - } - or nil - local name = self.app_show_name == true - and - { - widget = wibox.container.place, - halign = self.app_icon_halign, - { - widget = wibox.widget.textbox, - id = "text", - align = "center", - font = self.app_name_font, - markup = entry.name - } - } - or nil + local 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 = 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 { @@ -152,13 +163,29 @@ local function create_app_widget(self, entry) shape = self.app_shape, bg = self.app_normal_color, { - widget = wibox.container.place, - valign = self.app_content_valign, + widget = wibox.container.margin, + margins = self.app_content_padding, { - layout = wibox.layout.fixed.vertical, - spacing = self.app_content_spacing, - icon, - name + -- 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 = wibox.layout.fixed.vertical, + spacing = self.app_content_spacing, + icon, + { + widget = wibox.container.place, + halign = self.app_name_halign, + { + layout = wibox.layout.fixed.horizontal, + spacing = self.app_name_generic_name_spacing, + name, + generic_name + } + } + }, + nil } } } @@ -179,7 +206,9 @@ local function create_app_widget(self, entry) awful.spawn(entry.executable) end - self:hide() + if self.hide_on_launch then + self:hide() + end end app:connect_signal("mouse::enter", function(_self) @@ -255,7 +284,14 @@ local function search(self, text) if string.find(entry.name, case_insensitive_pattern(text)) ~= nil or self.search_commands and string.find(entry.commandline, case_insensitive_pattern(text)) ~= nil then - table.insert(self._private.matched_entries, { name = entry.name, commandline = entry.commandline, executable = entry.executable, terminal = entry.terminal, icon = entry.icon }) + 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 + }) end end @@ -295,6 +331,99 @@ local function search(self, text) 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)) + 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) + else + select_app(self, pos.row, 1) + end + end + end +end + local function scroll_up(self) if #self._private.grid.children < 1 then self._private.active_widget = nil @@ -313,27 +442,8 @@ local function scroll_up(self) else select_app(self, pos.row - 1, pos.col) end - -- Check if the current page is not the first - elseif self._private.current_page > 1 then - -- Remove the current page apps from the grid - self._private.grid:reset() - - local max_app_index_to_include = (self._private.current_page - 1) * self._private.apps_per_page - local min_app_index_to_include = max_app_index_to_include - self._private.apps_per_page - - for index, entry in pairs(self._private.matched_entries) do - -- Only add widgets that are between this range (part of the current page) - if index > min_app_index_to_include and index <= max_app_index_to_include then - self._private.grid:add(create_app_widget(self, entry)) - end - end - - -- If we scrolled up a page, selected app should be the last one - rows, columns = self._private.grid:get_dimension() - select_app(self, rows, columns) - - -- Current page should be decremented - self._private.current_page = self._private.current_page - 1 + else + page_backward(self, "up") end end @@ -346,7 +456,6 @@ local function scroll_down(self) local rows, columns = self._private.grid:get_dimension() local pos = self._private.grid:get_widget_position(self._private.active_widget) local is_less_than_max_app = self._private.grid:index(self._private.active_widget) < #self._private.grid.children - local is_less_than_max_page = self._private.current_page < self._private.pages_count -- Check if we can scroll down the app list if is_less_than_max_app then @@ -357,26 +466,8 @@ local function scroll_down(self) else select_app(self, pos.row + 1, pos.col) end - -- If we can't scroll down the app list, check if we can scroll down a page - elseif is_less_than_max_page then - -- Remove the current page apps from the grid - self._private.grid:reset() - - local min_app_index_to_include = self._private.apps_per_page * self._private.current_page - local max_app_index_to_include = min_app_index_to_include + self._private.apps_per_page - - for index, entry in pairs(self._private.matched_entries) do - -- Only add widgets that are between this range (part of the current page) - if index > min_app_index_to_include and index <= max_app_index_to_include then - self._private.grid:add(create_app_widget(self, entry)) - end - end - - -- Select app 1 when scrolling to the next page - select_app(self, 1, 1) - - -- Current page should be incremented - self._private.current_page = self._private.current_page + 1 + else + page_forward(self, "down") end end @@ -388,33 +479,13 @@ local function scroll_left(self) local pos = self._private.grid:get_widget_position(self._private.active_widget) local is_bigger_than_first_column = pos.col > 1 - local is_not_first_page = self._private.current_page > 1 -- Check if the current marked app is not the first if is_bigger_than_first_column then unselect_app(self) select_app(self, pos.row, pos.col - 1) - -- Check if the current page is not the first - elseif is_not_first_page then - -- Remove the current page apps from the grid - self._private.grid:reset() - - local max_app_index_to_include = (self._private.current_page - 1) * self._private.apps_per_page - local min_app_index_to_include = max_app_index_to_include - self._private.apps_per_page - - for index, entry in pairs(self._private.matched_entries) do - -- Only add widgets that are between this range (part of the current page) - if index > min_app_index_to_include and index <= max_app_index_to_include then - self._private.grid:add(create_app_widget(self, entry)) - end - end - - -- Keep the same row from last page - local rows, columns = self._private.grid:get_dimension() - select_app(self, pos.row, columns) - - -- Current page should be decremented - self._private.current_page = self._private.current_page - 1 + else + page_backward(self, "left") end end @@ -443,30 +514,12 @@ local function scroll_right(self) select_app(self, pos.row, pos.col + 1) end - -- If we can't scroll down the app list, check if we can scroll down a page - elseif is_less_than_max_page then - -- Remove the current page apps from the grid - self._private.grid:reset() - - local min_app_index_to_include = self._private.apps_per_page * self._private.current_page - local max_app_index_to_include = min_app_index_to_include + self._private.apps_per_page - - for index, entry in pairs(self._private.matched_entries) do - -- Only add widgets that are between this range (part of the current page) - if index > min_app_index_to_include and index <= max_app_index_to_include then - self._private.grid:add(create_app_widget(self, entry)) - end - end - - -- Keep the last row - select_app(self, math.min(pos.row, #self._private.grid.children), 1) - - -- Current page should be incremented - self._private.current_page = self._private.current_page + 1 + else + page_forward(self, "right") end end -local function init(self) +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 @@ -485,6 +538,96 @@ local function init(self) select_app(self, 1, 1) end +local function generate_apps(self) + self._private.all_entries = {} + self._private.matched_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)) + + -- 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 + -- Check if this app should be skipped becuase it's iconless depanding on skip_empty_icons + 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) + 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"}) + 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, { + name = name, + generic_name = generic_name, + commandline = commandline, + executable = executable, + terminal = terminal, + icon = icon + }) + end + end + end + end +end + --- Shows the app launcher function app_launcher:show() local screen = self.screen @@ -494,30 +637,46 @@ function app_launcher:show() screen.app_launcher = self._private.widget screen.app_launcher.screen = screen - screen.app_launcher.visible = true - self._private.prompt:run() + self._private.prompt:start() - local placement = self.placement - if placement then - local pos = placement(self.screen.app_launcher, {pretend = true}) - local animation = self.rubato - if animation ~= nil then - if animation.x then - animation.x.ended:unsubscribe() - animation.x:set(pos.x) - else - self._private.widget.x = pos.x - end - if animation.y then - animation.y.ended:unsubscribe() - animation.y:set(pos.y) - else - self._private.widget.y = pos.y - end - else - self._private.widget.x = pos.x - self._private.widget.y = pos.y + 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 + + 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 end self:emit_signal("bling::app_launcher::visibility", true) @@ -534,9 +693,7 @@ function app_launcher:hide() return end - -- There's no other way to stop the prompt? - root.fake_input("key_press", "Escape") - root.fake_input("key_release", "Escape") + self._private.prompt:stop() local animation = self.rubato if animation ~= nil then @@ -553,21 +710,21 @@ function app_launcher:hide() if turn_off_on_anim_x_end then animation.x.ended:subscribe(function() - init(self) + 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() - init(self) + 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 - init(self) + if self.reset_on_hide == true then reset(self) end screen.app_launcher.visible = false screen.app_launcher = nil end @@ -594,26 +751,34 @@ local function new(args) args = args or {} args.terminal = args.terminal or nil - args.search_commands = args.search_commands or true + 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 or false - args.sort_alphabetically = args.sort_alphabetically or true - args.select_before_spawn = args.select_before_spawn or true - args.hide_on_clicked_outside = args.hide_on_clicked_outside or true - args.try_to_keep_index_after_searching = args.try_to_keep_index_after_searching or false + args.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.icons_size = args.icons_size or nil - args.show_on_focused_screen = args.show_on_focused_screen or true + 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.shirnk_width = args.shirnk_width or false - args.shrink_height = args.shrink_height or false + args.shirnk_width = args.shirnk_width ~= nil and args.shirnk_width or false + args.shrink_height = args.shrink_height ~= nil and args.shrink_height or false args.background = args.background or "#000000" args.shape = args.shape or nil @@ -643,9 +808,9 @@ local function new(args) args.apps_margin = args.apps_margin or dpi(30) args.apps_spacing = args.apps_spacing or dpi(30) - args.expand_apps = args.expand_apps or true + 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(100) + 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 @@ -655,17 +820,19 @@ local function new(args) args.app_selected_hover_color = args.app_selected_hover_color or (color.is_dark(args.app_normal_color) or color.is_opaque(args.app_normal_color)) and color.rgba_to_hex(color.multiply(color.hex_to_rgba(args.app_selected_color), 2.5)) or color.rgba_to_hex(color.multiply(color.hex_to_rgba(args.app_selected_color), 0.5)) - args.app_content_valign = args.app_content_valign or "center" + args.app_content_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({}) ret._private = {} @@ -674,7 +841,7 @@ local function new(args) gtable.crush(ret, app_launcher) gtable.crush(ret, args) - -- Determines the grid width + -- Calculate the grid width and height local grid_width = ret.shirnk_width == false and dpi((ret.app_width * ret.apps_per_column) + ((ret.apps_per_column - 1) * ret.apps_spacing)) or nil @@ -683,22 +850,14 @@ local function new(args) or nil -- These widgets need to be later accessed - ret._private.prompt = awful.widget.prompt + ret._private.prompt = prompt { prompt = ret.prompt_text, text = ret.prompt_start_text, font = ret.prompt_font, - bg = ret.prompt_color, - fg = ret.prompt_text_color, + reset_on_stop = ret.reset_on_hide, bg_cursor = ret.prompt_cursor_color, - hooks = - { - -- Disable historyu scrolling with arrow keys - -- TODO: implement this as other keybind? tab? - {{}, "Up", function(command) return true, false end}, - {{}, "Down", function(command) return true, false end}, - {{}, "Return", function(command) return true, false end}, - }, + 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 @@ -763,6 +922,7 @@ local function new(args) type = "dock", visible = false, ontop = true, + placement = ret.placement, shape = ret.shape, bg = ret.background, widget = @@ -776,6 +936,7 @@ local function new(args) 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, { @@ -793,7 +954,7 @@ local function new(args) font = ret.prompt_icon_font, markup = ret.prompt_icon_markup }, - ret._private.prompt + ret._private.prompt.textbox } } } @@ -808,51 +969,13 @@ local function new(args) } -- Private variables to be used to be used by the scrolling and searching functions - ret._private.all_entries = {} - ret._private.matched_entries = {} ret._private.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 - local app_info = Gio.AppInfo - local apps = app_info.get_all() - if ret.sort_alphabetically then - table.sort(apps, function(a, b) return app_info.get_name(a):lower() < app_info.get_name(b):lower() end) - end - - local icon_theme = require(tostring(path):match(".*bling") .. ".helpers.icon_theme")(ret.icon_theme, ret.icon_size) - - for _, app in ipairs(apps) do - if app.should_show(app) then - local name = app_info.get_name(app) - local commandline = app_info.get_commandline(app) - local executable = app_info.get_executable(app) - local icon = icon_theme:get_gicon_path(app_info.get_icon(app)) - - -- Check if this app should be skipped, depanding on the skip_names / skip_commands table - if not has_value(ret.skip_names, name) and not has_value(ret.skip_commands, commandline) then - -- Check if this app should be skipped becuase it's iconless depanding on skip_empty_icons - if icon ~= "" or ret.skip_empty_icons == false then - if icon == "" then - if ret.default_app_icon_name ~= nil then - icon = icon_theme:get_icon_path(ret.default_app_icon_name) - elseif ret.default_app_icon_path ~= nil then - icon = ret.default_app_icon_path - else - icon = icon_theme:choose_icon({ "application-all", "application", "application-default-icon", "app" }) - end - end - - local desktop_app_info = Gio.DesktopAppInfo.new(app_info.get_id(app)) - local terminal = Gio.DesktopAppInfo.get_string(desktop_app_info, "Terminal") == "true" and true or false - table.insert(ret._private.all_entries, { name = name, commandline = commandline, executable = executable, terminal = terminal, icon = icon }) - end - end - end - end - - init(ret) + generate_apps(ret) + reset(ret) if ret.rubato and ret.rubato.x then ret.rubato.x:subscribe(function(pos) @@ -865,7 +988,7 @@ local function new(args) end) end - if ret.hide_on_clicked_outside then + if ret.hide_on_left_clicked_outside then awful.mouse.append_client_mousebinding( awful.button({ }, 1, function (c) ret:hide() @@ -877,7 +1000,8 @@ local function new(args) ret:hide() end) ) - + end + if ret.hide_on_right_clicked_outside then awful.mouse.append_client_mousebinding( awful.button({ }, 3, function (c) ret:hide() @@ -891,9 +1015,37 @@ 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) + 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 new file mode 100644 index 0000000..fae3b86 --- /dev/null +++ b/widget/app_launcher/prompt.lua @@ -0,0 +1,656 @@ +--------------------------------------------------------------------------- +--- 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