+----------------------------------------------------------------------------
+-- @author Lucas de Vries <lucas@tuple-typed.org>
+-- @copyright 2009-2010 Lucas de Vries
+-- Licensed under the WTFPL
+----------------------------------------------------------------------------
+
+-- Load awful
+require("awful")
+
+-- Load beautiful
+require("beautiful")
+
+---- {{{ Grab environment
+local ipairs = ipairs
+local pairs = pairs
+local print = print
+local type = type
+local tonumber = tonumber
+local tostring = tostring
+local unpack = unpack
+local math = math
+local table = table
+local awful = awful
+local os = os
+local io = io
+local string = string
+local awful = awful
+local beautiful = beautiful
+
+-- Grab C API
+local capi =
+{
+ root = root,
+ awesome = awesome,
+ screen = screen,
+ client = client,
+ mouse = mouse,
+ button = button,
+ titlebar = titlebar,
+ widget = widget,
+ hooks = hooks,
+ keygrabber = keygrabber,
+ wibox = wibox,
+ widget = widget,
+}
+
+-- }}}
+
+--- Utilities for controlling the cursor
+module("rodentbane")
+
+-- Local data
+local bindings = {}
+local history = {}
+local current = nil
+local wiboxes = nil
+
+--- Create the wiboxes to display.
+function init()
+ -- Wiboxes table
+ wiboxes = {}
+
+ -- Borders
+ local borders = {"horiz", "vert", "left", "right", "top", "bottom"}
+
+ -- Create wibox for each border
+ for i, border in ipairs(borders) do
+ wiboxes[border] = capi.wibox({
+ position = "floating",
+ bg = beautiful.rodentbane_bg or beautiful.border_focus or "#C50B0B",
+ ontop = true,
+ })
+ end
+end
+
+--- Draw the guidelines on screen using wiboxes.
+-- @param area The area of the screen to draw on, defaults to current area.
+function draw(area)
+ -- Default to current area
+ local ar = area or current
+
+ -- Get numbers
+ local rwidth = beautiful.rodentbane_width or 2
+
+ -- Stop if the area is too small
+ if ar.width < rwidth*3 or ar.height < rwidth*3 then
+ stop()
+ return false
+ end
+
+ -- Put the wiboxes on the correct screen
+ for border, box in pairs(wiboxes) do
+ box.screen = ar.screen
+ end
+
+ -- Horizontal border
+ wiboxes.horiz:geometry({
+ x = ar.x+rwidth,
+ y = ar.y+math.floor(ar.height/2),
+ height = rwidth,
+ width = ar.width-(rwidth*2),
+ })
+
+ -- Vertical border
+ wiboxes.vert:geometry({
+ x = ar.x+math.floor(ar.width/2),
+ y = ar.y+rwidth,
+ width = rwidth,
+ height = ar.height-(rwidth*2),
+ })
+
+ -- Left border
+ wiboxes.left:geometry({
+ x = ar.x,
+ y = ar.y,
+ width = rwidth,
+ height = ar.height,
+ })
+
+ -- Right border
+ wiboxes.right:geometry({
+ x = ar.x+ar.width-rwidth,
+ y = ar.y,
+ width = rwidth,
+ height = ar.height,
+ })
+
+ -- Top border
+ wiboxes.top:geometry({
+ x = ar.x,
+ y = ar.y,
+ height = rwidth,
+ width = ar.width,
+ })
+
+ -- Bottom border
+ wiboxes.bottom:geometry({
+ x = ar.x,
+ y = ar.y+ar.height-rwidth,
+ height = rwidth,
+ width = ar.width,
+ })
+end
+
+--- Cut the navigation area into a direction.
+-- @param dir Direction to cut to {"up", "right", "down", "left"}.
+function cut(dir)
+ -- Store previous area
+ table.insert(history, 1, awful.util.table.join(current))
+
+ -- Cut in a direction
+ if dir == "up" then
+ current.height = math.floor(current.height/2)
+ elseif dir == "down" then
+ current.y = current.y+math.floor(current.height/2)
+ current.height = math.floor(current.height/2)
+ elseif dir == "left" then
+ current.width = math.floor(current.width/2)
+ elseif dir == "right" then
+ current.x = current.x+math.floor(current.width/2)
+ current.width = math.floor(current.width/2)
+ end
+
+ -- Redraw the box
+ draw()
+end
+
+--- Move the navigation area in a direction.
+-- @param dir Direction to move to {"up", "right", "down", "left"}.
+-- @param ratio Ratio of movement, multiplied by the size of the current area,
+-- defaults to 0.5 (ie. half the area size.
+function move(dir, ratio)
+ -- Store previous area
+ table.insert(history, 1, awful.util.table.join(current))
+
+ -- Default to ratio 0.5
+ local rt = ratio or 0.5
+
+ -- Move to a direction
+ if dir == "up" then
+ current.y = current.y-math.floor(current.height*rt)
+ elseif dir == "down" then
+ current.y = current.y+math.floor(current.height*rt)
+ elseif dir == "left" then
+ current.x = current.x-math.floor(current.width*rt)
+ elseif dir == "right" then
+ current.x = current.x+math.floor(current.width*rt)
+ end
+
+ -- Redraw the box
+ draw()
+end
+
+--- Bind a key in rodentbane mode.
+-- @param modkeys Modifier key combination to bind to.
+-- @param key Main key to bind to.
+-- @param func Function to bind the keys to.
+function bind(modkeys, key, func)
+ -- Create binding
+ local bind = {modkeys, key, func}
+
+ -- Add to bindings table
+ table.insert(bindings, bind)
+end
+
+--- Callback function for the keygrabber.
+-- @param modkeys Modkeys that were pressed.
+-- @param key Main key that was pressed.
+-- @param evtype Pressed or released event.
+function keyevent(modkeys, key, evtype)
+ -- Ignore release events and modifier keys
+ if evtype == "release"
+ or key == "Shift_L"
+ or key == "Shift_R"
+ or key == "Control_L"
+ or key == "Control_R"
+ or key == "Super_L"
+ or key == "Super_R"
+ or key == "Hyper_L"
+ or key == "Hyper_R"
+ or key == "Alt_L"
+ or key == "Alt_R"
+ or key == "Meta_L"
+ or key == "Meta_R"
+ then
+ return true
+ end
+
+ -- Special cases for printable characters
+ -- HACK: Maybe we need a keygrabber that gives keycodes ?
+ if key == " " then
+ key = "Space"
+ end
+
+ -- Figure out what to call
+ for ind, bind in ipairs(bindings) do
+ if bind[2]:lower() == key:lower()
+ and table_equals(bind[1], modkeys)
+ then
+ -- Call the function
+ if type(bind[3]) == "table" then
+ -- Allow for easy passing of arguments
+ local func = bind[3][1]
+ local args = {}
+
+ -- Add the rest of the arguments
+ for i, arg in ipairs(bind[3]) do
+ if i > 1 then
+ table.insert(args, arg)
+ end
+ end
+
+ -- Call function with args
+ func(unpack(args))
+ else
+ -- Call function directly
+ bind[3]()
+ end
+
+ -- A bind was found, continue grabbing
+ return true
+ end
+ end
+
+ -- No key was found, stop grabbing
+ stop()
+ return false
+end
+
+--- Check if two tables have the same values.
+-- @param t1 First table to check.
+-- @param t2 Second table to check.
+-- @return True if the tables are equivalent, false otherwise.
+function table_equals(t1, t2)
+ -- Check first table
+ for i, item in ipairs(t1) do
+ if awful.util.table.hasitem(t2, item) == nil then
+ -- An unequal item was found
+ return false
+ end
+ end
+
+ -- Check second table
+ for i, item in ipairs(t2) do
+ if awful.util.table.hasitem(t1, item) == nil then
+ -- An unequal item was found
+ return false
+ end
+ end
+
+ -- All items were equal
+ return true
+end
+
+--- Warp the mouse to the center of the navigation area
+function warp()
+ capi.mouse.coords({
+ x = current.x+(current.width/2),
+ y = current.y+(current.height/2),
+ })
+end
+
+--- Click with a button
+-- @param button Button number to click with, defaults to left (1)
+function click(button)
+ -- Default to left click
+ local b = button or 1
+
+ -- TODO: Figure out a way to use fake_input for clicks
+ --capi.root.fake_input("button_press", button)
+ --capi.root.fake_input("button_release", button)
+
+ -- Use xdotool when available, otherwise try xte
+ command = "xdotool click "..b.." &> /dev/null"
+ .." || xte 'mouseclick "..b.."' &> /dev/null"
+ .." || echo 'W: rodentbane: either xdotool or xte"
+ .." is required to emulate mouse clicks, neither was found.'"
+
+ awful.util.spawn_with_shell(command)
+end
+
+--- Undo a change to the area
+function undo()
+ -- Restore area
+ if #history > 0 then
+ current = history[1]
+ table.remove(history, 1)
+
+ draw()
+ end
+end
+
+--- Convenience function to bind to default keys.
+function binddefault()
+ -- Cut with hjkl
+ bind({}, "h", {cut, "left"})
+ bind({}, "j", {cut, "down"})
+ bind({}, "k", {cut, "up"})
+ bind({}, "l", {cut, "right"})
+
+ -- Move with Shift+hjkl
+ bind({"Shift"}, "h", {move, "left"})
+ bind({"Shift"}, "j", {move, "down"})
+ bind({"Shift"}, "k", {move, "up"})
+ bind({"Shift"}, "l", {move, "right"})
+
+ -- Undo with u
+ bind({}, "u", undo)
+
+ -- Left click with space
+ bind({}, "Space", function ()
+ warp()
+ click()
+ stop()
+ end)
+
+ -- Double Left click with alt+space
+ bind({"Mod1"}, "Space", function ()
+ warp()
+ click()
+ click()
+ stop()
+ end)
+
+ -- Middle click with Control+space
+ bind({"Control"}, "Space", function ()
+ warp()
+ click(2)
+ stop()
+ end)
+
+ -- Right click with shift+space
+ bind({"Shift"}, "Space", function ()
+ warp()
+ click(3)
+ stop()
+ end)
+
+ -- Only warp with return
+ bind({}, "Return", function ()
+ warp()
+ end)
+end
+
+--- Start the navigation sequence.
+-- @param screen Screen to start navigation on, defaults to current screen.
+-- @param recall Whether the previous area should be recalled (defaults to
+-- false).
+function start(screen, recall)
+ -- Default to current screen
+ local scr = screen or capi.mouse.screen
+
+ -- Initialise if not already done
+ if wiboxes == nil then
+ -- Add default bindings if we have none ourselves
+ if #bindings == 0 then
+ binddefault()
+ end
+
+ -- Create the wiboxes
+ init()
+ end
+
+ -- Empty current area if needed
+ if not recall then
+ -- Start with a complete area
+ current = capi.screen[scr].workarea
+
+ -- Empty history
+ history = {}
+ end
+
+ -- Move to the right screen
+ current.screen = scr
+
+ -- Start the keygrabber
+ capi.keygrabber.run(keyevent)
+
+ -- Draw the box
+ draw()
+end
+
+--- Stop the navigation sequence without doing anything.
+function stop()
+ -- Stop the keygrabber
+ capi.keygrabber.stop()
+
+ -- Hide all wiboxes
+ for border, box in pairs(wiboxes) do
+ box.screen = nil
+ end
+end