├── screenshot.png ├── .travis.yml ├── .luacheckrc ├── README.md ├── LICENSE └── init.lua /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blueyed/awesome-cyclefocus/HEAD/screenshot.png -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # Based on https://github.com/mpeterv/hererocks 2 | 3 | language: python 4 | sudo: false 5 | 6 | env: 7 | - LUA="lua 5.3" 8 | 9 | install: 10 | - pip install hererocks 11 | - hererocks env --$LUA -rlatest 12 | - source env/bin/activate 13 | - luarocks install luacheck 14 | 15 | script: 16 | - luacheck *.lua 17 | 18 | branches: 19 | only: 20 | - master 21 | -------------------------------------------------------------------------------- /.luacheckrc: -------------------------------------------------------------------------------- 1 | -- Only allow symbols available in all Lua versions 2 | std = "min" 3 | 4 | -- Get rid of "unused argument self"-warnings 5 | self = false 6 | 7 | -- The default config may set global variables 8 | -- files["init.lua"].allow_defined_top = true 9 | 10 | -- This file itself 11 | files[".luacheckrc"].ignore = {"111", "112", "131"} 12 | 13 | -- Global objects defined by the C code 14 | read_globals = { 15 | "awesome", 16 | "button", 17 | "client", 18 | "dbus", 19 | "drawable", 20 | "drawin", 21 | "key", 22 | "keygrabber", 23 | "mousegrabber", 24 | "root", 25 | "selection", 26 | "tag", 27 | "window", 28 | -- Global settings. 29 | "modkey", 30 | } 31 | 32 | -- screen may not be read-only, because newer luacheck versions complain about 33 | -- screen[1].tags[1].selected = true. 34 | -- The same happens with the following code: 35 | -- local tags = mouse.screen.tags 36 | -- tags[7].index = 4 37 | -- client may not be read-only due to client.focus. 38 | globals = { 39 | "screen", 40 | "mouse", 41 | "client" 42 | } 43 | 44 | -- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 45 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | **Table of Contents** *generated with [DocToc](http://doctoc.herokuapp.com/)* 4 | 5 | - [awesome-cyclefocus](#awesome-cyclefocus) 6 | - [Screenshot](#screenshot) 7 | - [Installation](#installation) 8 | - [Keybindings](#keybindings) 9 | - [Example 1: cycle through all windows](#example-1-cycle-through-all-windows) 10 | - [Example 2: cycle through windows on the same screen and tag](#example-2-cycle-through-windows-on-the-same-screen-and-tag) 11 | - [`cycle_filters`](#cycle_filters) 12 | - [Predefined filters](#predefined-filters) 13 | - [Example 3: cycle through clients with the same class](#example-3-cycle-through-clients-with-the-same-class) 14 | - [Reference](#reference) 15 | - [Configuration](#configuration) 16 | - [Settings](#a-namesettingsasettings) 17 | - [Status](#status) 18 | - [Bugs, Feedback and Support](#bugs-feedback-and-support) 19 | - [Donate](#donate) 20 | 21 | 22 | 23 | # awesome-cyclefocus 24 | 25 | awesome-cyclefocus is a module/plugin for the [awesome window 26 | manager][], which provides methods to cycle through 27 | the most recently used clients (typically known as Alt-Tab). 28 | 29 | It allows to easily filter the list of windows to be cycled through, e.g. by 30 | screen, tags, window class, name/title etc. 31 | 32 | ## Screenshot 33 | 34 | ![Screenshot](screenshot.png) 35 | 36 | Please note that the graphical aspect needs to be developed, but since people 37 | like screenshots… 38 | 39 | ## Installation 40 | 41 | *Requirements:* awesome-cyclefocus requires Awesome 4+. 42 | 43 | Create a subdirectory `cyclefocus` in your awesome config directory, e.g. 44 | 45 | cd ~/.config/awesome 46 | git clone https://github.com/blueyed/awesome-cyclefocus cyclefocus 47 | 48 | Then include it from your config file (`~/.config/awesome/rc.lua`), somewhere 49 | at the beginning: 50 | 51 | ```lua 52 | local cyclefocus = require('cyclefocus') 53 | ``` 54 | 55 | ## Keybindings 56 | 57 | Then you can define the keybindings. 58 | 59 | While you can use it with the `globalkeys` configuration, you should use 60 | the `clientkeys` table for any bindings which use `cycle_filters`. 61 | 62 | The default for `modkey+Tab` in awesome (3.5.2) is: 63 | ```lua 64 | awful.key({ modkey, }, "Tab", 65 | function () 66 | awful.client.focus.history.previous() 67 | if client.focus then 68 | client.focus:raise() 69 | end 70 | end), 71 | ``` 72 | You should disable it (e.g. by commenting it out), and add your method below. 73 | 74 | Here are three methods to setup the key mappings: 75 | 76 | ### Example 1: cycle through all windows 77 | 78 | Setup `modkey+Tab` to cycle through all windows (assuming `modkey` is 79 | `Mod4`/`Super_L`, which is the default): 80 | 81 | ```lua 82 | -- modkey+Tab: cycle through all clients. 83 | awful.key({ modkey }, "Tab", function(c) 84 | cyclefocus.cycle({modifier="Super_L"}) 85 | end), 86 | -- modkey+Shift+Tab: backwards 87 | awful.key({ modkey, "Shift" }, "Tab", function(c) 88 | cyclefocus.cycle({modifier="Super_L"}) 89 | end), 90 | ``` 91 | 92 | You can pass a table of optional arguments. 93 | We need to pass the modifier (as seen by awesome's `keygrabber`) here. 94 | Internally the direction gets set according to if the `Shift` modifier key 95 | is present, so that the second definition is only necessary to trigger it in 96 | the opposite direction from the beginning. 97 | 98 | See the `init.lua` file (or the [settings section below](#settings)) for a full 99 | reference. 100 | 101 | ### Example 2: cycle through windows on the same screen and tag 102 | 103 | You can use `cyclefocus.key` (a wrapper around `awful.key`) like this: 104 | 105 | ```lua 106 | -- Alt-Tab: cycle through clients on the same screen. 107 | -- This must be a clientkeys mapping to have source_c available in the callback. 108 | cyclefocus.key({ "Mod1", }, "Tab", { 109 | -- cycle_filters as a function callback: 110 | -- cycle_filters = { function (c, source_c) return c.screen == source_c.screen end }, 111 | 112 | -- cycle_filters from the default filters: 113 | cycle_filters = { cyclefocus.filters.same_screen, cyclefocus.filters.common_tag }, 114 | keys = {'Tab', 'ISO_Left_Tab'} -- default, could be left out 115 | }), 116 | ``` 117 | 118 | The first two arguments are the same as with `awful.key`: a list of modifiers 119 | and the key. Then the table with optional arguments to `cyclefocus.cycle()` 120 | follows. 121 | (here the `modifier` argument is not required, because it gets used from 122 | the first argument). 123 | 124 | NOTE: this needs to go into `clientkeys`. 125 | 126 | #### `cycle_filters` 127 | 128 | In this case the `cycle_filters` argument is used, which is a list of filters 129 | to apply while cycling through the focus history: it gets passed a `client` 130 | object, and optionally another `client` object for the source (where the 131 | cycling started). 132 | For the source client to be available, it needs to be an entry in the 133 | `clientkeys` table. 134 | 135 | You can pass functions here, or use one of the predefined filters: 136 | 137 | #### Predefined filters 138 | 139 | The following filters are available by default: 140 | 141 | ```lua 142 | -- A set of default filters, which can be used for cyclefocus.cycle_filters. 143 | cyclefocus.filters = { 144 | -- Filter clients on the same screen. 145 | same_screen = function (c, source_c) return c.screen == source_c.screen end, 146 | 147 | same_class = function (c, source_c) 148 | return c.class == source_c.class 149 | end, 150 | 151 | -- Only marked clients (via awful.client.mark and .unmark). 152 | marked = function (c, source_c) 153 | return awful.client.ismarked(c) 154 | end, 155 | 156 | common_tag = function (c, source_c) 157 | for _, t in pairs(c:tags()) do 158 | for _, t2 in pairs(source_c:tags()) do 159 | if t == t2 then 160 | cyclefocus.debug("Filter: client shares tag '" .. t.name .. " with " .. c.name) 161 | return true 162 | end 163 | end 164 | end 165 | return false 166 | end 167 | } 168 | ``` 169 | 170 | ### Example 3: cycle through clients with the same class 171 | 172 | The following will cycle through windows, which share the same window class 173 | (e.g. only Firefox windows, when starting from a Firefox window): 174 | 175 | ```lua 176 | -- Alt-^: cycle through clients with the same class name. 177 | cyclefocus.key({ "Mod1", }, "#49", 1, { 178 | cycle_filter = function (c, source_c) return c.class == source_c.class end, 179 | keys = { "°", "^" }, -- the keys to be handled, wouldn't be required if the keycode was available in keygrabber. 180 | }), 181 | cyclefocus.key({ "Mod1", "Shift", }, "#49", -1, { -- keycode #49 => ^/° on german keyboard, upper left below Escape and next to 1. 182 | cycle_filter = function (c, source_c) return c.class == source_c.class end, 183 | keys = { "°", "^" }, -- the keys to be handled, wouldn't be required if the keycode was available in keygrabber. 184 | }), 185 | ``` 186 | 187 | The key argument uses the keycode notation (`#49`) and refers (probably) to the key 188 | below Escape, above Tab and next to the first digit (1). 189 | It should be the same shortcut, as what Ubuntu's Unity uses to cycle through 190 | the windows of a single application. 191 | 192 | NOTE: You need to pass the keys this refers to via the `keys` argument, so that 193 | the keygrabber considers those only. 194 | In the example above, `^` and `°` refers to the key on the German keyboard 195 | layout (un-shifted and shifted, i.e. with Shift pressed and released). 196 | 197 | NOTE: this needs to go into `clientkeys`. 198 | 199 | ## Reference 200 | 201 | ### Configuration 202 | 203 | awesome-cyclefocus can be configured by passing optional arguments to the 204 | `cyclefocus.cycle` or `cyclefocus.key` functions, or by setting defaults, after 205 | loading `cyclefocus`: 206 | 207 | #### Settings 208 | 209 | The default settings are: 210 | 211 | ```lua 212 | cyclefocus = { 213 | -- Should clients get shown during cycling? 214 | -- This should be a function (or `false` to disable showing clients), which 215 | -- receives a client object, and can make use of `cyclefocus.show_client` 216 | -- (the default implementation). 217 | show_clients = true, 218 | -- Should clients get focused during cycling? 219 | -- This is required for the tasklist to highlight the selected entry. 220 | focus_clients = true, 221 | -- Should the selected client get raised? 222 | -- This calls `cyclefocus.raise_client_without_focus` by default, which you 223 | -- can use when overriding this with a function (that gets the client as 224 | -- argument). 225 | raise_client = true, 226 | -- Should the mouse pointer be moved away during cycling? 227 | -- This is normally done to avoid interference from sloppy focus handling, 228 | -- but can be disabled if you do not use sloppy focus. 229 | move_mouse_pointer = true, 230 | 231 | -- How many entries should get displayed before and after the current one? 232 | display_next_count = 3, 233 | display_prev_count = 3, 234 | 235 | -- Default preset to use for entries. 236 | -- `preset_for_offset` (below) gets added to it. 237 | default_preset = {}, 238 | 239 | --- Templates for entries in the list. 240 | -- The following arguments get passed to a callback: 241 | -- - client: the current client object. 242 | -- - idx: index number of current entry in clients list. 243 | -- - displayed_list: the list of entries in the list, possibly filtered. 244 | preset_for_offset = { 245 | -- Default callback, which will gets applied for all offsets (first). 246 | default = function (preset, args) 247 | -- Default font and icon size (gets overwritten for current/0 index). 248 | preset.font = 'sans 8' 249 | preset.icon_size = dpi(24) 250 | preset.text = escape_markup(cyclefocus.get_client_title(args.client, false)) 251 | end, 252 | 253 | -- Preset for current entry. 254 | ["0"] = function (preset, args) 255 | preset.font = 'sans 14' 256 | preset.icon_size = dpi(36) 257 | preset.text = escape_markup(cyclefocus.get_client_title(args.client, true)) 258 | -- Add screen number if there is more than one. 259 | if screen.count() > 1 then 260 | preset.text = preset.text .. " [screen " .. tostring(args.client.screen.index) .. "]" 261 | end 262 | preset.text = preset.text .. " [#" .. args.idx .. "] " 263 | preset.text = '' .. preset.text .. '' 264 | end, 265 | 266 | -- You can refer to entries by their offset. 267 | -- ["-1"] = function (preset, args) 268 | -- -- preset.icon_size = 32 269 | -- end, 270 | -- ["1"] = function (preset, args) 271 | -- -- preset.icon_size = 32 272 | -- end 273 | }, 274 | 275 | -- Default builtin filters. 276 | -- (meant to get applied always, but you could override them) 277 | cycle_filters = { 278 | function(c, source_c) return not c.minimized end, --luacheck: no unused args 279 | }, 280 | 281 | -- Experimental: Width of icon column ("max_icon_size", used for margin). 282 | -- This could be "margin" etc instead, but currently only the width for the 283 | -- current entry is known. 284 | icon_col_width = dpi(36), 285 | 286 | -- EXPERIMENTAL: only add clients to the history that have been focused by 287 | -- cyclefocus. 288 | -- This allows to switch clients using other methods, but those are then 289 | -- not added to cyclefocus' internal history. 290 | -- The get_next_client function will then first consider the most recent 291 | -- entry in the history stack, if it's not focused currently. 292 | -- 293 | -- You can use cyclefocus.history.add to manually add an entry, or 294 | -- cyclefocus.history.append if you want to add it to the end of the stack. 295 | -- This might be useful in a request::activate signal handler. 296 | -- only_add_internal_focus_changes_to_history = true, 297 | 298 | -- The filter to ignore clients altogether (get not added to the history stack). 299 | -- This is different from the cycle_filters. 300 | -- The function should return true / the client if it's ok, nil otherwise. 301 | filter_focus_history = awful.client.focus.filter, 302 | 303 | -- Display notifications while cycling? 304 | -- WARNING: without raise_clients this will not make sense probably! 305 | display_notifications = true, 306 | 307 | -- Debugging: messages get printed, and should show up in ~/.xsession-errors etc. 308 | -- 1: enable, 2: verbose, 3: very verbose, 4: much verbose. 309 | debug_level = 0, 310 | -- Use naughty notifications for debugging (additional to printing)? 311 | debug_use_naughty_notify = false, 312 | } 313 | ``` 314 | 315 | You can change them like this: 316 | ```lua 317 | cyclefocus = require("cyclefocus") 318 | cyclefocus.debug_level = 2 319 | ``` 320 | 321 | You can also use custom settings when calling `cyclefocus.cycle` or 322 | `cyclefocus.key` via `args`, e.g. to not display notifications when switching 323 | between clients on the same tag: 324 | ```lua 325 | cyclefocus.key({ modkey, }, "Tab", 1, { 326 | cycle_filters = { cyclefocus.filters.common_tag }, 327 | display_notifications = false, 328 | modifier='Super_L', keys={'Tab', 'ISO_Left_Tab'} 329 | }), 330 | cyclefocus.key({ modkey, "Shift", }, "Tab", 1, { 331 | cycle_filters = { cyclefocus.filters.common_tag }, 332 | display_notifications = false, 333 | modifier='Super_L', keys={'Tab', 'ISO_Left_Tab'} 334 | }), 335 | ``` 336 | 337 | ## Status 338 | 339 | Stable: it works well for me and others. 340 | Internals, default settings and behavior might still change. 341 | 342 | I came up with this while dipping my toes in the waters of awesome. If you have 343 | problems, please enable `cyclefocus.debug_level` (goes up to 3) and report your 344 | findings on the [Github issue tracker][]. 345 | 346 | # Bugs, Feedback and Support 347 | 348 | You can report bugs and wishes at the [Github issue tracker][]. 349 | 350 | Pull requests would be awesome! :) 351 | 352 | ## Donate 353 | 354 | [![Flattr this git repo](http://api.flattr.com/button/flattr-badge-large.png)](https://flattr.com/submit/auto?user_id=blueyed&url=https://github.com/blueyed/awesome-cyclefocus&title=awesome-cyclefocus&language=en&tags=github&category=software) 355 | 356 | Bitcoin: 16EVhEpXxfNiT93qT2uxo4DsZSHzNdysSp 357 | 358 | [awesome window manager]: http://awesome.naquadah.org/ 359 | [Github issue tracker]: https://github.com/blueyed/awesome-cyclefocus/issues 360 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | {description} 294 | Copyright (C) {year} {fullname} 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | {signature of Ty Coon}, 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /init.lua: -------------------------------------------------------------------------------- 1 | --- Cycle through recently focused clients (Alt-Tab and more). 2 | -- 3 | -- Author: http://daniel.hahler.de 4 | -- Github: https://github.com/blueyed/awesome-cyclefocus 5 | 6 | local awful = require('awful') 7 | -- local setmetatable = setmetatable 8 | local naughty = require("naughty") 9 | local table = table 10 | local tostring = tostring 11 | local floor = require("math").floor 12 | local capi = { 13 | -- tag = tag, 14 | client = client, 15 | keygrabber = keygrabber, 16 | -- mousegrabber = mousegrabber, 17 | mouse = mouse, 18 | screen = screen, 19 | awesome = awesome, 20 | } 21 | local wibox = require("wibox") 22 | 23 | local xresources = require("beautiful").xresources 24 | local dpi = xresources and xresources.apply_dpi or function() end 25 | 26 | --- Escape pango markup, taken from naughty. 27 | local escape_markup = function(s) 28 | local escape_pattern = "[<>&]" 29 | local escape_subs = { ['<'] = "<", ['>'] = ">", ['&'] = "&" } 30 | return s:gsub(escape_pattern, escape_subs) 31 | end 32 | 33 | 34 | -- Configuration. This can be overridden: global or via args to cyclefocus.cycle. 35 | local cyclefocus 36 | cyclefocus = { 37 | -- Should clients get shown during cycling? 38 | -- This should be a function (or `false` to disable showing clients), which 39 | -- receives a client object, and can make use of `cyclefocus.show_client` 40 | -- (the default implementation). 41 | show_clients = true, 42 | -- Should clients get focused during cycling? 43 | -- This is required for the tasklist to highlight the selected entry. 44 | focus_clients = true, 45 | -- Should the selected client get raised? 46 | -- This calls `cyclefocus.raise_client_without_focus` by default, which you 47 | -- can use when overriding this with a function (that gets the client as 48 | -- argument). 49 | raise_client = true, 50 | -- Should the mouse pointer be moved away during cycling? 51 | -- This is normally done to avoid interference from sloppy focus handling, 52 | -- but can be disabled if you do not use sloppy focus. 53 | move_mouse_pointer = true, 54 | 55 | -- How many entries should get displayed before and after the current one? 56 | display_next_count = 3, 57 | display_prev_count = 3, 58 | 59 | -- Default preset to use for entries. 60 | -- `preset_for_offset` (below) gets added to it. 61 | default_preset = {}, 62 | 63 | --- Templates for entries in the list. 64 | -- The following arguments get passed to a callback: 65 | -- - client: the current client object. 66 | -- - idx: index number of current entry in clients list. 67 | -- - displayed_list: the list of entries in the list, possibly filtered. 68 | preset_for_offset = { 69 | -- Default callback, which will gets applied for all offsets (first). 70 | default = function (preset, args) 71 | -- Default font and icon size (gets overwritten for current/0 index). 72 | preset.font = 'sans 8' 73 | preset.icon_size = dpi(24) 74 | preset.text = escape_markup(cyclefocus.get_client_title(args.client, false)) 75 | end, 76 | 77 | -- Preset for current entry. 78 | ["0"] = function (preset, args) 79 | preset.font = 'sans 14' 80 | preset.icon_size = dpi(36) 81 | preset.text = escape_markup(cyclefocus.get_client_title(args.client, true)) 82 | -- Add screen number if there is more than one. 83 | if screen.count() > 1 then 84 | preset.text = preset.text .. " [screen " .. tostring(args.client.screen.index) .. "]" 85 | end 86 | preset.text = preset.text .. " [#" .. args.idx .. "] " 87 | preset.text = '' .. preset.text .. '' 88 | end, 89 | 90 | -- You can refer to entries by their offset. 91 | -- ["-1"] = function (preset, args) 92 | -- -- preset.icon_size = 32 93 | -- end, 94 | -- ["1"] = function (preset, args) 95 | -- -- preset.icon_size = 32 96 | -- end 97 | }, 98 | 99 | -- Default builtin filters. 100 | -- (meant to get applied always, but you could override them) 101 | cycle_filters = { 102 | function(c, source_c) return not c.minimized end, --luacheck: no unused args 103 | }, 104 | 105 | -- Experimental: Width of icon column ("max_icon_size", used for margin). 106 | -- This could be "margin" etc instead, but currently only the width for the 107 | -- current entry is known. 108 | icon_col_width = dpi(36), 109 | 110 | -- EXPERIMENTAL: only add clients to the history that have been focused by 111 | -- cyclefocus. 112 | -- This allows to switch clients using other methods, but those are then 113 | -- not added to cyclefocus' internal history. 114 | -- The get_next_client function will then first consider the most recent 115 | -- entry in the history stack, if it's not focused currently. 116 | -- 117 | -- You can use cyclefocus.history.add to manually add an entry, or 118 | -- cyclefocus.history.append if you want to add it to the end of the stack. 119 | -- This might be useful in a request::activate signal handler. 120 | -- XXX: needs to be also handled in request::activate then probably. 121 | -- TODO: make this configurable during runtime of the binding, e.g. by 122 | -- flagging entries in the stack or using different stacks. 123 | -- only_add_internal_focus_changes_to_history = true, 124 | 125 | -- The filter to ignore clients altogether (get not added to the history stack). 126 | -- This is different from the cycle_filters. 127 | -- The function should return true / the client if it's ok, nil otherwise. 128 | filter_focus_history = awful.client.focus.filter, 129 | 130 | -- Display notifications while cycling? 131 | -- WARNING: without raise_clients this will not make sense probably! 132 | display_notifications = true, 133 | 134 | -- Debugging: messages get printed, and should show up in ~/.xsession-errors etc. 135 | -- 1: enable, 2: verbose, 3: very verbose, 4: much verbose. 136 | debug_level = 0, 137 | -- Use naughty notifications for debugging (additional to printing)? 138 | debug_use_naughty_notify = false, 139 | } 140 | 141 | 142 | -- Wrap icon widget with margin. 143 | local get_icon_wrapper_widget = function(w, preset) 144 | local icon_margin = preset.icon_margin or dpi(5) 145 | local icon_center_margin = icon_margin + math.max(0, (cyclefocus.icon_col_width - preset.icon_size)/2) 146 | local iconmarginbox = wibox.container.margin(w) 147 | iconmarginbox:set_left(icon_center_margin) 148 | iconmarginbox:set_right(icon_center_margin) 149 | iconmarginbox:set_top(icon_margin) 150 | iconmarginbox:set_bottom(icon_margin) 151 | return iconmarginbox 152 | end 153 | 154 | -- Get widget for client icon. Uses awful.widget.clienticon if available. 155 | if awful.widget.clienticon then 156 | cyclefocus.get_client_icon_widget = function(c, preset) 157 | local w = awful.widget.clienticon(c) 158 | w:set_forced_width(preset.icon_size) 159 | w:set_forced_height(preset.icon_size) 160 | return get_icon_wrapper_widget(w, preset) 161 | end 162 | else 163 | local has_gears, gears = pcall(require, 'gears') 164 | local icon_loader 165 | if has_gears then 166 | -- Use gears to prevent memory leaking. 167 | icon_loader = gears.surface.load 168 | else 169 | icon_loader = function(icon) return icon end 170 | end 171 | 172 | cyclefocus.get_client_icon_widget = function(c, preset) 173 | if not c.icon then 174 | return 175 | end 176 | local icon = icon_loader(c.icon) 177 | local icon_size = preset.icon_size 178 | 179 | -- Code originally via naughty. 180 | local cairo = require("lgi").cairo 181 | local scaled = cairo.ImageSurface(cairo.Format.ARGB32, icon_size, icon_size) 182 | local cr = cairo.Context(scaled) 183 | cr:scale(preset.icon_size / icon:get_height(), preset.icon_size / icon:get_width()) 184 | cr:set_source_surface(icon, 0, 0) 185 | cr:paint() 186 | icon = scaled 187 | 188 | local iconbox = wibox.widget.imagebox() 189 | iconbox:set_resize(false) 190 | iconbox:set_image(icon) 191 | return get_icon_wrapper_widget(iconbox, preset) 192 | end 193 | end 194 | 195 | -- A set of default filters, which can be used for cyclefocus.cycle_filters. 196 | cyclefocus.filters = { 197 | -- Filter clients on the same screen. 198 | same_screen = function (c, source_c) 199 | return (c.screen or capi.mouse.screen) == source_c.screen 200 | end, 201 | 202 | same_class = function (c, source_c) 203 | return c.class == source_c.class 204 | end, 205 | 206 | -- Only marked clients (via awful.client.mark and .unmark). 207 | marked = function (c, source_c) --luacheck: no unused args 208 | return awful.client.ismarked(c) 209 | end, 210 | 211 | common_tag = function (c, source_c) 212 | if c == source_c then 213 | return true 214 | end 215 | cyclefocus.debug("common_tag_filter\n" 216 | .. cyclefocus.get_object_name(c) .. " <=> " .. cyclefocus.get_object_name(source_c), 3) 217 | for _, t in pairs(c:tags()) do 218 | for _, t2 in pairs(source_c:tags()) do 219 | if t == t2 then 220 | cyclefocus.debug('common_tag_filter: client shares tag "' 221 | .. cyclefocus.get_object_name(t) 222 | .. '" with "' .. cyclefocus.get_object_name(c)..'"', 2) 223 | return true 224 | end 225 | end 226 | end 227 | return false 228 | end, 229 | 230 | -- EXPERIMENTAL: 231 | -- Skip clients that were added through "focus" signal. 232 | -- Replaces only_add_internal_focus_changes_to_history. 233 | not_through_focus_signal = function (c, source_c) --luacheck: no unused args 234 | local attribs = cyclefocus.history.attribs(c) 235 | return not attribs.source or attribs.source ~= "focus" 236 | end, 237 | } 238 | 239 | local ignore_focus_signal = false -- Flag to ignore the focus signal internally. 240 | local showing_client 241 | 242 | -- This can be used in signal handlers to e.g. skip changing border_width. 243 | cyclefocus.get_shown_client = function() 244 | return showing_client 245 | end 246 | 247 | -- Debug function. Set focusstyle.debug to activate it. {{{ 248 | cyclefocus.debug = function(msg, level) 249 | level = level or 1 250 | if not cyclefocus.debug_level or cyclefocus.debug_level < level then 251 | return 252 | end 253 | 254 | if cyclefocus.debug_use_naughty_notify then 255 | naughty.notify({ 256 | -- TODO: use indenting 257 | -- text = tostring(msg)..' ['..tostring(level)..']', 258 | text = tostring(msg), 259 | timeout = 10, 260 | }) 261 | end 262 | print("cyclefocus: " .. msg) 263 | end 264 | 265 | local get_object_name = function (o) 266 | if not o then 267 | return '[no object]' 268 | elseif not o.valid then 269 | return '[invalid object]' 270 | elseif not o.name then 271 | return '[no object name]' 272 | else 273 | return o.name 274 | end 275 | end 276 | cyclefocus.get_object_name = get_object_name 277 | 278 | local utf8_truncate = function (s, length) 279 | if length == 0 then 280 | return s 281 | end 282 | local n = 0 283 | for i = 1, s:len() do 284 | local b = s:byte(i) 285 | if b < 0x80 or b >= 0xc0 then 286 | n = n + 1 287 | if n > length then 288 | return s:sub(1, i - 1) .. '…' 289 | end 290 | end 291 | end 292 | return s 293 | end 294 | 295 | cyclefocus.get_client_title = function (c, current) --luacheck: no unused args 296 | -- Use get_object_name to handle .name=nil. 297 | local title = cyclefocus.get_object_name(c) 298 | return utf8_truncate(title, 80) 299 | end 300 | -- }}} 301 | 302 | 303 | -- Internal functions to handle the focus history. {{{ 304 | -- Based on awful.client.focus.history. 305 | local history = { 306 | stack = {} 307 | } 308 | 309 | --- Remove a client from the history stack. 310 | -- @tparam table Client. 311 | function history.delete(c) 312 | local k = history._get_key(c) 313 | if k then 314 | table.remove(history.stack, k) 315 | end 316 | end 317 | 318 | function history._get_key(c) 319 | for k, v in ipairs(history.stack) do 320 | if v[1] == c then 321 | return k 322 | end 323 | end 324 | end 325 | 326 | function history.attribs(c) 327 | local k = history._get_key(c) 328 | if k then 329 | return history.stack[k][2] 330 | end 331 | end 332 | 333 | function history.clear() 334 | history.stack = {} 335 | end 336 | 337 | -- @param filter: a function / boolean to filter clients: true means to add it. 338 | function history.add(c, filter, append, attribs) 339 | filter = filter or cyclefocus.filter_focus_history 340 | append = append or false 341 | attribs = attribs or {} 342 | 343 | -- Less verbose debugging during startup/restart. 344 | cyclefocus.debug("history.add: " .. get_object_name(c), capi.awesome.startup and 4 or 2) 345 | 346 | if filter and type(filter) == "function" then 347 | if not filter(c) then 348 | cyclefocus.debug("Filtered! " .. get_object_name(c), 2) 349 | return true 350 | end 351 | end 352 | 353 | -- Remove any existing entries from the stack. 354 | history.delete(c) 355 | 356 | if append then 357 | table.insert(history.stack, {c, attribs}) 358 | else 359 | table.insert(history.stack, 1, {c, attribs}) 360 | end 361 | 362 | -- Manually add it to awesome's internal history (where we've removed the 363 | -- signal from). 364 | awful.client.focus.history.add(c) 365 | end 366 | 367 | function history.movetotop(c) 368 | local attribs = history.attribs(c) 369 | history.add(c, true, false, attribs) 370 | end 371 | 372 | function history.append(c, filter, attribs) 373 | return history.add(c, filter, true, attribs) 374 | end 375 | 376 | --- Save the history into a X property. 377 | function history.persist() 378 | local ids = {} 379 | for _, v in ipairs(history.stack) do 380 | table.insert(ids, v[1].window) 381 | end 382 | local xprop = table.concat(ids, " ") 383 | capi.awesome.set_xproperty('awesome.cyclefocus.history', xprop) 384 | end 385 | 386 | --- Load history from the X property. 387 | function history.load() 388 | local xprop = capi.awesome.get_xproperty('awesome.cyclefocus.history') 389 | if not xprop or xprop == "" then 390 | return 391 | end 392 | 393 | local cls = capi.client.get() 394 | local ids = {} 395 | for id in string.gmatch(xprop, "%S+") do 396 | table.insert(ids, 1, id) 397 | end 398 | for _,window in ipairs(ids) do 399 | for _,c in pairs(cls) do 400 | if tonumber(window) == c.window then 401 | history.add(c, true, false, {source="load"}) 402 | break 403 | end 404 | end 405 | end 406 | end 407 | 408 | -- Persist history when restarting awesome. 409 | capi.awesome.register_xproperty('awesome.cyclefocus.history', 'string') 410 | capi.awesome.connect_signal("exit", function(restarting) 411 | ignore_focus_signal = true 412 | if restarting then 413 | history.persist() 414 | end 415 | end) 416 | 417 | -- On startup / restart: load the history and jump to the last focused client. 418 | cyclefocus.load_on_startup = function() 419 | capi.awesome.disconnect_signal("refresh", cyclefocus.load_on_startup) 420 | 421 | ignore_focus_signal = true 422 | history.load() 423 | if history.stack[1] then 424 | showing_client = history.stack[1][1] 425 | showing_client:jump_to() 426 | showing_client = nil 427 | end 428 | ignore_focus_signal = false 429 | end 430 | capi.awesome.connect_signal("refresh", cyclefocus.load_on_startup) 431 | 432 | -- Export it. At least history.add should be. 433 | cyclefocus.history = history 434 | -- }}} 435 | 436 | -- Connect to signals. {{{ 437 | -- Add clients that got focused to the history stack, 438 | -- but not when we are cycling through the clients ourselves. 439 | capi.client.connect_signal("focus", function (c) 440 | if ignore_focus_signal or capi.awesome.startup then 441 | cyclefocus.debug("Ignoring focus signal: " .. get_object_name(c), 4) 442 | return 443 | end 444 | history.add(c, nil, nil, {source="focus"}) 445 | end) 446 | 447 | -- Disable awesome's internal history handler to handle `ignore_focus_signal`. 448 | -- https://github.com/awesomeWM/awesome/pull/906. 449 | if awful.client.focus.history.disable_tracking then 450 | awful.client.focus.history.disable_tracking() 451 | else 452 | capi.client.disconnect_signal("focus", awful.client.focus.history.add) 453 | end 454 | 455 | capi.client.connect_signal("manage", function (c) 456 | if ignore_focus_signal then 457 | cyclefocus.debug("Ignoring focus signal (manage): " .. get_object_name(c), 2) 458 | return 459 | end 460 | 461 | -- During startup: append any clients, to make them known, 462 | -- but not override history.load etc. 463 | if capi.awesome.startup then 464 | history.append(c) 465 | else 466 | history.add(c, nil, false, {source="manage"}) 467 | end 468 | end) 469 | 470 | capi.client.connect_signal("unmanage", function (c) 471 | history.delete(c) 472 | end) 473 | -- }}} 474 | 475 | -- Raise a client (does not include focusing). 476 | -- Default implementation for raise_client option. 477 | -- NOTE: awful.client.jumpto also focuses the screen / resets the mouse. 478 | -- See https://github.com/blueyed/awesome-cyclefocus/issues/6 479 | -- Based on awful.client.jumpto, without the code for mouse. 480 | -- Calls tag:viewonly always to update the tag history, also when 481 | -- the client is visible. 482 | cyclefocus.raise_client_without_focus = function(c) 483 | -- Try to make client visible, this also covers e.g. sticky 484 | local t = c:tags()[1] 485 | if t then 486 | t:view_only() 487 | end 488 | c:jump_to() 489 | end 490 | 491 | 492 | local restore_callback_show_client 493 | local show_client_restore_client_props = {} 494 | client.connect_signal("unmanage", function (c) 495 | if c == restore_callback_show_client then 496 | restore_callback_show_client = nil 497 | end 498 | if c == showing_client then 499 | showing_client = nil 500 | end 501 | 502 | if show_client_restore_client_props[c] then 503 | show_client_restore_client_props[c] = nil 504 | end 505 | end) 506 | 507 | 508 | local beautiful = require("beautiful") 509 | 510 | --- Callback to get properties for clients that are shown during cycling. 511 | -- @client c 512 | -- @return table 513 | cyclefocus.decorate_show_client = function(c) 514 | return { 515 | -- border_color = beautiful.fg_focus, 516 | border_color = beautiful.border_focus, 517 | border_width = c.border_width or 1, 518 | -- XXX: changes layout / triggers resizes. 519 | -- border_width = 10, 520 | } 521 | end 522 | --- Callback to get properties for other clients that are visible during cycling. 523 | -- @client c 524 | -- @return table 525 | cyclefocus.decorate_show_client_others = function(c) --luacheck: no unused args 526 | return { 527 | -- XXX: too distracting. 528 | -- opacity = 0.7 529 | } 530 | end 531 | 532 | local show_client_apply_props = {} 533 | 534 | local show_client_apply_props_others = {} 535 | local show_client_restore_client_props_others = {} 536 | 537 | local callback_show_client_lock 538 | local decorate_if_showing_client = function (c) 539 | if c == showing_client then 540 | cyclefocus.callback_show_client(c) 541 | end 542 | end 543 | -- A table with property callbacks. Could be merged with decorate_if_showing_client. 544 | local update_show_client_restore_client_props = {} 545 | --- Callback when a client gets shown during cycling. 546 | -- This can be overridden itself, but it's meant to be configured through 547 | -- decorate_show_client instead. 548 | -- @client c 549 | -- @param boolean Restore the previous state? 550 | cyclefocus.callback_show_client = function (c, restore) 551 | if callback_show_client_lock then return end 552 | callback_show_client_lock = true 553 | 554 | if restore then 555 | -- Restore all saved properties. 556 | if show_client_restore_client_props[c] then 557 | -- Disconnect signals. 558 | for k,_ in pairs(show_client_restore_client_props[c]) do 559 | client.disconnect_signal("property::" .. k, decorate_if_showing_client) 560 | client.disconnect_signal("property::" .. k, update_show_client_restore_client_props[c][k]) 561 | end 562 | 563 | for k,v in pairs(show_client_restore_client_props[c]) do 564 | c[k] = v 565 | end 566 | 567 | -- Restore properties for other clients. 568 | for _c,props in pairs(show_client_restore_client_props_others[c]) do 569 | for k,v in pairs(props) do 570 | -- XXX: might have an "invalid object" here! 571 | _c[k] = v 572 | end 573 | end 574 | 575 | show_client_apply_props[c] = nil 576 | show_client_restore_client_props[c] = nil 577 | show_client_restore_client_props_others[c] = nil 578 | end 579 | else 580 | -- Save orig settings on first call. 581 | local first_call = not show_client_restore_client_props[c] 582 | if first_call then 583 | show_client_restore_client_props[c] = {} 584 | show_client_apply_props[c] = {} 585 | 586 | -- Get props to apply and store original values. 587 | show_client_apply_props[c] = cyclefocus.decorate_show_client(c) 588 | update_show_client_restore_client_props[c] = {} 589 | for k,_ in pairs(show_client_apply_props[c]) do 590 | show_client_restore_client_props[c][k] = c[k] 591 | end 592 | 593 | -- Get props for other clients and store original values. 594 | -- TODO: handle all screens?! 595 | show_client_apply_props_others[c] = cyclefocus.decorate_show_client_others(c) 596 | show_client_restore_client_props_others[c] = {} 597 | for s in capi.screen do 598 | for _,_c in pairs(awful.client.visible(s)) do 599 | if _c ~= c then 600 | show_client_restore_client_props_others[c][_c] = {} 601 | for k,_ in pairs(show_client_apply_props_others[c]) do 602 | show_client_restore_client_props_others[c][_c][k] = _c[k] 603 | end 604 | end 605 | end 606 | end 607 | end 608 | -- Apply props from callback. 609 | for k,v in pairs(show_client_apply_props[c]) do 610 | c[k] = v 611 | end 612 | -- Apply props for other clients. 613 | for _c,_ in pairs(show_client_restore_client_props_others[c]) do 614 | for k,v in pairs(show_client_apply_props_others[c]) do 615 | _c[k] = v -- see: XXX_1 616 | end 617 | end 618 | 619 | if first_call then 620 | for k,_ in pairs(show_client_apply_props[c]) do 621 | client.connect_signal("property::" .. k, decorate_if_showing_client) 622 | 623 | -- Update client props to be restored during showing a client, 624 | -- e.g. border_color from focus signals. 625 | update_show_client_restore_client_props[c][k] = function() 626 | if c.valid then 627 | show_client_restore_client_props[c][k] = c[k] 628 | end 629 | end 630 | client.connect_signal("property::" .. k, update_show_client_restore_client_props[c][k]) 631 | end 632 | -- TODO: merge with above; also disconnect on restore. 633 | -- for k,v in pairs(show_client_apply_props_others[c]) do 634 | -- client.connect_signal("property::" .. k, decorate_if_showing_client) 635 | -- end 636 | end 637 | end 638 | 639 | callback_show_client_lock = false 640 | end 641 | 642 | -- Handle temporarily setting "ontop" for shown clients. 643 | -- This is a function that keeps track and handles the related "below", 644 | -- "above", and "fullscreen" properties. 645 | -- Ref: https://github.com/awesomeWM/awesome/issues/667 646 | local restore_ontop_c 647 | 648 | -- Helper function to restore state of the temporarily selected client. 649 | cyclefocus.show_client = function (c) 650 | showing_client = c 651 | 652 | if c then 653 | if restore_callback_show_client then 654 | cyclefocus.callback_show_client(restore_callback_show_client, true) 655 | end 656 | restore_callback_show_client = c 657 | 658 | -- Restore ontop (and related) properties. 659 | if restore_ontop_c then 660 | restore_ontop_c() 661 | restore_ontop_c = nil 662 | end 663 | 664 | -- Handle setting ontop for the current client. 665 | -- This involves managing other properties, since setting "ontop" 666 | -- resets "fullscreen", "below", and "above". 667 | if not c.ontop then 668 | if c.fullscreen then 669 | -- Keep fullscreen clients as is. 670 | -- This requires to temporarily unset ontop for others. 671 | -- NOTE: the client might not be visible with other ontop clients 672 | -- after selecting it. This could be handled by setting 673 | -- ontop in the end (unsetting its fullscreen then though). 674 | local ontop_restore_clients = {} 675 | for _,_c in pairs(awful.client.visible(client.screen)) do 676 | if _c.ontop then 677 | table.insert(ontop_restore_clients, _c) 678 | _c.ontop = false 679 | end 680 | end 681 | if #ontop_restore_clients then 682 | function restore_ontop_c() 683 | for _,_c in pairs(ontop_restore_clients) do 684 | if _c.valid then 685 | _c.ontop = true 686 | end 687 | end 688 | end 689 | end 690 | else 691 | local ontop_orig_props = {c.ontop, c.below, c.above, c.fullscreen} 692 | function restore_ontop_c() 693 | if c.valid then 694 | c.ontop = ontop_orig_props[1] 695 | c.below = ontop_orig_props[2] 696 | c.above = ontop_orig_props[3] 697 | c.fullscreen = ontop_orig_props[4] 698 | end 699 | end 700 | c.ontop = true 701 | end 702 | end 703 | 704 | -- Make the clients tag visible, if it currently is not. 705 | local sel_tags = c.screen.selected_tags 706 | local c_tag = c.first_tag or c:tags()[1] 707 | if c_tag and not awful.util.table.hasitem(sel_tags, c_tag) then 708 | -- Select only the client's first tag, after de-selecting 709 | -- all others. 710 | 711 | -- Make the client sticky temporarily, so it will be 712 | -- considered visbile internally. 713 | -- NOTE: this is done for client_maybevisible (used by autofocus). 714 | local restore_sticky = c.sticky 715 | c.sticky = true 716 | 717 | for _, t in pairs(c.screen.tags) do 718 | if t ~= c_tag then 719 | t.selected = false 720 | end 721 | end 722 | c_tag.selected = true 723 | 724 | -- Restore. 725 | c.sticky = restore_sticky 726 | end 727 | cyclefocus.callback_show_client(c, false) 728 | 729 | else -- No client provided, restore only. 730 | if restore_ontop_c then 731 | restore_ontop_c() 732 | restore_ontop_c = nil 733 | end 734 | cyclefocus.callback_show_client(restore_callback_show_client, true) 735 | showing_client = nil 736 | end 737 | end 738 | 739 | --- Cached main wibox. 740 | local wbox 741 | local wbox_screen 742 | local layout 743 | 744 | -- Main function. 745 | cyclefocus.cycle = function(startdirection_or_args, args) 746 | if type(startdirection_or_args) == 'number' then 747 | awful.util.deprecate('startdirection is not used anymore: pass in args only', {raw=true}) 748 | else 749 | args = startdirection_or_args 750 | end 751 | args = awful.util.table.join(awful.util.table.clone(cyclefocus), args) 752 | -- The key name of the (last) modifier: this gets used for the "release" event. 753 | local modifier = args.modifier or 'Alt_L' 754 | local keys = args.keys or {'Tab', 'ISO_Left_Tab'} 755 | local shift = args.shift or 'Shift' 756 | -- cycle_filters: merge with defaults from module. 757 | local cycle_filters = awful.util.table.join(args.cycle_filters or {}, 758 | cyclefocus.cycle_filters) 759 | 760 | -- Use "Escape" as exit_key if not used as key. 761 | local exit_key = args.exit_key 762 | if exit_key == nil then 763 | for _,key in pairs({'Escape', 'q'}) do 764 | if not awful.util.table.hasitem(keys, key) then 765 | exit_key = key 766 | break 767 | end 768 | end 769 | end 770 | 771 | -- Not documented. 772 | local get_client_icon_widget = args.get_client_icon_widget 773 | 774 | local filter_result_cache = {} -- Holds cached filter results. 775 | 776 | local show_clients = args.show_clients 777 | if show_clients and type(show_clients) ~= 'function' then 778 | show_clients = cyclefocus.show_client 779 | end 780 | 781 | local raise_client_fn = args.raise_client 782 | if raise_client_fn and type(raise_client_fn) ~= 'function' then 783 | raise_client_fn = cyclefocus.raise_client_without_focus 784 | end 785 | 786 | -- Support single filter. 787 | if args.cycle_filter then 788 | cycle_filters = awful.util.table.clone(cycle_filters) 789 | table.insert(cycle_filters, args.cycle_filter) 790 | end 791 | 792 | -- Set flag to ignore any focus events while cycling through clients. 793 | ignore_focus_signal = true 794 | 795 | -- Internal state. 796 | local initiating_client = args.initiating_client or capi.client.focus -- Will be jumped to via Escape (abort). 797 | 798 | -- Save list of selected tags for all screens. 799 | local restore_tag_selected = {} 800 | for s in capi.screen do 801 | restore_tag_selected[s] = {} 802 | for _,t in pairs(s.tags) do 803 | restore_tag_selected[s][t] = t.selected 804 | end 805 | end 806 | 807 | --- Helper function to get the next client. 808 | -- @param direction 1 (forward) or -1 (backward). 809 | -- @param idx Current index in the stack. 810 | -- @param stack Current stack (default: history.stack). 811 | -- @param consider_cur_idx Also look at the current idx, and consider it 812 | -- when it's not focused. 813 | -- @return client or nil and current index in stack. 814 | local get_next_client = function(direction, idx, stack, consider_cur_idx) 815 | local startidx = idx 816 | stack = stack or history.stack 817 | consider_cur_idx = consider_cur_idx or args.focus_clients 818 | 819 | local nextc 820 | 821 | cyclefocus.debug('get_next_client: #' .. idx .. ", dir=" .. direction 822 | .. ", start=" .. startidx .. ", consider_cur=" .. tostring(consider_cur_idx), 2) 823 | 824 | local n = #stack 825 | if consider_cur_idx then 826 | local c_top = stack[idx][1] 827 | if c_top ~= capi.client.focus then 828 | n = n+1 829 | cyclefocus.debug("Considering nextc from top of stack: " .. tostring(c_top), 2) 830 | else 831 | consider_cur_idx = false 832 | end 833 | end 834 | for loop_stack_i = 1, n do 835 | if not consider_cur_idx or loop_stack_i ~= 1 then 836 | idx = idx + direction 837 | if idx < 1 then 838 | idx = #stack 839 | elseif idx > #stack then 840 | idx = 1 841 | end 842 | end 843 | cyclefocus.debug('find loop: #' .. idx .. ", dir=" .. direction, 3) 844 | nextc = stack[idx][1] 845 | 846 | if nextc then 847 | -- Filtering. 848 | if cycle_filters then 849 | -- Get and init filter cache data structure. {{{ 850 | -- TODO: move function(s) up? 851 | local get_cached_filter_result = function(f, a, b) 852 | b = b or false -- handle nil 853 | if filter_result_cache[f] == nil then 854 | filter_result_cache[f] = { [a] = { [b] = { } } } 855 | return nil 856 | elseif filter_result_cache[f][a] == nil then 857 | filter_result_cache[f][a] = { [b] = { } } 858 | return nil 859 | elseif filter_result_cache[f][a][b] == nil then 860 | return nil 861 | end 862 | return filter_result_cache[f][a][b] 863 | end 864 | local set_cached_filter_result = function(f, a, b, value) 865 | b = b or false -- handle nil 866 | get_cached_filter_result(f, a, b) -- init 867 | filter_result_cache[f][a][b] = value 868 | end -- }}} 869 | 870 | -- Apply filters, while looking up cache. 871 | local filter_result 872 | for _k, filter in pairs(cycle_filters) do 873 | cyclefocus.debug("Checking filter ".._k.."/"..#cycle_filters..": "..tostring(filter), 4) 874 | filter_result = get_cached_filter_result(filter, nextc, initiating_client) 875 | if filter_result ~= nil then 876 | if not filter_result then 877 | nextc = false 878 | break 879 | end 880 | else 881 | filter_result = filter(nextc, initiating_client) 882 | set_cached_filter_result(filter, nextc, initiating_client, filter_result) 883 | if not filter_result then 884 | cyclefocus.debug("Filtering/skipping client: " .. get_object_name(nextc), 3) 885 | nextc = false 886 | break 887 | end 888 | end 889 | end 890 | end 891 | if nextc then 892 | -- Found client to switch to. 893 | break 894 | end 895 | end 896 | end 897 | cyclefocus.debug("get_next_client returns: " .. get_object_name(nextc) .. ', idx=' .. idx, 1) 898 | return nextc, idx 899 | end 900 | 901 | local first_run = true 902 | local nextc 903 | local idx = 1 -- Currently focused client in the stack. 904 | 905 | -- Get the screen before moving the mouse. 906 | local initial_screen = awful.screen.focused and awful.screen.focused() or mouse.screen 907 | 908 | -- Move mouse pointer away to avoid sloppy focus kicking in. 909 | local restore_mouse_coords 910 | if show_clients and args.move_mouse_pointer then 911 | local s = capi.screen[capi.mouse.screen] 912 | local coords = capi.mouse.coords() 913 | restore_mouse_coords = {s = s, x = coords.x, y = coords.y} 914 | local pos = {x = s.geometry.x, y = s.geometry.y} 915 | -- move cursor without triggering signals mouse::enter and mouse::leave 916 | capi.mouse.coords(pos, true) 917 | restore_mouse_coords.moved = pos 918 | end 919 | 920 | capi.keygrabber.run(function(mod, key, event) 921 | -- Helper function to exit out of the keygrabber. 922 | -- If a client is given, it will be jumped to. 923 | local exit_grabber = function(c) 924 | cyclefocus.debug("exit_grabber: " .. get_object_name(c), 2) 925 | if wbox then 926 | wbox.visible = false 927 | end 928 | capi.keygrabber.stop() 929 | 930 | -- Restore. 931 | if show_clients then 932 | show_clients() 933 | end 934 | 935 | -- Restore previously selected tags for screen(s). 936 | -- With a given client, handle other screens first, otherwise 937 | -- the focus might be on the wrong screen. 938 | if restore_tag_selected then 939 | for s in capi.screen do 940 | if not c or s ~= c.screen then 941 | for _,t in pairs(s.tags) do 942 | t.selected = restore_tag_selected[s][t] 943 | end 944 | end 945 | end 946 | end 947 | 948 | -- Restore mouse if it has not been moved during cycling. 949 | if restore_mouse_coords then 950 | if restore_mouse_coords.s == capi.screen[capi.mouse.screen] then 951 | local coords = capi.mouse.coords() 952 | local moved_coords = restore_mouse_coords.moved 953 | if moved_coords.x == coords.x and moved_coords.y == coords.y then 954 | capi.mouse.coords({x = restore_mouse_coords.x, y = restore_mouse_coords.y}, true) 955 | end 956 | end 957 | end 958 | 959 | if c then 960 | showing_client = c 961 | raise_client_fn(c) 962 | if c ~= initiating_client then 963 | history.movetotop(c) 964 | end 965 | showing_client = nil 966 | end 967 | ignore_focus_signal = false 968 | 969 | return true 970 | end 971 | 972 | cyclefocus.debug("grabber: mod: " .. table.concat(mod, ',') 973 | .. ", key: " .. tostring(key) 974 | .. ", event: " .. tostring(event) 975 | .. ", modifier_key: " .. tostring(modifier), 3) 976 | 977 | if exit_key and key == exit_key then 978 | return exit_grabber(initiating_client) 979 | elseif #mod == 1 and mod[1] == 'Control' and key == 'c' then 980 | -- exit on Ctrl-C always. 981 | return exit_grabber(initiating_client) 982 | end 983 | 984 | -- Direction (forward/backward) is determined by status of shift. 985 | local direction = awful.util.table.hasitem(mod, shift) and -1 or 1 986 | 987 | if event == "release" and key == modifier then 988 | -- Focus selected client when releasing modifier. 989 | -- When coming here on first run, the trigger was pressed quick and 990 | -- we need to fetch the next client while exiting. 991 | if first_run then 992 | nextc, idx = get_next_client(direction, idx) 993 | end 994 | if show_clients then 995 | show_clients(nextc) 996 | end 997 | return exit_grabber(nextc) 998 | end 999 | 1000 | -- Ignore any "release" events and unexpected keys, except for the first run. 1001 | if not first_run then 1002 | if not awful.util.table.hasitem(keys, key) then 1003 | cyclefocus.debug("Ignoring unexpected key: " .. tostring(key), 1) 1004 | return true 1005 | end 1006 | if event == "release" then 1007 | return true 1008 | end 1009 | end 1010 | first_run = false 1011 | 1012 | nextc, idx = get_next_client(direction, idx) 1013 | if not nextc then 1014 | return exit_grabber() 1015 | end 1016 | 1017 | -- Show the client, which triggers setup of restore_callback_show_client etc. 1018 | if show_clients then 1019 | show_clients(nextc) 1020 | end 1021 | -- Focus client. 1022 | if args.focus_clients then 1023 | capi.client.focus = nextc 1024 | end 1025 | 1026 | if not args.display_notifications then 1027 | return true 1028 | end 1029 | 1030 | local container_margin_top_bottom = dpi(5) 1031 | local container_margin_left_right = dpi(5) 1032 | if not wbox then 1033 | wbox = wibox({ontop = true }) 1034 | wbox._for_screen = mouse.screen 1035 | wbox:set_fg(beautiful.fg_normal) 1036 | wbox:set_bg("#ffffff00") 1037 | 1038 | local container_inner = wibox.layout.align.vertical() 1039 | local container_layout = wibox.container.margin( 1040 | container_inner, 1041 | container_margin_left_right, container_margin_left_right, 1042 | container_margin_top_bottom, container_margin_top_bottom) 1043 | container_layout = wibox.container.background(container_layout) 1044 | container_layout:set_bg(beautiful.bg_normal..'cc') 1045 | 1046 | wbox:set_widget(container_layout) 1047 | -- "fixed" appears to work better for when there are no icons to 1048 | -- prevent cropping of the text. 1049 | layout = wibox.layout.fixed.vertical() 1050 | container_inner:set_middle(layout) 1051 | else 1052 | layout:reset() 1053 | end 1054 | 1055 | -- Set geometry always, the screen might have changed. 1056 | if not wbox_screen or wbox_screen ~= initial_screen then 1057 | wbox_screen = initial_screen 1058 | local wa = screen[wbox_screen].workarea 1059 | local w = math.ceil(wa.width * 0.618) 1060 | wbox:geometry({ 1061 | -- right-align. 1062 | x = math.ceil(wa.x + wa.width - w), 1063 | width = w, 1064 | }) 1065 | end 1066 | local wbox_height = 0 1067 | 1068 | -- Create entry with index, name and screen. 1069 | local display_entry_for_idx_offset = function(offset, c, _idx, displayed_list) -- {{{ 1070 | local preset = awful.util.table.clone(args.default_preset) 1071 | 1072 | -- Callback. 1073 | local args_for_cb = { 1074 | client=c, 1075 | offset=offset, 1076 | idx=_idx, 1077 | displayed_list=displayed_list } 1078 | local preset_for_offset = args.preset_for_offset 1079 | -- Callback for all. 1080 | if preset_for_offset.default then 1081 | preset_for_offset.default(preset, args_for_cb) 1082 | end 1083 | -- Callback for offset. 1084 | local preset_cb = preset_for_offset[tostring(offset)] 1085 | if preset_cb then 1086 | preset_cb(preset, args_for_cb) 1087 | end 1088 | 1089 | local entry_layout = wibox.layout.fixed.horizontal() 1090 | 1091 | if preset.icon_size then 1092 | local icon_widget = get_client_icon_widget(c, preset) 1093 | if icon_widget then 1094 | entry_layout:add(icon_widget) 1095 | end 1096 | end 1097 | 1098 | local textbox = wibox.widget.textbox() 1099 | textbox:set_markup(preset.text) 1100 | textbox:set_font(preset.font) 1101 | textbox:set_wrap("word_char") 1102 | textbox:set_ellipsize("middle") 1103 | -- Set height to no wrap with fixed main layout. 1104 | local _, h = textbox:get_preferred_size(c.screen) 1105 | textbox:set_forced_height(h) 1106 | local textbox_margin = wibox.container.margin(textbox) 1107 | textbox_margin:set_margins(dpi(5)) 1108 | 1109 | entry_layout:add(textbox_margin) 1110 | entry_layout = wibox.container.margin( 1111 | entry_layout, dpi(5), dpi(5), dpi(2), dpi(2)) 1112 | local entry_with_bg = wibox.container.background(entry_layout) 1113 | if offset == 0 then 1114 | entry_with_bg:set_fg(beautiful.fg_focus) 1115 | entry_with_bg:set_bg(beautiful.bg_focus) 1116 | else 1117 | entry_with_bg:set_fg(beautiful.fg_normal) 1118 | -- entry_with_bg:set_bg(beautiful.bg_normal.."dd") 1119 | end 1120 | layout:add(entry_with_bg) 1121 | 1122 | -- Add height to outer wibox. 1123 | local context = {dpi=beautiful.xresources.get_dpi(initial_screen)} 1124 | _, h = entry_with_bg:fit(context, wbox.width, 2^20) 1125 | wbox_height = wbox_height + h 1126 | end -- }}} 1127 | 1128 | -- Get clients before and after currently selected one. 1129 | local prevnextlist = awful.util.table.clone(history.stack) -- Use a copy, entries will get nil'ed. 1130 | local _idx = idx 1131 | 1132 | local dlist = {} -- A table with offset => stack index. 1133 | 1134 | dlist[0] = _idx 1135 | prevnextlist[_idx][1] = false 1136 | 1137 | -- Build dlist for both directions, depending on how many entries should get displayed. 1138 | for _,dir in ipairs({1, -1}) do 1139 | _idx = dlist[0] 1140 | local n = dir == 1 and args.display_next_count or args.display_prev_count 1141 | for i = 1, n do 1142 | local _i = i * dir 1143 | _, _idx = get_next_client(dir, _idx, prevnextlist, false) 1144 | if _ then 1145 | dlist[_i] = _idx 1146 | end 1147 | prevnextlist[_idx][1] = false 1148 | end 1149 | end 1150 | 1151 | -- Sort the offsets. 1152 | local offsets = {} 1153 | for n in pairs(dlist) do table.insert(offsets, n) end 1154 | table.sort(offsets) 1155 | 1156 | -- Display the wibox. 1157 | for _,i in ipairs(offsets) do 1158 | _idx = dlist[i] 1159 | display_entry_for_idx_offset(i, history.stack[_idx][1], _idx, dlist) 1160 | end 1161 | local wa = screen[initial_screen].workarea 1162 | local h = wbox_height + container_margin_top_bottom*2 1163 | wbox:geometry({ 1164 | height = h, 1165 | y = wa.y + floor(wa.height/2 - h/2), 1166 | }) 1167 | wbox.visible = true 1168 | return true 1169 | end) 1170 | end 1171 | 1172 | 1173 | -- A helper method to wrap awful.key. 1174 | function cyclefocus.key(mods, key, startdirection_or_args, args) 1175 | mods = mods or {modkey} or {"Mod4"} 1176 | key = key or "Tab" 1177 | if type(startdirection_or_args) == 'number' then 1178 | awful.util.deprecate('startdirection is not used anymore: pass in mods, key, args', {raw=true}) 1179 | else 1180 | args = startdirection_or_args 1181 | end 1182 | args = args and awful.util.table.clone(args) or {} 1183 | if not args.keys then 1184 | if key == "Tab" then 1185 | args.keys = {"Tab", "ISO_Left_Tab"} 1186 | else 1187 | args.keys = {key} 1188 | end 1189 | end 1190 | if not args.modifier then 1191 | -- Convert modifier to key name. 1192 | -- Table from awful.key. 1193 | local conversion = { 1194 | mod4 = "Super_L", 1195 | control = "Control_L", 1196 | shift = "Shift_L", 1197 | mod1 = "Alt_L", 1198 | -- AltGr (https://github.com/awesomeWM/awesome/pull/2515). 1199 | mod5 = "ISO_Level3_Shift", 1200 | } 1201 | args.modifier = conversion[mods[1]:lower()] 1202 | if not args.modifier then 1203 | args.modifier = mods[1] 1204 | end 1205 | end 1206 | 1207 | return awful.key(mods, key, function(c) 1208 | args.initiating_client = c -- only for clientkeys, might be nil! 1209 | cyclefocus.cycle(args) 1210 | end) 1211 | end 1212 | 1213 | return cyclefocus 1214 | --------------------------------------------------------------------------------