Initial commit

This commit is contained in:
mut-ex 2020-08-31 22:48:32 -04:00
commit d4e057b2ee
8 changed files with 1500 additions and 0 deletions

22
LICENSE.md Normal file
View File

@ -0,0 +1,22 @@
MIT License
Copyright (c) 2020 mut-ex
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

129
README.md Normal file
View File

@ -0,0 +1,129 @@
# :thumbsup:nice
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...
* ...adds a **subtle 3D look**, and soft, **rounded anti-aliased, corners** to windows
* ...picks the window **decoration color based on the client content for a seamless look** , and **adjusts the window title text color** accordingly
* ...**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!
* ...allows you to **customize** which **titlebar buttons** to include, their order, and their layout
* ...adds the **ability to maximize/unmaximize** floating windows by **double clicking the titlebar**, and of course, **moving them by clicking and holding**
* ...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!
![Preview](https://raw.githubusercontent.com/mut-ex/awesome-wm-nice/master/preview.png)
## Getting Started
### Prerequisites
* You need to be using **[Awesome WM](https://awesomewm.org/)** as your window manager, and should already have a working basic configuration file. I have developed and tested nice only on **awesome v4.3 git version**
* I **highly** suggest using a compositor such as **[picom](https://github.com/yshui/picom)**. My recommended shadow settings are given below
```
shadow = true;
shadow-radius = 40;
shadow-opacity = .55;
shadow-offset-x = -40;
shadow-offset-y = -20;
shadow-exclude = [
"_NET_WM_WINDOW_TYPE:a = '_NET_WM_WINDOW_TYPE_NOTIFICATION'",
"_NET_WM_STATE@:32a *= '_NET_WM_STATE_HIDDEN'",
"_GTK_FRAME_EXTENTS@:c"
];
```
* For GTK apps, I suggest adding the following line to **~/.config/gtk-3.0/settings.ini** under the **[Settings]** section
```
gtk-decoration-layout=menu:
```
### Installation
The easiest and quickest way to get started is by cloning this repository to your awesome configuration directory
```shell
$ cd ~/.config/awesome
$ git clone https://github.com/mut-ex/awesome-wm-nice.git nice
```
## Usage
To use nice, you first need to load the module. You can do so by placing the following line right after `beautiful.init(...)`
```lua
local nice = require("nice")
nice()
```
If you are fine using the default configuration, you are all done! However if you like, you can override the defaults you wish to change by passing your own configuration. There are a lot of parameters you can change! The commented out lines in the code block before represent the default values
```lua
local nice = require("nice")
nice = {
-- * == Titlebar specific == *
-- titlebar_color = "#1E1E24"
-- titlebar_height = 38
-- titlebar_radius = 9
-- titlebar_margin_left = 0
-- titlebar_margin_right = 0
-- titlebar_font = "Sans 10"
-- titlebar_items = {
-- left = {"close", "minimize", "maximize"},
-- middle = {"title"},
-- right = {"sticky", "ontop", "floating"},
-- }
-- context_menu_theme = {
-- bg_normal = "#5e6472",
-- fg_normal = "#fefefa",
-- bg_focus = "#aed9e0",
-- fg_focus = "#242424",
-- border_color = "#00000000",
-- border_width = 0,
-- height = 35,
-- width = 250,
-- font = "Sans 10",
-- }
-- window_shade_enabled = false
-- * == Button specific == *
-- button_margin_horizontal = 5
-- button_margin_top = 2
-- button_size = 16
-- close_color = "#ee4266"
-- minimize_color = "#ffb400"
-- maximize_color = "#4CBB17"
-- floating_color = "#f6a2ed"
-- ontop_color = "#f6a2ed"
-- sticky_color = "#f6a2ed"
-- tooltips_enabled = true
-- tooltip_messages = {
-- close = "close",
-- minimize = "minimize",
-- floating_active = "enable tiling mode",
-- floating_inactive = "enable floating mode",
-- maximize_active = "unmaximize",
-- maximize_inactive = "maximize",
-- ontop_active = "don't keep above other windows",
-- ontop_inactive = "keep above other windows",
-- sticky_active = "disable sticky mode",
-- sticky_inactive = "enable sticky mode",
-- }
}
```
## License
[![License](http://img.shields.io/:license-mit-blue.svg)](http://doge.mit-license.org)

25
color_rules Normal file
View File

@ -0,0 +1,25 @@
return {
-- Table: {1}
{
["code-oss"]="#2d2b55",
["nextcloud"]="#242424",
["magnus"]="#2d2d2d",
["Navigator"]="#2d234d",
["firefox"]="#181818",
["kitty"]="#202745",
["simplescreenrecorder"]="#161925",
["gimp-2.10"]="#454545",
["balena-etcher"]="#4d5057",
["eog"]="#2d2d2d",
["ghb"]="#181818",
["electron"]="#181818",
["shutter"]="#181818",
["typora"]="#2d2d2d",
["Firefox"]="#181818",
["keditfiletype5"]="#161925",
["kvantummanager"]="#161925ff",
["dolphin"]="#161925",
["vlc"]="#161925",
["org.gnome.Nautilus"]="#242424",
},
}

159
colors.lua Normal file
View File

@ -0,0 +1,159 @@
-- => Colors
-- Provides utility functions for handling colors
-- ============================================================
local math = math
local floor = math.floor
local max = math.max
local min = math.min
local pow = math.pow
local random = math.random
local debug = require("helpers").debug
-- Returns a value that is clipped to interval edges if it falls outside the interval
local function clip(num, min_num, max_num) return
max(min(num, max_num), min_num) end
-- Converts the given hex color to normalized rgba
local function hex2rgb(color)
color = color:gsub("#", "")
local strlen = color:len()
if strlen == 6 then
return tonumber("0x" .. color:sub(1, 2)) / 255,
tonumber("0x" .. color:sub(3, 4)) / 255,
tonumber("0x" .. color:sub(5, 6)) / 255, 1
end
if strlen == 8 then
return tonumber("0x" .. color:sub(1, 2)) / 255,
tonumber("0x" .. color:sub(3, 4)) / 255,
tonumber("0x" .. color:sub(5, 6)) / 255,
tonumber("0x" .. color:sub(7, 8)) / 255
end
end
-- Converts the given hex color to hsv
local function hex2hsv(color)
local r, g, b = hex2rgb(color)
local C_max = max(r, g, b)
local C_min = min(r, g, b)
local delta = C_max - C_min
local H, S, V
if delta == 0 then
H = 0
elseif C_max == r then
H = 60 * (((g - b) / delta) % 6)
elseif C_max == g then
H = 60 * (((b - r) / delta) + 2)
elseif C_max == b then
H = 60 * (((r - g) / delta) + 4)
end
if C_max == 0 then
S = 0
else
S = delta / C_max
end
V = C_max
return H, S * 100, V * 100
end
-- Converts the given hsv color to hex
local function hsv2hex(H, S, V)
S = S / 100
V = V / 100
if H > 360 then H = 360 end
if H < 0 then H = 0 end
local C = V * S
local X = C * (1 - math.abs(((H / 60) % 2) - 1))
local m = V - C
local r_, g_, b_ = 0, 0, 0
if H >= 0 and H < 60 then
r_, g_, b_ = C, X, 0
elseif H >= 60 and H < 120 then
r_, g_, b_ = X, C, 0
elseif H >= 120 and H < 180 then
r_, g_, b_ = 0, C, X
elseif H >= 180 and H < 240 then
r_, g_, b_ = 0, X, C
elseif H >= 240 and H < 300 then
r_, g_, b_ = X, 0, C
elseif H >= 300 and H < 360 then
r_, g_, b_ = C, 0, X
end
local r, g, b = (r_ + m) * 255, (g_ + m) * 255, (b_ + m) * 255
return ("#%02x%02x%02x"):format(floor(r), floor(g), floor(b))
end
-- Calculates the relative luminance of the given color
local function relative_luminance(color)
local r, g, b = hex2rgb(color)
local function from_sRGB(u)
return u <= 0.0031308 and 25 * u / 323 or
pow(((200 * u + 11) / 211), 12 / 5)
end
return 0.2126 * from_sRGB(r) + 0.7152 * from_sRGB(g) + 0.0722 * from_sRGB(b)
end
-- Calculates the contrast ratio between the two given colors
local function contrast_ratio(fg, bg)
return (relative_luminance(fg) + 0.05) / (relative_luminance(bg) + 0.05)
end
-- Returns true if the contrast between the two given colors is suitable
local function is_contrast_acceptable(fg, bg)
return contrast_ratio(fg, bg) >= 7 and true
end
-- Returns a bright-ish, saturated-ish, color of random hue
local function rand_hex(lb_angle, ub_angle)
return hsv2hex(random(lb_angle or 0, ub_angle or 360), 70, 90)
end
-- Rotates the hue of the given hex color by the specified angle (in degrees)
local function rotate_hue(color, angle)
local H, S, V = hex2hsv(color)
angle = clip(angle or 0, 0, 360)
H = (H + angle) % 360
return hsv2hex(H, S, V)
end
-- Lightens a given hex color by the specified amount
local function lighten(color, amount)
local r, g, b
r, g, b = hex2rgb(color)
r = 255 * r
g = 255 * g
b = 255 * b
r = r + floor(2.55 * amount)
g = g + floor(2.55 * amount)
b = b + floor(2.55 * amount)
r = r > 255 and 255 or r
g = g > 255 and 255 or g
b = b > 255 and 255 or b
return ("#%02x%02x%02x"):format(r, g, b)
end
-- Darkens a given hex color by the specified amount
local function darken(color, amount)
local r, g, b
r, g, b = hex2rgb(color)
r = 255 * r
g = 255 * g
b = 255 * b
r = max(0, r - floor(r * (amount / 100)))
g = max(0, g - floor(g * (amount / 100)))
b = max(0, b - floor(b * (amount / 100)))
return ("#%02x%02x%02x"):format(r, g, b)
end
return {
clip = clip,
hex2rgb = hex2rgb,
hex2hsv = hex2hsv,
hsv2hex = hsv2hex,
relative_luminance = relative_luminance,
contrast_ratio = contrast_ratio,
is_contrast_acceptable = is_contrast_acceptable,
rand_hex = rand_hex,
rotate_hue = rotate_hue,
lighten = lighten,
darken = darken,
}

853
init.lua Normal file
View File

@ -0,0 +1,853 @@
----
-- ============================================================
-- > Awesome WM LIBS
local awful = require("awful")
local atooltip = awful.tooltip
local wibox = require("wibox")
-- Widgets
local imagebox = wibox.widget.imagebox
local textbox = wibox.widget.textbox
-- Layouts
local wlayout = wibox.layout
local wlayout_align_horizontal = wlayout.align.horizontal
local wlayout_fixed_horizontal = wlayout.fixed.horizontal
-- Containers
local wcontainer = wibox.container
local wcontainer_background = wcontainer.background
local wcontainer_place = wcontainer.place
local wcontainer_constraint = wcontainer.constraint
-- Gears
local gsurface = require("gears.surface")
local gtimer = require("gears.timer")
local gtimer_weak_start_new = gtimer.weak_start_new
-- > MATH LIBS
local math = math
local max = math.max
local abs = math.abs
local rad = math.rad
local floor = math.floor
-- > LGI LIBS
local lgi = require("lgi")
local cairo = lgi.cairo
local gdk = lgi.Gdk
local pixbuf_get_from_window = gdk.pixbuf_get_from_window
local get_default_root_window = gdk.get_default_root_window
local pixbuf_get_from_surface = gdk.pixbuf_get_from_surface
-- > NICE LIBS
-- Colors
local colors = require("nice.colors")
local color_lighten = colors.lighten
local color_darken = colors.darken
local is_contrast_acceptable = colors.is_contrast_acceptable
local relative_luminance = colors.relative_luminance
-- Shapes
local shapes = require("nice.shapes")
local create_corner_top_left = shapes.create_corner_top_left
local create_edge_top_middle = shapes.create_edge_top_middle
local create_edge_left = shapes.create_edge_left
local gradient = shapes.duotone_gradient_vertical
gdk.init({})
-- => Local settings
-- ============================================================
local bottom_edge_height = 3
local double_click_jitter_tolerance = 4
local double_click_time_window_ms = 250
local stroke_inner_bottom_lighten_mul = 0.4
local stroke_inner_sides_lighten_mul = 0.4
local stroke_outer_top_darken_mul = 0.7
local title_color_dark = "#242424"
local title_color_light = "#fefefa"
local title_unfocused_opacity = 0.7
local titlebar_gradient_c1_lighten = 1
local titlebar_gradient_c2_offset = 0.5
local function rel_lighten(lum) return lum * 90 + 10 end
local function rel_darken(lum) return -(lum * 70) + 100 end
-- ------------------------------------------------------------
local nice = {}
-- => Defaults
-- ============================================================
local _private = {}
_private.max_width = 0
_private.max_height = 0
-- Titlebar
_private.titlebar_color = "#1E1E24"
_private.titlebar_height = 38
_private.titlebar_radius = 9
_private.titlebar_margin_left = 0
_private.titlebar_margin_right = 0
_private.titlebar_font = "Inter Regular 10"
_private.titlebar_items = {
left = {"close", "minimize", "maximize"},
middle = {"title"},
right = {"sticky", "ontop", "floating"},
}
_private.context_menu_theme = {
bg_normal = "#5e6472",
fg_normal = "#fefefa",
bg_focus = "#aed9e0",
fg_focus = "#242424",
border_color = "#00000000",
border_width = 0,
height = 35,
width = 250,
font = "Inter Regular 10",
}
_private.window_shade_enabled = true
-- Button
_private.button_margin_horizontal = 5
_private.button_margin_top = 2
_private.button_size = 16
_private.tooltips_enabled = true
_private.tooltip_messages = {
close = "close",
minimize = "minimize",
floating_active = "enable tiling mode",
floating_inactive = "enable floating mode",
maximize_active = "unmaximize",
maximize_inactive = "maximize",
ontop_active = "don't keep above other windows",
ontop_inactive = "keep above other windows",
sticky_active = "disable sticky mode",
sticky_inactive = "enable sticky mode",
}
_private.close_color = "#ee4266"
_private.minimize_color = "#ffb400"
_private.maximize_color = "#4CBB17"
_private.floating_color = "#f6a2ed"
_private.ontop_color = "#f6a2ed"
_private.sticky_color = "#f6a2ed"
-- ------------------------------------------------------------
-- => Saving and loading of color rules
-- ============================================================
local table = table
local t = require("nice.table")
table.save = t.save
table.load = t.load
-- Load the color rules or create an empty table if there aren't any
local gfilesys = require("gears.filesystem")
local config_dir = gfilesys.get_configuration_dir()
local color_rules_filename = "color_rules"
local color_rules_filepath = config_dir .. "/nice/" .. color_rules_filename
_private.color_rules = table.load(color_rules_filepath) or {}
-- Saves the contents of _private.color_rules table to file
local function save_color_rules()
table.save(_private.color_rules, color_rules_filepath)
end
-- Adds a color rule entry to the color_rules table for the given client and saves to file
local function set_color_rule(c, color)
_private.color_rules[c.instance] = color
save_color_rules()
end
-- Fetches the color rule for the given client instance
local function get_color_rule(c) return _private.color_rules[c.instance] end
-- ------------------------------------------------------------
-- Returns the hex color for the pixel at the given coordinates on the screen
local function get_pixel_at(x, y)
local pixbuf = pixbuf_get_from_window(get_default_root_window(), x, y, 1, 1)
local bytes = pixbuf:get_pixels()
return "#" ..
bytes:gsub(
".", function(c) return ("%02x"):format(c:byte()) end)
end
-- Determines the dominant color of the client's top region
local function get_dominant_color(client)
local color
gsurface(client.content):write_to_png(
"/home/mutex/nice/" .. client.class .. "_" .. client.instance .. ".png")
local pb
local bytes
local tally = {}
local content = gsurface(client.content)
local cgeo = client:geometry()
local x_offset = 2
local y_offset = 2
local x_lim = floor(cgeo.width / 2)
for x_pos = 0, x_lim, 2 do
for y_pos = 0, 8, 1 do
pb = pixbuf_get_from_surface(
content, x_offset + x_pos, y_offset + y_pos, 1, 1)
bytes = pb:get_pixels()
color = "#" ..
bytes:gsub(
".",
function(c)
return ("%02x"):format(c:byte())
end)
if not tally[color] then
tally[color] = 1
else
tally[color] = tally[color] + 1
end
end
end
local mode
local mode_c = 0
for kolor, kount in pairs(tally) do
if kount > mode_c then
mode_c = kount
mode = kolor
end
end
color = mode
set_color_rule(client, color)
return color
end
-- Returns a color that is analogous to the last color returned
-- 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
local next_color = colors.rand_hex()
local function get_next_color()
local prev_color = next_color
next_color = colors.rotate_hue(prev_color, 33)
return prev_color
end
-- Returns (or generates) a button image based on the given params
local function create_button_image(name, is_focused, event, is_on)
local focus_state = is_focused and "focused" or "unfocused"
local key_img
-- If it is a toggle button, then the key has an extra param
if is_on ~= nil then
local toggle_state = is_on and "on" or "off"
key_img = ("%s_%s_%s_%s"):format(name, toggle_state, focus_state, event)
else
key_img = ("%s_%s_%s"):format(name, focus_state, event)
end
-- If an image already exists, then we are done
if _private[key_img] then return _private[key_img] end
-- The color key just has _color at the end
local key_color = key_img .. "_color"
-- If the user hasn't provided a color, then we have to generate one
if not _private[key_color] then
local key_base_color = name .. "_color"
-- Maybe the user has at least provided a base color? If not we just pick a pesudo-random color
local base_color = _private[key_base_color] or get_next_color()
_private[key_base_color] = base_color
local button_color = base_color
local H = colors.hex2hsv(base_color)
-- Unfocused buttons are desaturated and darkened (except when they are being hovered over)
if not is_focused and event ~= "hover" then
button_color = colors.hsv2hex(H, 0, 50)
end
-- 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
button_color =
(event == "hover") and colors.lighten(button_color, 25) or
(event == "press") and colors.darken(button_color, 25) or
button_color
-- Save the generate color because why not lol
_private[key_color] = button_color
end
local button_size = _private.button_size
-- If it is a toggle button, we create an outline instead of a filled shape if it is in off state
-- _private[key_img] = (is_on ~= nil and is_on == false) and
-- shapes.circle_outline(
-- _private[key_color], button_size,
-- _private.button_border_width) or
-- shapes.circle_filled(
-- _private[key_color], button_size)
_private[key_img] = shapes.circle_filled(_private[key_color], button_size)
return _private[key_img]
end
-- Creates a titlebar button widget
local function create_titlebar_button(c, name, button_callback, property)
local button_img = imagebox(nil, false)
if _private.tooltips_enabled then
local tooltip = atooltip {
timer_function = function()
return _private.tooltip_messages[name ..
(property and
(c[property] and "_active" or "_inactive") or "")]
end,
delay_show = 0.5,
margins_leftright = 12,
margins_topbottom = 6,
timeout = 0.25,
align = "bottom_right",
}
tooltip:add_to_object(button_img)
end
local is_on, is_focused
local event = "normal"
local function update()
is_focused = c.active
-- If the button is for a property that can be toggled
if property then
is_on = c[property]
button_img.image = create_button_image(
name, is_focused, event, is_on)
else
button_img.image = create_button_image(name, is_focused, event)
end
end
-- Update the button when the client gains/loses focus
c:connect_signal("unfocus", update)
c:connect_signal("focus", update)
-- If the button is for a property that can be toggled, update it accordingly
if property then c:connect_signal("property::" .. property, update) end
-- Update the button on mouse hover/leave
button_img:connect_signal(
"mouse::enter", function()
event = "hover"
update()
end)
button_img:connect_signal(
"mouse::leave", function()
event = "normal"
update()
end)
-- The button is updated on both click and release, but the call back is executed on release
button_img.buttons = awful.button(
{}, awful.button.names.LEFT, function()
event = "press"
update()
end, function()
if button_callback then
event = "normal"
button_callback()
else
event = "hover"
end
update()
end)
local margin = _private.button_spacing
button_img.id = "button_image"
update()
return wibox.widget {
widget = wcontainer_place,
{
widget = wcontainer.margin,
top = _private.button_margin_top or _private.button_margin_vertical or
_private.button_margin,
bottom = _private.button_margin_bottom or
_private.button_margin_vertical or _private.button_margin,
left = _private.button_margin_left or
_private.button_margin_horizontal or _private.button_margin,
right = _private.button_margin_right or
_private.button_margin_horizontal or _private.button_margin,
{
button_img,
widget = wcontainer_constraint,
height = _private.button_size,
width = _private.button_size,
strategy = "exact",
},
},
}
end
-- Creates a client title widget
local function create_titlebar_title(c)
local client_color = c._nice_base_color
local shade_enabled = _private.window_shade_enabled
-- Add functionality for double click to (un)maximize, and single click and hold to move
local clicks = 0
local tolerance = double_click_jitter_tolerance
local buttons = {
awful.button(
{}, awful.button.names.LEFT, function()
local cx, cy = _G.mouse.coords().x, _G.mouse.coords().y
local delta = double_click_time_window_ms / 1000
clicks = clicks + 1
if clicks == 2 then
local nx, ny = _G.mouse.coords().x, _G.mouse.coords().y
-- 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
if abs(cx - nx) <= tolerance and abs(cy - ny) <= tolerance then
if shade_enabled then
_private.shade_roll_down(c)
end
c.maximized = not c.maximized
end
else
if shade_enabled then
_private.shade_roll_down(c)
end
c:activate{context = "titlebar", action = "mouse_move"}
end
-- Start a timer to clear the click count
gtimer_weak_start_new(
delta, function() clicks = 0 end)
end),
awful.button(
{}, awful.button.names.RIGHT, function()
local menu_items = {}
local function add_item(text, callback)
-- right_click_menu(right_click_menu, {text, callback})
menu_items[#menu_items + 1] = {text, callback}
end
-- TODO: Add client control options as menu enteries for options that haven't had their buttons added
add_item(
"Redo Window Decorations", function()
c._nice_base_color = get_dominant_color(c)
set_color_rule(c, c._nice_base_color)
_private.add_window_decorations(c)
end)
local picked_color
add_item(
"Manually Pick Color", function()
_G.mousegrabber.run(
function(m)
if m.buttons[1] then
c._nice_base_color = get_pixel_at(m.x, m.y)
set_color_rule(c, c._nice_base_color)
_private.add_window_decorations(c)
return false
end
return true
end, "crosshair")
end)
-- if c._nice_window_shade then
-- local win_shade = c._nice_window_shade
-- add_item(
-- not win_shade.visible and "Roll Up" or "Roll Down",
-- function()
-- _private.shade_toggle(c)
-- end)
-- end
add_item("Nevermind...", function() end)
if c._nice_right_click_menu then
c._nice_right_click_menu:hide()
end
c._nice_right_click_menu =
awful.menu {
items = menu_items,
theme = _private.context_menu_theme,
}
c._nice_right_click_menu:show()
end),
}
if _private.window_shade_enabled then
buttons[#buttons + 1] = awful.button(
{}, awful.button.names.SCROLL_UP,
function()
_private.shade_roll_up(c)
end)
buttons[#buttons + 1] = awful.button(
{}, awful.button.names.SCROLL_DOWN,
function()
_private.shade_roll_down(c)
end)
end
local title_widget = wibox.widget {
align = "center",
buttons = buttons,
ellipsize = "middle",
font = _private.titlebar_font,
opacity = c.active and 1 or title_unfocused_opacity,
valign = "center",
widget = textbox,
}
local function update()
local text_color = is_contrast_acceptable(
title_color_light, client_color) and
title_color_light or title_color_dark
title_widget.markup =
("<span foreground='%s' font='%s'>%s</span>"):format(
text_color, _private.titlebar_font or "IBM Plex Sans 11", c.name)
end
c:connect_signal("property::name", update)
-- c:connect_signal("property::_nice_color", update)
c:connect_signal(
"unfocus", function()
title_widget.opacity = title_unfocused_opacity
end)
c:connect_signal("focus", function() title_widget.opacity = 1 end)
update()
return {title_widget, widget = wcontainer.margin, left = 4, right = 4}
end
-- Creates titlebar items for a given group of item names
local function create_titlebar_items(c, group)
if not group then return nil end
local titlebar_group_items = wibox.widget {
layout = wlayout_fixed_horizontal,
}
local item
for _, name in ipairs(group) do
if name == "close" then
item = create_titlebar_button(
c, name, function() c:kill() end)
elseif name == "maximize" then
item = create_titlebar_button(
c, name, function()
c.maximized = not c.maximized
end, "maximized")
elseif name == "minimize" then
item = create_titlebar_button(
c, name, function() c.minimized = true end)
elseif name == "ontop" then
item = create_titlebar_button(
c, name, function() c.ontop = not c.ontop end, "ontop")
elseif name == "floating" then
item = create_titlebar_button(
c, name, function()
c.floating = not c.floating
if c.floating then c.maximized = false end
end, "floating")
elseif name == "sticky" then
item = create_titlebar_button(
c, name, function()
c.sticky = not c.sticky
return c.sticky
end, "sticky")
elseif name == "title" then
item = create_titlebar_title(c)
end
if #group == 1 then return item end
titlebar_group_items:add(item)
end
return titlebar_group_items
end
-- ------------------------------------------------------------
-- Adds a window shade to the given client
local function add_window_shade(c, src_top, src_bottom)
local geo = c:geometry()
local w = wibox()
w.width = geo.width
w.background = "transparent"
w.x = geo.x
w.y = geo.y
w.height = _private.titlebar_height + 3
w.ontop = true
w.visible = false
w.shape= shapes.rounded_rect {
tl = _private.titlebar_radius+1,
tr = _private.titlebar_radius+1,
bl = 4,
br = 4,
}
w.widget = wibox.widget {
{
src_top,
src_bottom,
layout = wlayout.fixed.vertical,
},
widget = wcontainer_background,
bg = "transparent",
}
c:connect_signal(
"request::unmanage", function()
if c._nice_window_shade then
c._nice_window_shade.visible = false
c._nice_window_shade = nil
end
collectgarbage()
collectgarbage()
end)
c._nice_window_shade = w
end
-- Shows the window contents
function _private.shade_roll_down(c)
c.minimized = false
c._nice_window_shade.visible = false
end
-- Hides the window contents
function _private.shade_roll_up(c)
local w = c._nice_window_shade
local geo = c:geometry()
w.x = geo.x
w.y = geo.y
w.width = geo.width
c.minimized = true
w.visible = true
w.ontop = true
end
-- Toggles the window shade state
function _private.shade_toggle(c)
c.minimized = not c.minimized
c._nice_window_shade.visible = c.minimized
end
-- Puts all the pieces together and decorates the given client
function _private.add_window_decorations(c)
local client_color = c._nice_base_color
-- Closures to avoid repitition
local lighten = function(amount)
return color_lighten(client_color, amount)
end
local darken =
function(amount) return color_darken(client_color, amount) end
-- > Color computations
local luminance = relative_luminance(client_color)
local lighten_amount = rel_lighten(luminance)
local darken_amount = rel_darken(luminance)
-- Inner strokes
local stroke_color_inner_top = lighten(lighten_amount)
local stroke_color_inner_sides = lighten(
lighten_amount *
stroke_inner_sides_lighten_mul)
local stroke_color_inner_bottom = lighten(
lighten_amount *
stroke_inner_bottom_lighten_mul)
-- Outer strokes
local stroke_color_outer_top = darken(
darken_amount *
stroke_outer_top_darken_mul)
local stroke_color_outer_sides = darken(darken_amount)
local stroke_color_outer_bottom = darken(darken_amount)
local titlebar_height = _private.titlebar_height
local background_fill_top = gradient(
lighten(titlebar_gradient_c1_lighten),
client_color, titlebar_height, 0,
titlebar_gradient_c2_offset)
-- The top left corner of the titlebar
local corner_top_left_img = create_corner_top_left {
background_source = background_fill_top,
color = client_color,
height = titlebar_height,
radius = _private.titlebar_radius,
stroke_offset_inner = 1.5,
stroke_width_inner = 1,
stroke_offset_outer = 0.5,
stroke_width_outer = 1,
stroke_source_inner = gradient(
stroke_color_inner_top, stroke_color_inner_sides, titlebar_height),
stroke_source_outer = gradient(
stroke_color_outer_top, stroke_color_outer_sides, titlebar_height),
}
-- The top right corner of the titlebar
local corner_top_right_img = shapes.flip(corner_top_left_img, "horizontal")
-- The middle part of the titlebar
local top_edge = create_edge_top_middle {
background_source = background_fill_top,
color = client_color,
height = titlebar_height,
stroke_color_inner = stroke_color_inner_top,
stroke_color_outer = stroke_color_outer_top,
stroke_offset_inner = 1.25,
stroke_offset_outer = 0.5,
stroke_width_inner = 2,
stroke_width_outer = 1,
width = _private.max_width,
}
-- Create the titlebar
local titlebar = awful.titlebar(
c, {size = titlebar_height, bg = "transparent"})
-- Arrange the graphics
titlebar.widget = {
imagebox(corner_top_left_img, false),
{
{
{
create_titlebar_items(c, _private.titlebar_items.left),
widget = wcontainer.margin,
left = _private.titlebar_margin_left,
},
create_titlebar_items(c, _private.titlebar_items.middle),
{
create_titlebar_items(c, _private.titlebar_items.right),
widget = wcontainer.margin,
right = _private.titlebar_margin_right,
},
layout = wlayout_align_horizontal,
},
widget = wcontainer_background,
bgimage = top_edge,
},
imagebox(corner_top_right_img, false),
layout = wlayout_align_horizontal,
}
-- The left side border
local left_border_img = create_edge_left {
client_color = client_color,
height = _private.max_height,
stroke_offset_outer = 0.5,
stroke_width_outer = 1,
stroke_color_outer = stroke_color_outer_sides,
stroke_offset_inner = 1.5,
stroke_width_inner = 1.5,
inner_stroke_color = stroke_color_inner_sides,
}
-- The right side border
local right_border_img = shapes.flip(left_border_img, "horizontal")
local left_side_border = awful.titlebar(
c, {
position = "left",
size = 2,
bg = client_color,
widget = wcontainer_background,
})
left_side_border:setup{
widget = wcontainer_background,
bgimage = left_border_img,
buttons = {
awful.button(
{}, 1, function()
c:activate{context = "mouse_click", action = "mouse_resize"}
end),
},
}
local right_side_border = awful.titlebar(
c, {
position = "right",
size = 2,
bg = client_color,
widget = wcontainer_background,
})
right_side_border:setup{
widget = wcontainer_background,
bgimage = right_border_img,
buttons = {
awful.button(
{}, 1, function()
c:activate{context = "mouse_click", action = "mouse_resize"}
end),
},
}
local corner_bottom_left_img = shapes.flip(
create_corner_top_left {
color = client_color,
radius = bottom_edge_height,
height = bottom_edge_height,
background_source = background_fill_top,
stroke_offset_inner = 1.5,
stroke_offset_outer = 0.5,
stroke_source_outer = gradient(
stroke_color_outer_bottom, stroke_color_outer_sides,
bottom_edge_height, 0, 0.25),
stroke_source_inner = gradient(
stroke_color_inner_bottom, stroke_color_inner_sides,
bottom_edge_height),
stroke_width_inner = 1.5,
stroke_width_outer = 2,
}, "vertical")
local corner_bottom_right_img = shapes.flip(
corner_bottom_left_img, "horizontal")
local bottom_edge = shapes.flip(
create_edge_top_middle {
color = client_color,
height = bottom_edge_height,
background_source = background_fill_top,
stroke_color_inner = stroke_color_inner_bottom,
stroke_color_outer = stroke_color_outer_bottom,
stroke_offset_inner = 1.25,
stroke_offset_outer = 0.5,
stroke_width_inner = 1,
stroke_width_outer = 1,
width = _private.max_width,
}, "vertical")
local bottom = awful.titlebar(
c, {
size = bottom_edge_height,
bg = "transparent",
position = "bottom",
})
bottom:setup{
imagebox(corner_bottom_left_img, false),
{widget = wcontainer_background, bgimage = bottom_edge},
imagebox(corner_bottom_right_img, false),
layout = wlayout_align_horizontal,
}
if _private.window_shade_enabled then
add_window_shade(c, titlebar.widget, bottom.widget)
end
-- Clean up
collectgarbage()
collectgarbage()
end
local function update_max_screen_dims()
local max_height, max_width = 0, 0
for s in _G.screen do
max_height = max(max_height, s.geometry.height)
max_width = max(max_width, s.geometry.width)
end
_private.max_height = max_height * 1.5
_private.max_width = max_width * 1.5
end
--[[
]]
function nice.initialize(args)
update_max_screen_dims()
_G.screen.connect_signal("list", update_max_screen_dims)
local crush = require("gears.table").crush
local table_args = {
titlebar_items = true,
context_menu_theme = true,
tooltip_messages = true,
}
if args then
for prop, value in pairs(args) do
if table_args[prop] == true then
crush(_private[prop], value)
elseif prop == "titlebar_radius" then
value = max(3, value)
_private[prop] = value
else
_private[prop] = value
end
end
end
_G.client.connect_signal(
"request::titlebars", function(c)
-- Callback
c._cb_add_window_decorations =
function()
gtimer_weak_start_new(
0.25, function()
c._nice_base_color = get_dominant_color(c)
set_color_rule(c, c._nice_base_color)
_private.add_window_decorations(c)
-- table.save(_private, config_dir .. "/nice/private")
c:disconnect_signal(
"request::activate",
c._cb_add_window_decorations)
end)
end -- _cb_add_window_decorations
-- Check if a color rule already exists...
local base_color = get_color_rule(c)
if base_color then
-- If so, use that color rule
c._nice_base_color = base_color
_private.add_window_decorations(c)
else
-- Otherwise use the default titlebar temporarily
c._nice_base_color = _private.titlebar_color
_private.add_window_decorations(c)
-- Connect a signal to determine the client color and then re-decorate it
c:connect_signal(
"request::activate", c._cb_add_window_decorations)
end
-- Shape the client, unnecessary if there are no shadows under the clients.
c.shape = shapes.rounded_rect {
tl = _private.titlebar_radius,
tr = _private.titlebar_radius,
bl = 4,
br = 4,
}
end)
end
return setmetatable(
nice, {__call = function(_, ...) return nice.initialize(...) end})

BIN
preview.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

212
shapes.lua Normal file
View File

@ -0,0 +1,212 @@
-- => Shapes
-- Provides utility functions for handling cairo shapes and geometry
-- ============================================================
--
local lgi = require("lgi")
local colors = require("nice.colors")
local hex2rgb = colors.hex2rgb
local darken = colors.darken
local cairo = lgi.cairo
local math = math
local rad = math.rad
local floor = math.floor
-- Returns a shape function for a rounded rectangle with independently configurable corner radii
local function rounded_rect(args)
local r1 = args.tl or 0
local r2 = args.bl or 0
local r3 = args.br or 0
local r4 = args.tr or 0
return function(cr, width, height)
cr:new_sub_path()
cr:arc(width - r1, r1, r1, rad(-90), rad(0))
cr:arc(width - r2, height - r2, r2, rad(0), rad(90))
cr:arc(r3, height - r3, r3, rad(90), rad(180))
cr:arc(r4, r4, r4, rad(180), rad(270))
cr:close_path()
end
end
-- Returns a circle of the specified size filled with the specified color
local function circle_filled(color, size)
color = color or "#fefefa"
local surface = cairo.ImageSurface.create("ARGB32", size, size)
local cr = cairo.Context.create(surface)
cr:arc(size / 2, size / 2, size / 2, rad(0), rad(360))
cr:set_source_rgba(hex2rgb(color))
cr.antialias = cairo.Antialias.BEST
cr:fill()
-- cr:arc(
-- size / 2, size / 2, size / 2 - 0.5, rad(135), rad(270))
-- cr:set_source_rgba(hex2rgb(darken(color, 25)))
-- cr.line_width = 1
-- cr:stroke()
collectgarbage()
collectgarbage()
return surface
end
-- Returns a vertical gradient pattern going from cololr_1 -> color_2
local function duotone_gradient_vertical(color_1, color_2, height, offset_1,
offset_2)
local fill_pattern = cairo.Pattern.create_linear(0, 0, 0, height)
local r, g, b, a
r, g, b, a = hex2rgb(color_1)
fill_pattern:add_color_stop_rgba(offset_1 or 0, r, g, b, a)
r, g, b, a = hex2rgb(color_2)
fill_pattern:add_color_stop_rgba(offset_2 or 1, r, g, b, a)
return fill_pattern
end
-- Flips the given surface around the specified axis
local function flip(surface, axis)
local width = surface:get_width()
local height = surface:get_height()
local flipped = cairo.ImageSurface.create("ARGB32", width, height)
local cr = cairo.Context.create(flipped)
local source_pattern = cairo.Pattern.create_for_surface(surface)
if axis == "horizontal" then
source_pattern.matrix = cairo.Matrix {xx = -1, yy = 1, x0 = width}
elseif axis == "vertical" then
source_pattern.matrix = cairo.Matrix {xx = 1, yy = -1, y0 = height}
elseif axis == "both" then
source_pattern.matrix = cairo.Matrix {
xx = -1,
yy = -1,
x0 = width,
y0 = height,
}
end
cr.source = source_pattern
cr:rectangle(0, 0, width, height)
cr:paint()
collectgarbage()
collectgarbage()
return flipped
end
-- Draws the left corner of the titlebar
local function create_corner_top_left(args)
local radius = args.radius
local height = args.height
local surface = cairo.ImageSurface.create("ARGB32", radius, height)
local cr = cairo.Context.create(surface)
-- Create the corner shape and fill it with a gradient
local radius_offset = 1 -- To soften the corner
cr:move_to(0, height)
cr:line_to(0, radius - radius_offset)
cr:arc(
radius + radius_offset, radius + radius_offset, radius, rad(180),
rad(270))
cr:line_to(radius, height)
cr:close_path()
cr.source = args.background_source
cr.antialias = cairo.Antialias.BEST
cr:fill()
-- Next add the subtle 3D look
local function add_stroke(nargs)
local arc_radius = nargs.radius
local offset_x = nargs.offset_x
local offset_y = nargs.offset_y
cr:new_sub_path()
cr:move_to(offset_x, height)
cr:line_to(offset_x, arc_radius + offset_y)
cr:arc(
arc_radius + offset_x, arc_radius + offset_y, arc_radius, rad(180),
rad(270))
cr.source = nargs.source
cr.line_width = nargs.width
cr.antialias = cairo.Antialias.BEST
cr:stroke()
end
-- Outer dark stroke
add_stroke {
offset_x = args.stroke_offset_outer,
offset_y = args.stroke_offset_outer,
radius = radius + 0.5,
source = args.stroke_source_outer,
width = args.stroke_width_outer,
}
-- Inner light stroke
add_stroke {
offset_x = args.stroke_offset_inner,
offset_y = args.stroke_offset_inner,
radius = radius,
width = args.stroke_width_inner,
source = args.stroke_source_inner,
}
collectgarbage()
collectgarbage()
return surface
end
-- Draws the middle of the titlebar
local function create_edge_top_middle(args)
local client_color = args.color
local height = args.height
local width = args.width
local surface = cairo.ImageSurface.create("ARGB32", width, height)
local cr = cairo.Context.create(surface)
-- Create the background shape and fill it with a gradient
cr:rectangle(0, 0, width, height)
cr.source = args.background_source
cr:fill()
-- Then add the light and dark strokes for that 3D look
local function add_stroke(stroke_width, stroke_offset, stroke_color)
cr:new_sub_path()
cr:move_to(0, stroke_offset)
cr:line_to(width, stroke_offset)
cr.line_width = stroke_width
cr:set_source_rgb(hex2rgb(stroke_color))
cr:stroke()
end
-- Inner light stroke
add_stroke(
args.stroke_width_inner, args.stroke_offset_inner,
args.stroke_color_inner)
-- Outer dark stroke
add_stroke(
args.stroke_width_outer, args.stroke_offset_outer,
args.stroke_color_outer)
collectgarbage()
collectgarbage()
return surface
end
local function create_edge_left(args)
local height = args.height
local width = 2
-- height = height or 1080
local surface = cairo.ImageSurface.create("ARGB32", width, height)
local cr = cairo.Context.create(surface)
cr:rectangle(0, 0, 2, args.height)
cr:set_source_rgb(hex2rgb(args.client_color))
cr:fill()
-- Inner light stroke
cr:new_sub_path()
cr:move_to(args.stroke_offset_inner, 0) -- 1/5
cr:line_to(args.stroke_offset_inner, height)
cr.line_width = args.stroke_width_inner -- 1.5
cr:set_source_rgb(hex2rgb(args.inner_stroke_color))
cr:stroke()
-- Outer dark stroke
cr:new_sub_path()
cr:move_to(args.stroke_offset_outer, 0)
cr:line_to(args.stroke_offset_outer, height)
cr.line_width = args.stroke_width_outer -- 1
cr:set_source_rgb(hex2rgb(args.stroke_color_outer))
cr:stroke()
collectgarbage()
collectgarbage()
return surface
end
return {
rounded_rect = rounded_rect,
circle_filled = circle_filled,
duotone_gradient_vertical = duotone_gradient_vertical,
flip = flip,
create_corner_top_left = create_corner_top_left,
create_edge_top_middle = create_edge_top_middle,
create_edge_left = create_edge_left,
}

100
table.lua Normal file
View File

@ -0,0 +1,100 @@
--[[
Courtesy of: http://lua-users.org/wiki/SaveTableToFile
]] local function exportstring(s) return string.format("%q", s) end
-- // The Save Function
local function save(tbl, filename)
local charS, charE = " ", "\n"
local file, err = io.open(filename, "wb")
if err then return err end
-- Initialize variables for save procedure
local tables, lookup = {tbl}, {[tbl] = 1}
file:write("return {" .. charE)
for idx, t in ipairs(tables) do
file:write("-- Table: {" .. idx .. "}" .. charE)
file:write("{" .. charE)
local thandled = {}
for i, v in ipairs(t) do
thandled[i] = true
local stype = type(v)
-- only handle value
if stype == "table" then
if not lookup[v] then
table.insert(tables, v)
lookup[v] = #tables
end
file:write(charS .. "{" .. lookup[v] .. "}," .. charE)
elseif stype == "string" then
file:write(charS .. exportstring(v) .. "," .. charE)
elseif stype == "number" then
file:write(charS .. tostring(v) .. "," .. charE)
end
end
for i, v in pairs(t) do
-- escape handled values
if (not thandled[i]) then
local str = ""
local stype = type(i)
-- handle index
if stype == "table" then
if not lookup[i] then
table.insert(tables, i)
lookup[i] = #tables
end
str = charS .. "[{" .. lookup[i] .. "}]="
elseif stype == "string" then
str = charS .. "[" .. exportstring(i) .. "]="
elseif stype == "number" then
str = charS .. "[" .. tostring(i) .. "]="
end
if str ~= "" then
stype = type(v)
-- handle value
if stype == "table" then
if not lookup[v] then
table.insert(tables, v)
lookup[v] = #tables
end
file:write(str .. "{" .. lookup[v] .. "}," .. charE)
elseif stype == "string" then
file:write(str .. exportstring(v) .. "," .. charE)
elseif stype == "number" then
file:write(str .. tostring(v) .. "," .. charE)
end
end
end
end
file:write("}," .. charE)
end
file:write("}")
file:close()
end
-- The Load Function
local function load(sfile)
local ftables, err = loadfile(sfile)
if err then return _, err end
local tables = ftables()
for idx = 1, #tables do
local tolinki = {}
for i, v in pairs(tables[idx]) do
if type(v) == "table" then tables[idx][i] = tables[v[1]] end
if type(i) == "table" and tables[i[1]] then
table.insert(tolinki, {i, tables[i[1]]})
end
end
-- link indices
for _, v in ipairs(tolinki) do
tables[idx][v[2]], tables[idx][v[1]] = tables[idx][v[1]], nil
end
end
return tables[1]
end
return {save = save, load = load}