├── .gitignore ├── LICENSE.md ├── README.md ├── colors.lua ├── demo.m4v ├── init.lua ├── preview.png ├── shapes.lua └── table.lua /.gitignore: -------------------------------------------------------------------------------- 1 | color_rules -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 mut-ex 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # :thumbsup:nice 2 | 3 |
4 | If you would like to show your appreciation for this project,
please consider a donation :)
5 |
6 |
7 |
8 | 9 | **N.B. This branch is for Awesome v4.3 git. [You can find the branch for Awesome v4.3 stable here](https://github.com/mut-ex/awesome-wm-nice/tree/awesome-4v3-stable)** 10 | 11 | nice is an easy to use, highly configurable extension for **[Awesome WM](https://awesomewm.org/)** that adds beautiful window decorations (and extra functionality!) to clients. It... 12 | 13 | * ...adds a **subtle 3D look**, and soft, **rounded anti-aliased, corners** to windows 14 | * ...picks the window **decoration color based on the client content for a seamless look** , and adjusts the window title text color accordingly 15 | * ...**auto-generates titlebar buttons** (and their states) for you based on the colors your pick *or* you can let it pick the colors for you! 16 | * ...allows you to customize which titlebar buttons to include, their order, and their layout 17 | * ...adds the **ability to maximize/unmaximize** floating windows by **double clicking the titlebar**, and of course, **moving them by clicking and holding** 18 | * ...adds the ability to **"roll up"** and **"roll down"** the client window like a **window shade**! Scroll up over the titlebar to **instantly hide the window contents but keep the title bar** right where it is. And then either scroll down or click the titlebar to make the window contents visible again! 19 | 20 |  21 | 22 | ## Getting Started 23 | 24 | ### Prerequisites 25 | 26 | * You need **[Awesome WM](https://awesomewm.org/)** with a working basic configuration. **This branch is for Awesome v4.3 git. [You can find the branch for Awesome v4.3 stable here](https://github.com/mut-ex/awesome-wm-nice/tree/awesome-4v3-stable)** 27 | 28 | * You also need **[picom](https://github.com/yshui/picom)**. Make sure you have `shadow-ignore-shaped = false` in your configuration otherwise picom will not draw shadows. My recommended shadow settings are given below: 29 | 30 | ``` 31 | shadow = true; 32 | shadow-radius = 40; 33 | shadow-opacity = .55; 34 | shadow-offset-x = -40; 35 | shadow-offset-y = -20; 36 | shadow-exclude = [ 37 | "_NET_WM_WINDOW_TYPE:a = '_NET_WM_WINDOW_TYPE_NOTIFICATION'", 38 | "_NET_WM_STATE@:32a *= '_NET_WM_STATE_HIDDEN'", 39 | "_GTK_FRAME_EXTENTS@:c" 40 | ]; 41 | shadow-ignore-shaped = false 42 | ``` 43 | 44 | * For **GTK** applications add the following line to **~/.config/gtk-3.0/settings.ini** under the **[Settings]** section to hide client-side window control buttons: 45 | 46 | ``` 47 | gtk-decoration-layout=menu: 48 | ``` 49 | 50 | * Within you Awesome configuration, make sure that you do not already have code in place that request default titlebars for clients. Something like this: 51 | 52 | ```lua 53 | client.connect_signal("request::titlebars", function(c) ... end) -- Remove this 54 | ``` 55 | 56 | * Additionally, nice only adds window decorations to clients that have the `titlebars_enabled` property set to true. So configure your client rules accordingly. 57 | 58 | ### Installation 59 | 60 | The easiest and quickest way to get started is by cloning this repository to your awesome configuration directory 61 | 62 | ```shell 63 | $ cd ~/.config/awesome 64 | $ git clone https://github.com/mut-ex/awesome-wm-nice.git nice 65 | ``` 66 | 67 | 68 | 69 | ## Usage 70 | 71 | To use nice, you first need to load the module. To do that, put the following line right after `beautiful.init(...)` 72 | 73 | ```lua 74 | local nice = require("nice") 75 | nice() 76 | ``` 77 | 78 | If you are fine using the default configuration, you are all done! 79 | 80 | nice will automatically detect and change the window decoration color to match the client. However... 81 | 82 | * To pick the window decoration color yourself, right-click the titlebar and select **'Manually Pick Color'** 83 | * To update the window decoration colors, right-click on the titlebar and select **'Redo Window Decorations'** 84 | * Scroll-up with your mouse over the titlebar to "roll up" the window shade. Scroll-down over the titlebar, or left-click to "roll down" the window shade 85 | * nice saves its color rules in the **color_rules** file within the module directory. If you wish you can manually edit it, or delete the file if you want to start again. 86 | 87 | 88 | 89 | ## Configuration 90 | 91 | You can override the defaults by passing your own configuration. For example 92 | 93 | ```lua 94 | local nice = require("nice") 95 | nice { 96 | titlebar_color = "#00ff00", 97 | 98 | -- You only need to pass the parameter you are changing 99 | context_menu_theme = { 100 | width = 300, 101 | }, 102 | 103 | -- Swap the designated buttons for resizing, and opening the context menu 104 | mb_resize = nice.MB_MIDDLE, 105 | mb_contextmenu = nice.MB_RIGHT, 106 | } 107 | ``` 108 | 109 | Below you will find further details explaining the configuration parameters for nice. 110 | 111 | | Parameter | Type | Description | Default | 112 | | --------------------- | :--: | ----------- | ------------------- | 113 | | `titlebar_height` | integer | The height of the titlebar | `38` | 114 | | `titlebar_radius` | integer | The radius of the top left and top right corners of the titlebar. Should be `>= 3` and `<= titlebar_height` | `9` | 115 | | `titlebar_color` | string | The default color of the titlebar and window decorations. Should be a hex color string | `"#1e1e24"` | 116 | | `titlebar_padding_left` | integer | The padding on the left side of the titlebar | `0` | 117 | | `titlebar_padding_right` | integer | The padding on the right side of the titlebar | `0` | 118 | | `titlebar_font` | string | The font and font size for text within the titlebar. See the default value for an example of the format | `"Sans 11"` | 119 | | `win_shade_enabled` | boolean | Whether the window shade feature should be enabled | `true` | 120 | | `no_titlebar_maximized` | boolean | Whether the titlebar should be hidden for maximized windows | `false` | 121 | | `mb_move` | integer or named constant | Mouse button to move a window. | `nice.MB_LEFT` | 122 | | `mb_contextmenu` | integer or named constant | Mouse button to open the nice context menu | `nice.MB_MIDDLE` | 123 | | `mb_resize` | integer or named constant | Mouse button to resize a window | `nice.MB_RIGHT` | 124 | | `mb_win_shade_rollup` | integer or named constant | Mouse button to roll up/hide window contents | `nice.MB_SCROLL_UP` | 125 | | `mb_win_shade_rolldown` | integer or named constant | Mouse button to roll down/show window contents | `nice.MB_SCROLL_DOWN` | 126 | | `button_size` | integer | The size (diameter) of the titlebar buttons | 16 | 127 | | `button_margin_horizontal` | integer | The horizontal margin around each titlebar button. `button_margin_left` and `button_margin_right`can override this parameter. | 5 | 128 | | `button_margin_vertical` | integer | The vertical margin above and below each titlebar button. `button_margin_top` and `button_margin_bottom` can override this parameter. | nil | 129 | | `button_margin_top` | integer | The margin above each titlebar button | 2 | 130 | | `button_margin_bottom` | integer | The margin below each titlebar button | nil | 131 | | `button_margin_left` | integer | The margin to the left of each titlebar button | 0 | 132 | | `button_margin_right` | integer | The margin to the right of each titlebar button | 0 | 133 | | `tooltips_enabled` | boolean | If tooltip hints should be shown when the mouse cursor is hovered over a titlebar button | nil | 134 | | `close_color` | string | The base color for the close button | "#ee4266" | 135 | | `minimize_color` | string | The base color for the minimize button | "#ffb400" | 136 | | `maximize_color` | string | The base color for the maximize button | "#4cbb17" | 137 | | `floating_color` | string | The base color for the floating mode toggle button | "#f6a2ed" | 138 | | `ontop_color` | string | The base color for the on top mode toggle button | "#f6a2ed" | 139 | | `sticky_color` | string | The base color for the sticky mode toggle button | "#f6a2ed" | 140 | 141 | In addition to the above mentioned parameters, there some more parameters that require a little more explanation: 142 | 143 | ### titlebar_items 144 | 145 | `titlebar_items` — Specifies the titlebar items to include 146 | 147 | * It should be a table with the following keys: 148 | * `left` — Specifies the item(s) to place on the left side of the titlebar 149 | * `middle` — Specifies the item(s) to place in the middle of the titlebar 150 | * `right` — Specifies the items(s) to place on the right side of the titlebar 151 | * Multiple items should be passed as an array of identifiers. For a single item simply passing the identifier is sufficient 152 | * Valid titlebar item identifiers are: 153 | * `"close"` 154 | * `"minimize"` 155 | * `"maximize"` 156 | * `"floating"` 157 | * `"ontop"` 158 | * `"sticky"` 159 | * `"title"` 160 | * Default value for `titlebar_items` is: 161 | 162 | ```lua 163 | titlebar_items = { 164 | left = {"close", "minimize", "maximize"}, 165 | middle = "title", 166 | right = {"sticky", "ontop", "floating"}, 167 | } 168 | ``` 169 | 170 | ### context_menu_theme 171 | 172 | `context_menu_theme` — Specifies theming parameters for the context (default right-click) menu 173 | 174 | * It should be a table with the following keys: 175 | * `bg_focus` — Background color of focused menu item 176 | * `bg_normal` — Background color of not-focused menu items 177 | * `border_color` — Color of the border around the entire menu 178 | * `border_width` — Width of the border around the entire menu 179 | * `fg_focus` — Foreground color of focused menu item 180 | * `fg_normal` — Foreground color of not-focused menu items 181 | * `font` — Font used for menu text 182 | * `height` — Height of each menu list item 183 | * `width` — Width of the menu 184 | * Default value for `context_menu_theme` is: 185 | 186 | ```lua 187 | context_menu_theme = { 188 | bg_focus = "#aed9e0", 189 | bg_normal = "#5e6472", 190 | border_color = "#00000000", 191 | border_width = 0, 192 | fg_focus = "#242424", 193 | fg_normal = "#fefefa", 194 | font = "Sans 11", 195 | height = 27.5, 196 | width = 250, 197 | } 198 | ``` 199 | 200 | ### tooltip_messages 201 | 202 | `tooltip_messages` — Specifies the hints that are shown when the mouse cursor is hovered over a titlebar button 203 | 204 | * It should be a table with the following keys: 205 | * `close` — Text shown when hovering over the close button 206 | * `minimize` — Text shown when hovering over the minimize button 207 | * `maximize_active` — Text shown when hovering over the maximize button when the window is maximized 208 | * `maximize_inactive` — Text shown when hovering over the maximize button when the window is not maximized 209 | * `floating_active` — Text shown when hovering over the floating button when the window is floating 210 | * `floating_inactive` — Text shown when hovering over the floating button when the window is tiled 211 | * `ontop_active` — Text shown when hovering over the ontop button when the window is set to be above other windows 212 | * `ontop_inactive` — Text shown when hovering over the ontop button when the window is not set to be above other windows 213 | * `sticky_active` — Text shown when hovering over the sticky button when the window is set to be available on all tags 214 | * `sticky_inactive` — Text shown when hovering over the sticky button when the window is not to be available on all tags 215 | 216 | The default value for `tooltip_messages` is: 217 | 218 | ```lua 219 | tooltip_messages = { 220 | close = "close", 221 | minimize = "minimize", 222 | maximize_active = "unmaximize", 223 | maximize_inactive = "maximize", 224 | floating_active = "enable tiling mode", 225 | floating_inactive = "enable floating mode", 226 | ontop_active = "don't keep above other windows", 227 | ontop_inactive = "keep above other windows", 228 | sticky_active = "disable sticky mode", 229 | sticky_inactive = "enable sticky mode", 230 | } 231 | ``` 232 | 233 | 234 | 235 | ## Using 236 | 237 | nice will automatically detect and change the window decoration color to match the client. However... 238 | 239 | * If nice doesn't pick the right color or you want to specify it yourself, right-click the titlebar and select 'Manually Pick Color' 240 | * If the client theme changes (for example if you change your terminal emulator colors), to update the window decoration colors, right-click on the titlebar and select 'Redo Window Decorations' 241 | * Scroll-up with your mouse over the titlebar to "roll-up" the window shade. Scroll-down over the titlebar, or left-click to "roll-down" the window shade 242 | * nice saves its color rules in the color_rules file within the module directory. If you wish you can manually edit it, or delete the file if you want to start again. 243 | 244 | 245 | 246 | ## Issues 247 | 248 | If you face any bugs or issues (or have a feature request), please feel free to open an issue on here 249 | 250 | 251 | 252 | ## License 253 | 254 | [](http://doge.mit-license.org) 255 | 256 | 257 | 258 | -------------------------------------------------------------------------------- /colors.lua: -------------------------------------------------------------------------------- 1 | -- => Colors 2 | -- Provides utility functions for handling colors 3 | -- ============================================================ 4 | local math = math 5 | local floor = math.floor 6 | local max = math.max 7 | local min = math.min 8 | local pow = math.pow 9 | local random = math.random 10 | local gcolor = require("gears.color") 11 | local parse_color = gcolor.parse_color 12 | 13 | -- Returns a value that is clipped to interval edges if it falls outside the interval 14 | local function clip(num, min_num, max_num) return 15 | max(min(num, max_num), min_num) end 16 | 17 | -- Converts the given hex color to normalized rgba 18 | local function hex2rgb(color) 19 | -- color = color:gsub("#", "") 20 | -- local strlen = color:len() 21 | -- if strlen == 6 then 22 | -- return tonumber("0x" .. color:sub(1, 2)) / 255, 23 | -- tonumber("0x" .. color:sub(3, 4)) / 255, 24 | -- tonumber("0x" .. color:sub(5, 6)) / 255, 1 25 | -- end 26 | -- if strlen == 8 then 27 | -- return tonumber("0x" .. color:sub(1, 2)) / 255, 28 | -- tonumber("0x" .. color:sub(3, 4)) / 255, 29 | -- tonumber("0x" .. color:sub(5, 6)) / 255, 30 | -- tonumber("0x" .. color:sub(7, 8)) / 255 31 | -- end 32 | return parse_color(color) 33 | end 34 | 35 | -- Converts the given hex color to hsv 36 | local function hex2hsv(color) 37 | local r, g, b = hex2rgb(color) 38 | local C_max = max(r, g, b) 39 | local C_min = min(r, g, b) 40 | local delta = C_max - C_min 41 | local H, S, V 42 | if delta == 0 then 43 | H = 0 44 | elseif C_max == r then 45 | H = 60 * (((g - b) / delta) % 6) 46 | elseif C_max == g then 47 | H = 60 * (((b - r) / delta) + 2) 48 | elseif C_max == b then 49 | H = 60 * (((r - g) / delta) + 4) 50 | end 51 | if C_max == 0 then 52 | S = 0 53 | else 54 | S = delta / C_max 55 | end 56 | V = C_max 57 | return H, S * 100, V * 100 58 | end 59 | 60 | -- Converts the given hsv color to hex 61 | local function hsv2hex(H, S, V) 62 | S = S / 100 63 | V = V / 100 64 | if H > 360 then H = 360 end 65 | if H < 0 then H = 0 end 66 | local C = V * S 67 | local X = C * (1 - math.abs(((H / 60) % 2) - 1)) 68 | local m = V - C 69 | local r_, g_, b_ = 0, 0, 0 70 | if H >= 0 and H < 60 then 71 | r_, g_, b_ = C, X, 0 72 | elseif H >= 60 and H < 120 then 73 | r_, g_, b_ = X, C, 0 74 | elseif H >= 120 and H < 180 then 75 | r_, g_, b_ = 0, C, X 76 | elseif H >= 180 and H < 240 then 77 | r_, g_, b_ = 0, X, C 78 | elseif H >= 240 and H < 300 then 79 | r_, g_, b_ = X, 0, C 80 | elseif H >= 300 and H < 360 then 81 | r_, g_, b_ = C, 0, X 82 | end 83 | local r, g, b = (r_ + m) * 255, (g_ + m) * 255, (b_ + m) * 255 84 | return ("#%02x%02x%02x"):format(floor(r), floor(g), floor(b)) 85 | end 86 | 87 | -- Calculates the relative luminance of the given color 88 | local function relative_luminance(color) 89 | local r, g, b = hex2rgb(color) 90 | local function from_sRGB(u) 91 | return u <= 0.0031308 and 25 * u / 323 or 92 | pow(((200 * u + 11) / 211), 12 / 5) 93 | end 94 | return 0.2126 * from_sRGB(r) + 0.7152 * from_sRGB(g) + 0.0722 * from_sRGB(b) 95 | end 96 | 97 | -- Calculates the contrast ratio between the two given colors 98 | local function contrast_ratio(fg, bg) 99 | return (relative_luminance(fg) + 0.05) / (relative_luminance(bg) + 0.05) 100 | end 101 | 102 | -- Returns true if the contrast between the two given colors is suitable 103 | local function is_contrast_acceptable(fg, bg) 104 | return contrast_ratio(fg, bg) >= 7 and true 105 | end 106 | 107 | -- Returns a bright-ish, saturated-ish, color of random hue 108 | local function rand_hex(lb_angle, ub_angle) 109 | return hsv2hex(random(lb_angle or 0, ub_angle or 360), 70, 90) 110 | end 111 | 112 | -- Rotates the hue of the given hex color by the specified angle (in degrees) 113 | local function rotate_hue(color, angle) 114 | local H, S, V = hex2hsv(color) 115 | angle = clip(angle or 0, 0, 360) 116 | H = (H + angle) % 360 117 | return hsv2hex(H, S, V) 118 | end 119 | 120 | -- Lightens a given hex color by the specified amount 121 | local function lighten(color, amount) 122 | local r, g, b 123 | r, g, b = hex2rgb(color) 124 | r = 255 * r 125 | g = 255 * g 126 | b = 255 * b 127 | r = r + floor(2.55 * amount) 128 | g = g + floor(2.55 * amount) 129 | b = b + floor(2.55 * amount) 130 | r = r > 255 and 255 or r 131 | g = g > 255 and 255 or g 132 | b = b > 255 and 255 or b 133 | return ("#%02x%02x%02x"):format(r, g, b) 134 | end 135 | 136 | -- Darkens a given hex color by the specified amount 137 | local function darken(color, amount) 138 | local r, g, b 139 | r, g, b = hex2rgb(color) 140 | r = 255 * r 141 | g = 255 * g 142 | b = 255 * b 143 | r = max(0, r - floor(r * (amount / 100))) 144 | g = max(0, g - floor(g * (amount / 100))) 145 | b = max(0, b - floor(b * (amount / 100))) 146 | return ("#%02x%02x%02x"):format(r, g, b) 147 | end 148 | 149 | return { 150 | clip = clip, 151 | hex2rgb = hex2rgb, 152 | hex2hsv = hex2hsv, 153 | hsv2hex = hsv2hex, 154 | relative_luminance = relative_luminance, 155 | contrast_ratio = contrast_ratio, 156 | is_contrast_acceptable = is_contrast_acceptable, 157 | rand_hex = rand_hex, 158 | rotate_hue = rotate_hue, 159 | lighten = lighten, 160 | darken = darken, 161 | } 162 | -------------------------------------------------------------------------------- /demo.m4v: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mut-ex/awesome-wm-nice/810aa72bbebfee15d3375fdcb8d8a09f5c7741c8/demo.m4v -------------------------------------------------------------------------------- /init.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | ███╗ ██╗██╗ ██████╗███████╗ 3 | ████╗ ██║██║██╔════╝██╔════╝ 4 | ██╔██╗ ██║██║██║ █████╗ 5 | ██║╚██╗██║██║██║ ██╔══╝ 6 | ██║ ╚████║██║╚██████╗███████╗ 7 | ╚═╝ ╚═══╝╚═╝ ╚═════╝╚══════╝ 8 | Author: mu-tex 9 | License: MIT 10 | Repository: https://github.com/mut-ex/awesome-wm-nice 11 | ]] -- ============================================================ 12 | -- local ret, helpers = pcall(require, "helpers") 13 | -- local debug = ret and helpers.debug or function() end 14 | -- => Awesome WM 15 | -- ============================================================ 16 | local awful = require("awful") 17 | local atooltip = awful.tooltip 18 | local abutton = awful.button 19 | local wibox = require("wibox") 20 | local get_font_height = require("beautiful").get_font_height 21 | -- Widgets 22 | local imagebox = wibox.widget.imagebox 23 | local textbox = wibox.widget.textbox 24 | -- Layouts 25 | local wlayout = wibox.layout 26 | local wlayout_align_horizontal = wlayout.align.horizontal 27 | local wlayout_fixed_horizontal = wlayout.fixed.horizontal 28 | local wlayout_flex_horizontal = wlayout.flex.horizontal 29 | -- Containers 30 | local wcontainer = wibox.container 31 | local wcontainer_background = wcontainer.background 32 | local wcontainer_constraint = wcontainer.constraint 33 | local wcontainer_margin = wcontainer.margin 34 | local wcontainer_place = wcontainer.place 35 | -- Gears 36 | local gsurface = require("gears.surface") 37 | local gtimer = require("gears.timer") 38 | local gtimer_weak_start_new = gtimer.weak_start_new 39 | -- ------------------------------------------------------------ 40 | 41 | -- => Math + standard Lua methods 42 | -- ============================================================ 43 | local math = math 44 | local max = math.max 45 | local abs = math.abs 46 | local rad = math.rad 47 | local floor = math.floor 48 | local pairs = pairs 49 | local ipairs = ipairs 50 | 51 | -- ------------------------------------------------------------ 52 | 53 | -- => LGI 54 | -- ============================================================ 55 | local lgi = require("lgi") 56 | local cairo = lgi.cairo 57 | local gdk = lgi.Gdk 58 | local get_default_root_window = gdk.get_default_root_window 59 | local pixbuf_get_from_surface = gdk.pixbuf_get_from_surface 60 | local pixbuf_get_from_window = gdk.pixbuf_get_from_window 61 | -- ------------------------------------------------------------ 62 | 63 | -- => nice 64 | -- ============================================================ 65 | -- Colors 66 | local colors = require("nice.colors") 67 | local color_darken = colors.darken 68 | local color_lighten = colors.lighten 69 | local is_contrast_acceptable = colors.is_contrast_acceptable 70 | local relative_luminance = colors.relative_luminance 71 | -- Shapes 72 | local shapes = require("nice.shapes") 73 | local create_corner_top_left = shapes.create_corner_top_left 74 | local create_edge_left = shapes.create_edge_left 75 | local create_edge_top_middle = shapes.create_edge_top_middle 76 | local gradient = shapes.duotone_gradient_vertical 77 | -- ------------------------------------------------------------ 78 | 79 | gdk.init({}) 80 | 81 | -- => Local settings 82 | -- ============================================================ 83 | local bottom_edge_height = 3 84 | local double_click_jitter_tolerance = 4 85 | local double_click_time_window_ms = 250 86 | local stroke_inner_bottom_lighten_mul = 0.4 87 | local stroke_inner_sides_lighten_mul = 0.4 88 | local stroke_outer_top_darken_mul = 0.7 89 | local title_color_dark = "#242424" 90 | local title_color_light = "#fefefa" 91 | local title_unfocused_opacity = 0.7 92 | local titlebar_gradient_c1_lighten = 1 93 | local titlebar_gradient_c2_offset = 0.5 94 | 95 | local function rel_lighten(lum) return lum * 90 + 10 end 96 | local function rel_darken(lum) return -(lum * 70) + 100 end 97 | -- ------------------------------------------------------------ 98 | 99 | local nice = { 100 | MB_LEFT = 1, 101 | MB_MIDDLE = 2, 102 | MB_RIGHT = 3, 103 | MB_SCROLL_UP = 4, 104 | MB_SCROLL_DOWN = 5, 105 | } 106 | 107 | -- => Defaults 108 | -- ============================================================ 109 | local _private = {} 110 | _private.max_width = 0 111 | _private.max_height = 0 112 | 113 | -- Titlebar 114 | _private.titlebar_height = 38 115 | _private.titlebar_radius = 9 116 | _private.titlebar_color = "#1E1E24" 117 | _private.titlebar_margin_left = 0 118 | _private.titlebar_margin_right = 0 119 | _private.titlebar_font = "Sans 11" 120 | _private.titlebar_items = { 121 | left = {"close", "minimize", "maximize"}, 122 | middle = "title", 123 | right = {"sticky", "ontop", "floating"}, 124 | } 125 | _private.context_menu_theme = { 126 | bg_focus = "#aed9e0", 127 | bg_normal = "#5e6472", 128 | border_color = "#00000000", 129 | border_width = 0, 130 | fg_focus = "#242424", 131 | fg_normal = "#fefefa", 132 | font = "Sans 11", 133 | height = 27.5, 134 | width = 250, 135 | } 136 | _private.win_shade_enabled = true 137 | _private.no_titlebar_maximized = false 138 | _private.mb_move = nice.MB_LEFT 139 | _private.mb_contextmenu = nice.MB_MIDDLE 140 | _private.mb_resize = nice.MB_RIGHT 141 | _private.mb_win_shade_rollup = nice.MB_SCROLL_UP 142 | _private.mb_win_shade_rolldown = nice.MB_SCROLL_DOWN 143 | 144 | -- Titlebar Items 145 | _private.button_size = 16 146 | _private.button_margin_horizontal = 5 147 | -- _private.button_margin_vertical 148 | _private.button_margin_top = 2 149 | -- _private.button_margin_bottom = 0 150 | -- _private.button_margin_left = 0 151 | -- _private.button_margin_right = 0 152 | _private.tooltips_enabled = true 153 | _private.tooltip_messages = { 154 | close = "close", 155 | minimize = "minimize", 156 | maximize_active = "unmaximize", 157 | maximize_inactive = "maximize", 158 | floating_active = "enable tiling mode", 159 | floating_inactive = "enable floating mode", 160 | ontop_active = "don't keep above other windows", 161 | ontop_inactive = "keep above other windows", 162 | sticky_active = "disable sticky mode", 163 | sticky_inactive = "enable sticky mode", 164 | } 165 | _private.close_color = "#ee4266" 166 | _private.minimize_color = "#ffb400" 167 | _private.maximize_color = "#4CBB17" 168 | _private.floating_color = "#f6a2ed" 169 | _private.ontop_color = "#f6a2ed" 170 | _private.sticky_color = "#f6a2ed" 171 | -- ------------------------------------------------------------ 172 | 173 | -- => Saving and loading of color rules 174 | -- ============================================================ 175 | local table = table 176 | local t = require("nice.table") 177 | table.save = t.save 178 | table.load = t.load 179 | 180 | -- Load the color rules or create an empty table if there aren't any 181 | local gfilesys = require("gears.filesystem") 182 | local config_dir = gfilesys.get_configuration_dir() 183 | local color_rules_filename = "color_rules" 184 | local color_rules_filepath = config_dir .. "/nice/" .. color_rules_filename 185 | _private.color_rules = table.load(color_rules_filepath) or {} 186 | 187 | -- Saves the contents of _private.color_rules table to file 188 | local function save_color_rules() 189 | table.save(_private.color_rules, color_rules_filepath) 190 | end 191 | 192 | -- Adds a color rule entry to the color_rules table for the given client and saves to file 193 | local function set_color_rule(c, color) 194 | _private.color_rules[c.instance] = color 195 | save_color_rules() 196 | end 197 | 198 | -- Fetches the color rule for the given client instance 199 | local function get_color_rule(c) return _private.color_rules[c.instance] end 200 | -- ------------------------------------------------------------ 201 | 202 | -- Returns the hex color for the pixel at the given coordinates on the screen 203 | local function get_pixel_at(x, y) 204 | local pixbuf = pixbuf_get_from_window(get_default_root_window(), x, y, 1, 1) 205 | local bytes = pixbuf:get_pixels() 206 | return "#" .. 207 | bytes:gsub( 208 | ".", function(c) return ("%02x"):format(c:byte()) end) 209 | end 210 | 211 | -- Determines the dominant color of the client's top region 212 | local function get_dominant_color(client) 213 | local color 214 | -- gsurface(client.content):write_to_png( 215 | -- "/home/mutex/nice/" .. client.class .. "_" .. client.instance .. ".png") 216 | local pb 217 | local bytes 218 | local tally = {} 219 | local content = gsurface(client.content) 220 | local cgeo = client:geometry() 221 | local x_offset = 2 222 | local y_offset = 2 223 | local x_lim = floor(cgeo.width / 2) 224 | for x_pos = 0, x_lim, 2 do 225 | for y_pos = 0, 8, 1 do 226 | pb = pixbuf_get_from_surface( 227 | content, x_offset + x_pos, y_offset + y_pos, 1, 1) 228 | bytes = pb:get_pixels() 229 | color = "#" .. 230 | bytes:gsub( 231 | ".", 232 | function(c) 233 | return ("%02x"):format(c:byte()) 234 | end) 235 | if not tally[color] then 236 | tally[color] = 1 237 | else 238 | tally[color] = tally[color] + 1 239 | end 240 | end 241 | end 242 | local mode 243 | local mode_c = 0 244 | for kolor, kount in pairs(tally) do 245 | if kount > mode_c then 246 | mode_c = kount 247 | mode = kolor 248 | end 249 | end 250 | color = mode 251 | set_color_rule(client, color) 252 | return color 253 | end 254 | 255 | -- Returns a color that is analogous to the last color returned 256 | -- To make sure that the "randomly" generated colors look cohesive, only the first color is truly random, the rest are generated by offseting the hue by +33 degrees 257 | local next_color = colors.rand_hex() 258 | local function get_next_color() 259 | local prev_color = next_color 260 | next_color = colors.rotate_hue(prev_color, 33) 261 | return prev_color 262 | end 263 | 264 | -- Returns (or generates) a button image based on the given params 265 | local function create_button_image(name, is_focused, event, is_on) 266 | local focus_state = is_focused and "focused" or "unfocused" 267 | local key_img 268 | -- If it is a toggle button, then the key has an extra param 269 | if is_on ~= nil then 270 | local toggle_state = is_on and "on" or "off" 271 | key_img = ("%s_%s_%s_%s"):format(name, toggle_state, focus_state, event) 272 | else 273 | key_img = ("%s_%s_%s"):format(name, focus_state, event) 274 | end 275 | -- If an image already exists, then we are done 276 | if _private[key_img] then return _private[key_img] end 277 | -- The color key just has _color at the end 278 | local key_color = key_img .. "_color" 279 | -- If the user hasn't provided a color, then we have to generate one 280 | if not _private[key_color] then 281 | local key_base_color = name .. "_color" 282 | -- Maybe the user has at least provided a base color? If not we just pick a pesudo-random color 283 | local base_color = _private[key_base_color] or get_next_color() 284 | _private[key_base_color] = base_color 285 | local button_color = base_color 286 | local H = colors.hex2hsv(base_color) 287 | -- Unfocused buttons are desaturated and darkened (except when they are being hovered over) 288 | if not is_focused and event ~= "hover" then 289 | button_color = colors.hsv2hex(H, 0, 50) 290 | end 291 | -- Then the color is lightened if the button is being hovered over, or darkened if it is being pressed, otherwise it is left as is 292 | button_color = 293 | (event == "hover") and colors.lighten(button_color, 25) or 294 | (event == "press") and colors.darken(button_color, 25) or 295 | button_color 296 | -- Save the generate color because why not lol 297 | _private[key_color] = button_color 298 | end 299 | local button_size = _private.button_size 300 | -- If it is a toggle button, we create an outline instead of a filled shape if it is in off state 301 | -- _private[key_img] = (is_on ~= nil and is_on == false) and 302 | -- shapes.circle_outline( 303 | -- _private[key_color], button_size, 304 | -- _private.button_border_width) or 305 | -- shapes.circle_filled( 306 | -- _private[key_color], button_size) 307 | _private[key_img] = shapes.circle_filled(_private[key_color], button_size) 308 | return _private[key_img] 309 | end 310 | 311 | -- Creates a titlebar button widget 312 | local function create_titlebar_button(c, name, button_callback, property) 313 | local button_img = imagebox(nil, false) 314 | if _private.tooltips_enabled then 315 | local tooltip = atooltip { 316 | timer_function = function() 317 | return _private.tooltip_messages[name .. 318 | (property and 319 | (c[property] and "_active" or "_inactive") or "")] 320 | end, 321 | delay_show = 0.5, 322 | margins_leftright = 12, 323 | margins_topbottom = 6, 324 | timeout = 0.25, 325 | align = "bottom_right", 326 | } 327 | tooltip:add_to_object(button_img) 328 | end 329 | local is_on, is_focused 330 | local event = "normal" 331 | local function update() 332 | is_focused = c.active 333 | -- If the button is for a property that can be toggled 334 | if property then 335 | is_on = c[property] 336 | button_img.image = create_button_image( 337 | name, is_focused, event, is_on) 338 | else 339 | button_img.image = create_button_image(name, is_focused, event) 340 | end 341 | end 342 | -- Update the button when the client gains/loses focus 343 | c:connect_signal("unfocus", update) 344 | c:connect_signal("focus", update) 345 | -- If the button is for a property that can be toggled, update it accordingly 346 | if property then c:connect_signal("property::" .. property, update) end 347 | -- Update the button on mouse hover/leave 348 | button_img:connect_signal( 349 | "mouse::enter", function() 350 | event = "hover" 351 | update() 352 | end) 353 | button_img:connect_signal( 354 | "mouse::leave", function() 355 | event = "normal" 356 | update() 357 | end) 358 | -- The button is updated on both click and release, but the call back is executed on release 359 | button_img.buttons = abutton( 360 | {}, nice.MB_LEFT, function() 361 | event = "press" 362 | update() 363 | end, function() 364 | if button_callback then 365 | event = "normal" 366 | button_callback() 367 | else 368 | event = "hover" 369 | end 370 | update() 371 | end) 372 | button_img.id = "button_image" 373 | update() 374 | return wibox.widget { 375 | widget = wcontainer_place, 376 | { 377 | widget = wcontainer_margin, 378 | top = _private.button_margin_top or _private.button_margin_vertical or 379 | _private.button_margin, 380 | bottom = _private.button_margin_bottom or 381 | _private.button_margin_vertical or _private.button_margin, 382 | left = _private.button_margin_left or 383 | _private.button_margin_horizontal or _private.button_margin, 384 | right = _private.button_margin_right or 385 | _private.button_margin_horizontal or _private.button_margin, 386 | { 387 | button_img, 388 | widget = wcontainer_constraint, 389 | height = _private.button_size, 390 | width = _private.button_size, 391 | strategy = "exact", 392 | }, 393 | }, 394 | } 395 | end 396 | 397 | local function get_titlebar_mouse_bindings(c) 398 | local client_color = c._nice_base_color 399 | local shade_enabled = _private.win_shade_enabled 400 | -- Add functionality for double click to (un)maximize, and single click and hold to move 401 | local clicks = 0 402 | local tolerance = double_click_jitter_tolerance 403 | local buttons = { 404 | abutton( 405 | {}, _private.mb_move, function() 406 | local cx, cy = _G.mouse.coords().x, _G.mouse.coords().y 407 | local delta = double_click_time_window_ms / 1000 408 | clicks = clicks + 1 409 | if clicks == 2 then 410 | local nx, ny = _G.mouse.coords().x, _G.mouse.coords().y 411 | -- The second click is only counted as a double click if it is within the neighborhood of the first click's position, and occurs within the set time window 412 | if abs(cx - nx) <= tolerance and abs(cy - ny) <= tolerance then 413 | if shade_enabled then 414 | _private.shade_roll_down(c) 415 | end 416 | c.maximized = not c.maximized 417 | end 418 | else 419 | if shade_enabled and c._nice_window_shade_up then 420 | -- _private.shade_roll_down(c) 421 | awful.mouse.wibox.move(c._nice_window_shade) 422 | else 423 | c:activate{context = "titlebar", action = "mouse_move"} 424 | end 425 | end 426 | -- Start a timer to clear the click count 427 | gtimer_weak_start_new( 428 | delta, function() clicks = 0 end) 429 | end), 430 | abutton( 431 | {}, _private.mb_contextmenu, function() 432 | 433 | local menu_items = {} 434 | local function add_item(text, callback) 435 | menu_items[#menu_items + 1] = {text, callback} 436 | end 437 | -- TODO: Add client control options as menu entries for options that haven't had their buttons added 438 | add_item( 439 | "Redo Window Decorations", function() 440 | c._nice_base_color = get_dominant_color(c) 441 | set_color_rule(c, c._nice_base_color) 442 | _private.add_window_decorations(c) 443 | end) 444 | local picked_color 445 | add_item( 446 | "Manually Pick Color", function() 447 | _G.mousegrabber.run( 448 | function(m) 449 | if m.buttons[1] then 450 | c._nice_base_color = get_pixel_at(m.x, m.y) 451 | set_color_rule(c, c._nice_base_color) 452 | _private.add_window_decorations(c) 453 | return false 454 | end 455 | return true 456 | end, "crosshair") 457 | end) 458 | add_item("Nevermind...", function() end) 459 | if c._nice_right_click_menu then 460 | c._nice_right_click_menu:hide() 461 | end 462 | c._nice_right_click_menu = 463 | awful.menu { 464 | items = menu_items, 465 | theme = _private.context_menu_theme, 466 | } 467 | c._nice_right_click_menu:show() 468 | end), 469 | abutton( 470 | {}, _private.mb_resize, function() 471 | c:activate{context = "mouse_click", action = "mouse_resize"} 472 | end), 473 | } 474 | 475 | if _private.win_shade_enabled then 476 | buttons[#buttons + 1] = abutton( 477 | {}, _private.mb_win_shade_rollup, 478 | function() 479 | _private.shade_roll_up(c) 480 | end) 481 | buttons[#buttons + 1] = abutton( 482 | {}, _private.mb_win_shade_rolldown, 483 | function() 484 | _private.shade_roll_down(c) 485 | end) 486 | end 487 | return buttons 488 | end 489 | 490 | -- Returns a titlebar widget for the given client 491 | local function create_titlebar_title(c) 492 | local client_color = c._nice_base_color 493 | 494 | local title_widget = wibox.widget { 495 | align = "center", 496 | ellipsize = "middle", 497 | opacity = c.active and 1 or title_unfocused_opacity, 498 | valign = "center", 499 | widget = textbox, 500 | } 501 | 502 | local function update() 503 | local text_color = is_contrast_acceptable( 504 | title_color_light, client_color) and 505 | title_color_light or title_color_dark 506 | title_widget.markup = 507 | ("%s"):format( 508 | text_color, _private.titlebar_font, c.name) 509 | end 510 | c:connect_signal("property::name", update) 511 | c:connect_signal( 512 | "unfocus", function() 513 | title_widget.opacity = title_unfocused_opacity 514 | end) 515 | c:connect_signal("focus", function() title_widget.opacity = 1 end) 516 | update() 517 | local titlebar_font_height = get_font_height(_private.titlebar_font) 518 | local leftover_space = _private.titlebar_height - titlebar_font_height 519 | local margin_vertical = leftover_space > 1 and leftover_space / 2 or 0 520 | return { 521 | title_widget, 522 | widget = wcontainer_margin, 523 | top = margin_vertical, 524 | bottom = margin_vertical, 525 | } 526 | end 527 | 528 | -- Returns a titlebar item 529 | local function get_titlebar_item(c, name) 530 | if name == "close" then 531 | return create_titlebar_button(c, name, function() c:kill() end) 532 | elseif name == "maximize" then 533 | return create_titlebar_button( 534 | c, name, function() c.maximized = not c.maximized end, 535 | "maximized") 536 | elseif name == "minimize" then 537 | return create_titlebar_button( 538 | c, name, function() c.minimized = true end) 539 | elseif name == "ontop" then 540 | return create_titlebar_button( 541 | c, name, function() c.ontop = not c.ontop end, "ontop") 542 | elseif name == "floating" then 543 | return create_titlebar_button( 544 | c, name, function() 545 | c.floating = not c.floating 546 | if c.floating then c.maximized = false end 547 | end, "floating") 548 | elseif name == "sticky" then 549 | return create_titlebar_button( 550 | c, name, function() 551 | c.sticky = not c.sticky 552 | return c.sticky 553 | end, "sticky") 554 | elseif name == "title" then 555 | return create_titlebar_title(c) 556 | end 557 | end 558 | 559 | -- Creates titlebar items for a given group of item names 560 | local function create_titlebar_items(c, group) 561 | if not group then return nil end 562 | if type(group) == "string" then return get_titlebar_item(c, group) end 563 | local titlebar_group_items = wibox.widget { 564 | layout = wlayout_fixed_horizontal, 565 | } 566 | local item 567 | for _, name in ipairs(group) do 568 | item = get_titlebar_item(c, name) 569 | if item then titlebar_group_items:add(item) end 570 | end 571 | return titlebar_group_items 572 | end 573 | -- ------------------------------------------------------------ 574 | 575 | -- Adds a window shade to the given client 576 | local function add_window_shade(c, src_top, src_bottom) 577 | local geo = c:geometry() 578 | local w = wibox() 579 | w.width = geo.width 580 | w.background = "transparent" 581 | w.x = geo.x 582 | w.y = geo.y 583 | w.height = _private.titlebar_height + bottom_edge_height 584 | w.ontop = true 585 | w.visible = false 586 | w.shape = shapes.rounded_rect { 587 | tl = _private.titlebar_radius, 588 | tr = _private.titlebar_radius, 589 | bl = 4, 590 | br = 4, 591 | } 592 | -- Need to use a manual layout because layout fixed seems to introduce a thin gap 593 | src_top.point = {x = 0, y = 0} 594 | src_top.forced_width = geo.width 595 | src_bottom.point = {x = 0, y = _private.titlebar_height} 596 | w.widget = {src_top, src_bottom, layout = wlayout.manual} 597 | -- Clean up resources when a client is killed 598 | c:connect_signal( 599 | "request::unmanage", function() 600 | if c._nice_window_shade then 601 | c._nice_window_shade.visible = false 602 | c._nice_window_shade = nil 603 | end 604 | -- Clean up 605 | collectgarbage("collect") 606 | end) 607 | c._nice_window_shade_up = false 608 | c._nice_window_shade = w 609 | end 610 | 611 | -- Shows the window contents 612 | function _private.shade_roll_down(c) 613 | if not c._nice_window_shade_up then return end 614 | c:geometry{x = c._nice_window_shade.x, y = c._nice_window_shade.y} 615 | c:activate() 616 | c._nice_window_shade.visible = false 617 | c._nice_window_shade_up = false 618 | 619 | end 620 | 621 | -- Hides the window contents 622 | function _private.shade_roll_up(c) 623 | if c._nice_window_shade_up then return end 624 | local w = c._nice_window_shade 625 | local geo = c:geometry() 626 | w.x = geo.x 627 | w.y = geo.y 628 | w.width = geo.width 629 | c.minimized = true 630 | w.visible = true 631 | w.ontop = true 632 | c._nice_window_shade_up = true 633 | end 634 | 635 | -- Toggles the window shade state 636 | function _private.shade_toggle(c) 637 | c.minimized = not c.minimized 638 | c._nice_window_shade.visible = c.minimized 639 | end 640 | 641 | -- Puts all the pieces together and decorates the given client 642 | function _private.add_window_decorations(c) 643 | local client_color = c._nice_base_color 644 | -- Closures to avoid repitition 645 | local lighten = function(amount) 646 | return color_lighten(client_color, amount) 647 | end 648 | local darken = 649 | function(amount) return color_darken(client_color, amount) end 650 | -- > Color computations 651 | local luminance = relative_luminance(client_color) 652 | local lighten_amount = rel_lighten(luminance) 653 | local darken_amount = rel_darken(luminance) 654 | -- Inner strokes 655 | local stroke_color_inner_top = lighten(lighten_amount) 656 | local stroke_color_inner_sides = lighten( 657 | 658 | 659 | lighten_amount * 660 | stroke_inner_sides_lighten_mul) 661 | local stroke_color_inner_bottom = lighten( 662 | 663 | 664 | lighten_amount * 665 | stroke_inner_bottom_lighten_mul) 666 | -- Outer strokes 667 | local stroke_color_outer_top = darken( 668 | darken_amount * 669 | stroke_outer_top_darken_mul) 670 | local stroke_color_outer_sides = darken(darken_amount) 671 | local stroke_color_outer_bottom = darken(darken_amount) 672 | local titlebar_height = _private.titlebar_height 673 | local background_fill_top = gradient( 674 | lighten(titlebar_gradient_c1_lighten), 675 | client_color, titlebar_height, 0, 676 | titlebar_gradient_c2_offset) 677 | -- The top left corner of the titlebar 678 | local corner_top_left_img = create_corner_top_left { 679 | background_source = background_fill_top, 680 | color = client_color, 681 | height = titlebar_height, 682 | radius = _private.titlebar_radius, 683 | stroke_offset_inner = 1.5, 684 | stroke_width_inner = 1, 685 | stroke_offset_outer = 0.5, 686 | stroke_width_outer = 1, 687 | stroke_source_inner = gradient( 688 | stroke_color_inner_top, stroke_color_inner_sides, titlebar_height), 689 | stroke_source_outer = gradient( 690 | stroke_color_outer_top, stroke_color_outer_sides, titlebar_height), 691 | } 692 | -- The top right corner of the titlebar 693 | local corner_top_right_img = shapes.flip(corner_top_left_img, "horizontal") 694 | 695 | -- The middle part of the titlebar 696 | local top_edge = create_edge_top_middle { 697 | background_source = background_fill_top, 698 | color = client_color, 699 | height = titlebar_height, 700 | stroke_color_inner = stroke_color_inner_top, 701 | stroke_color_outer = stroke_color_outer_top, 702 | stroke_offset_inner = 1.25, 703 | stroke_offset_outer = 0.5, 704 | stroke_width_inner = 2, 705 | stroke_width_outer = 1, 706 | width = _private.max_width, 707 | } 708 | -- Create the titlebar 709 | local titlebar = awful.titlebar( 710 | c, {size = titlebar_height, bg = "transparent"}) 711 | -- Arrange the graphics 712 | titlebar.widget = { 713 | imagebox(corner_top_left_img, false), 714 | { 715 | { 716 | { 717 | create_titlebar_items(c, _private.titlebar_items.left), 718 | widget = wcontainer_margin, 719 | left = _private.titlebar_margin_left, 720 | }, 721 | { 722 | create_titlebar_items(c, _private.titlebar_items.middle), 723 | buttons = get_titlebar_mouse_bindings(c), 724 | layout = wlayout_flex_horizontal, 725 | }, 726 | { 727 | create_titlebar_items(c, _private.titlebar_items.right), 728 | widget = wcontainer_margin, 729 | right = _private.titlebar_margin_right, 730 | }, 731 | layout = wlayout_align_horizontal, 732 | }, 733 | widget = wcontainer_background, 734 | bgimage = top_edge, 735 | }, 736 | imagebox(corner_top_right_img, false), 737 | layout = wlayout_align_horizontal, 738 | } 739 | 740 | local resize_button = { 741 | abutton( 742 | {}, 1, function() 743 | c:activate{context = "mouse_click", action = "mouse_resize"} 744 | end), 745 | } 746 | 747 | -- The left side border 748 | local left_border_img = create_edge_left { 749 | client_color = client_color, 750 | height = _private.max_height, 751 | stroke_offset_outer = 0.5, 752 | stroke_width_outer = 1, 753 | stroke_color_outer = stroke_color_outer_sides, 754 | stroke_offset_inner = 1.5, 755 | stroke_width_inner = 1.5, 756 | inner_stroke_color = stroke_color_inner_sides, 757 | } 758 | -- The right side border 759 | local right_border_img = shapes.flip(left_border_img, "horizontal") 760 | local left_side_border = awful.titlebar( 761 | c, { 762 | position = "left", 763 | size = 2, 764 | bg = client_color, 765 | widget = wcontainer_background, 766 | }) 767 | left_side_border:setup{ 768 | buttons = resize_button, 769 | widget = wcontainer_background, 770 | bgimage = left_border_img, 771 | } 772 | local right_side_border = awful.titlebar( 773 | c, { 774 | position = "right", 775 | size = 2, 776 | bg = client_color, 777 | widget = wcontainer_background, 778 | }) 779 | right_side_border:setup{ 780 | widget = wcontainer_background, 781 | bgimage = right_border_img, 782 | buttons = resize_button, 783 | } 784 | local corner_bottom_left_img = shapes.flip( 785 | create_corner_top_left { 786 | color = client_color, 787 | radius = bottom_edge_height, 788 | height = bottom_edge_height, 789 | background_source = background_fill_top, 790 | stroke_offset_inner = 1.5, 791 | stroke_offset_outer = 0.5, 792 | stroke_source_outer = gradient( 793 | stroke_color_outer_bottom, stroke_color_outer_sides, 794 | bottom_edge_height, 0, 0.25), 795 | stroke_source_inner = gradient( 796 | stroke_color_inner_bottom, stroke_color_inner_sides, 797 | bottom_edge_height), 798 | stroke_width_inner = 1.5, 799 | stroke_width_outer = 2, 800 | }, "vertical") 801 | local corner_bottom_right_img = shapes.flip( 802 | corner_bottom_left_img, "horizontal") 803 | local bottom_edge = shapes.flip( 804 | create_edge_top_middle { 805 | color = client_color, 806 | height = bottom_edge_height, 807 | background_source = background_fill_top, 808 | stroke_color_inner = stroke_color_inner_bottom, 809 | stroke_color_outer = stroke_color_outer_bottom, 810 | stroke_offset_inner = 1.25, 811 | stroke_offset_outer = 0.5, 812 | stroke_width_inner = 1, 813 | stroke_width_outer = 1, 814 | width = _private.max_width, 815 | }, "vertical") 816 | local bottom = awful.titlebar( 817 | c, { 818 | size = bottom_edge_height, 819 | bg = "transparent", 820 | position = "bottom", 821 | }) 822 | bottom.widget = wibox.widget { 823 | imagebox(corner_bottom_left_img, false), 824 | -- {widget = wcontainer_background, bgimage = bottom_edge}, 825 | imagebox(bottom_edge, false), 826 | 827 | imagebox(corner_bottom_right_img, false), 828 | layout = wlayout_align_horizontal, 829 | buttons = resize_button, 830 | } 831 | if _private.win_shade_enabled then 832 | add_window_shade(c, titlebar.widget, bottom.widget) 833 | end 834 | 835 | if _private.no_titlebar_maximized then 836 | c:connect_signal( 837 | "property::maximized", function() 838 | if c.maximized then 839 | local curr_screen_workarea = client.focus.screen.workarea 840 | awful.titlebar.hide(c) 841 | c.shape = nil 842 | c:geometry{ 843 | x = curr_screen_workarea.x, 844 | y = curr_screen_workarea.y, 845 | width = curr_screen_workarea.width, 846 | height = curr_screen_workarea.height, 847 | } 848 | else 849 | awful.titlebar.show(c) 850 | -- Shape the client 851 | c.shape = shapes.rounded_rect { 852 | tl = _private.titlebar_radius, 853 | tr = _private.titlebar_radius, 854 | bl = 4, 855 | br = 4, 856 | } 857 | end 858 | end) 859 | end 860 | 861 | -- Clean up 862 | collectgarbage("collect") 863 | end 864 | 865 | local function update_max_screen_dims() 866 | local max_height, max_width = 0, 0 867 | for s in _G.screen do 868 | max_height = max(max_height, s.geometry.height) 869 | max_width = max(max_width, s.geometry.width) 870 | end 871 | _private.max_height = max_height * 1.5 872 | _private.max_width = max_width * 1.5 873 | end 874 | 875 | local function validate_mb_bindings() 876 | local action_mbs = { 877 | "mb_move", 878 | "mb_contextmenu", 879 | "mb_resize", 880 | "mb_win_shade_rollup", 881 | "mb_win_shade_rolldown", 882 | } 883 | local mb_specified = {false, false, false, false, false} 884 | local mb 885 | local mb_conflict_test 886 | for i, action_mb in ipairs(action_mbs) do 887 | mb = _private[action_mb] 888 | if mb then 889 | assert(mb >= 1 and mb <= 5, "Invalid mouse button specified!") 890 | mb_conflict_test = mb_specified[mb] 891 | if not mb_conflict_test then 892 | mb_specified[mb] = action_mb 893 | else 894 | error( 895 | 896 | 897 | ("%s and %s can not be bound to the same mouse button"):format( 898 | action_mb, mb_conflict_test)) 899 | end 900 | else 901 | 902 | end 903 | end 904 | end 905 | 906 | function nice.initialize(args) 907 | update_max_screen_dims() 908 | _G.screen.connect_signal("list", update_max_screen_dims) 909 | local crush = require("gears.table").crush 910 | local table_args = { 911 | titlebar_items = true, 912 | context_menu_theme = true, 913 | tooltip_messages = true, 914 | } 915 | if args then 916 | for prop, value in pairs(args) do 917 | if table_args[prop] == true then 918 | crush(_private[prop], value) 919 | elseif prop == "titlebar_radius" then 920 | value = max(3, value) 921 | _private[prop] = value 922 | else 923 | _private[prop] = value 924 | end 925 | end 926 | end 927 | 928 | validate_mb_bindings() 929 | 930 | _G.client.connect_signal( 931 | "request::titlebars", function(c) 932 | -- Callback 933 | c._cb_add_window_decorations = 934 | function() 935 | gtimer_weak_start_new( 936 | 0.25, function() 937 | c._nice_base_color = get_dominant_color(c) 938 | set_color_rule(c, c._nice_base_color) 939 | _private.add_window_decorations(c) 940 | -- table.save(_private, config_dir .. "/nice/private") 941 | c:disconnect_signal( 942 | "request::activate", 943 | c._cb_add_window_decorations) 944 | end) 945 | end -- _cb_add_window_decorations 946 | -- Check if a color rule already exists... 947 | local base_color = get_color_rule(c) 948 | if base_color then 949 | -- If so, use that color rule 950 | c._nice_base_color = base_color 951 | _private.add_window_decorations(c) 952 | else 953 | -- Otherwise use the default titlebar temporarily 954 | c._nice_base_color = _private.titlebar_color 955 | _private.add_window_decorations(c) 956 | -- Connect a signal to determine the client color and then re-decorate it 957 | c:connect_signal( 958 | "request::activate", c._cb_add_window_decorations) 959 | end 960 | -- Shape the client 961 | c.shape = shapes.rounded_rect { 962 | tl = _private.titlebar_radius, 963 | tr = _private.titlebar_radius, 964 | bl = 4, 965 | br = 4, 966 | } 967 | end) 968 | end 969 | 970 | return setmetatable( 971 | nice, {__call = function(_, ...) return nice.initialize(...) end}) 972 | -------------------------------------------------------------------------------- /preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mut-ex/awesome-wm-nice/810aa72bbebfee15d3375fdcb8d8a09f5c7741c8/preview.png -------------------------------------------------------------------------------- /shapes.lua: -------------------------------------------------------------------------------- 1 | -- => Shapes 2 | -- Provides utility functions for handling cairo shapes and geometry 3 | -- ============================================================ 4 | -- 5 | local lgi = require("lgi") 6 | local colors = require("nice.colors") 7 | local hex2rgb = colors.hex2rgb 8 | local darken = colors.darken 9 | local cairo = lgi.cairo 10 | local math = math 11 | local rad = math.rad 12 | local floor = math.floor 13 | local min = math.min 14 | 15 | -- Returns a shape function for a rounded rectangle with independently configurable corner radii 16 | local function rounded_rect(args) 17 | local r1 = args.tl or 0 18 | local r2 = args.bl or 0 19 | local r3 = args.br or 0 20 | local r4 = args.tr or 0 21 | return function(cr, width, height) 22 | cr:new_sub_path() 23 | cr:arc(width - r1, r1, r1, rad(-90), rad(0)) 24 | cr:arc(width - r2, height - r2, r2, rad(0), rad(90)) 25 | cr:arc(r3, height - r3, r3, rad(90), rad(180)) 26 | cr:arc(r4, r4, r4, rad(180), rad(270)) 27 | cr:close_path() 28 | end 29 | end 30 | 31 | -- Returns a circle of the specified size filled with the specified color 32 | local function circle_filled(color, size) 33 | color = color or "#fefefa" 34 | local surface = cairo.ImageSurface.create("ARGB32", size, size) 35 | local cr = cairo.Context.create(surface) 36 | cr:arc(size / 2, size / 2, size / 2, rad(0), rad(360)) 37 | cr:set_source_rgba(hex2rgb(color)) 38 | cr.antialias = cairo.Antialias.BEST 39 | cr:fill() 40 | -- cr:arc( 41 | -- size / 2, size / 2, size / 2 - 0.5, rad(135), rad(270)) 42 | -- cr:set_source_rgba(hex2rgb(darken(color, 25))) 43 | -- cr.line_width = 1 44 | -- cr:stroke() 45 | 46 | return surface 47 | end 48 | 49 | -- Returns a vertical gradient pattern going from cololr_1 -> color_2 50 | local function duotone_gradient_vertical(color_1, color_2, height, offset_1, 51 | offset_2) 52 | local fill_pattern = cairo.Pattern.create_linear(0, 0, 0, height) 53 | local r, g, b, a 54 | r, g, b, a = hex2rgb(color_1) 55 | fill_pattern:add_color_stop_rgba(offset_1 or 0, r, g, b, a) 56 | r, g, b, a = hex2rgb(color_2) 57 | fill_pattern:add_color_stop_rgba(offset_2 or 1, r, g, b, a) 58 | return fill_pattern 59 | end 60 | 61 | -- Returns a horizontal gradient pattern going from cololr_1 -> color_2 62 | local function duotone_gradient_horizontal(color, width) 63 | local fill_pattern = cairo.Pattern.create_linear(0, 0, width, 0) 64 | local r, g, b, a 65 | r, g, b, a = hex2rgb(color) 66 | fill_pattern:add_color_stop_rgba(0, r, g, b, a) 67 | r, g, b, a = hex2rgb(color) 68 | fill_pattern:add_color_stop_rgba(0.5, r, g, b, a) 69 | r, g, b, a = hex2rgb("#00000000") 70 | fill_pattern:add_color_stop_rgba(0.6, r, g, b, a) 71 | r, g, b, a = hex2rgb(color) 72 | fill_pattern:add_color_stop_rgba(0.7, r, g, b, a) 73 | r, g, b, a = hex2rgb(color) 74 | fill_pattern:add_color_stop_rgba(1, r, g, b, a) 75 | return fill_pattern 76 | end 77 | 78 | -- Flips the given surface around the specified axis 79 | local function flip(surface, axis) 80 | local width = surface:get_width() 81 | local height = surface:get_height() 82 | local flipped = cairo.ImageSurface.create("ARGB32", width, height) 83 | local cr = cairo.Context.create(flipped) 84 | local source_pattern = cairo.Pattern.create_for_surface(surface) 85 | if axis == "horizontal" then 86 | source_pattern.matrix = cairo.Matrix {xx = -1, yy = 1, x0 = width} 87 | elseif axis == "vertical" then 88 | source_pattern.matrix = cairo.Matrix {xx = 1, yy = -1, y0 = height} 89 | elseif axis == "both" then 90 | source_pattern.matrix = cairo.Matrix { 91 | xx = -1, 92 | yy = -1, 93 | x0 = width, 94 | y0 = height, 95 | } 96 | end 97 | cr.source = source_pattern 98 | cr:rectangle(0, 0, width, height) 99 | cr:paint() 100 | 101 | return flipped 102 | end 103 | 104 | -- Draws the left corner of the titlebar 105 | local function create_corner_top_left(args) 106 | local radius = args.radius 107 | local height = args.height 108 | local surface = cairo.ImageSurface.create("ARGB32", radius, height) 109 | local cr = cairo.Context.create(surface) 110 | -- Create the corner shape and fill it with a gradient 111 | local radius_offset = 1 -- To soften the corner 112 | cr:move_to(0, height) 113 | cr:line_to(0, radius - radius_offset) 114 | cr:arc( 115 | radius + radius_offset, radius + radius_offset, radius, rad(180), 116 | rad(270)) 117 | cr:line_to(radius, height) 118 | cr:close_path() 119 | cr.source = args.background_source 120 | cr.antialias = cairo.Antialias.BEST 121 | cr:fill() 122 | -- Next add the subtle 3D look 123 | local function add_stroke(nargs) 124 | local arc_radius = nargs.radius 125 | local offset_x = nargs.offset_x 126 | local offset_y = nargs.offset_y 127 | cr:new_sub_path() 128 | cr:move_to(offset_x, height) 129 | cr:line_to(offset_x, arc_radius + offset_y) 130 | cr:arc( 131 | arc_radius + offset_x, arc_radius + offset_y, arc_radius, rad(180), 132 | rad(270)) 133 | cr.source = nargs.source 134 | cr.line_width = nargs.width 135 | cr.antialias = cairo.Antialias.BEST 136 | cr:stroke() 137 | end 138 | -- Outer dark stroke 139 | add_stroke { 140 | offset_x = args.stroke_offset_outer, 141 | offset_y = args.stroke_offset_outer, 142 | radius = radius + 0.5, 143 | source = args.stroke_source_outer, 144 | width = args.stroke_width_outer, 145 | } 146 | -- Inner light stroke 147 | add_stroke { 148 | offset_x = args.stroke_offset_inner, 149 | offset_y = args.stroke_offset_inner, 150 | radius = radius, 151 | width = args.stroke_width_inner, 152 | source = args.stroke_source_inner, 153 | } 154 | 155 | return surface 156 | end 157 | 158 | -- Draws the middle of the titlebar 159 | local function create_edge_top_middle(args) 160 | local client_color = args.color 161 | local height = args.height 162 | local width = args.width 163 | local surface = cairo.ImageSurface.create("ARGB32", width, height) 164 | local cr = cairo.Context.create(surface) 165 | -- Create the background shape and fill it with a gradient 166 | cr:rectangle(0, 0, width, height) 167 | cr.source = args.background_source 168 | cr:fill() 169 | -- Then add the light and dark strokes for that 3D look 170 | local function add_stroke(stroke_width, stroke_offset, stroke_color) 171 | cr:new_sub_path() 172 | cr:move_to(0, stroke_offset) 173 | cr:line_to(width, stroke_offset) 174 | cr.line_width = stroke_width 175 | cr:set_source_rgb(hex2rgb(stroke_color)) 176 | cr:stroke() 177 | end 178 | -- Inner light stroke 179 | add_stroke( 180 | args.stroke_width_inner, args.stroke_offset_inner, 181 | args.stroke_color_inner) 182 | -- Outer dark stroke 183 | add_stroke( 184 | args.stroke_width_outer, args.stroke_offset_outer, 185 | args.stroke_color_outer) 186 | 187 | return surface 188 | end 189 | 190 | local function create_edge_left(args) 191 | local height = args.height 192 | local width = 2 193 | -- height = height or 1080 194 | local surface = cairo.ImageSurface.create("ARGB32", width, height) 195 | local cr = cairo.Context.create(surface) 196 | cr:rectangle(0, 0, 2, args.height) 197 | cr:set_source_rgb(hex2rgb(args.client_color)) 198 | cr:fill() 199 | -- Inner light stroke 200 | cr:new_sub_path() 201 | cr:move_to(args.stroke_offset_inner, 0) -- 1/5 202 | cr:line_to(args.stroke_offset_inner, height) 203 | cr.line_width = args.stroke_width_inner -- 1.5 204 | cr:set_source_rgb(hex2rgb(args.inner_stroke_color)) 205 | cr:stroke() 206 | -- Outer dark stroke 207 | cr:new_sub_path() 208 | cr:move_to(args.stroke_offset_outer, 0) 209 | cr:line_to(args.stroke_offset_outer, height) 210 | cr.line_width = args.stroke_width_outer -- 1 211 | cr:set_source_rgb(hex2rgb(args.stroke_color_outer)) 212 | cr:stroke() 213 | 214 | return surface 215 | end 216 | 217 | local function set_font(cr, font) 218 | cr:set_font_size(font.size) 219 | cr:select_font_face( 220 | font.font or "Inter", font.italic and 1 or 0, font.bold and 1 or 0) 221 | end 222 | 223 | local function text_label(args) 224 | local surface = cairo.ImageSurface.create("ARGB32", 1, 1) 225 | local cr = cairo.Context.create(surface) 226 | set_font(cr, args.font) 227 | local text = args.text 228 | local kern = args.font.kerning or 0 229 | local ext = cr:text_extents(text) 230 | surface = cairo.ImageSurface.create( 231 | "ARGB32", ext.width + string.len(text) * kern, ext.height) 232 | cr = cairo.Context.create(surface) 233 | set_font(cr, args.font) 234 | cr:move_to(0, ext.height) 235 | cr:set_source_rgb(hex2rgb(args.color)) 236 | -- cr:show_text(text) 237 | text:gsub( 238 | ".", function(c) 239 | -- do something with c 240 | cr:show_text(c) 241 | cr:rel_move_to(kern, 0) 242 | end) 243 | return surface 244 | end 245 | 246 | return { 247 | rounded_rect = rounded_rect, 248 | circle_filled = circle_filled, 249 | duotone_gradient_vertical = duotone_gradient_vertical, 250 | flip = flip, 251 | create_corner_top_left = create_corner_top_left, 252 | create_edge_top_middle = create_edge_top_middle, 253 | create_edge_left = create_edge_left, 254 | text_label = text_label, 255 | } 256 | -------------------------------------------------------------------------------- /table.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Courtesy of: http://lua-users.org/wiki/SaveTableToFile 3 | ]] local function exportstring(s) return string.format("%q", s) end 4 | 5 | -- The Save Function 6 | local function save(tbl, filename) 7 | local charS, charE = " ", "\n" 8 | local file, err = io.open(filename, "wb") 9 | if err then return err end 10 | 11 | -- Initialize variables for save procedure 12 | local tables, lookup = {tbl}, {[tbl] = 1} 13 | file:write("return {" .. charE) 14 | 15 | for idx, t in ipairs(tables) do 16 | file:write("-- Table: {" .. idx .. "}" .. charE) 17 | file:write("{" .. charE) 18 | local thandled = {} 19 | 20 | for i, v in ipairs(t) do 21 | thandled[i] = true 22 | local stype = type(v) 23 | -- only handle value 24 | if stype == "table" then 25 | if not lookup[v] then 26 | table.insert(tables, v) 27 | lookup[v] = #tables 28 | end 29 | file:write(charS .. "{" .. lookup[v] .. "}," .. charE) 30 | elseif stype == "string" then 31 | file:write(charS .. exportstring(v) .. "," .. charE) 32 | elseif stype == "number" then 33 | file:write(charS .. tostring(v) .. "," .. charE) 34 | end 35 | end 36 | 37 | for i, v in pairs(t) do 38 | -- escape handled values 39 | if (not thandled[i]) then 40 | 41 | local str = "" 42 | local stype = type(i) 43 | -- handle index 44 | if stype == "table" then 45 | if not lookup[i] then 46 | table.insert(tables, i) 47 | lookup[i] = #tables 48 | end 49 | str = charS .. "[{" .. lookup[i] .. "}]=" 50 | elseif stype == "string" then 51 | str = charS .. "[" .. exportstring(i) .. "]=" 52 | elseif stype == "number" then 53 | str = charS .. "[" .. tostring(i) .. "]=" 54 | end 55 | 56 | if str ~= "" then 57 | stype = type(v) 58 | -- handle value 59 | if stype == "table" then 60 | if not lookup[v] then 61 | table.insert(tables, v) 62 | lookup[v] = #tables 63 | end 64 | file:write(str .. "{" .. lookup[v] .. "}," .. charE) 65 | elseif stype == "string" then 66 | file:write(str .. exportstring(v) .. "," .. charE) 67 | elseif stype == "number" then 68 | file:write(str .. tostring(v) .. "," .. charE) 69 | end 70 | end 71 | end 72 | end 73 | file:write("}," .. charE) 74 | end 75 | file:write("}") 76 | file:close() 77 | end 78 | 79 | -- The Load Function 80 | local function load(sfile) 81 | local ftables, err = loadfile(sfile) 82 | if err then return _, err end 83 | local tables = ftables() 84 | for idx = 1, #tables do 85 | local tolinki = {} 86 | for i, v in pairs(tables[idx]) do 87 | if type(v) == "table" then tables[idx][i] = tables[v[1]] end 88 | if type(i) == "table" and tables[i[1]] then 89 | table.insert(tolinki, {i, tables[i[1]]}) 90 | end 91 | end 92 | -- link indices 93 | for _, v in ipairs(tolinki) do 94 | tables[idx][v[2]], tables[idx][v[1]] = tables[idx][v[1]], nil 95 | end 96 | end 97 | return tables[1] 98 | end 99 | 100 | return {save = save, load = load} 101 | --------------------------------------------------------------------------------