├── mouse.lua ├── layout.lua ├── util.lua ├── init.lua ├── README.md ├── resize.lua ├── max.lua ├── focus.lua └── screen.lua /mouse.lua: -------------------------------------------------------------------------------- 1 | local capi = {screen=screen,client=client,mouse=mouse, keygrabber = keygrabber} 2 | local math,table = math,table 3 | local wibox = require( "wibox" ) 4 | local awful = require( "awful" ) 5 | local cairo = require( "lgi" ).cairo 6 | local color = require( "gears.color" ) 7 | local beautiful = require( "beautiful" ) 8 | local surface = require( "gears.surface" ) 9 | local col_utils = require( "collision.util" ) 10 | 11 | local module = {} 12 | 13 | local w = nil 14 | 15 | function module.highlight() 16 | if not w then 17 | w = wibox{} 18 | w.height = 100 19 | w.width = 100 20 | w.ontop = true 21 | 22 | local img = cairo.ImageSurface(cairo.Format.A8, 100,100) 23 | local cr = cairo.Context(img) 24 | -- cr:set_operator(cairo.Operator.CLEAR) 25 | cr:set_source_rgba(0,0,0,0) 26 | cr:paint() 27 | cr:set_operator(cairo.Operator.SOURCE) 28 | cr:set_source_rgba(1,1,1,1) 29 | 30 | cr:save() 31 | cr:translate(0,-30) 32 | cr:move_to(50,50) 33 | cr:rotate(math.pi) 34 | col_utils.arrow_path(cr,40,10) 35 | cr:fill() 36 | cr:restore() 37 | 38 | cr:save() 39 | cr:translate(-30,0) 40 | cr:move_to(50,50) 41 | cr:rotate(math.pi/2) 42 | col_utils.arrow_path(cr,40,10) 43 | cr:fill() 44 | cr:restore() 45 | 46 | cr:save() 47 | cr:translate(30,0) 48 | cr:move_to(50,50) 49 | cr:rotate(-math.pi/2) 50 | col_utils.arrow_path(cr,40,10) 51 | cr:fill() 52 | cr:restore() 53 | 54 | cr:save() 55 | cr:translate(0,30) 56 | cr:move_to(50,50) 57 | col_utils.arrow_path(cr,40,10) 58 | cr:fill() 59 | cr:restore() 60 | 61 | cr:arc(50,50,50-3,0,2*math.pi) 62 | cr:set_line_width(5) 63 | cr:stroke() 64 | 65 | cr:set_source_rgba(1,0,0,1) 66 | w.shape_bounding = img._native 67 | 68 | 69 | img = cairo.ImageSurface(cairo.Format.ARGB32, 100,100) 70 | cr = cairo.Context(img) 71 | cr:set_source(color(beautiful.bg_urgent)) 72 | cr:paint() 73 | cr:set_source(color(beautiful.fg_normal)) 74 | cr:arc(50,50,50-3,0,2*math.pi) 75 | cr:set_line_width(5) 76 | cr:stroke() 77 | w:set_bg(cairo.Pattern.create_for_surface(img)) 78 | end 79 | w.x = capi.mouse.coords().x -50 80 | w.y = capi.mouse.coords().y -50 81 | w.visible = true 82 | end 83 | 84 | function module.hide() 85 | if w then 86 | w.visible = false 87 | end 88 | end 89 | 90 | return module 91 | -------------------------------------------------------------------------------- /layout.lua: -------------------------------------------------------------------------------- 1 | -- This helper module help retro-generate the clients layout from awful 2 | -- this is a giant hack and doesn't even always work and require upstream 3 | -- patches 4 | 5 | local setmetatable = setmetatable 6 | local ipairs,math = ipairs,math 7 | local awful = require("awful") 8 | local beautiful = require("beautiful") 9 | local color = require( "gears.color") 10 | local util = require( "collision.util" ) 11 | local shape = require( "gears.shape" ) 12 | local capi = { screen = screen, client=client } 13 | 14 | local module = {} 15 | local margin = 2 16 | local radius = 4 17 | 18 | -- Emulate a client using meta table magic 19 | local function gen_cls(c,results) 20 | local ret = setmetatable({},{__index = function(t,i) 21 | local ret2 = c[i] 22 | if type(ret2) == "function" then 23 | if i == "geometry" then 24 | return function(self,...) 25 | if #{...} > 0 then 26 | local geom = ({...})[1] 27 | -- Make a copy as the original will be changed 28 | results[c] = awful.util.table.join(({...})[1],{}) 29 | return geom 30 | end 31 | return c:geometry() 32 | end 33 | else 34 | return function(self,...) return ret2(c,...) end 35 | end 36 | end 37 | return ret2 38 | end}) 39 | return ret 40 | end 41 | 42 | function module.get_geometry(tag) 43 | local cls,results,flt = {},setmetatable({},{__mode="k"}),{} 44 | local s = tag.screen 45 | local l = tag.layout 46 | local focus,focus_wrap = capi.client.focus,nil 47 | for k,c in ipairs (tag:clients()) do 48 | -- Handle floating client separately 49 | if not c.minimized then 50 | local floating = c.floating 51 | if (not floating) and (not l == awful.layout.suit.floating) then 52 | cls[#cls+1] = gen_cls(c,results) 53 | if c == focus then 54 | focus_wrap = cls[#cls] 55 | end 56 | else 57 | flt[#flt+1] = c:geometry() 58 | end 59 | end 60 | end 61 | 62 | -- The magnifier layout require a focussed client 63 | -- there wont be any as that layout is not selected 64 | -- take one at random or (TODO) use stack data 65 | if not focus_wrap then 66 | focus_wrap = cls[1] 67 | end 68 | 69 | local param = { 70 | tag = tag, 71 | screen = 1, 72 | clients = cls, 73 | focus = focus_wrap, 74 | workarea = capi.screen[s or 1].workarea 75 | } 76 | 77 | l.arrange(param) 78 | 79 | return results,flt 80 | end 81 | 82 | function module.draw(tag,cr,width,height) 83 | local worked = false 84 | local l,l2 = module.get_geometry(tag) 85 | local s = tag.screen 86 | local scr_geo = capi.screen[s or 1].workarea 87 | local ratio = height/scr_geo.height 88 | local w_stretch = width/(scr_geo.width*ratio) 89 | local r,g,b = util.get_rgb( 90 | beautiful.collision_max_fg or beautiful.fg_normal 91 | ) 92 | 93 | local lshape = beautiful.collision_layout_shape or shape.rounded_rect 94 | 95 | cr:set_line_width(3) 96 | for c,ll in ipairs({l,l2}) do 97 | for c,geom in pairs(ll) do 98 | shape.transform(lshape) 99 | : translate(geom.x*ratio*w_stretch+margin, geom.y*ratio+margin) ( 100 | cr, 101 | geom.width*ratio*w_stretch-margin*2, 102 | geom.height*ratio-margin*2, 103 | radius 104 | ) 105 | cr:close_path() 106 | cr:set_source_rgba(r,g,b,0.7) 107 | cr:stroke_preserve() 108 | cr:set_source_rgba(r,g,b,0.2) 109 | cr:fill() 110 | 111 | -- Draw an icon in the region 112 | --TODO 113 | 114 | worked = true 115 | end 116 | end 117 | --TODO floating clients 118 | return worked 119 | end 120 | 121 | return module 122 | -- kate: space-indent on; indent-width 2; replace-tabs on; 123 | -------------------------------------------------------------------------------- /util.lua: -------------------------------------------------------------------------------- 1 | local capi = {screen = screen} 2 | local math = math 3 | local color = require( "gears.color" ) 4 | local beautiful = require( "beautiful" ) 5 | local glib = require("lgi").GLib 6 | local cairo = require( "lgi" ).cairo 7 | 8 | local module = {settings={}} 9 | 10 | 11 | local rr,rg,rb 12 | function module.get_rgb(col) 13 | if not rr then 14 | local pat = color(col or beautiful.fg_normal) 15 | local s,r,g,b,a = pat:get_rgba() 16 | rr,rg,rb = r,g,b 17 | end 18 | return rr,rg,rb 19 | end 20 | 21 | --- Draw an arrow path. The current context position is the center of the arrow 22 | -- By default, the arrow is pointing up, use context rotation to get other directions 23 | -- 24 | -- To get an arrow pointing down: 25 | -- @usage cr:move_to(width/2, height/2) 26 | -- cr:rotate(math.pi) 27 | -- 28 | -- 29 | function module.arrow_path(cr, width, sidesize) 30 | cr:rel_move_to( 0 , -width/2 ) 31 | cr:rel_line_to( width/2 , width/2 ) 32 | cr:rel_line_to( -sidesize , 0 ) 33 | cr:rel_line_to( 0 , width/2 ) 34 | cr:rel_line_to( (-width)+2*sidesize , 0 ) 35 | cr:rel_line_to( 0 , -width/2 ) 36 | cr:rel_line_to( -sidesize , 0 ) 37 | cr:rel_line_to( width/2 , -width/2 ) 38 | cr:close_path() 39 | end 40 | 41 | function module.arrow_path2(cr, width, height, head_width, shaft_width, shaft_length) 42 | local shaft_length = shaft_length or height / 2 43 | local shaft_width = shaft_width or width / 2 44 | local head_width = head_width or width 45 | local head_length = height - shaft_length 46 | 47 | cr:move_to( width/2 , 0 ) 48 | cr:rel_line_to( head_width/2 , head_length ) 49 | cr:rel_line_to( -(head_width-shaft_width)/2 , 0 ) 50 | cr:rel_line_to( 0 , shaft_length ) 51 | cr:rel_line_to( -shaft_width , 0 ) 52 | cr:rel_line_to( 0 , -shaft_length ) 53 | cr:rel_line_to( -(head_width-shaft_width)/2 , 0 ) 54 | cr:close_path() 55 | end 56 | 57 | function module.arrow(width, sidesize, margin, bg_color, fg_color) 58 | local img = cairo.ImageSurface(cairo.Format.ARGB32, width+2*margin, width+2*margin) 59 | local cr = cairo.Context(img) 60 | cr:set_source(color(bg_color)) 61 | cr:paint() 62 | cr:set_source(color(fg_color)) 63 | cr:set_antialias(cairo.Antialias.NONE) 64 | cr:move_to(margin+width/2, margin+width/2) 65 | module.arrow_path(cr, width, sidesize) 66 | cr:fill() 67 | return cairo.Pattern.create_for_surface(img) 68 | end 69 | 70 | function module.draw_round_rect(cr,x,y,w,h,radius) 71 | cr:save() 72 | cr:translate(x,y) 73 | cr:move_to(0,radius) 74 | cr:arc(radius,radius,radius,math.pi,3*(math.pi/2)) 75 | cr:arc(w-radius,radius,radius,3*(math.pi/2),math.pi*2) 76 | cr:arc(w-radius,h-radius,radius,math.pi*2,math.pi/2) 77 | cr:arc(radius,h-radius,radius,math.pi/2,math.pi) 78 | cr:close_path() 79 | cr:restore() 80 | end 81 | 82 | local function refresh_dt(last_sec,last_usec,callback,delay) 83 | local tv = glib.TimeVal() 84 | glib.get_current_time(tv) 85 | local dt = (tv.tv_sec*1000000+tv.tv_usec)-(last_sec*1000000+last_usec) 86 | if dt < delay then 87 | callback() 88 | end 89 | return tv.tv_sec,tv.tv_usec 90 | end 91 | 92 | function module.double_click(callback,delay) 93 | delay = delay or 250000 94 | local ds,du = 0,0 95 | return function() 96 | ds,du = refresh_dt(ds,du,callback,delay) 97 | end 98 | end 99 | 100 | -- Screen order is not always geometrical, sort them 101 | local screens,screens_inv 102 | function module.get_ordered_screens() 103 | if screens then return screens,screens_inv end 104 | 105 | screens = {} 106 | for i=1,capi.screen.count() do 107 | local geom = capi.screen[i].geometry 108 | if #screens == 0 then 109 | screens[1] = capi.screen[i] 110 | elseif geom.x < capi.screen[screens[1]].geometry.x then 111 | table.insert(screens,1,capi.screen[i]) 112 | else 113 | for j=#screens,1,-1 do 114 | if geom.x > capi.screen[screens[j]].geometry.x then 115 | table.insert(screens,j+1,capi.screen[i]) 116 | break 117 | end 118 | end 119 | end 120 | end 121 | 122 | screens_inv = {} 123 | for k,v in ipairs(screens) do 124 | screens_inv[v] = k 125 | end 126 | 127 | return screens,screens_inv 128 | end 129 | 130 | capi.screen.connect_signal("added", function() 131 | screens,screens_inv = nil, nil 132 | end) 133 | capi.screen.connect_signal("removed", function() 134 | screens,screens_inv = nil, nil 135 | end) 136 | 137 | --- Setup the whole thing and call fct(cr, width, height) then apply the shape 138 | -- fct should not set the source or color 139 | function module.apply_shape_bounding(c_or_w, fct) 140 | local geo = c_or_w:geometry() 141 | 142 | local img = cairo.ImageSurface(cairo.Format.A1, geo.width, geo.height) 143 | local cr = cairo.Context(img) 144 | 145 | cr:set_source_rgba(0,0,0,0) 146 | cr:paint() 147 | cr:set_operator(cairo.Operator.SOURCE) 148 | cr:set_source_rgba(1,1,1,1) 149 | 150 | fct(cr, geo.width, geo.height) 151 | 152 | cr:fill() 153 | 154 | c_or_w.shape_bounding = img._native 155 | end 156 | 157 | return module 158 | -- kate: space-indent on; indent-width 2; replace-tabs on; 159 | -------------------------------------------------------------------------------- /init.lua: -------------------------------------------------------------------------------- 1 | local capi = {root=root,client=client,mouse=mouse, timer=timer, 2 | screen = screen, keygrabber = keygrabber} 3 | local util = require( "awful.util" ) 4 | local awful = require( "awful" ) 5 | local glib = require( "lgi" ).GLib 6 | local col_utils = require( "collision.util" ) 7 | local unpack = unpack or table.unpack -- luacheck: globals unpack (compatibility with Lua 5.1) 8 | local module = { 9 | _focus = require( "collision.focus" ), 10 | _resize = require( "collision.resize"), 11 | _max = require( "collision.max" ), 12 | _screen = require( "collision.screen"), 13 | mouse = require( "collision.mouse" ), 14 | settings= col_utils.settings , 15 | util = col_utils , 16 | } 17 | 18 | local current_mode = "focus" 19 | 20 | local event_callback = { 21 | focus = module._focus._global_bydirection_key, 22 | move = module._focus._global_bydirection_key, 23 | resize = module._resize.resize , 24 | max = module._max.change_focus , 25 | tag = module._max.change_tag , 26 | screen = module._screen.reload , 27 | } 28 | 29 | local start_callback = { 30 | focus = module._focus.display , 31 | move = module._focus.display , 32 | resize = module._resize.display , 33 | max = module._max.display_clients, 34 | tag = module._max.display_tags , 35 | screen = module._screen.display , 36 | } 37 | 38 | local exit_callback = { 39 | focus = module._focus._quit , 40 | move = module._focus._quit , 41 | resize = module._resize.hide , 42 | max = module._max.hide , 43 | tag = module._max.hide , 44 | screen = module._screen.hide , 45 | } 46 | 47 | local keys = {--Normal Xephyr G510 alt G510 48 | up = {"Up" --[[, "&" , "XF86AudioPause" , "F15"]] }, 49 | down = {"Down" --[[, "KP_Enter" , "XF86WebCam" , "F14"]] }, 50 | left = {"Left" --[[, "#" , "Cancel" , "F13"]] }, 51 | right = {"Right" --[[, "\"" , "XF86Paste" , "F17"]] }, 52 | } 53 | 54 | local function exit_loop() 55 | exit_callback[current_mode]() 56 | capi.keygrabber.stop() 57 | return false 58 | end 59 | 60 | -- Event loop 61 | local function start_loop(is_swap,is_max) 62 | capi.keygrabber.run(function(mod, key, event) 63 | -- Detect the direction 64 | for k,v in pairs(keys) do 65 | if util.table.hasitem(v,key) then 66 | if event == "press" then 67 | if not event_callback[current_mode](mod,key,event,k,is_swap,is_max) then 68 | return exit_loop() 69 | end 70 | return 71 | end 72 | return #mod > 0 73 | end 74 | end 75 | 76 | if key == "Shift_L" or key == "Shift_R" then 77 | is_swap = event == "press" 78 | return #mod > 0 79 | elseif key == "Control_L" or key == "Control_R" then 80 | is_max = event == "press" 81 | return #mod > 0 and awful.util.table.hasitem(mod,"Mod4") or exit_loop() 82 | elseif key == "Alt_L" or key == "Alt_R" then 83 | exit_callback[current_mode]() 84 | current_mode = event == "press" and "resize" or "focus" 85 | start_callback[current_mode](mod,key,event,k,is_swap,is_max) 86 | return #mod > 0 and awful.util.table.hasitem(mod,"Mod4") or exit_loop() 87 | end 88 | 89 | return exit_loop() 90 | end) 91 | end 92 | 93 | function module.focus(direction,c,max) 94 | local screen = (c or ((capi.client.focus and capi.client.focus.focusable) and capi.client.focus or capi.mouse)).screen 95 | -- Useless when there is only 1 client tiled, incompatible with the "max_out" mode (in this case, focus floating mode) 96 | if awful.layout.get(screen) == awful.layout.suit.max and #awful.client.tiled(screen) > 1 and not max then 97 | current_mode = "max" 98 | module._max.display_clients(screen,direction) 99 | else 100 | current_mode = "focus" 101 | module._focus.global_bydirection(direction,c,false,max) 102 | end 103 | start_loop(false,max) 104 | end 105 | 106 | function module.move(direction,c,max) 107 | current_mode = "move" 108 | module._focus.global_bydirection(direction,c,true,max) 109 | start_loop(true,max) 110 | end 111 | 112 | function module.resize(direction,c,max) 113 | current_mode = "resize" 114 | start_loop(false,max) 115 | module._resize.display(c) 116 | end 117 | 118 | function module.tag(direction,swap,max) 119 | current_mode = "tag" 120 | local c = capi.client.focus 121 | module._max.display_tags((c) and c.screen or capi.mouse.screen,direction,c,swap,max) 122 | start_loop(swap,max) 123 | end 124 | 125 | function module.screen(direction, move) 126 | current_mode = "screen" 127 | module._screen.display(nil,direction,move) 128 | start_loop(false,max) 129 | end 130 | 131 | function module.select_screen(idx) 132 | if idx and idx > 0 and idx <= capi.screen.count() then 133 | module._screen.select_screen(idx) 134 | end 135 | end 136 | 137 | function module.highlight_cursor(timeout) 138 | module.mouse.highlight() 139 | if timer then 140 | local timer = capi.timer({ timeout = timeout }) -- 30 mins 141 | timer:connect_signal("timeout", function() 142 | module.mouse.hide() 143 | timer:stop() 144 | end) 145 | timer:start() 146 | end 147 | end 148 | 149 | local function new(k) 150 | -- Replace the keys array. The new one has to have a valid mapping 151 | keys = k or keys 152 | local aw = {} 153 | 154 | -- This have to be executer after rc.lua 155 | glib.idle_add(glib.PRIORITY_DEFAULT_IDLE, function() 156 | for k,v in pairs(keys) do 157 | for _,key_name in ipairs(v) do 158 | aw[#aw+1] = awful.key({ "Mod4", }, key_name, function () module.focus (k ) end, 159 | { description = "Change focus to the "..key_name, group = "Collision" }) 160 | aw[#aw+1] = awful.key({ "Mod4", "Mod1" }, key_name, function () module.resize(k ) end, 161 | { description = "Resize to the "..key_name, group = "Collision" }) 162 | aw[#aw+1] = awful.key({ "Mod4", "Shift" }, key_name, function () module.move (k ) end, 163 | { description = "Move to the "..key_name, group = "Collision" }) 164 | aw[#aw+1] = awful.key({ "Mod4", "Shift", "Control" }, key_name, function () module.move (k,nil ,true) end, 165 | { description = "", group = "Collision" }) 166 | aw[#aw+1] = awful.key({ "Mod4", "Control" }, key_name, function () module.focus (k,nil ,true) end, 167 | { description = "Change floating focus to the "..key_name, group = "Collision" }) 168 | aw[#aw+1] = awful.key({ "Mod4", "Mod1" , "Control" }, key_name, function () module.screen(k ) end, 169 | { description = "Change screen to the "..key_name, group = "Collision" }) 170 | aw[#aw+1] = awful.key({ "Mod1", "Shift", "Control", "Mod4" }, key_name, function () module.screen(k,true ) end, 171 | { description = "Move tag screen to the "..key_name, group = "Collision" }) 172 | if k == "left" or k =="right" then -- Conflict with my text editor, so I say no 173 | aw[#aw+1] = awful.key({ "Mod1", "Control" }, key_name, function () module.tag (k,nil ,true) end, 174 | { description = "Select tag to the "..key_name, group = "Collision" }) 175 | aw[#aw+1] = awful.key({ "Mod1", "Shift", "Control" }, key_name, function () module.tag (k,true,true) end, 176 | { description = "Move tag to the "..key_name, group = "Collision" }) 177 | end 178 | end 179 | end 180 | capi.root.keys(awful.util.table.join(capi.root.keys(),unpack(aw))) 181 | end) 182 | 183 | return module 184 | end 185 | 186 | return setmetatable(module, { __call = function(_, ...) return new(...) end }) 187 | -- kate: space-indent on; indent-width 2; replace-tabs on; 188 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Collision window navigation module for AwesomeWM 2 | ================================================ 3 | 4 | **Warning:** The master branch is for Awesome 4.0+, use the `awesome3.5` 5 | branch for older versions of Awesome. 6 | 7 | This module add some visual indicators for common window management operations. 8 | It is now easier to know the impact of a given command as a visual queue will 9 | be printed on the screen. Collision has 3 modes: 10 | 11 | * **Focus**: Move the focus from client to client 12 | * **Move**: Move a client 13 | * **Resize**: Change a client dimensions 14 | * **Tag**: Move to the previous/next tag 15 | 16 | # Installation 17 | 18 | First, clone the repository 19 | 20 | ```lua 21 | mkdir -p ~/.config/awesome 22 | cd ~/.config/awesome 23 | git clone https://github.com/Elv13/collision 24 | ``` 25 | 26 | Now, open ~/.config/awesome/rc.lua (or copy /etc/xdg/awesome/rc.lua to 27 | ~/.config/awesome/rc.lua fist if you never modified your Awesome config before) 28 | and add this line somewhere in your `rc.lua`: 29 | 30 | ```lua 31 | require("collision")() 32 | ``` 33 | 34 | It is a very good idea to also change the default Mod4+arrow shortcut to 35 | something else: 36 | 37 | ```lua 38 | --Remove those lines: 39 | awful.key({ modkey, }, "Left", awful.tag.viewprev, 40 | {description = "view previous", group = "tag"}), 41 | awful.key({ modkey, }, "Right", awful.tag.viewnext, 42 | {description = "view next", group = "tag"}), 43 | ``` 44 | 45 | Your done! 46 | 47 | # Usage 48 | 49 | Using Collision is easy. You just have to hit the arrow keys ( ) 50 | with some modifiers keys. The Shift key is usually used for grabbing something 51 | while the Control key is used to max out the effect. 52 | 53 | | Modifier 1 | Modifier 2 | Modifier 3 | Effect | 54 | | :----------: | :----------: | :----------: | ------------------------------------------------------- | 55 | | Mod4 | | | Move the focus on the tiled layer | 56 | | Mod4 | | Control | Move the focus on the floating layer | 57 | | Mod4 | Shift | | Move a client in the tiled or floating layer | 58 | | Mod4 | Shift | Control | Move a floating client to the far side of that screen | 59 | | Mod4 | Mod1 (Alt) | | Increase a floating client size | 60 | | Mod4 | Mod1 (Alt) | Shift | Reduce a floating client size | 61 | | Control | Mod1 (Alt) | | Move to the next/previous tag | 62 | | Control | Mod4 | Mod1 (Alt) | Move to the next/previous screen | 63 | | Control | Mod4 | Mod1 (Alt) | + Shift Move tag to the next/previous screen | 64 | 65 | # Using different keys 66 | 67 | Due to the large ammount of keyboard shortcut Collision create, they are 68 | auto-generated automatically. While this make installation simpler, it also 69 | make Collision somewhat hard-coded magic. Some alternative keymaps can also 70 | be ackward to use because of the reliance on mod keys such as Alt and Control. 71 | 72 | That being said, Collision allow some basic remapping. Instead of: 73 | 74 | ```lua 75 | require("collision")() 76 | ``` 77 | 78 | This can be used: 79 | 80 | ```lua 81 | require("collision") { 82 | -- Normal Xephyr Vim G510 83 | up = { "Up" , "&" , "k" , "F15" }, 84 | down = { "Down" , "KP_Enter" , "j" , "F14" }, 85 | left = { "Left" , "#" , "h" , "F13" }, 86 | right = { "Right" , "\"" , "l" , "F17" }, 87 | } 88 | ``` 89 | 90 | Of course, if the `Vim` keys are used, any other shortcut binded to them have to 91 | be removed from rc.lua. 92 | 93 | # Appearance 94 | 95 | Collision appearance can be changed in yout theme using the `collision` 96 | namespace 97 | 98 | ## Focus 99 | 100 | | Variable | Description | 101 | | :---------------------------------: | ------------------------------------------------------ | 102 | | collision_resize_width | The size of the resize handles | 103 | | collision_resize_shape | The gears.shape used for the resize handle | 104 | | collision_resize_border_width | The resize handles border width | 105 | | collision_resize_border_color | The resize handles border colors | 106 | | collision_resize_padding | The resize handles padding between the boder and arrow | 107 | | collision_resize_bg | The resize handles background color | 108 | | collision_resize_fg | The resize handles arrow color | 109 | | collision_resize_arrow_border_color | The arrow border color | 110 | | collision_resize_arrow_border_width | The arrow border width | 111 | | collision_resize_size | Enable the size widget when resizng | 112 | | collision_resize_shape | | 113 | | collision_resize_size_border_width | | 114 | | collision_resize_size_border_color | | 115 | | collision_resize_size_font | | 116 | | collision_resize_size_shape | | 117 | | collision_resize_size_bg | | 118 | | collision_resize_size_fg | | 119 | | collision_focus_shape | The outer shape of the "arrow" widgets | 120 | | collision_focus_border_width | | 121 | | collision_focus_border_color | | 122 | | collision_focus_padding | | 123 | | collision_focus_bg | The background of the focus change arrow | 124 | | collision_focus_fg | The foregroung filling color of the arrow | 125 | | collision_focus_bg_center | The focussed client circle background | 126 | | collision_focus_shape_center | The focused client widget shape (default: circle) | 127 | | collision_focus_arrow_border_color | The color of the arrow border | 128 | | collision_focus_arrow_border_width | The width of the arrow border | 129 | | collision_screen_shape | | 130 | | collision_screen_border_width | | 131 | | collision_screen_border_color | | 132 | | collision_screen_padding | | 133 | | collision_screen_bg | | 134 | | collision_screen_fg | | 135 | | collision_screen_bg_focus | | 136 | | collision_screen_fg_focus | | 137 | 138 | # Settings 139 | 140 | ```lua 141 | -- Swap clients across screen instead of adding them to the other tag 142 | collision.settings.swap_across_screen = true 143 | ``` 144 | 145 | # Other 146 | 147 | The `collision.select_screen(idx)` function allow to select a screen and can be 148 | assigned to shortcuts. 149 | 150 | The `collision.highlight_cursor(timeout)` method will highlight the current mouse 151 | cursor position. 152 | 153 | Use `collision.mouse.highlight()` and `collision.mouse.hide()` 154 | 155 | # Notes 156 | 157 | Using the focus arrows to select empty screens only work in Awesome 3.5.7+ 158 | -------------------------------------------------------------------------------- /resize.lua: -------------------------------------------------------------------------------- 1 | local capi = { client = client } 2 | local wibox = require( "wibox" ) 3 | local beautiful = require( "beautiful" ) 4 | local awful = require( "awful" ) 5 | local surface = require( "gears.surface" ) 6 | local shape = require( "gears.shape" ) 7 | 8 | local module, indicators, cur_c, sizeboxes = {},nil,nil, {} 9 | 10 | local values = {"top" , "top_right" , "right" , "bottom_right" , 11 | "bottom" , "bottom_left", "left" , "top_left" } 12 | 13 | local invert = { 14 | left = "right", 15 | right = "left" , 16 | up = "down" , 17 | down = "up" , 18 | } 19 | 20 | local r_ajust = { 21 | left = function(c, d) return { x = c.x - d, width = c.width + d } end, 22 | right = function(c, d) return { width = c.width + d, } end, 23 | up = function(c, d) return { y = c.y - d, height = c.height + d } end, 24 | down = function(c, d) return { height = c.height + d, } end, 25 | } 26 | 27 | -- Resize tiled using the keyboard 28 | local layouts_all = { 29 | [awful.layout.suit.floating] = { right = "" }, 30 | [awful.layout.suit.tile] = { right = {mwfact= 0.05}, left = {mwfact=-0.05}, up ={wfact=-0.1 }, down = {wfact = 0.1 } }, 31 | [awful.layout.suit.tile.left] = { right = {mwfact=-0.05}, left = {mwfact= 0.05}, up ={wfact= 0.1 }, down = {wfact =-0.1 } }, 32 | [awful.layout.suit.tile.bottom] = { right = {wfact=-0.1 }, left = {wfact= 0.1 }, up ={mwfact=-0.05}, down = {mwfact= 0.05} }, 33 | [awful.layout.suit.tile.top] = { right = {wfact=-0.1 }, left = {wfact= 0.1 }, up ={mwfact= 0.05}, down = {mwfact=-0.05} }, 34 | [awful.layout.suit.spiral] = { right = {wfact=-0.1 }, left = {wfact= 0.1 }, up ={mwfact= 0.05}, down = {mwfact=-0.05} }, 35 | [awful.layout.suit.magnifier] = { right = {mwfact= 0.05}, left = {mwfact=-0.05}, up ={mwfact= 0.05}, down = {mwfact=-0.05} }, 36 | -- The other layouts cannot be resized using variables 37 | } 38 | 39 | local function update_size_boxes(c, float_only) 40 | if not awful.popup then return end 41 | if not beautiful.collision_resize_size then return end 42 | 43 | local clients = c and {c} or awful.client.visible() 44 | 45 | if c and float_only then 46 | for c2, wb in pairs(sizeboxes) do 47 | if c2 ~= c and not c2.floating then 48 | wb.visible = false 49 | end 50 | end 51 | end 52 | 53 | for _, c in ipairs(clients) do 54 | if (not float_only) or c.floating then 55 | local wb = sizeboxes[c] 56 | 57 | if not wb then 58 | sizeboxes[c] = awful.popup { 59 | widget = { 60 | { 61 | id = "tb", 62 | text = "0x0", 63 | font = beautiful.collision_resize_size_font, 64 | widget = wibox.widget.textbox 65 | }, 66 | margins = beautiful.collision_resize_size_padding or 4, 67 | widget = wibox.container.margin 68 | }, 69 | visible = false, 70 | ontop = true, 71 | bg = beautiful.collision_resize_size_bg or beautiful.bg_normal, 72 | fg = beautiful.collision_resize_size_fg or beautiful.fg_normal, 73 | border_width = beautiful.collision_resize_size_border_width, 74 | border_color = beautiful.collision_resize_size_border_color, 75 | shape = beautiful.collision_resize_shape, 76 | placement = function(o) 77 | return awful.placement.centered(o, {parent = c}) 78 | end, 79 | } 80 | 81 | wb = sizeboxes[c] 82 | end 83 | 84 | local geo = c:geometry() 85 | sizeboxes[c].visible = true 86 | sizeboxes[c].widget.tb.text = geo.width .. "x" .. geo.height 87 | end 88 | end 89 | end 90 | 91 | local function create_indicators() 92 | local ret = {} 93 | local angle = -((2*math.pi)/8) 94 | 95 | -- Get the parameters 96 | local size = beautiful.collision_resize_width or 40 97 | local s = beautiful.collision_resize_shape or shape.circle 98 | local bw = beautiful.collision_resize_border_width 99 | local bc = beautiful.collision_resize_border_color 100 | local padding = beautiful.collision_resize_padding or 7 101 | local bg = beautiful.collision_resize_bg or beautiful.bg_alternate or "#ff0000" 102 | local fg = beautiful.collision_resize_fg or beautiful.fg_normal or "#0000ff" 103 | local arrow_bc = beautiful.collision_resize_arrow_border_color 104 | local arrow_bw = beautiful.collision_resize_arrow_border_width or 0 105 | 106 | for k,v in ipairs(values) do 107 | local w = wibox { 108 | width = size, 109 | height = size, 110 | ontop = true, 111 | visible = true 112 | } 113 | 114 | angle = angle + (2*math.pi)/8 115 | 116 | local tr = (size - 2*arrow_bw - 2*padding) / 2 117 | 118 | w:setup { 119 | { 120 | { 121 | { 122 | widget = wibox.widget.imagebox 123 | }, 124 | shape = shape.transform(shape.arrow) 125 | : translate( tr,tr ) 126 | : rotate ( angle ) 127 | : translate( -tr,-tr ), 128 | bg = fg, 129 | border_color = arrow_bc, 130 | border_width = arrow_bw, 131 | widget = wibox.container.background 132 | }, 133 | margins = padding, 134 | widget = wibox.container.margin, 135 | }, 136 | bg = bg, 137 | shape = s, 138 | shape_border_width = bw, 139 | shape_border_color = bc, 140 | widget = wibox.container.background 141 | } 142 | 143 | if awesome.version >= "v4.1" then 144 | w.shape = s 145 | else 146 | surface.apply_shape_bounding(w, s) 147 | end 148 | 149 | ret[v] = w 150 | end 151 | 152 | return ret 153 | end 154 | 155 | function module.hide() 156 | if not indicators then return end 157 | 158 | for k, v in ipairs(values) do indicators[v].visible = false end 159 | 160 | for _, wb in pairs(sizeboxes) do wb.visible = false end 161 | 162 | if not cur_c then return end 163 | 164 | cur_c:disconnect_signal("property::geometry", module.display) 165 | cur_c = nil 166 | 167 | sizeboxes = {} 168 | end 169 | 170 | function module.display(c,toggle) 171 | if type(c) ~= "client" then --HACK 172 | c = capi.client.focus 173 | end 174 | 175 | if not c then return end 176 | 177 | indicators = indicators or create_indicators() 178 | 179 | if c ~= cur_c then 180 | if cur_c then 181 | cur_c:disconnect_signal("property::geometry", module.display) 182 | end 183 | c:connect_signal("property::geometry", module.display) 184 | cur_c = c 185 | elseif toggle == true then 186 | module.hide() 187 | end 188 | 189 | for k,v in ipairs(values) do 190 | local w = indicators[v] 191 | awful.placement[v](w, {parent=c}) 192 | w.visible = true 193 | end 194 | 195 | update_size_boxes(c.floating and c or nil, c.floating) 196 | end 197 | 198 | function module.resize(mod,key,event,direction,is_swap,is_max) 199 | local c = capi.client.focus 200 | if not c then return true end 201 | 202 | local del = is_swap and -100 or 100 203 | direction = is_swap and invert[direction] or direction 204 | 205 | local lay = awful.layout.get(c.screen) 206 | 207 | if c.floating or lay == awful.layout.suit.floating then 208 | c:emit_signal("request::geometry", "mouse.resize", r_ajust[direction](c, del)) 209 | update_size_boxes(c, true) 210 | elseif layouts_all[lay] then 211 | local ret = layouts_all[lay][direction] 212 | if ret.mwfact then 213 | awful.tag.incmwfact(ret.mwfact) 214 | end 215 | if ret.wfact then 216 | awful.client.incwfact(ret.wfact, c) 217 | end 218 | 219 | update_size_boxes() 220 | end 221 | 222 | 223 | return true 224 | end 225 | 226 | -- Always display the arrows when resizing 227 | awful.mouse.resize.add_enter_callback(module.display, "mouse.resize") 228 | awful.mouse.resize.add_leave_callback(module.hide , "mouse.resize") 229 | 230 | return module 231 | -- kate: space-indent on; indent-width 4; replace-tabs on; 232 | -------------------------------------------------------------------------------- /max.lua: -------------------------------------------------------------------------------- 1 | local capi = {screen=screen,client=client,mouse=mouse} 2 | local wibox = require("wibox") 3 | local awful = require("awful") 4 | local cairo = require( "lgi" ).cairo 5 | local color = require( "gears.color" ) 6 | local beautiful = require( "beautiful" ) 7 | local surface = require( "gears.surface" ) 8 | local layout = require( "collision.layout" ) 9 | local util = require( "collision.util" ) 10 | local shape = require( "gears.shape" ) 11 | local pango = require("lgi").Pango 12 | local pangocairo = require("lgi").PangoCairo 13 | local module = {} 14 | 15 | local w = nil 16 | local rad = 10 17 | local border = 3 18 | 19 | local function init() 20 | w = wibox{} 21 | w.ontop = true 22 | w.visible = true 23 | end 24 | 25 | local margin = 15 26 | 27 | local function create_arrow(cr,x,y,width, height,direction) 28 | local r,g,b = util.get_rgb() 29 | cr:set_source_rgba(r,g,b,0.15) 30 | cr:set_antialias(1) 31 | 32 | local s = shape.transform(shape.arrow) 33 | : rotate_at(width/4, height/4, math.pi/2) 34 | 35 | if direction == 1 then 36 | s : rotate_at(width/4, height/4, math.pi) 37 | end 38 | 39 | s : translate(x, y) 40 | 41 | s(cr, width/2, height/2, nil) 42 | 43 | cr:fill() 44 | end 45 | 46 | local pango_l = nil 47 | local function draw_shape(s,collection,current_idx,icon_f,y,text_height) 48 | local geo = capi.screen[s].geometry 49 | local wa =capi.screen[s].workarea 50 | 51 | --Compute thumb dimensions 52 | local margins = 2*20 + (#collection-1)*20 53 | local width = (geo.width - margins) / #collection 54 | local ratio = geo.height/geo.width 55 | local height = width*ratio 56 | local dx = 20 57 | 58 | -- Do not let the thumb get too big 59 | if height > 150 then 60 | height = 150 61 | width = 150 * (1.0/ratio) 62 | dx = (wa.width-margins-(#collection*width))/2 + 20 63 | end 64 | 65 | -- Resize the wibox 66 | w.x,w.y,w.width,w.height = math.floor(geo.x),math.floor(y or (wa.y+wa.height) - margin - height),math.floor(geo.width),math.floor(height) 67 | 68 | local rshape = beautiful.collision_max_shape or shape.rounded_rect 69 | local img = cairo.ImageSurface(cairo.Format.ARGB32, geo.width,geo.height) 70 | local img3 = cairo.ImageSurface(cairo.Format.ARGB32, geo.width,geo.height) 71 | local cr = cairo.Context(img) 72 | local cr3 = cairo.Context(img3) 73 | cr:set_source_rgba(0,0,0,0) 74 | cr:paint() 75 | 76 | -- Get the colors 77 | local white,bg = color("#FFFFFF"),color( 78 | beautiful.collision_max_bg or beautiful.menu_bg_normal or beautiful.bg_normal 79 | ) 80 | 81 | local normal, focus = color(beautiful.collision_max_fg or beautiful.fg_normal), color(beautiful.bg_urgent) 82 | 83 | -- Init the text properties 84 | if not pango_l then 85 | local pango_crx = pangocairo.font_map_get_default():create_context() 86 | pango_l = pango.Layout.new(pango_crx) 87 | pango_l:set_font_description(beautiful.get_font(font)) 88 | pango_l:set_alignment("CENTER") 89 | pango_l:set_wrap("CHAR") 90 | end 91 | 92 | for k,v in ipairs(collection) do 93 | -- Shape bounding 94 | cr:set_source(white) 95 | 96 | local s = shape.transform(rshape) : translate(dx, 0) 97 | s(cr, width, height, 10) 98 | cr:fill() 99 | 100 | -- Borders and background 101 | cr3:set_source(k==current_idx and focus or normal) 102 | 103 | cr3:move_to(0,0) 104 | s = shape.transform(rshape) : translate(dx+border, border) 105 | s(cr3, width-2*border, height-2*border, rad) 106 | 107 | cr3:set_line_width(2*border + 4) -- The 4 is required to cover the non-antialiased region 108 | cr3:stroke_preserve() 109 | cr3:set_source(bg) 110 | cr3:fill() 111 | 112 | -- Print the icon 113 | local icon = icon_f(v,width-20,height-20-text_height) 114 | if icon then 115 | cr3:save() 116 | cr3:translate(dx+10,10) 117 | cr3:set_source_surface(icon) 118 | cr3:paint_with_alpha(0.7) 119 | cr3:restore() 120 | end 121 | 122 | -- Print a pretty line 123 | local r,g,b = util.get_rgb(normal) 124 | cr3:set_source_rgba(r,g,b,0.7) 125 | cr3:set_line_width(1) 126 | cr3:move_to(dx+margin,height - text_height-border) 127 | cr3:line_to(dx+margin+width-2*margin,height - text_height-border) 128 | cr3:stroke() 129 | 130 | -- Pring the text 131 | pango_l.text = v.name 132 | pango_l.width = pango.units_from_double(width-16) 133 | pango_l.height = pango.units_from_double(height-text_height-10) 134 | cr3:move_to(dx+8,height-text_height-0) 135 | cr3:show_layout(pango_l) 136 | 137 | -- Draw an arrow 138 | if current_idx and k == current_idx-1 then 139 | create_arrow(cr3,dx,0,width,height,1) 140 | else 141 | create_arrow(cr3,dx,0,width,height,nil) 142 | end 143 | 144 | dx = dx + width + 20 145 | end 146 | 147 | w:set_bg(cairo.Pattern.create_for_surface(img3)) 148 | w.shape_bounding = img._native 149 | w.visible = true 150 | end 151 | 152 | function module.hide() 153 | w.visible = false 154 | end 155 | 156 | --Client related 157 | local function client_icon(c,width,height) 158 | -- Get the content 159 | --TODO detect pure black frames 160 | local img = cairo.ImageSurface(cairo.Format.ARGB32, width, height) 161 | local rshape = beautiful.collision_max_shape or shape.rounded_rect 162 | local cr = cairo.Context(img) 163 | 164 | local geom = c:geometry() 165 | local scale = width/geom.width 166 | if geom.height*scale > height then 167 | scale = height/geom.height 168 | end 169 | local w,h = geom.width*scale,geom.height*scale 170 | 171 | -- Create a mask 172 | local s = shape.transform(rshape) : translate((width-w)/2, (height-h)/2) 173 | s(cr, w, h, 10) 174 | 175 | cr:clip() 176 | 177 | cr:translate((width-w)/2,(height-h)/2) 178 | 179 | -- Create a matrix to scale down the screenshot 180 | cr:save() 181 | cr:scale(scale,scale) 182 | 183 | -- Paint the screenshot in the rounded rectangle 184 | cr:set_source_surface(surface(c.content)) 185 | 186 | cr:paint() 187 | cr:restore() 188 | 189 | -- Add icon on top, "solve" the black window issue 190 | local icon = surface(c.icon) 191 | 192 | if not icon then 193 | return img 194 | end 195 | 196 | -- local w,h = icon:get_width(),icon:get_height() 197 | -- local aspect,aspect_h = width / w,(height) / h 198 | -- if aspect > aspect_h then aspect = aspect_h end 199 | -- cr:translate((width-w*aspect)/2,(height-h*aspect)/2) 200 | -- cr:scale(aspect, aspect) 201 | -- cr:set_source_surface(icon) 202 | -- cr:paint_with_alpha(0.5) 203 | 204 | return img 205 | end 206 | 207 | function module.display_clients(s,direction) 208 | if not w then 209 | init() 210 | end 211 | if direction then 212 | awful.client.focus.byidx(direction == "right" and 1 or -1) 213 | capi.client.focus:raise() 214 | end 215 | local clients = awful.client.tiled(s) 216 | local fk = awful.util.table.hasitem(clients,capi.client.focus) 217 | draw_shape(s,clients,fk,client_icon,nil,50) 218 | end 219 | 220 | function module.change_focus(mod,key,event,direction,is_swap,is_max) 221 | awful.client.focus.byidx(direction == "right" and 1 or -1) 222 | local c = capi.client.focus 223 | local s = c.screen 224 | c:raise() 225 | local clients = awful.client.tiled(s) 226 | local fk = awful.util.table.hasitem(clients,c) 227 | draw_shape(s,clients,fk,client_icon,nil,50) 228 | return true 229 | end 230 | 231 | --Tag related 232 | local function tag_icon(t,width,height) 233 | local img = cairo.ImageSurface(cairo.Format.ARGB32, width, height) 234 | local cr = cairo.Context(img) 235 | 236 | local has_layout = layout.draw(t,cr,width,height) 237 | 238 | -- Create a monochrome representation of the icon 239 | local icon_orig = surface(t.icon) 240 | if icon_orig then 241 | local icon = cairo.ImageSurface(cairo.Format.ARGB32, icon_orig:get_width(), icon_orig:get_height()) 242 | local cr2 = cairo.Context(icon) 243 | cr2:set_source_surface(icon_orig) 244 | cr2:paint() 245 | 246 | cr2:set_source(color(beautiful.fg_normal)) 247 | cr2:set_operator(cairo.Operator.IN) 248 | cr2:paint() 249 | 250 | local w,h = icon:get_width(),icon:get_height() 251 | local aspect,aspect_h = width / w,(height) / h 252 | if aspect > aspect_h then aspect = aspect_h end 253 | cr:translate((width-w*aspect)/2,(height-h*aspect)/2) 254 | cr:scale(aspect, aspect) 255 | cr:set_source_surface(icon) 256 | cr:paint_with_alpha(has_layout and 0.35 or 1) 257 | end 258 | return img 259 | end 260 | 261 | local function change_tag(s,direction,is_swap) 262 | local s = capi.client.focus and capi.client.focus.screen or capi.mouse.screen 263 | if not is_swap then 264 | awful.tag[direction == "left" and "viewprev" or "viewnext"](s) 265 | else 266 | -- Move the tag 267 | local t = capi.screen[s].selected_tag 268 | local cur_idx,count = awful.tag.getidx(t),#capi.screen[s].tags 269 | cur_idx = cur_idx + (direction == "left" and -1 or 1) 270 | cur_idx = cur_idx == 0 and count or cur_idx > count and 1 or cur_idx 271 | t.index = cur_idx 272 | end 273 | local tags = capi.screen[s].tags 274 | local fk = awful.util.table.hasitem(tags,capi.screen[s].selected_tag) 275 | draw_shape(s,tags,fk,tag_icon,capi.screen[s].workarea.y + 15,20) 276 | end 277 | 278 | function module.display_tags(s,direction,c,is_swap,is_max) 279 | if not w then 280 | init() 281 | end 282 | change_tag(s,direction,is_swap) 283 | end 284 | 285 | function module.change_tag(mod,key,event,direction,is_swap,is_max) 286 | local s = capi.client.focus and capi.client.focus.screen or capi.mouse.screen 287 | change_tag(s,direction,is_swap) 288 | return true 289 | end 290 | 291 | return module 292 | -- kate: space-indent on; indent-width 2; replace-tabs on; 293 | -------------------------------------------------------------------------------- /focus.lua: -------------------------------------------------------------------------------- 1 | local capi = { client = client , mouse = mouse , 2 | screen = screen , keygrabber = keygrabber,} 3 | 4 | local setmetatable = setmetatable 5 | local ipairs = ipairs 6 | local surface = require( "gears.surface" ) 7 | local shape = require( "gears.shape" ) 8 | local util = require( "awful.util" ) 9 | local client = require( "awful.client" ) 10 | local tag = require( "awful.tag" ) 11 | local alayout = require( "awful.layout" ) 12 | local wibox = require( "wibox" ) 13 | local cairo = require( "lgi" ).cairo 14 | local beautiful = require( "beautiful" ) 15 | local color = require( "gears.color" ) 16 | local col_utils = require( "collision.util" ) 17 | local grect = require( "gears.geometry" ).rectangle 18 | local placement = require( "awful.placement") 19 | 20 | local module = {} 21 | local wiboxes,delta = nil,100 22 | local edge = nil 23 | 24 | ---------------- Visual ----------------------- 25 | local function init() 26 | wiboxes = {} 27 | 28 | local s = beautiful.collision_focus_shape or shape.rounded_rect 29 | local bw = beautiful.collision_focus_border_width 30 | local bc = beautiful.collision_focus_border_color 31 | local padding = beautiful.collision_focus_padding or 7 32 | local bg = beautiful.collision_focus_bg or beautiful.bg_alternate or "#ff0000" 33 | local fg = beautiful.collision_focus_fg or beautiful.fg_normal or "#0000ff" 34 | local bg_focus = beautiful.collision_focus_bg_center or beautiful.bg_urgent or "#ff0000" 35 | local sw = beautiful.collision_shape_width or 75 36 | local sh = beautiful.collision_shape_height or 75 37 | local arrow_bc = beautiful.collision_focus_arrow_border_color 38 | local arrow_bw = beautiful.collision_focus_arrow_border_width or 0 39 | local cshape = beautiful.collision_focus_shape_center or shape.circle 40 | 41 | for k,v in ipairs({"up","right","down","left","center"}) do 42 | wiboxes[v] = wibox { 43 | height = sh, 44 | width = sw, 45 | ontop = true 46 | } 47 | 48 | local r_shape = v == "center" and cshape or s 49 | local r_bg = v == "center" and bg_focus or bg 50 | local x = (sw - arrow_bw*2)/2 - padding 51 | local y = (sh - arrow_bw*2)/2 - padding 52 | 53 | wiboxes[v]:setup { 54 | v ~= "center" and { 55 | { 56 | { 57 | widget = wibox.widget.imagebox 58 | }, 59 | bg = fg, 60 | border_color = arrow_bc, 61 | border_width = arrow_bw, 62 | widget = wibox.container.background, 63 | shape = shape.transform(col_utils.arrow_path2) 64 | : rotate_at(x, y, (k-1)*(2*math.pi)/4), 65 | }, 66 | margins = padding, 67 | widget = wibox.container.margin, 68 | } or { 69 | widget = wibox.widget.imagebox 70 | }, 71 | bg = r_bg, 72 | shape = r_shape, 73 | shape_border_width = bw, 74 | shape_border_color = bc, 75 | widget = wibox.container.background 76 | } 77 | 78 | if awesome.version >= "v4.1" then 79 | wiboxes[v].shape = r_shape 80 | else 81 | surface.apply_shape_bounding(wiboxes[v], r_shape) 82 | end 83 | end 84 | end 85 | 86 | local function emulate_client(screen) 87 | return {is_screen = true, screen=screen, geometry=function() return capi.screen[screen].workarea end} 88 | end 89 | 90 | local function display_wiboxes(cltbl,geomtbl,float,swap,c) 91 | if not wiboxes then 92 | init() 93 | end 94 | local fc = capi.client.focus or emulate_client(capi.mouse.screen) 95 | for k,v in ipairs({"left","right","up","down","center"}) do 96 | local next_clients = (float and swap) and c or cltbl[grect.get_in_direction(v , geomtbl, fc:geometry())] 97 | if next_clients or k==5 then 98 | local parent = k==5 and fc or next_clients 99 | wiboxes[v].visible = true 100 | placement.centered(wiboxes[v], {parent = parent}) 101 | else 102 | wiboxes[v].visible = false 103 | end 104 | end 105 | end 106 | 107 | ---------------- Position ----------------------- 108 | local function float_move(dir,c) 109 | return ({left={x=c:geometry().x-delta},right={x=c:geometry().x+delta},up={y=c:geometry().y-delta},down={y=c:geometry().y+delta}})[dir] 110 | end 111 | 112 | local function float_move_max(dir,c) 113 | return ({left={x=capi.screen[c.screen].workarea.x},right={x=capi.screen[c.screen].workarea.width+capi.screen[c.screen].workarea.x-c:geometry().width} 114 | ,up={y=capi.screen[c.screen].workarea.y},down={y=capi.screen[c.screen].workarea.y+capi.screen[c.screen].workarea.height-c:geometry().height}})[dir] 115 | end 116 | 117 | local function floating_clients() 118 | local ret = {} 119 | for v in util.table.iterate(client.visible(),function(c) return c.floating end) do 120 | ret[#ret+1] = v 121 | end 122 | return ret 123 | end 124 | 125 | local function bydirection(dir, c, swap,max) 126 | if not c then 127 | c = emulate_client(capi.mouse.screen) 128 | end 129 | 130 | local float = nil 131 | 132 | if c.is_screen then 133 | float = false 134 | else 135 | float = (c.floating or alayout.get(c.screen) == alayout.suit.floating) 136 | end 137 | 138 | -- Move the client if floating, swaping wont work anyway 139 | if swap and float then 140 | c:geometry((max and float_move_max or float_move)(dir,c)) 141 | display_wiboxes(nil,nil,float,swap,c) 142 | else 143 | 144 | if not edge then 145 | local scrs =col_utils.get_ordered_screens() 146 | local last_geo =capi.screen[scrs[#scrs]].geometry 147 | edge = last_geo.x + last_geo.width 148 | end 149 | 150 | -- Get all clients rectangle 151 | local cltbl,geomtbl,scrs,roundr,roundl = max and floating_clients() or client.tiled(),{},{},{},{} 152 | for i,cl in ipairs(cltbl) do 153 | local geo = cl:geometry() 154 | geomtbl[i] = geo 155 | scrs[capi.screen[cl.screen or 1]] = true 156 | if geo.x == 0 then 157 | roundr[#roundr+1] = cl 158 | elseif geo.x + geo.width >= edge -2 then 159 | roundl[#roundl+1] = cl 160 | end 161 | end 162 | 163 | --Add first client at the end to be able to rotate selection 164 | for k,c in ipairs(roundr) do 165 | local geo = c:geometry() 166 | geomtbl[#geomtbl+1] = {x=edge,width=geo.width,y=geo.y,height=geo.height} 167 | cltbl[#geomtbl] = c 168 | end 169 | for k,c in ipairs(roundl) do 170 | local geo = c:geometry() 171 | geomtbl[#geomtbl+1] = {x=-geo.width,width=geo.width,y=geo.y,height=geo.height} 172 | cltbl[#geomtbl] = c 173 | end 174 | 175 | -- Add rectangles for empty screens too 176 | for i = 1, capi.screen.count() do 177 | if not scrs[capi.screen[i]] then 178 | geomtbl[#geomtbl+1] = capi.screen[i].workarea 179 | cltbl[#geomtbl] = emulate_client(i) 180 | end 181 | end 182 | 183 | local target = grect.get_in_direction(dir, geomtbl, c:geometry()) 184 | if swap ~= true then 185 | -- If we found a client to focus, then do it. 186 | if target then 187 | local cl = cltbl[target] 188 | if cl and cl.is_screen then 189 | capi.client.focus = nil --TODO Fix upstream fix 190 | capi.mouse.screen = capi.screen[cl.screen] 191 | else 192 | local old_src = capi.client.focus and capi.client.focus.screen 193 | capi.client.focus = cltbl[((not cl and #cltbl == 1) and 1 or target)] 194 | capi.client.focus:raise() 195 | if not old_src or capi.client.focus.screen ~= capi.screen[old_src] then 196 | capi.mouse.coords(capi.client.focus:geometry()) 197 | end 198 | end 199 | end 200 | else 201 | if target then 202 | -- We found a client to swap 203 | local other = cltbl[((not cltbl[target] and #cltbl == 1) and 1 or target)] 204 | if capi.screen[other.screen] == capi.screen[c.screen] or col_utils.settings.swap_across_screen then 205 | --BUG swap doesn't work if the screen is not the same 206 | c:swap(other) 207 | else 208 | local t = capi.screen[other.screen].selected_tag --TODO get index 209 | c.screen = capi.screen[ other.screen] 210 | c:tags({t}) 211 | end 212 | else 213 | -- No client to swap, try to find a screen. 214 | local screen_geom = {} 215 | for i = 1, capi.screen.count() do 216 | screen_geom[i] = capi.screen[i].workarea 217 | end 218 | target = grect.get_in_direction(dir, screen_geom, c:geometry()) 219 | if target and target ~= c.screen then 220 | local t = capi.screen[target].selected_tag 221 | c.screen = target 222 | c:tags({t}) 223 | c:raise() 224 | end 225 | end 226 | if target then 227 | -- Geometries have changed by swapping, so refresh. 228 | cltbl,geomtbl = max and floating_clients() or client.tiled(),{} 229 | for i,cl in ipairs(cltbl) do 230 | geomtbl[i] = cl:geometry() 231 | end 232 | end 233 | end 234 | display_wiboxes(cltbl,geomtbl,float,swap,c) 235 | end 236 | end 237 | 238 | function module.global_bydirection(dir, c,swap,max) 239 | bydirection(dir, c or capi.client.focus, swap,max) 240 | end 241 | 242 | function module._global_bydirection_key(mod,key,event,direction,is_swap,is_max) 243 | bydirection(direction,capi.client.focus,is_swap,is_max) 244 | return true 245 | end 246 | 247 | function module.display(mod,key,event,direction,is_swap,is_max) 248 | local c = capi.client.focus 249 | local cltbl,geomtbl = max and floating_clients() or client.tiled(),{} 250 | for i,cl in ipairs(cltbl) do 251 | geomtbl[i] = cl:geometry() 252 | end 253 | 254 | -- Sometime, there is no focussed clients 255 | if not c then 256 | c = geomtbl[1] or cltbl[1] 257 | end 258 | 259 | -- If there is still no accessible clients, there is nothing to display 260 | if not c then return end 261 | 262 | display_wiboxes(cltbl,geomtbl,c.floating or alayout.get(c.screen) == alayout.suit.floating,is_swap,c) 263 | end 264 | 265 | function module._quit() 266 | if not wiboxes then return end 267 | for k,v in ipairs({"left","right","up","down","center"}) do 268 | wiboxes[v].visible = false 269 | end 270 | end 271 | 272 | return setmetatable(module, { __call = function(_, ...) return new(...) end }) 273 | -- kate: space-indent on; indent-width 4; replace-tabs on; 274 | -------------------------------------------------------------------------------- /screen.lua: -------------------------------------------------------------------------------- 1 | local capi = {screen=screen,client=client,mouse=mouse, keygrabber = keygrabber} 2 | local math = math 3 | local wibox = require( "wibox" ) 4 | local awful = require( "awful" ) 5 | local beautiful = require( "beautiful" ) 6 | local surface = require( "gears.surface" ) 7 | local pango = require( "lgi" ).Pango 8 | local pangocairo = require( "lgi" ).PangoCairo 9 | local mouse = require( "collision.mouse" ) 10 | local util = require( "collision.util" ) 11 | local shape = require( "gears.shape" ) 12 | local scale = pango.SCALE 13 | 14 | local module = {} 15 | 16 | local wiboxes = {} 17 | local wiboxes_s = setmetatable({},{__mode="k"}) 18 | local bgs = {} 19 | local size = 75 20 | local pss = capi.mouse.screen 21 | 22 | -- Keep an index of the last selection client for each screen 23 | local last_clients = setmetatable({},{__mode="kv"}) 24 | local last_clients_coords = {} 25 | 26 | local screens,screens_inv = util.get_ordered_screens() 27 | 28 | local function current_screen(focus) 29 | return (not focus) and capi.mouse.screen or (capi.client.focus and capi.client.focus.screen or capi.mouse.screen) 30 | end 31 | 32 | local function fit(self,_,width,height) 33 | -- Compute the optimal font size 34 | local padding = self._private.padding or 10 35 | local tm = self._private.top_margin or 0 36 | height = math.max(0, height - tm) 37 | local min = math.min(width, height) 38 | local font_size = math.max(0, min - 2*padding) 39 | 40 | self._private.desc:set_size( font_size * scale ) 41 | self._private.layout:set_font_description(self._private.desc) 42 | 43 | local _, geo = self._private.layout:get_pixel_extents() 44 | 45 | -- The extra "min" is in case a font "lie" and is too big. 46 | return math.min(geo.x+geo.width, min), math.min(geo.y+geo.height, min) + tm 47 | end 48 | 49 | local function draw(self, _, cr, width, height) 50 | -- Some layouts never call fit()... 51 | fit(self, context, width, height) 52 | 53 | local _, geo = self._private.layout:get_pixel_extents() 54 | local tm = self._private.top_margin or 0 55 | local sz = math.min(width, height - tm) 56 | 57 | -- Translate the canvas to center the text 58 | local dy = ( sz - geo.height )/2 - geo.y + tm 59 | local dx = ( sz - geo.width )/2 - geo.x 60 | cr:move_to(dx, dy) 61 | 62 | -- Show the text 63 | cr:show_layout(self._private.layout) 64 | end 65 | 66 | local function set_screen(self, s) 67 | self._private.layout.text = tostring(s.index) 68 | self:emit_signal("widget::layout_changed") 69 | end 70 | 71 | local function set_padding(self, padding) 72 | self._private.padding = padding 73 | self:emit_signal("widget::layout_changed") 74 | end 75 | 76 | local function set_top_margin(margin) 77 | self._private.top_margin = margin or 0 78 | self:emit_signal("widget::layout_changed") 79 | end 80 | 81 | local function new_constrained_text(s) 82 | local wdg = wibox.widget.base.empty_widget() 83 | 84 | -- Add the basic functions 85 | rawset(wdg, "draw" , draw ) 86 | rawset(wdg, "fit" , fit ) 87 | rawset(wdg, "set_screen" , set_screen ) 88 | rawset(wdg, "set_padding" , set_padding ) 89 | rawset(wdg, "set_top_margin" , set_top_margin ) 90 | 91 | -- Create the pango objects 92 | local pango_crx = pangocairo.font_map_get_default():create_context() 93 | local pango_l = pango.Layout.new(pango_crx) 94 | local desc = pango.FontDescription() 95 | desc:set_family( "Verdana" ) 96 | desc:set_weight( pango.Weight.BOLD ) 97 | 98 | wdg._private.layout = pango_l 99 | wdg._private.desc = desc 100 | wdg:emit_signal("widget::layout_changed") 101 | 102 | if s then set_screen(wdg, s) end 103 | 104 | return wdg 105 | end 106 | 107 | local function create_wibox(s) 108 | s = capi.screen[s] 109 | 110 | if wiboxes_s[s] then return wiboxes_s[s] end 111 | 112 | local wa = s.workarea 113 | 114 | -- Create a round wibox 115 | local w = wibox { 116 | width = size, 117 | height = size, 118 | x = math.floor(wa.x+wa.width /2-size/2), 119 | y = math.floor(wa.y+wa.height/2-size/2), 120 | ontop = true 121 | } 122 | 123 | -- Theme options 124 | local sh = beautiful.collision_screen_shape or shape.circle 125 | local bw = beautiful.collision_screen_border_width 126 | local bc = beautiful.collision_screen_border_color 127 | local padding = beautiful.collision_screen_padding or 10 128 | local bg = beautiful.collision_screen_bg or beautiful.bg_alternate or "#ff0000" 129 | local fg = beautiful.collision_screen_fg or beautiful.fg_normal or "#0000ff" 130 | local bg_focus = beautiful.collision_screen_bg_focus or beautiful.bg_urgent or "#ff0000" 131 | local fg_focus = beautiful.collision_screen_fg_focus or beautiful.fg_urgent or "#ff0000" 132 | 133 | -- Setup the widgets 134 | w:setup { 135 | { 136 | nil, 137 | { 138 | nil, 139 | { 140 | screen = s, 141 | padding = padding, 142 | widget = new_constrained_text 143 | }, 144 | nil, 145 | layout = wibox.layout.align.vertical 146 | }, 147 | nil, 148 | layout = wibox.layout.align.horizontal 149 | }, 150 | bg = bg, 151 | shape = sh or shape.circle, 152 | shape_border_width = bw, 153 | shape_border_color = bc, 154 | id = "main_background", 155 | widget = wibox.container.background 156 | } 157 | 158 | -- Set the wibox shape 159 | surface.apply_shape_bounding(w, sh) 160 | 161 | wiboxes_s[s] = w 162 | wiboxes[s.index] = w --DEPRECATED 163 | bgs[s] = w:get_children_by_id("main_background")[1] 164 | 165 | return w 166 | end 167 | 168 | -- Hopefully, the wiboxes will be gargabe collected 169 | local init = false 170 | local function init_wiboxes() 171 | if init then return end 172 | 173 | awful.screen.connect_for_each_screen(function(s) 174 | create_wibox(s) 175 | end) 176 | 177 | init = true 178 | 179 | return true 180 | end 181 | 182 | local function select_screen(scr_index,move,old_screen) 183 | if capi.screen[scr_index] ~= capi.screen[old_screen or 1] then 184 | local c = last_clients[capi.screen[scr_index]] 185 | 186 | -- If the client is leaked elsewhere, prevent an error message 187 | if c and not pcall(function() return c.valid end) and not c.valid then 188 | last_clients[capi.screen[scr_index]] = nil 189 | c = nil 190 | end 191 | 192 | if c and c.valid and c:isvisible() then 193 | local geom = c:geometry() 194 | if last_clients_coords[scr_index] and last_clients_coords[scr_index].client == c then 195 | capi.mouse.coords(last_clients_coords[scr_index]) 196 | else 197 | capi.mouse.coords({x=geom.x+geom.width/2,y=geom.y+geom.height/2+55}) 198 | end 199 | else 200 | local geom = capi.screen[scr_index].geometry 201 | capi.mouse.coords({x=geom.x+geom.width/2,y=geom.y+geom.height/2+55}) 202 | end 203 | mouse.highlight() 204 | end 205 | 206 | if move then 207 | local t = capi.screen[scr_index].selected_tag 208 | if t then 209 | t.screen = old_screen 210 | awful.tag.viewonly(t) 211 | end 212 | else 213 | local c = capi.mouse.current_client 214 | if c then 215 | capi.client.focus = c 216 | end 217 | end 218 | 219 | return capi.screen[scr_index] 220 | end 221 | 222 | local function in_rect(c,point) 223 | if not c then return true end 224 | local geo = c:geometry() 225 | return ( 226 | geo.x < point.x and geo.y < point.y and 227 | geo.x + geo.width > point.x and geo.y + geo.height > point.y 228 | ) 229 | end 230 | 231 | local function save_cursor_position() 232 | local coords = capi.mouse.coords() 233 | local c = capi.client.focus 234 | -- Be sure that that mouse in inside of the selected client before doing that 235 | if c and in_rect(c,coords) then 236 | last_clients_coords[c.screen] = { 237 | client = c, 238 | x = coords.x, 239 | y = coords.y, 240 | } 241 | else 242 | last_clients_coords[capi.mouse.screen] = nil 243 | end 244 | end 245 | 246 | local function next_screen(ss,dir,move) 247 | if capi.screen.count() == 1 then return 1 end 248 | 249 | local scr_index = capi.screen[screens_inv[ss]].index 250 | 251 | if type(scr_index) == "screen" then 252 | scr_index = scr_index.index 253 | end 254 | if dir == "left" then 255 | scr_index = scr_index == 1 and #screens or scr_index - 1 256 | elseif dir == "right" then 257 | scr_index = scr_index == #screens and 1 or scr_index+1 258 | end 259 | 260 | return select_screen(screens_inv[capi.screen[scr_index]],move,ss) 261 | end 262 | 263 | function module.display(_,dir,move) 264 | if #wiboxes == 0 then 265 | init_wiboxes() 266 | end 267 | save_cursor_position() 268 | module.reload(nil,dir) 269 | local ss = current_screen(move) 270 | next_screen(ss,dir,move) 271 | module.reload(nil,dir) 272 | end 273 | 274 | local function highlight_screen(ss) 275 | ss = capi.screen[ss] 276 | if pss ~= ss then 277 | 278 | local bg = beautiful.collision_screen_bg or beautiful.bg_alternate or "#ff0000" 279 | local fg = beautiful.collision_screen_fg or beautiful.fg_normal or "#0000ff" 280 | local bg_focus = beautiful.collision_screen_bg_focus or beautiful.bg_urgent or "#ff0000" 281 | local fg_focus = beautiful.collision_screen_fg_focus or beautiful.fg_urgent or "#ff0000" 282 | 283 | if pss then 284 | bgs[pss].bg = bg 285 | bgs[pss].fg = fg 286 | end 287 | 288 | pss = ss 289 | 290 | bgs[ss].bg = bg_focus 291 | bgs[ss].fg = fg_focus 292 | end 293 | end 294 | 295 | function module.hide() 296 | if #wiboxes == 0 then return end 297 | 298 | for s=1, capi.screen.count() do 299 | wiboxes[s].visible = false 300 | end 301 | mouse.hide() 302 | end 303 | 304 | local function show() 305 | for s=1, capi.screen.count() do 306 | wiboxes[s].visible = true 307 | end 308 | end 309 | 310 | function module.reload(mod,dir,_,_,move) 311 | local ss = current_screen(move) 312 | if dir then 313 | ss = next_screen(ss,dir:lower(),move or (mod and #mod == 4)) 314 | end 315 | 316 | highlight_screen(ss) 317 | 318 | show() 319 | 320 | return true 321 | end 322 | 323 | function module.select_screen(idx) 324 | save_cursor_position() 325 | select_screen(screens_inv[idx],false) 326 | if #wiboxes == 0 then 327 | init_wiboxes() 328 | end 329 | 330 | highlight_screen(screens_inv[idx]) 331 | 332 | show() 333 | 334 | capi.keygrabber.run(function(_, _, event) 335 | if event == "release" then 336 | module.hide() 337 | mouse.hide() 338 | capi.keygrabber.stop() 339 | return false 340 | end 341 | return true 342 | end) 343 | end 344 | 345 | -- Make sure this keeps working when a new screen is added 346 | awful.screen.connect_for_each_screen(function(s) 347 | if next(wiboxes) then 348 | create_wibox(s) 349 | end 350 | end) 351 | 352 | capi.client.connect_signal("focus",function(c) 353 | last_clients[c.screen] = c 354 | end) 355 | 356 | return module 357 | -- kate: space-indent on; indent-width 4; replace-tabs on; 358 | --------------------------------------------------------------------------------