---------------------------------------------------------------------------
-- @author Julien Danjou <julien@danjou.info>
-- @copyright 2008 Julien Danjou
-- @release @AWESOME_VERSION@
---------------------------------------------------------------------------
-- Grab environment we need
local type = type
local string = string
local assert = assert
local loadstring = loadstring
local ipairs = ipairs
local pairs = pairs
local os = os
local io = io
local math = math
local setmetatable = setmetatable
local table = table
local otable = otable
local capi =
{
awesome = awesome,
screen = screen,
client = client,
mouse = mouse,
button = button,
titlebar = titlebar,
widget = widget,
hooks = hooks,
keygrabber = keygrabber
}
--- awful: AWesome Functions very UsefuL
module("awful")
-- Local variable handling theme
local theme = {}
-- mapping of command/completion function
local bashcomp_funcs = {}
local bashcomp_src = "/etc/bash_completion"
-- Various public structures
beautiful = {}
hooks = {}
hooks.user = {}
prompt = {}
prompt.history = {}
prompt.history.data = {}
completion = {}
screen = {}
layout = {}
client = {}
client.data = {}
client.data.maximize = otable()
client.focus = {}
client.focus.history = {}
client.focus.history.data = {}
tag = {}
tag.history = {}
tag.history.data = {}
tag.history.data.past = {}
tag.history.data.current = {}
titlebar = {}
titlebar.data = otable()
widget = {}
widget.taglist = {}
widget.taglist.label = {}
widget.tasklist = {}
widget.tasklist.label = {}
client.urgent = {}
client.urgent.stack = {}
client.urgent.stack.data = {}
placement = {}
--- Strip alpha part of color.
-- @param color The color.
-- @return The color without alpha channel.
local function color_strip_alpha(color)
if color:len() == 9 then
color = color:sub(1, 7)
end
return color
end
--- Make i cycle.
-- @param t A length.
-- @param i An absolute index to fit into #t.
-- @return The object at new index.
local function cycle(t, i)
while i > t do i = i - t end
while i < 1 do i = i + t end
return i
end
--- Create a directory
-- @param dir The directory.
-- @return mkdir return code
function mkdir(dir)
return os.execute("mkdir -p " .. dir)
end
--- Get the first client that got the urgent hint.
-- @return The first urgent client.
function client.urgent.get()
if #client.urgent.stack.data > 0 then
return client.urgent.stack.data[1]
else
-- fallback behaviour: iterate through clients and get the first urgent
local clients = capi.client.get()
for k, cl in pairs(clients) do
if cl.urgent then
return cl
end
end
end
end
--- Jump to the client that received the urgent hint first.
function client.urgent.jumpto()
local c = client.urgent.get()
if c then
local s = capi.client.focus and capi.client.focus.screen or capi.mouse.screen
-- focus the screen
if s ~= c.screen then
capi.mouse.screen = c.screen
end
-- focus the tag
tag.viewonly(c:tags()[1])
-- focus the client
capi.client.focus = c
c:raise()
end
end
--- Adds client to urgent stack.
-- @param The client object.
function client.urgent.stack.add(c)
table.insert(client.urgent.stack.data, c)
end
--- Remove client from urgent stack.
-- @param The client object.
function client.urgent.stack.delete(c)
for k, cl in ipairs(client.urgent.stack.data) do
if c == cl then
table.remove(client.urgent.stack.data, k)
break
end
end
end
--- Remove a client from the focus history
-- @param c The client that must be removed.
function client.focus.history.delete(c)
for k, v in ipairs(client.focus.history.data) do
if v == c then
table.remove(client.focus.history.data, k)
break
end
end
end
--- Update client focus history.
-- @param c The client that has been focused.
function client.focus.history.add(c)
-- Remove the client if its in stack
client.focus.history.delete(c)
-- Record the client has latest focused
table.insert(client.focus.history.data, 1, c)
end
--- Get the latest focused client for a screen in history.
-- @param screen The screen number to look for.
-- @param idx The index: 0 will return first candidate,
-- 1 will return second, etc.
-- @return A client.
function client.focus.history.get(screen, idx)
-- When this counter is equal to idx, we return the client
local counter = 0
local vc = capi.client.visible_get(screen)
for k, c in ipairs(client.focus.history.data) do
if c.screen == screen then
for j, vcc in ipairs(vc) do
if vcc == c then
if counter == idx then
return c
end
-- We found one, increment the counter only.
counter = counter + 1
break
end
end
end
end
-- Argh nobody found in history, give the first one visible if there is one
if counter == 0 then
return vc[1]
end
end
--- Focus the previous client in history.
function client.focus.history.previous()
local sel = capi.client.focus
local s
if sel then
s = sel.screen
else
s = capi.mouse.screen
end
local c = client.focus.history.get(s, 1)
if c then capi.client.focus = c end
end
--- Get a client by its relative index to the focused window.
-- @usage Set i to 1 to get next, -1 to get previous.
-- @param i The index.
-- @param c Optional client.
-- @return A client, or nil if no client is available.
function client.next(i, c)
-- Get currently focused client
local sel = c or capi.client.focus
if sel then
-- Get all visible clients
local cls = capi.client.visible_get(sel.screen)
-- Loop upon each client
for idx, c in ipairs(cls) do
if c == sel then
-- Cycle
return cls[cycle(#cls, idx +i)]
end
end
end
end
--- Return true whether client B is in the right direction
-- compared to client A.
-- @param dir The direction.
-- @param cA The first client.
-- @param cB The second client.
-- @return True if B is in the direction of A.
local function is_in_direction(dir, cA, cB)
if dir == "up" then
return cA['y'] > cB['y']
elseif dir == "down" then
return cA['y'] < cB['y']
elseif dir == "left" then
return cA['x'] > cB['x']
elseif dir == "right" then
return cA['x'] < cB['x']
end
return false
end
--- Calculate distance between two points.
-- i.e: if we want to move to the right, we will take the right border
-- of the currently focused client and the left side of the checked client.
-- This avoid the focus of an upper client when you move to the right in a
-- tilebottom layout with nmaster=2 and 5 clients open, for instance.
-- @param dir The direction.
-- @param cA The first client.
-- @param cB The second client.
-- @return The distance between the clients.
local function calculate_distance(dir, cA, cB)
local xA = cA['x']
local xB = cB['x']
local yA = cA['y']
local yB = cB['y']
if dir == "up" then
yB = yB + cB['height']
elseif dir == "down" then
yA = yA + cA['height']
elseif dir == "left" then
xB = xB + cB['width']
elseif dir == "right" then
xA = xA + cA['width']
end
return math.sqrt(math.pow(xB - xA, 2) + math.pow(yB - yA, 2))
end
--- Focus a client by the given direction.
-- @param dir The direction, can be either "up", "down", "left" or "right".
-- @param c Optional client.
function client.focusbydirection(dir, c)
local sel = c or capi.client.focus
if sel then
local coords = sel:coords()
local dist, dist_min
local target = nil
local cls = capi.client.visible_get(sel.screen)
-- We check each client.
for i, c in ipairs(cls) do
-- Check coords to see if client is located in the right direction.
if is_in_direction(dir, coords, c:coords()) then
-- Calculate distance between focused client and checked client.
dist = calculate_distance(dir, coords, c:coords())
-- If distance is shorter then keep the client.
if not target or dist < dist_min then
target = c
dist_min = dist
end
end
end
-- If we found a client to focus, then do it.
if target then
capi.client.focus = target
end
end
end
--- Focus a client by its relative index.
-- @param i The index.
-- @param c Optional client.
function client.focusbyidx(i, c)
local target = client.next(i, c)
if target then
capi.client.focus = target
end
end
--- Swap a client by its relative index.
-- @param i The index.
-- @param c Optional client, otherwise focused one is used.
function client.swap(i, c)
local sel = c or capi.client.focus
local target = client.next(i, sel)
if target then
target:swap(sel)
end
end
--- Get the master window.
-- @param screen Optional screen number, otherwise screen mouse is used.
-- @return The master window.
function client.master(screen)
local s = screen or capi.mouse.screen
return capi.client.visible_get(s)[1]
end
-- Set the client as slave: put it at the end of other windows.
-- @param c The window to set as slave.
-- @return
function client.setslave(c)
local cls = capi.client.visible_get(c.screen)
for k, v in pairs(cls) do
c:swap(v)
end
end
--- Move/resize a client relative to current coordinates.
-- @param x The relative x coordinate.
-- @param y The relative y coordinate.
-- @param w The relative width.
-- @param h The relative height.
-- @param c The optional client, otherwise focused one is used.
function client.moveresize(x, y, w, h, c)
local sel = c or capi.client.focus
local coords = sel:coords()
coords['x'] = coords['x'] + x
coords['y'] = coords['y'] + y
coords['width'] = coords['width'] + w
coords['height'] = coords['height'] + h
sel:coords(coords)
end
--- Maximize a client to use the full workarea.
-- @param c A client, or the focused one if nil.
function client.maximize(c)
local sel = c or capi.client.focus
if sel then
local ws = capi.screen[sel.screen].workarea
ws.width = ws.width - 2 * sel.border_width
ws.height = ws.height - 2 * sel.border_width
if sel.floating and client.data.maximize[sel] then
sel.floating = client.data.maximize[sel].floating
if sel.floating then
sel:coords(client.data.maximize[sel].coords)
end
client.data.maximize[sel] = nil
else
client.data.maximize[sel] = { coords = sel:coords(), floating = sel.floating }
sel.floating = true
sel:coords(ws)
end
end
end
--- Erase eventual client data in maximize.
-- @param c The client.
local function client_maximize_clean(c)
client.data.maximize[c] = nil
end
--- Give the focus to a screen, and move pointer.
-- @param Screen number.
function screen.focus(i)
local s = cycle(capi.screen.count(), capi.mouse.screen + i)
local c = client.focus.history.get(s, 0)
if c then capi.client.focus = c end
-- Move the mouse on the screen
capi.mouse.screen = s
end
--- Compare 2 tables of tags.
-- @param a The first table.
-- @param b The second table of tags.
-- @return True if the tables are identical, false otherwise.
local function tag_compare_select(a, b)
if not a or not b then
return false
end
-- Quick size comparison
if #a ~= #b then
return false
end
for ka, va in pairs(a) do
if b[ka] ~= va.selected then
return false
end
end
for kb, vb in pairs(b) do
if a[kb].selected ~= vb then
return false
end
end
return true
end
--- Update the tag history.
-- @param screen The screen number.
function tag.history.update(screen)
local curtags = capi.screen[screen]:tags()
if not tag_compare_select(curtags, tag.history.data.current[screen]) then
tag.history.data.past[screen] = tag.history.data.current[screen]
tag.history.data.current[screen] = {}
for k, v in ipairs(curtags) do
tag.history.data.current[screen][k] = v.selected
end
end
end
-- Revert tag history.
-- @param screen The screen number.
function tag.history.restore(screen)
local s = screen or capi.mouse.screen
local tags = capi.screen[s]:tags()
for k, t in pairs(tags) do
t.selected = tag.history.data.past[s][k]
end
end
--- Return a table with all visible tags
-- @param s Screen number.
-- @return A table with all selected tags.
function tag.selectedlist(s)
local screen = s or capi.mouse.screen
local tags = capi.screen[screen]:tags()
local vtags = {}
for i, t in pairs(tags) do
if t.selected then
vtags[#vtags + 1] = t
end
end
return vtags
end
--- Return only the first visible tag.
-- @param s Screen number.
function tag.selected(s)
return tag.selectedlist(s)[1]
end
--- Set master width factor.
-- @param mwfact Master width factor.
function tag.setmwfact(mwfact)
local t = tag.selected()
if t then
t.mwfact = mwfact
end
end
--- Increase master width factor.
-- @param add Value to add to master width factor.
function tag.incmwfact(add)
local t = tag.selected()
if t then
t.mwfact = t.mwfact + add
end
end
--- Set the number of master windows.
-- @param nmaster The number of master windows.
function tag.setnmaster(nmaster)
local t = tag.selected()
if t then
t.nmaster = nmaster
end
end
--- Increase the number of master windows.
-- @param add Value to add to number of master windows.
function tag.incnmaster(add)
local t = tag.selected()
if t then
t.nmaster = t.nmaster + add
end
end
--- Set number of column windows.
-- @param ncol The number of column.
function tag.setncol(ncol)
local t = tag.selected()
if t then
t.ncol = ncol
end
end
--- Increase number of column windows.
-- @param add Value to add to number of column windows.
function tag.incncol(add)
local t = tag.selected()
if t then
t.ncol = t.ncol + add
end
end
--- View no tag.
-- @param Optional screen number.
function tag.viewnone(screen)
local tags = capi.screen[screen or capi.mouse.screen]:tags()
for i, t in pairs(tags) do
t.selected = false
end
end
--- View a tag by its index.
-- @param i The relative index to see.
-- @param screen Optional screen number.
function tag.viewidx(i, screen)
local tags = capi.screen[screen or capi.mouse.screen]:tags()
local sel = tag.selected()
tag.viewnone()
for k, t in ipairs(tags) do
if t == sel then
tags[cycle(#tags, k + i)].selected = true
end
end
end
--- View next tag. This is the same as tag.viewidx(1).
function tag.viewnext()
return tag.viewidx(1)
end
--- View previous tag. This is the same a tag.viewidx(-1).
function tag.viewprev()
return tag.viewidx(-1)
end
--- View only a tag.
-- @param t The tag object.
function tag.viewonly(t)
tag.viewnone(t.screen)
t.selected = true
end
--- View only a set of tags.
-- @param tags A table with tags to view only.
-- @param screen Optional screen number of the tags.
function tag.viewmore(tags, screen)
tag.viewnone(screen)
for i, t in pairs(tags) do
t.selected = true
end
end
--- Move a client to a tag.
-- @param target The tag to move the client to.
-- @param c Optional client to move, otherwise the focused one is used.
function client.movetotag(target, c)
local sel = c or capi.client.focus
if sel then
-- Check that tag and client screen are identical
if sel.screen ~= target.screen then return end
sel:tags({ target })
end
end
--- Toggle a tag on a client.
-- @param target The tag to toggle.
-- @param c Optional client to toggle, otherwise the focused one is used.
function client.toggletag(target, c)
local sel = c or capi.client.focus
-- Check that tag and client screen are identical
if sel and sel.screen == target.screen then
local tags = sel:tags()
if tags[target] then
-- If it's the only tag for the window, stop.
if #tags == 1 then return end
tags[tags[target]] = nil
else
tags[target] = target
end
sel:tags(tags)
end
end
--- Toggle the floating status of a client.
-- @param c Optional client, the focused on if not set.
function client.togglefloating(c)
local sel = c or capi.client.focus
if sel then
sel.floating = not sel.floating
end
end
--- Move a client to a screen. Default is next screen, cycling.
-- @param c The client to move.
-- @param s The screen number, default to current + 1.
function client.movetoscreen(c, s)
local sel = c or capi.client.focus
if sel then
local sc = capi.screen.count()
if not s then
s = sel.screen + 1
end
if s > sc then s = 1 elseif s < 1 then s = sc end
sel.screen = s
capi.mouse.coords(capi.screen[s].coords)
capi.client.focus = sel
end
end
--- Get the current layout name.
-- @param screen The screen number.
function layout.get(screen)
local t = tag.selected(screen)
if t then
return t.layout
end
end
--- Create a new userhook (for external libs).
-- @param name Hook name.
function hooks.user.create(name)
hooks[name] = {}
hooks[name].callbacks = {}
hooks[name].register = function (f)
table.insert(hooks[name].callbacks, f)
end
hooks[name].unregister = function (f)
for k, h in ipairs(hooks[name].callbacks) do
if h == f then
table.remove(hooks[name].callbacks, k)
break
end
end
end
end
--- Call a created userhook (for external libs).
-- @param name Hook name.
function hooks.user.call(name, ...)
for name, callback in pairs(hooks[name].callbacks) do
callback(...)
end
end
-- Just set an awful mark to a client to move it later.
local awfulmarked = {}
hooks.user.create('marked')
hooks.user.create('unmarked')
--- Mark a client, and then call 'marked' hook.
-- @param c The client to mark, the focused one if not specified.
-- @return True if the client has been marked. False if the client was already marked.
function client.mark (c)
local cl = c or capi.client.focus
if cl then
for k, v in pairs(awfulmarked) do
if cl == v then
return false
end
end
table.insert(awfulmarked, cl)
-- Call callback
hooks.user.call('marked', cl)
return true
end
end
--- Unmark a client and then call 'unmarked' hook.
-- @param c The client to unmark, or the focused one if not specified.
-- @return True if the client has been unmarked. False if the client was not marked.
function client.unmark(c)
local cl = c or capi.client.focus
for k, v in pairs(awfulmarked) do
if cl == v then
table.remove(awfulmarked, k)
hooks.user.call('unmarked', cl)
return true
end
end
return false
end
--- Check if a client is marked.
-- @param c The client to check, or the focused one otherwise.
function client.ismarked(c)
local cl = c or capi.client.focus
if cl then
for k, v in pairs(awfulmarked) do
if cl == v then
return true
end
end
end
return false
end
--- Toggle a client as marked.
-- @param c The client to toggle mark.
function client.togglemarked(c)
local cl = c or capi.client.focus
if not client.mark(c) then
client.unmark(c)
end
end
--- Return the marked clients and empty the marked table.
-- @return A table with all marked clients.
function client.getmarked()
for k, v in pairs(awfulmarked) do
hooks.user.call('unmarked', v)
end
t = awfulmarked
awfulmarked = {}
return t
end
--- Change the layout of the current tag.
-- @param layouts A table of layouts.
-- @param i Relative index.
function layout.inc(layouts, i)
local t = tag.selected()
local number_of_layouts = 0
local rev_layouts = {}
for i, v in ipairs(layouts) do
rev_layouts[v] = i
number_of_layouts = number_of_layouts + 1
end
if t then
local cur_layout = layout.get()
local new_layout_index = (rev_layouts[cur_layout] + i) % number_of_layouts
if new_layout_index == 0 then
new_layout_index = number_of_layouts
end
t.layout = layouts[new_layout_index]
end
end
--- Set the layout of the current tag by name.
-- @param layout Layout name.
function layout.set(layout)
local t = tag.selected()
if t then
t.layout = layout
end
end
-- Autodeclare awful.hooks.* functions
-- mapped to awesome hooks.* functions
for name, hook in pairs(capi.hooks) do
if name ~= 'timer' then
hooks[name] = {}
hooks[name].register = function (f)
if not hooks[name].callbacks then
hooks[name].callbacks = {}
hook(function (...)
for i, callback in ipairs(hooks[name].callbacks) do
callback(...)
end
end)
end
table.insert(hooks[name].callbacks, f)
end
hooks[name].unregister = function (f)
if hooks[name].callbacks then
for k, h in ipairs(hooks[name].callbacks) do
if h == f then
table.remove(hooks[name].callbacks, k)
break
end
end
end
end
else
hooks[name] = {}
hooks[name].register = function (time, f, runnow)
if type(time) ~= 'number' or type(f) ~= 'function' or time <= 0 then
return
end
local new_timer
if hooks[name].timer then
-- Take the smallest between current and new
new_timer = math.min(time, hooks[name].timer)
else
new_timer = time
end
if not hooks[name].callbacks then
hooks[name].callbacks = {}
end
if hooks[name].timer ~= new_timer then
hooks[name].timer = new_timer
hook(hooks[name].timer, function (...)
for i, callback in ipairs(hooks[name].callbacks) do
callback['counter'] = callback['counter'] + hooks[name].timer
if callback['counter'] >= callback['timer'] then
callback['callback'](...)
callback['counter'] = 0
end
end
end)
end
if runnow then
table.insert(hooks[name].callbacks, { callback = f, timer = time, counter = time })
else
table.insert(hooks[name].callbacks, { callback = f, timer = time, counter = 0 })
end
end
hooks[name].unregister = function (f)
if hooks[name].callbacks then
for k, h in ipairs(hooks[name].callbacks) do
if h.callback == f then
table.remove(hooks[name].callbacks, k)
break
end
end
end
end
end
end
--- Spawn a program.
-- @param cmd The command.
-- @param screen The screen where to spawn window.
-- @return The awesome.spawn return value.
function spawn(cmd, screen)
if cmd and cmd ~= "" then
return capi.awesome.spawn(cmd .. "&", screen or capi.mouse.screen)
end
end
-- Read a program output and returns its output as a string.
-- @param cmd The command to run.
-- @return A string with the program output.
function pread(cmd)
if cmd and cmd ~= "" then
local f = io.popen(cmd, 'r')
local s = f:read("*all")
f:close()
return s
end
end
--- Eval Lua code.
-- @return The return value of Lua code.
function eval(s)
return assert(loadstring("return " .. s))()
end
--- Load history file in history table
-- @param id The prompt history identifier which is the path to the filename
-- @param max Optional parameter, the maximum number of entries in file
local function prompt_history_check_load(id, max)
if id and id ~= ""
and not prompt.history[id] then
prompt.history[id] = { max = 50, table = {} }
if max then
prompt.history[id].max = max
end
local f = io.open(id, "r")
-- Read history file
if f then
for line in f:lines() do
table.insert(prompt.history[id].table, line)
if #prompt.history[id].table >= prompt.history[id].max then
break
end
end
end
end
end
--- Save history table in history file
-- @param id The prompt history identifier
local function prompt_history_save(id)
if prompt.history[id] then
local f = io.open(id, "w")
if not f then
local i = 0
for d in id:gmatch(".-/") do
i = i + #d
end
mkdir(id:sub(1, i - 1))
f = assert(io.open(id, "w"))
end
for i = 1, math.min(#prompt.history[id].table, prompt.history[id].max) do
f:write(prompt.history[id].table[i] .. "\n")
end
f:close()
end
end
--- Return the number of items in history table regarding the id
-- @param id The prompt history identifier
-- @return the number of items in history table, -1 if history is disabled
local function prompt_history_items(id)
if prompt.history[id] then
return #prompt.history[id].table
else
return -1
end
end
--- Add an entry to the history file
-- @param id The prompt history identifier
-- @param command The command to add
local function prompt_history_add(id, command)
if prompt.history[id] then
if command ~= ""
and command ~= prompt.history[id].table[#prompt.history[id].table] then
table.insert(prompt.history[id].table, command)
-- Do not exceed our max_cmd
if #prompt.history[id].table > prompt.history[id].max then
table.remove(prompt.history[id].table, 1)
end
prompt_history_save(id)
end
end
end
--- Enable programmable bash completion in awful.completion.bash at the price of
-- a slight overhead
-- @param src The bash completion source file, /etc/bash_completion by default.
function completion.bashcomp_load(src)
if src then bashcomp_src = src end
local c = io.popen("/usr/bin/env bash -c 'source " .. bashcomp_src .. "; complete -p'")
while true do
local line = c:read("*line")
if not line then break end
-- if a bash function is used for completion, register it
if line:match(".* -F .*") then
bashcomp_funcs[line:gsub(".* (%S+)$","%1")] = line:gsub(".*-F +(%S+) .*$", "%1")
end
end
c:close()
end
--- Use bash completion system to complete command and filename.
-- @param command The command line.
-- @param cur_pos The cursor position.
-- @param ncomp The element number to complete.
-- @return The new command and the new cursor position.
function completion.bash(command, cur_pos, ncomp)
local wstart = 1
local wend = 1
local words = {}
local cword_index = 0
local cword_start = 0
local cword_end = 0
local i = 1
local comptype = "file"
-- do nothing if we are on a letter, i.e. not at len + 1 or on a space
if cur_pos ~= #command + 1 and command:sub(cur_pos, cur_pos) ~= " " then
return command, cur_pos
elseif #command == 0 then
return command, cur_pos
end
while wend <= #command do
wend = command:find(" ", wstart)
if not wend then wend = #command + 1 end
table.insert(words, command:sub(wstart, wend - 1))
if cur_pos >= wstart and cur_pos <= wend + 1 then
cword_start = wstart
cword_end = wend
cword_index = i
end
wstart = wend + 1
i = i + 1
end
if cword_index == 1 then
comptype = "command"
end
local bash_cmd
if bashcomp_funcs[words[1]] then
-- fairly complex command with inline bash script to get the possible completions
bash_cmd = "/usr/bin/env bash -c 'source " .. bashcomp_src .. "; " ..
"__print_completions() { for ((i=0;i<${#COMPREPLY[*]};i++)); do echo ${COMPREPLY[i]}; done }; " ..
"COMP_WORDS=(" .. command .."); COMP_LINE=\"" .. command .. "\"; " ..
"COMP_COUNT=" .. cur_pos .. "; COMP_CWORD=" .. cword_index-1 .. "; " ..
bashcomp_funcs[words[1]] .. "; __print_completions | sort -u'"
else
bash_cmd = "/usr/bin/env bash -c 'compgen -A " .. comptype .. " " .. words[cword_index] .. "'"
end
local c = io.popen(bash_cmd)
local output = {}
i = 0
while true do
local line = c:read("*line")
if not line then break end
table.insert(output, line)
end
c:close()
-- no completion, return
if #output == 0 then
return command, cur_pos
end
-- cycle
while ncomp > #output do
ncomp = ncomp - #output
end
local str = command:sub(1, cword_start - 1) .. output[ncomp] .. command:sub(cword_end)
cur_pos = cword_end + #output[ncomp] + 1
return str, cur_pos
end
--- Draw the prompt text with a cursor.
-- @param text The text.
-- @param text_color The text color.
-- @param cursor_color The cursor color.
-- @param cursor_pos The cursor position.
local function prompt_text_with_cursor(text, text_color, cursor_color, cursor_pos)
local char
if not text then text = "" end
if #text < cursor_pos then
char = " "
else
char = escape(text:sub(cursor_pos, cursor_pos))
end
local text_start = escape(text:sub(1, cursor_pos - 1))
local text_end = escape(text:sub(cursor_pos + 1))
return text_start .. "" .. char .. "" .. text_end
end
--- Run a prompt in a box.
-- @param args A table with optional arguments: fg_cursor, bg_cursor, prompt.
-- @param textbox The textbox to use for the prompt.
-- @param exe_callback The callback function to call with command as argument when finished.
-- @param completion_callback The callback function to call to get completion.
-- @param history_path Optional parameter: file path where the history should be saved, set nil to disable history
-- @param history_max Optional parameter: set the maximum entries in history file, 50 by default
-- @param done_callback Optional parameter: the callback function to always call without arguments, regardless of whether the prompt was cancelled.
function prompt.run(args, textbox, exe_callback, completion_callback, history_path, history_max, done_callback)
if not args then args = {} end
local command = ""
local command_before_comp
local cur_pos_before_comp
local prettyprompt = args.prompt or ""
local inv_col = args.fg_cursor or theme.fg_focus or "black"
local cur_col = args.bg_cursor or theme.bg_focus or "white"
prompt_history_check_load(history_path, history_max)
local history_index = prompt_history_items(history_path) + 1
-- The cursor position
local cur_pos = 1
-- The completion element to use on completion request.
local ncomp = 1
if not textbox or not exe_callback then
return
end
textbox.text = prettyprompt .. prompt_text_with_cursor(text, inv_col, cur_col, cur_pos)
capi.keygrabber.run(
function (mod, key)
-- Get out cases
if mod.Control then
if key == "c" or key == "g" then
textbox.text = ""
if done_callback then done_callback() end
return false
elseif key == "j" or key == "m" then
textbox.text = ""
exec_callback(command)
if done_callback then done_callback() end
return false
end
else
if key == "Return" then
textbox.text = ""
prompt_history_add(history_path, command)
exe_callback(command)
if done_callback then done_callback() end
return false
elseif key == "Escape" then
textbox.text = ""
if done_callback then done_callback() end
return false
end
end
-- Control cases
if mod.Control then
if key == "a" then
cur_pos = 1
elseif key == "b" then
if cur_pos > 1 then
cur_pos = cur_pos - 1
end
elseif key == "d" then
if cur_pos <= #command then
command = command:sub(1, cur_pos - 1) .. command:sub(cur_pos + 1)
end
elseif key == "e" then
cur_pos = #command + 1
elseif key == "f" then
if cur_pos <= #command then
cur_pos = cur_pos + 1
end
elseif key == "h" then
if cur_pos > 1 then
command = command:sub(1, cur_pos - 2) .. command:sub(cur_pos)
cur_pos = cur_pos - 1
end
elseif key == "k" then
command = command:sub(1, cur_pos - 1)
elseif key == "u" then
command = command:sub(cur_pos, #command)
cur_pos = 1
elseif key == "w" then
local wstart = 1
local wend = 1
local cword_start = 1
local cword_end = 1
while wend < cur_pos do
wend = command:find(" ", wstart)
if not wend then wend = #command + 1 end
if cur_pos >= wstart and cur_pos <= wend + 1 then
cword_start = wstart
cword_end = cur_pos - 1
break
end
wstart = wend + 1
end
command = command:sub(1, cword_start - 1) .. command:sub(cword_end + 1)
cur_pos = cword_start
end
else
if completion_callback then
-- That's tab
if key:byte() == 9 then
if ncomp == 1 then
command_before_comp = command
cur_pos_before_comp = cur_pos
end
command, cur_pos = completion_callback(command_before_comp, cur_pos_before_comp, ncomp)
ncomp = ncomp + 1
key = ""
else
ncomp = 1
end
end
-- Typin cases
if key == "Home" then
cur_pos = 1
elseif key == "End" then
cur_pos = #command + 1
elseif key == "BackSpace" then
if cur_pos > 1 then
command = command:sub(1, cur_pos - 2) .. command:sub(cur_pos)
cur_pos = cur_pos - 1
end
-- That's DEL
elseif key:byte() == 127 then
command = command:sub(1, cur_pos - 1) .. command:sub(cur_pos + 1)
elseif key == "Left" then
cur_pos = cur_pos - 1
elseif key == "Right" then
cur_pos = cur_pos + 1
elseif key == "Up" then
if history_index > 1 then
history_index = history_index - 1
command = prompt.history[history_path].table[history_index]
cur_pos = #command + 2
end
elseif key == "Down" then
if history_index < prompt_history_items(history_path) then
history_index = history_index + 1
command = prompt.history[history_path].table[history_index]
cur_pos = #command + 2
elseif history_index == prompt_history_items(history_path) then
history_index = history_index + 1
command = ""
cur_pos = 1
end
else
-- len() 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:len() == 1 then
command = command:sub(1, cur_pos - 1) .. key .. command:sub(cur_pos)
cur_pos = cur_pos + #key
end
end
if cur_pos < 1 then
cur_pos = 1
elseif cur_pos > #command + 1 then
cur_pos = #command + 1
end
end
-- Update textbox
textbox.text = prettyprompt .. prompt_text_with_cursor(command, inv_col, cur_col, cur_pos)
return true
end)
end
--- Escape a string from XML char.
-- Useful to set raw text in textbox.
-- @param text Text to escape.
-- @return Escape text.
function escape(text)
if text then
text = text:gsub("&", "&")
text = text:gsub("<", "<")
text = text:gsub(">", ">")
text = text:gsub("'", "'")
text = text:gsub("\"", """)
end
return text
end
--- Unescape a string from entities.
-- @param text Text to unescape.
-- @return Unescaped text.
function unescape(text)
if text then
text = text:gsub("&", "&")
text = text:gsub("<", "<")
text = text:gsub(">", ">")
text = text:gsub("'", "'")
text = text:gsub(""", "\"")
end
return text
end
--- Return labels for a taglist widget with all tag from screen.
-- It returns the tag name and set a special
-- foreground and background color for selected tags.
-- @param t The tag.
-- @param args The arguments table.
-- bg_focus The background color for selected tag.
-- fg_focus The foreground color for selected tag.
-- bg_urgent The background color for urgent tags.
-- fg_urgent The foreground color for urgent tags.
-- taglist_squares Optional: set "true" or nil to display the taglist squares.
-- taglist_squares_sel Optional: an user provided image for selected squares.
-- taglist_squares_unsel Optional: an user provided image for unselected squares.
-- @return A string to print.
function widget.taglist.label.all(t, args)
if not args then args = {} end
local fg_focus = args.fg_focus or theme.taglist_fg_focus or theme.fg_focus
local bg_focus = args.bg_focus or theme.taglist_bg_focus or theme.bg_focus
local fg_urgent = args.fg_urgent or theme.taglist_fg_urgent or theme.fg_urgent
local bg_urgent = args.bg_urgent or theme.taglist_bg_urgent or theme.bg_urgent
local taglist_squares = args.taglist_squares or theme.taglist_squares
local taglist_squares_sel = args.squares_sel or theme.squares_sel
local taglist_squares_unsel = args.squares_unsel or theme.squares_unsel
local text
local background = ""
local sel = capi.client.focus
local bg_color = nil
local fg_color = nil
if t.selected then
bg_color = bg_focus
fg_color = fg_focus
end
if sel and sel:tags()[t] then
if not taglist_squares or taglist_squares == "true" then
if taglist_squares_sel then
background = "resize=\"true\" image=\"" .. taglist_squares_sel .. "\""
else
background = "resize=\"true\" image=\"@AWESOME_ICON_PATH@/taglist/squarefw.png\""
end
end
elseif bg_urgent or fg_urgent then
for k, c in pairs(t:clients()) do
if not taglist_squares or taglist_squares == "true" then
if taglist_squares_unsel then
background = "resize=\"true\" image=\"" .. taglist_squares_unsel .. "\""
else
background = "resize=\"true\" image=\"@AWESOME_ICON_PATH@/taglist/squarew.png\""
end
end
if c.urgent then
bg_color = bg_urgent
fg_color = fg_urgent
break
end
end
end
if bg_color and fg_color then
text = " "..escape(t.name).." "
else
text = " "..escape(t.name).." "
end
return text
end
--- Return labels for a taglist widget with all *non empty* tags from screen.
-- It returns the tag name and set a special
-- foreground and background color for selected tags.
-- @param t The tag.
-- @param args The arguments table.
-- bg_focus The background color for selected tag.
-- fg_focus The foreground color for selected tag.
-- bg_urgent The background color for urgent tags.
-- fg_urgent The foreground color for urgent tags.
-- @return A string to print.
function widget.taglist.label.noempty(t, args)
if #t:clients() > 0 or t.selected then
if not args then args = {} end
local fg_focus = args.fg_focus or theme.taglist_fg_focus or theme.fg_focus
local bg_focus = args.bg_focus or theme.taglist_bg_focus or theme.bg_focus
local fg_urgent = args.fg_urgent or theme.taglist_fg_urgent or theme.fg_urgent
local bg_urgent = args.bg_urgent or theme.taglist_bg_urgent or theme.bg_urgent
local bg_color = nil
local fg_color = nil
local text
if t.selected then
bg_color = bg_focus
fg_color = fg_focus
end
if bg_urgent and fg_urgent then
for k, c in pairs(t:clients()) do
if c.urgent then
bg_color = bg_urgent
fg_color = fg_urgent
break
end
end
end
if fg_color and bg_color then
text = " " .. escape(t.name) .. " "
else
text = " " .. escape(t.name) .. " "
end
return text
end
end
local function widget_tasklist_label_common(c, args)
if not args then args = {} end
local fg_focus = args.fg_focus or theme.tasklist_fg_focus or theme.fg_focus
local bg_focus = args.bg_focus or theme.tasklist_bg_focus or theme.bg_focus
local fg_urgent = args.fg_urgent or theme.tasklist_fg_urgent or theme.fg_urgent
local bg_urgent = args.bg_urgent or theme.tasklist_bg_urgent or theme.bg_urgent
local text = ""
local name
if c.floating then
text = ""
end
if c.hidden then
name = escape(c.icon_name) or ""
else
name = escape(c.name) or ""
end
if capi.client.focus == c then
if bg_focus and fg_focus then
text = text .. " "..name.." "
else
text = text .. " "..name.." "
end
elseif c.urgent and bg_urgent and fg_urgent then
text = text .. " "..name.." "
else
text = text .. " "..name.." "
end
return text
end
--- Return labels for a tasklist widget with clients from all tags and screen.
-- It returns the client name and set a special
-- foreground and background color for focused client.
-- It also puts a special icon for floating windows.
-- @param c The client.
-- @param screen The screen we are drawing on.
-- @param args The arguments table.
-- bg_focus The background color for focused client.
-- fg_focus The foreground color for focused client.
-- bg_urgent The background color for urgent clients.
-- fg_urgent The foreground color for urgent clients.
-- @return A string to print.
function widget.tasklist.label.allscreen(c, screen, args)
return widget_tasklist_label_common(c, args)
end
--- Return labels for a tasklist widget with clients from all tags.
-- It returns the client name and set a special
-- foreground and background color for focused client.
-- It also puts a special icon for floating windows.
-- @param c The client.
-- @param screen The screen we are drawing on.
-- @param args The arguments table.
-- bg_focus The background color for focused client.
-- fg_focus The foreground color for focused client.
-- bg_urgent The background color for urgent clients.
-- fg_urgent The foreground color for urgent clients.
-- @return A string to print.
function widget.tasklist.label.alltags(c, screen, args)
-- Only print client on the same screen as this widget
if c.screen ~= screen then return end
return widget_tasklist_label_common(c, args)
end
--- Return labels for a tasklist widget with clients from currently selected tags.
-- It returns the client name and set a special
-- foreground and background color for focused client.
-- It also puts a special icon for floating windows.
-- @param c The client.
-- @param screen The screen we are drawing on.
-- @param args The arguments table.
-- bg_focus The background color for focused client.
-- fg_focus The foreground color for focused client.
-- bg_urgent The background color for urgent clients.
-- fg_urgent The foreground color for urgent clients.
-- @return A string to print.
function widget.tasklist.label.currenttags(c, screen, args)
-- Only print client on the same screen as this widget
if c.screen ~= screen then return end
for k, t in ipairs(capi.screen[screen]:tags()) do
if t.selected and c:tags()[t] then
return widget_tasklist_label_common(c, args)
end
end
end
--- Create a standard titlebar.
-- @param c The client.
-- @param args Arguments.
-- fg: the foreground color.
-- bg: the background color.
-- fg_focus: the foreground color for focused window.
-- fg_focus: the background color for focused window.
function titlebar.add(c, args)
if not c or c.type ~= "normal" then return end
if not args then args = {} end
-- Store colors
titlebar.data[c] = {}
titlebar.data[c].fg = args.fg or theme.titlebar_fg_normal or theme.fg_normal
titlebar.data[c].bg = args.bg or theme.titlebar_bg_normal or theme.bg_normal
titlebar.data[c].fg_focus = args.fg_focus or theme.titlebar_fg_focus or theme.fg_focus
titlebar.data[c].bg_focus = args.bg_focus or theme.titlebar_bg_focus or theme.bg_focus
-- Built args
local targs = {}
if args.fg then targs.fg = args.fg end
if args.bg then targs.bg = args.bg end
local tb = capi.titlebar(targs)
local title = capi.widget({ type = "textbox", name = "title", align = "flex" })
local bts =
{
capi.button({ }, 1, function (t) t.client:mouse_move() end),
capi.button({ args.modkey }, 3, function (t) t.client:mouse_resize() end)
}
title:buttons(bts)
if theme.titlebar_close_button == "true" then
local closef = widget.button({ name = "closef", align = "right",
image = theme.titlebar_close_button_focus
or theme.titlebar_close_button_img_focus
or "@AWESOME_ICON_PATH@/titlebar/closer.png" })
local close = widget.button({ name = "close", align = "right",
image = theme.titlebar_close_button_normal
or theme.titlebar_close_button_img_normal
or "@AWESOME_ICON_PATH@/titlebar/close.png" })
-- Bind kill button
local b = capi.button({ }, 1, nil, function (t) t.client:kill() end)
local bts = closef:buttons()
bts[#bts + 1] = b
closef:buttons(bts)
bts = close:buttons()
bts[#bts + 1] = b
close:buttons(bts)
tb:widgets({
capi.widget({ type = "appicon", name = "appicon", align = "left" }),
title,
closef, close
})
else
tb:widgets({
capi.widget({ type = "appicon", name = "appicon", align = "left" }),
title
})
end
c.titlebar = tb
titlebar.update(c)
end
--- Update a titlebar. This should be called in some hooks.
-- @param c The client to update.
function titlebar.update(c)
if c.titlebar and titlebar.data[c] then
local widgets = c.titlebar:widgets()
local title, close, closef
for k, v in ipairs(widgets) do
if v.name == "title" then title = v end
if v.name == "close" then close = v end
if v.name == "closef" then closef = v end
if title and close and closef then break end
end
if title then
title.text = " " .. escape(c.name) .. " "
end
if capi.client.focus == c then
c.titlebar.fg = titlebar.data[c].fg_focus
c.titlebar.bg = titlebar.data[c].bg_focus
if closef then closef.visible = true end
if close then close.visible = false end
else
c.titlebar.fg = titlebar.data[c].fg
c.titlebar.bg = titlebar.data[c].bg
if closef then closef.visible = false end
if close then close.visible = true end
end
end
end
--- Remove a titlebar from a client.
-- @param c The client.
function titlebar.remove(c)
c.titlebar = nil
titlebar.data[c] = nil
end
--- Set the beautiful theme if any.
-- @param The beautiful theme.
function beautiful.register(btheme)
if btheme then
theme = btheme
else
theme = {}
end
end
--- Create a button widget. When clicked, the image is deplaced to make it like
-- a real button.
-- @param args Standard widget table arguments, plus image for the image path.
-- @return A textbox widget configured as a button.
function widget.button(args)
if not args then return end
args.type = "textbox"
local w = capi.widget(args)
local img_release = ""
local img_press = ""
w.text = img_release
w:buttons({ capi.button({}, 1, function () w.text = img_press end, function () w.text = img_release end) })
function w.mouse_leave(s) w.text = img_release end
function w.mouse_enter(s) if capi.mouse.coords().buttons[1] then w.text = img_press end end
return w
end
--- Check if an area intersect another area.
-- @param a The area.
-- @param b The other area.
-- @return True if they intersect, false otherwise.
local function area_intersect_area(a, b)
return (b.x < a.x + a.width
and b.x + b.width > a.x
and b.y < a.y + a.height
and b.y + b.height > a.y)
end
--- Get the intersect area between a and b.
-- @param a The area.
-- @param b The other area.
-- @return The intersect area.
local function area_intersect_area_get(a, b)
local g = {}
g.x = math.max(a.x, b.x)
g.y = math.max(a.y, b.y)
g.width = math.min(a.x + a.width, b.x + b.width) - g.x
g.height = math.min(a.y + a.height, b.y + b.height) - g.y
return g
end
--- Remove an area from a list, splitting the space between several area that
-- can overlap.
-- @param areas Table of areas.
-- @param elem Area to remove.
-- @return The new area list.
local function area_remove(areas, elem)
local newareas = areas
for i, r in ipairs(areas) do
-- Check if the 'elem' intersect
if area_intersect_area(r, elem) then
-- It does? remove it
table.remove(areas, i)
local inter = area_intersect_area_get(r, elem)
if inter.x > r.x then
table.insert(newareas, {
x = r.x,
y = r.y,
width = inter.x - r.x,
height = r.height
})
end
if inter.y > r.y then
table.insert(newareas, {
x = r.x,
y = r.y,
width = r.width,
height = inter.y - r.y
})
end
if inter.x + inter.width < r.x + r.width then
table.insert(newareas, {
x = inter.x + inter.width,
y = r.y,
width = (r.x + r.width) - (inter.x + inter.width),
height = r.height
})
end
if inter.y + inter.height < r.y + r.height then
table.insert(newareas, {
x = r.x,
y = inter.y + inter.height,
width = r.width,
height = (r.y + r.height) - (inter.y + inter.height)
})
end
end
end
return newareas
end
--- Place the client without it being outside the screen.
-- @param c The client.
function placement.no_offscreen(c)
local geometry = c:fullcoords()
local screen_geometry = capi.screen[c.screen].workarea
if geometry.x + geometry.width > screen_geometry.x + screen_geometry.width then
geometry.x = screen_geometry.x + screen_geometry.width - geometry.width
elseif geometry.x < screen_geometry.x then
geometry.x = screen_geometry.x
end
if geometry.y + geometry.height > screen_geometry.y + screen_geometry.height then
geometry.y = screen_geometry.y + screen_geometry.height - geometry.height
elseif geometry.y < screen_geometry.y then
geometry.y = screen_geometry.y
end
c:fullcoords(geometry)
end
--- Place the client where there's place available with minimum overlap.
-- @param c The client.
function placement.no_overlap(c)
local cls = capi.client.visible_get(c.screen)
local layout = layout.get()
local areas = { capi.screen[c.screen].workarea }
local coords = c:coords()
local fullcoords = c:fullcoords()
for i, cl in pairs(cls) do
if cl ~= c and (cl.floating or layout == "floating") then
areas = area_remove(areas, cl:fullcoords())
end
end
-- Look for available space
local found = false
local new = { x = coords.x, y = coords.y, width = 0, height = 0 }
for i, r in ipairs(areas) do
if r.width >= fullcoords.width
and r.height >= fullcoords.height
and r.width * r.height > new.width * new.height then
found = true
new = r
end
end
-- We did not foudn an area with enough space for our size:
-- just take the biggest available one and go in
if not found then
for i, r in ipairs(areas) do
if r.width * r.height > new.width * new.height then
new = r
end
end
end
-- Restore height and width
new.width = coords.width
new.height = coords.height
c:coords(new)
end
--- Place the client under the mouse.
-- @param c The client.
function placement.under_mouse(c)
local c_coords = c:coords()
local m_coords = capi.mouse.coords()
c:coords({ x = m_coords.x - c_coords.width / 2,
y = m_coords.y - c_coords.height / 2 })
end
-- Register standards hooks
hooks.arrange.register(tag.history.update)
hooks.focus.register(client.focus.history.add)
hooks.unmanage.register(client.focus.history.delete)
hooks.unmanage.register(client_maximize_clean)
hooks.focus.register(titlebar.update)
hooks.unfocus.register(titlebar.update)
hooks.titleupdate.register(titlebar.update)
hooks.unmanage.register(titlebar.remove)
hooks.urgent.register(client.urgent.stack.add)
hooks.focus.register(client.urgent.stack.delete)
hooks.unmanage.register(client.urgent.stack.delete)
-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:encoding=utf-8:textwidth=80