├── LICENSE ├── README.md ├── init.lua ├── noicon.png └── screenshot.gif /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014-2016 Joren Heit 2 | Copyright (c) 2016 Matthias Berla 3 | 4 | Permission to use, copy, modify, and/or distribute this software for any 5 | purpose with or without fee is hereby granted, provided that the above 6 | copyright notice and this permission notice appear in all copies. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | awesome-switcher 2 | ================ 3 | 4 | This plugin integrates the familiar application switcher functionality in the 5 | [awesome window manager](https://github.com/awesomeWM/awesome). 6 | 7 | ![Screenshot of awesome-switcher](screenshot.gif) 8 | 9 | Features: 10 | 11 | * Live previews while alt-tabbing AND/OR Opacity effects for unselected clients 12 | * Easily adjustable settings 13 | * No previews when modifier (e.g.: Alt) is released within some time-frame 14 | * Backward cycle using second modifier (e.g.: Shift) 15 | * Intuitive order, respecting your client history 16 | * Includes minimized clients (in contrast to some of the default window-switching utilies) 17 | * Preview selectable by mouse 18 | 19 | ## Installation ## 20 | 21 | Clone the repo into your `$XDG_CONFIG_HOME/awesome` directory: 22 | 23 | ```Shell 24 | cd "$XDG_CONFIG_HOME/awesome" 25 | git clone https://github.com/berlam/awesome-switcher.git awesome-switcher 26 | ``` 27 | 28 | Then add the dependency to your Awesome `rc.lua` config file: 29 | 30 | ```Lua 31 | local switcher = require("awesome-switcher") 32 | ``` 33 | 34 | ## Configuration ## 35 | 36 | Optionally edit any subset of the following settings, the defaults are: 37 | 38 | ```Lua 39 | switcher.settings.preview_box = true, -- display preview-box 40 | switcher.settings.preview_box_bg = "#ddddddaa", -- background color 41 | switcher.settings.preview_box_border = "#22222200", -- border-color 42 | switcher.settings.preview_box_fps = 30, -- refresh framerate 43 | switcher.settings.preview_box_delay = 150, -- delay in ms 44 | switcher.settings.preview_box_title_font = {"sans","italic","normal"},-- the font for cairo 45 | switcher.settings.preview_box_title_font_size_factor = 0.8, -- the font sizing factor 46 | switcher.settings.preview_box_title_color = {0,0,0,1}, -- the font color 47 | 48 | switcher.settings.client_opacity = false, -- opacity for unselected clients 49 | switcher.settings.client_opacity_value = 0.5, -- alpha-value for any client 50 | switcher.settings.client_opacity_value_in_focus = 0.5, -- alpha-value for the client currently in focus 51 | switcher.settings.client_opacity_value_selected = 1, -- alpha-value for the selected client 52 | 53 | switcher.settings.cycle_raise_client = true, -- raise clients on cycle 54 | ``` 55 | 56 | Then add key-bindings. On my particular system I switch to the next client by Alt-Tab and 57 | back with Alt-Shift-Tab. Therefore, this is what my keybindings look like: 58 | 59 | ```Lua 60 | awful.key({ "Mod1", }, "Tab", 61 | function () 62 | switcher.switch( 1, "Mod1", "Alt_L", "Shift", "Tab") 63 | end), 64 | 65 | awful.key({ "Mod1", "Shift" }, "Tab", 66 | function () 67 | switcher.switch(-1, "Mod1", "Alt_L", "Shift", "Tab") 68 | end), 69 | ``` 70 | 71 | Please keep in mind that "Mod1" and "Shift" are actual modifiers and not real keys. 72 | This is important for the keygrabber as the keygrabber uses "Shift_L" for a pressed (left) "Shift" key. 73 | 74 | ## Credits ## 75 | 76 | This plugin was created by [Joren Heit](https://github.com/jorenheit) 77 | and later improved upon by [Matthias Berla](https://github.com/berlam). 78 | 79 | ## License ## 80 | 81 | See [LICENSE](LICENSE). 82 | -------------------------------------------------------------------------------- /init.lua: -------------------------------------------------------------------------------- 1 | local cairo = require("lgi").cairo 2 | local mouse = mouse 3 | local screen = screen 4 | local wibox = require('wibox') 5 | local table = table 6 | local keygrabber = keygrabber 7 | local math = require('math') 8 | local awful = require('awful') 9 | local gears = require("gears") 10 | local timer = gears.timer 11 | local client = client 12 | awful.client = require('awful.client') 13 | 14 | local naughty = require("naughty") 15 | local string = string 16 | local tostring = tostring 17 | local tonumber = tonumber 18 | local debug = debug 19 | local pairs = pairs 20 | local unpack = unpack or table.unpack 21 | 22 | local surface = cairo.ImageSurface(cairo.Format.RGB24,20,20) 23 | local cr = cairo.Context(surface) 24 | 25 | local _M = {} 26 | 27 | -- settings 28 | 29 | _M.settings = { 30 | preview_box = true, 31 | preview_box_bg = "#ddddddaa", 32 | preview_box_border = "#22222200", 33 | preview_box_fps = 30, 34 | preview_box_delay = 150, 35 | preview_box_title_font = {"sans","italic","normal"}, 36 | preview_box_title_font_size_factor = 0.8, 37 | preview_box_title_color = {0,0,0,1}, 38 | 39 | client_opacity = false, 40 | client_opacity_value_selected = 1, 41 | client_opacity_value_in_focus = 0.5, 42 | client_opacity_value = 0.5, 43 | 44 | cycle_raise_client = true, 45 | } 46 | 47 | -- Create a wibox to contain all the client-widgets 48 | _M.preview_wbox = wibox({ width = screen[mouse.screen].geometry.width }) 49 | _M.preview_wbox.border_width = 3 50 | _M.preview_wbox.ontop = true 51 | _M.preview_wbox.visible = false 52 | 53 | _M.preview_live_timer = timer({ timeout = 1/_M.settings.preview_box_fps }) 54 | _M.preview_widgets = {} 55 | 56 | _M.altTabTable = {} 57 | _M.altTabIndex = 1 58 | 59 | _M.source = string.sub(debug.getinfo(1,'S').source, 2) 60 | _M.path = string.sub(_M.source, 1, string.find(_M.source, "/[^/]*$")) 61 | _M.noicon = _M.path .. "noicon.png" 62 | 63 | -- simple function for counting the size of a table 64 | function _M.tableLength(T) 65 | local count = 0 66 | for _ in pairs(T) do count = count + 1 end 67 | return count 68 | end 69 | 70 | -- this function returns the list of clients to be shown. 71 | function _M.getClients() 72 | local clients = {} 73 | 74 | -- Get focus history for current tag 75 | local s = mouse.screen; 76 | local idx = 0 77 | local c = awful.client.focus.history.get(s, idx) 78 | 79 | while c do 80 | table.insert(clients, c) 81 | 82 | idx = idx + 1 83 | c = awful.client.focus.history.get(s, idx) 84 | end 85 | 86 | -- Minimized clients will not appear in the focus history 87 | -- Find them by cycling through all clients, and adding them to the list 88 | -- if not already there. 89 | -- This will preserve the history AND enable you to focus on minimized clients 90 | 91 | local t = s.selected_tag 92 | local all = client.get(s) 93 | 94 | for i = 1, #all do 95 | local c = all[i] 96 | local ctags = c:tags(); 97 | 98 | -- check if the client is on the current tag 99 | local isCurrentTag = false 100 | for j = 1, #ctags do 101 | if t == ctags[j] then 102 | isCurrentTag = true 103 | break 104 | end 105 | end 106 | 107 | if isCurrentTag then 108 | -- check if client is already in the history 109 | -- if not, add it 110 | local addToTable = true 111 | for k = 1, #clients do 112 | if clients[k] == c then 113 | addToTable = false 114 | break 115 | end 116 | end 117 | 118 | 119 | if addToTable then 120 | table.insert(clients, c) 121 | end 122 | end 123 | end 124 | 125 | return clients 126 | end 127 | 128 | -- here we populate altTabTable using the list of clients taken from 129 | -- _M.getClients(). In case we have altTabTable with some value, the list of the 130 | -- old known clients is restored. 131 | function _M.populateAltTabTable() 132 | local clients = _M.getClients() 133 | 134 | if _M.tableLength(_M.altTabTable) then 135 | for ci = 1, #clients do 136 | for ti = 1, #_M.altTabTable do 137 | if _M.altTabTable[ti].client == clients[ci] then 138 | _M.altTabTable[ti].client.opacity = _M.altTabTable[ti].opacity 139 | _M.altTabTable[ti].client.minimized = _M.altTabTable[ti].minimized 140 | break 141 | end 142 | end 143 | end 144 | end 145 | 146 | _M.altTabTable = {} 147 | 148 | for i = 1, #clients do 149 | table.insert(_M.altTabTable, { 150 | client = clients[i], 151 | minimized = clients[i].minimized, 152 | opacity = clients[i].opacity 153 | }) 154 | end 155 | end 156 | 157 | -- If the length of list of clients is not equal to the length of altTabTable, 158 | -- we need to repopulate the array and update the UI. This function does this 159 | -- check. 160 | function _M.clientsHaveChanged() 161 | local clients = _M.getClients() 162 | return _M.tableLength(clients) ~= _M.tableLength(_M.altTabTable) 163 | end 164 | 165 | function _M.createPreviewText(client) 166 | if client.class then 167 | return " - " .. client.class 168 | else 169 | return " - " .. client.name 170 | end 171 | end 172 | 173 | -- Preview is created here. 174 | function _M.clientOpacity() 175 | if not _M.settings.client_opacity then return end 176 | 177 | local opacity = _M.settings.client_opacity_value 178 | if opacity > 1 then opacity = 1 end 179 | for i,data in pairs(_M.altTabTable) do 180 | data.client.opacity = opacity 181 | end 182 | 183 | if client.focus == _M.altTabTable[_M.altTabIndex].client then 184 | -- Let's normalize the value up to 1. 185 | local opacityFocusSelected = _M.settings.client_opacity_value_selected + _M.settings.client_opacity_value_in_focus 186 | if opacityFocusSelected > 1 then opacityFocusSelected = 1 end 187 | client.focus.opacity = opacityFocusSelected 188 | else 189 | -- Let's normalize the value up to 1. 190 | local opacityFocus = _M.settings.client_opacity_value_in_focus 191 | if opacityFocus > 1 then opacityFocus = 1 end 192 | local opacitySelected = _M.settings.client_opacity_value_selected 193 | if opacitySelected > 1 then opacitySelected = 1 end 194 | 195 | client.focus.opacity = opacityFocus 196 | _M.altTabTable[_M.altTabIndex].client.opacity = opacitySelected 197 | end 198 | end 199 | 200 | -- This is called any _M.settings.preview_box_fps milliseconds. In case the list 201 | -- of clients is changed, we need to redraw the whole preview box. Otherwise, a 202 | -- simple widget::updated signal is enough 203 | function _M.updatePreview() 204 | if _M.clientsHaveChanged() then 205 | _M.populateAltTabTable() 206 | _M.preview() 207 | end 208 | 209 | for i = 1, #_M.preview_widgets do 210 | _M.preview_widgets[i]:emit_signal("widget::updated") 211 | end 212 | end 213 | 214 | function _M.cycle(dir) 215 | -- Switch to next client 216 | _M.altTabIndex = _M.altTabIndex + dir 217 | if _M.altTabIndex > #_M.altTabTable then 218 | _M.altTabIndex = 1 -- wrap around 219 | elseif _M.altTabIndex < 1 then 220 | _M.altTabIndex = #_M.altTabTable -- wrap around 221 | end 222 | 223 | _M.updatePreview() 224 | 225 | _M.altTabTable[_M.altTabIndex].client.minimized = false 226 | 227 | if not _M.settings.preview_box and not _M.settings.client_opacity then 228 | client.focus = _M.altTabTable[_M.altTabIndex].client 229 | end 230 | 231 | if _M.settings.client_opacity and _M.preview_wbox.visible then 232 | _M.clientOpacity() 233 | end 234 | 235 | if _M.settings.cycle_raise_client == true then 236 | _M.altTabTable[_M.altTabIndex].client:raise() 237 | end 238 | end 239 | 240 | function _M.preview() 241 | if not _M.settings.preview_box then return end 242 | 243 | -- Apply settings 244 | _M.preview_wbox:set_bg(_M.settings.preview_box_bg) 245 | _M.preview_wbox.border_color = _M.settings.preview_box_border 246 | 247 | -- Make the wibox the right size, based on the number of clients 248 | local n = math.max(7, #_M.altTabTable) 249 | local W = screen[mouse.screen].geometry.width -- + 2 * _M.preview_wbox.border_width 250 | local w = W / n -- widget width 251 | local h = w * 0.75 -- widget height 252 | local textboxHeight = w * 0.125 253 | 254 | local x = screen[mouse.screen].geometry.x - _M.preview_wbox.border_width 255 | local y = screen[mouse.screen].geometry.y + (screen[mouse.screen].geometry.height - h - textboxHeight) / 2 256 | _M.preview_wbox:geometry({x = x, y = y, width = W, height = h + textboxHeight}) 257 | 258 | -- create a list that holds the clients to preview, from left to right 259 | local leftRightTab = {} 260 | local leftRightTabToAltTabIndex = {} -- save mapping from leftRightTab to altTabTable as well 261 | local nLeft 262 | local nRight 263 | if #_M.altTabTable == 2 then 264 | nLeft = 0 265 | nRight = 2 266 | else 267 | nLeft = math.floor(#_M.altTabTable / 2) 268 | nRight = math.ceil(#_M.altTabTable / 2) 269 | end 270 | 271 | for i = 1, nLeft do 272 | table.insert(leftRightTab, _M.altTabTable[#_M.altTabTable - nLeft + i].client) 273 | table.insert(leftRightTabToAltTabIndex, #_M.altTabTable - nLeft + i) 274 | end 275 | for i = 1, nRight do 276 | table.insert(leftRightTab, _M.altTabTable[i].client) 277 | table.insert(leftRightTabToAltTabIndex, i) 278 | end 279 | 280 | -- determine fontsize -> find maximum classname-length 281 | local text, textWidth, textHeight, maxText 282 | local maxTextWidth = 0 283 | local maxTextHeight = 0 284 | local bigFont = textboxHeight / 2 285 | cr:set_font_size(fontSize) 286 | for i = 1, #leftRightTab do 287 | text = _M.createPreviewText(leftRightTab[i]) 288 | textWidth = cr:text_extents(text).width 289 | textHeight = cr:text_extents(text).height 290 | if textWidth > maxTextWidth or textHeight > maxTextHeight then 291 | maxTextHeight = textHeight 292 | maxTextWidth = textWidth 293 | maxText = text 294 | end 295 | end 296 | 297 | while true do 298 | cr:set_font_size(bigFont) 299 | textWidth = cr:text_extents(maxText).width 300 | textHeight = cr:text_extents(maxText).height 301 | 302 | if textWidth < w - textboxHeight and textHeight < textboxHeight then 303 | break 304 | end 305 | 306 | bigFont = bigFont - 1 307 | end 308 | local smallFont = bigFont * _M.settings.preview_box_title_font_size_factor 309 | 310 | _M.preview_widgets = {} 311 | 312 | -- create all the widgets 313 | for i = 1, #leftRightTab do 314 | _M.preview_widgets[i] = wibox.widget.base.make_widget() 315 | _M.preview_widgets[i].fit = function(preview_widget, width, height) 316 | return w, h 317 | end 318 | local c = leftRightTab[i] 319 | _M.preview_widgets[i].draw = function(preview_widget, preview_wbox, cr, width, height) 320 | if width ~= 0 and height ~= 0 then 321 | 322 | local a = 0.8 323 | local overlay = 0.6 324 | local fontSize = smallFont 325 | if c == _M.altTabTable[_M.altTabIndex].client then 326 | a = 0.9 327 | overlay = 0 328 | fontSize = bigFont 329 | end 330 | 331 | local sx, sy, tx, ty 332 | 333 | -- Icons 334 | local icon 335 | if c.icon == nil then 336 | icon = gears.surface(gears.surface.load(_M.noicon)) 337 | else 338 | icon = gears.surface(c.icon) 339 | end 340 | 341 | local iconboxWidth = 0.9 * textboxHeight 342 | local iconboxHeight = iconboxWidth 343 | 344 | -- Titles 345 | cr:select_font_face(unpack(_M.settings.preview_box_title_font)) 346 | cr:set_font_face(cr:get_font_face()) 347 | cr:set_font_size(fontSize) 348 | 349 | text = _M.createPreviewText(c) 350 | textWidth = cr:text_extents(text).width 351 | textHeight = cr:text_extents(text).height 352 | 353 | local titleboxWidth = textWidth + iconboxWidth 354 | local titleboxHeight = textboxHeight 355 | 356 | -- Draw icons 357 | tx = (w - titleboxWidth) / 2 358 | ty = h 359 | sx = iconboxWidth / icon.width 360 | sy = iconboxHeight / icon.height 361 | 362 | cr:translate(tx, ty) 363 | cr:scale(sx, sy) 364 | cr:set_source_surface(icon, 0, 0) 365 | cr:paint() 366 | cr:scale(1/sx, 1/sy) 367 | cr:translate(-tx, -ty) 368 | 369 | -- Draw titles 370 | tx = tx + iconboxWidth 371 | ty = h + (textboxHeight + textHeight) / 2 372 | 373 | cr:set_source_rgba(unpack(_M.settings.preview_box_title_color)) 374 | cr:move_to(tx, ty) 375 | cr:show_text(text) 376 | cr:stroke() 377 | 378 | -- Draw previews 379 | local cg = c:geometry() 380 | if cg.width > cg.height then 381 | sx = a * w / cg.width 382 | sy = math.min(sx, a * h / cg.height) 383 | else 384 | sy = a * h / cg.height 385 | sx = math.min(sy, a * h / cg.width) 386 | end 387 | 388 | tx = (w - sx * cg.width) / 2 389 | ty = (h - sy * cg.height) / 2 390 | 391 | local tmp = gears.surface(c.content) 392 | cr:translate(tx, ty) 393 | cr:scale(sx, sy) 394 | cr:set_source_surface(tmp, 0, 0) 395 | cr:paint() 396 | tmp:finish() 397 | 398 | -- Overlays 399 | cr:scale(1/sx, 1/sy) 400 | cr:translate(-tx, -ty) 401 | cr:set_source_rgba(0,0,0,overlay) 402 | cr:rectangle(tx, ty, sx * cg.width, sy * cg.height) 403 | cr:fill() 404 | end 405 | end 406 | 407 | -- Add mouse handler 408 | _M.preview_widgets[i]:connect_signal("mouse::enter", function() 409 | _M.cycle(leftRightTabToAltTabIndex[i] - _M.altTabIndex) 410 | end) 411 | end 412 | 413 | -- Spacers left and right 414 | local spacer = wibox.widget.base.make_widget() 415 | spacer.fit = function(leftSpacer, width, height) 416 | return (W - w * #_M.altTabTable) / 2, _M.preview_wbox.height 417 | end 418 | spacer.draw = function(preview_widget, preview_wbox, cr, width, height) end 419 | 420 | --layout 421 | preview_layout = wibox.layout.fixed.horizontal() 422 | 423 | preview_layout:add(spacer) 424 | for i = 1, #leftRightTab do 425 | preview_layout:add(_M.preview_widgets[i]) 426 | end 427 | preview_layout:add(spacer) 428 | 429 | _M.preview_wbox:set_widget(preview_layout) 430 | end 431 | 432 | 433 | -- This starts the timer for updating and it shows the preview UI. 434 | function _M.showPreview() 435 | _M.preview_live_timer.timeout = 1 / _M.settings.preview_box_fps 436 | _M.preview_live_timer:connect_signal("timeout", _M.updatePreview) 437 | _M.preview_live_timer:start() 438 | 439 | _M.preview() 440 | _M.preview_wbox.visible = true 441 | 442 | _M.clientOpacity() 443 | end 444 | 445 | function _M.switch(dir, mod_key1, release_key, mod_key2, key_switch) 446 | _M.populateAltTabTable() 447 | 448 | if #_M.altTabTable == 0 then 449 | return 450 | elseif #_M.altTabTable == 1 then 451 | _M.altTabTable[1].client.minimized = false 452 | _M.altTabTable[1].client:raise() 453 | return 454 | end 455 | 456 | -- reset index 457 | _M.altTabIndex = 1 458 | 459 | -- preview delay timer 460 | local previewDelay = _M.settings.preview_box_delay / 1000 461 | _M.previewDelayTimer = timer({timeout = previewDelay}) 462 | _M.previewDelayTimer:connect_signal("timeout", function() 463 | _M.previewDelayTimer:stop() 464 | _M.showPreview() 465 | end) 466 | _M.previewDelayTimer:start() 467 | 468 | -- Now that we have collected all windows, we should run a keygrabber 469 | -- as long as the user is alt-tabbing: 470 | keygrabber.run( 471 | function (mod, key, event) 472 | -- Stop alt-tabbing when the alt-key is released 473 | if gears.table.hasitem(mod, mod_key1) then 474 | if (key == release_key or key == "Escape") and event == "release" then 475 | if _M.preview_wbox.visible == true then 476 | _M.preview_wbox.visible = false 477 | _M.preview_live_timer:stop() 478 | else 479 | _M.previewDelayTimer:stop() 480 | end 481 | 482 | if key == "Escape" then 483 | for i = 1, #_M.altTabTable do 484 | _M.altTabTable[i].client.opacity = _M.altTabTable[i].opacity 485 | _M.altTabTable[i].client.minimized = _M.altTabTable[i].minimized 486 | end 487 | else 488 | -- Raise clients in order to restore history 489 | local c 490 | for i = 1, _M.altTabIndex - 1 do 491 | c = _M.altTabTable[_M.altTabIndex - i].client 492 | if not _M.altTabTable[i].minimized then 493 | c:raise() 494 | client.focus = c 495 | end 496 | end 497 | 498 | -- raise chosen client on top of all 499 | c = _M.altTabTable[_M.altTabIndex].client 500 | c:raise() 501 | client.focus = c 502 | 503 | -- restore minimized clients 504 | for i = 1, #_M.altTabTable do 505 | if i ~= _M.altTabIndex and _M.altTabTable[i].minimized then 506 | _M.altTabTable[i].client.minimized = true 507 | end 508 | _M.altTabTable[i].client.opacity = _M.altTabTable[i].opacity 509 | end 510 | end 511 | 512 | keygrabber.stop() 513 | 514 | elseif key == key_switch and event == "press" then 515 | if gears.table.hasitem(mod, mod_key2) then 516 | -- Move to previous client on Shift-Tab 517 | _M.cycle(-1) 518 | else 519 | -- Move to next client on each Tab-press 520 | _M.cycle( 1) 521 | end 522 | end 523 | end 524 | end 525 | ) 526 | 527 | -- switch to next client 528 | _M.cycle(dir) 529 | 530 | end -- function altTab 531 | 532 | return {switch = _M.switch, settings = _M.settings} 533 | -------------------------------------------------------------------------------- /noicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/berlam/awesome-switcher/5d76e415af98801fca21e08e10d6b0fe49d54dfb/noicon.png -------------------------------------------------------------------------------- /screenshot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/berlam/awesome-switcher/5d76e415af98801fca21e08e10d6b0fe49d54dfb/screenshot.gif --------------------------------------------------------------------------------