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
This commit is contained in:
parent
92dabc890e
commit
3c8b3b8cc0
|
@ -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 = "<span foreground='" .. self.app_name_selected_color .. "'>" .. text_widget.text .. "</span>"
|
||||
local name_widget = self._private.active_widget:get_children_by_id("name")[1]
|
||||
if name_widget then
|
||||
name_widget.markup = string.format("<span foreground='%s'>%s</span>", 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("<i><span weight='300'foreground='%s'>%s</span></i>", 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 = "<span foreground='" .. self.app_name_normal_color .. "'>" .. text_widget.text .. "</span>"
|
||||
local name_widget = self._private.active_widget:get_children_by_id("name")[1]
|
||||
if name_widget then
|
||||
name_widget.markup = string.format("<span foreground='%s'>%s</span>", 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("<i><span weight='300'foreground='%s'>%s</span></i>", 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,
|
||||
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.container.place,
|
||||
halign = self.app_icon_halign,
|
||||
} or nil
|
||||
|
||||
local name = self.app_show_name == true and
|
||||
{
|
||||
widget = wibox.widget.textbox,
|
||||
id = "text",
|
||||
align = "center",
|
||||
id = "name",
|
||||
font = self.app_name_font,
|
||||
markup = entry.name
|
||||
}
|
||||
}
|
||||
or nil
|
||||
} 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 "<span weight='300'> <i>(" .. entry.generic_name .. ")</i></span>" 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,
|
||||
{
|
||||
-- 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,
|
||||
name
|
||||
{
|
||||
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,8 +206,10 @@ local function create_app_widget(self, entry)
|
|||
awful.spawn(entry.executable)
|
||||
end
|
||||
|
||||
if self.hide_on_launch then
|
||||
self:hide()
|
||||
end
|
||||
end
|
||||
|
||||
app:connect_signal("mouse::enter", function(_self)
|
||||
local widget = capi.mouse.current_wibox
|
||||
|
@ -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 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(pos.x)
|
||||
else
|
||||
self._private.widget.x = pos.x
|
||||
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(pos.y)
|
||||
else
|
||||
self._private.widget.y = pos.y
|
||||
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
|
||||
self._private.widget.x = pos.x
|
||||
self._private.widget.y = pos.y
|
||||
end
|
||||
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
|
|
@ -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 .. "<span background=\"" .. cursor_color ..
|
||||
"\" foreground=\"" .. text_color .. "\" underline=\"" .. underline ..
|
||||
"\">" .. char .. "</span>" .. 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)
|
Loading…
Reference in New Issue