obsolete.computer/geekery/

Color All The Things, With Pywal

As with many folks who like to play with desktop cusomization, inevitably my desktop config tweaking always tends to be centered around matching the colors in the wallpaper. With the help of a tool called Pywal and a bit of Bash scripting, it turns out to be pretty easy... though it took a lot of tinkering to get all the pieces working just right.

Color Schemes Based on the Wallpaper - 1 of 5 Color Schemes Based on the Wallpaper - 2 of 5 Color Schemes Based on the Wallpaper - 3 of 5 Color Schemes Based on the Wallpaper - 4 of 5 Color Schemes Based on the Wallpaper - 5 of 5

Choosing the wallpaper

Even though Pywal itself is capable of choosing the wallpaper, the solution I finally landed on (which seems to often happen) is a Bash script that gets the wallpaper last set by Nitrogen. I also added the ability to randomly choose a wallpaper from a couple of different directories, with it choosing a different folder in the winter months so I get snowy scenes then rather than my usual mountain & forest landscapes (mostly from Unsplash but some photos taken by friends or myself). Then in my Awesome menu I added an option to randomly choose a new wallpaper by running the script behind the scenes.

Here is the wallpaper script (this code embedded in the HTML will change as I work on it):

#!/bin/bash

PAPES=/home/sean/Nextcloud/Media/backgrounds/4k/
WINTERPAPES=/home/sean/Nextcloud/Media/backgrounds/4kw/

if [[ "$1" == "--random" || "$2" == "--random" ]]; then
    if [ `date +%m` = 12 -o `date +%m` = 01 -o `date +%m` = 02 ]; then
        BGDIR="$WINTERPAPES"
    else
        BGDIR="$PAPES"
    fi

    rm -f "$HOME/.config/nitrogen/bg-saved.cfg"
    nitrogen --set-zoom-fill --random --save "$BGDIR"

elif [[ "$1" == "--from-wal" || "$2" == "--from-wal" ]]; then
    nitrogen --set-zoom-fill --save "$(cat "$HOME/.cache/wal/wal")"
else
    nitrogen --restore
fi

BGIMAGE="$(cat "$HOME/.config/nitrogen/bg-saved.cfg" | grep file= | head -1 | cut -d'=' -f 2)"
wal -i "$BGIMAGE" -n
#wal --backend colorz -i "$BGIMAGE" -n

COLORS=(`cat "$HOME/.cache/wal/colors" | tr -d '#"'`)

# 0 - GTX 2080
# 1 - Logitech G303
# 2 - MSI Motherboard
# 3 - Corsair CPU cooler

(
DEVICES=$( openrgb -l | grep '^[0-9]:' | wc -l )
for (( i = 0 ; i <= $(($DEVICES - 1)) ; i++)); do
    openrgb -d $i -m direct -c ${COLORS[$(($i + 1))]}
done
) &

if [[ "$1" == "--restart-awesome"  || "$2" == "--restart-awesome" ]]; then
    ps x -u $USER -U $USER | grep -v grep | grep awesome > /dev/null && { echo 'awesome.restart()' | awesome-client ; }
fi

Terminal Color Scheme

Pywal itself will set the terminal colors for all compatible terminals on your system when you first run it. And, simply adding source ~/.cache/wal/sequences to your .bashrc (or .zshrc) will cause all future terminal windows to use your chosen theme. So this part was fairly simple. In the wallpaper script I simply run Pywal with the option to not set the wallpaper because Nitrogen is already doing that.

Awesome Theme Colors

When Pywal is run, it creates easily-parsible files in various formats (and you can create your own using a basic template system). So I added a function to my utility library which parses out ~/.cache/wal/colors into an array containing all the color values, and played around for a while picking different colors for different UI elements until I landed on selections that work pretty well for both dark and light wallpapers.

My theme is based on the Powerarrow theme from the Awesome-Copycats developers. At least at the time that I started working on it, the theme had a color scheme hard-coded into it for the arrow-shaped widgets in the Wibar. So I simply replaced each hard-coded value with elements in the Pywal color array.

See the theme file for my current color selections:

--[[

     Powerarrow Awesome WM theme
     github.com/lcpz

--]]

local gears = require("gears")
local lain  = require("lain")
local awful = require("awful")
local wibox = require("wibox")
local ruari = require("ruari")

local os, math, string = os, math, string
local my_table = awful.util.table or gears.table -- 4.{0,1} compatibility

local wal_colors = ruari.util.get_wal_colors()
local wal_color_start = 2

local theme                                     = {}
theme.dir                                       = awful.util.getdir("config") .. "/themes/powerarrow"
theme.wallpaper                                 = ruari.util.set_wallpaper()
theme.font                                      = ruari.util.get_system_font()
theme.monospace_font                            = ruari.util.get_mono_font()
theme.fg_normal                                 = wal_colors[7]
theme.fg_focus                                  = wal_colors[1]
theme.fg_urgent                                 = wal_colors[2]
theme.arrow_fg_normal                           = "#FEFEFE"      --had to hardcode this because it's the icon color.
theme.arrow_fg_alt                              = wal_colors[1]
theme.bg_normal                                 = wal_colors[1]
theme.bg_focus                                  = wal_colors[9]
theme.bg_urgent                                 = wal_colors[1]
theme.taglist_bg_focus                          = theme.bg_focus
theme.taglist_bg_normal                         = theme.bg_normal
theme.taglist_fg_focus                          = wal_colors[1]
theme.taglist_fg_normal                         = wal_colors[3]
theme.tasklist_bg_focus                         = theme.bg_focus
theme.tasklist_bg_normal                        = theme.bg_normal
theme.tasklist_fg_focus                         = wal_colors[1]
theme.tasklist_fg_normal                        = wal_colors[3]
theme.border_width                              = 2
theme.border_normal                             = wal_colors[1]
theme.border_focus                              = wal_colors[3]
theme.border_marked                             = wal_colors[2]
theme.titlebar_bg_focus                         = theme.bg_normal
theme.titlebar_bg_normal                        = theme.bg_normal
theme.titlebar_fg_focus                         = wal_colors[8]
theme.titlebar_fg_normal                        = wal_colors[7]
theme.menu_height                               = 18
theme.menu_width                                = 200
theme.menu_submenu_icon                         = theme.dir .. "/icons/submenu.png"
theme.awesome_icon                              = theme.dir .. "/icons/awesome.png"
theme.taglist_squares_sel                       = theme.dir .. "/icons/square_sel.png"
theme.taglist_squares_unsel                     = theme.dir .. "/icons/square_unsel.png"
theme.layout_tile                               = theme.dir .. "/icons/tile.png"
theme.layout_tileleft                           = theme.dir .. "/icons/tileleft.png"
theme.layout_tilebottom                         = theme.dir .. "/icons/tilebottom.png"
theme.layout_tiletop                            = theme.dir .. "/icons/tiletop.png"
theme.layout_fairv                              = theme.dir .. "/icons/fairv.png"
theme.layout_fairh                              = theme.dir .. "/icons/fairh.png"
theme.layout_spiral                             = theme.dir .. "/icons/spiral.png"
theme.layout_dwindle                            = theme.dir .. "/icons/dwindle.png"
theme.layout_max                                = theme.dir .. "/icons/max.png"
theme.layout_fullscreen                         = theme.dir .. "/icons/fullscreen.png"
theme.layout_magnifier                          = theme.dir .. "/icons/magnifier.png"
theme.layout_floating                           = theme.dir .. "/icons/floating.png"
theme.layout_centerwork                         = theme.dir .. "/icons/centerwork.png"
theme.layout_centerworkh                        = theme.dir .. "/icons/centerworkh.png"
theme.layout_multicenterwork                    = theme.dir .. "/icons/multicenterwork.png"
theme.layout_multicenterworkh                   = theme.dir .. "/icons/multicenterworkh.png"
theme.layout_centerfair                         = theme.dir .. "/icons/centerfair.png"
theme.widget_ac                                 = theme.dir .. "/icons/ac.png"
theme.widget_battery                            = theme.dir .. "/icons/battery.png"
theme.widget_battery_low                        = theme.dir .. "/icons/battery_low.png"
theme.widget_battery_empty                      = theme.dir .. "/icons/battery_empty.png"
theme.widget_mem                                = theme.dir .. "/icons/mem.png"
theme.widget_cpu                                = theme.dir .. "/icons/cpu.png"
theme.widget_clock                              = theme.dir .. "/icons/clock.png"
theme.widget_temp                               = theme.dir .. "/icons/temp.png"
theme.widget_net                                = theme.dir .. "/icons/net.png"
theme.widget_hdd                                = theme.dir .. "/icons/hdd.png"
theme.widget_music                              = theme.dir .. "/icons/note.png"
theme.widget_music_on                           = theme.dir .. "/icons/note_on.png"
theme.widget_music_pause                        = theme.dir .. "/icons/pause.png"
theme.widget_music_stop                         = theme.dir .. "/icons/stop.png"
theme.widget_vol                                = theme.dir .. "/icons/vol.png"
theme.widget_vol_low                            = theme.dir .. "/icons/vol_low.png"
theme.widget_vol_no                             = theme.dir .. "/icons/vol_no.png"
theme.widget_vol_mute                           = theme.dir .. "/icons/vol_mute.png"
theme.widget_mail                               = theme.dir .. "/icons/mail.png"
theme.widget_mail_on                            = theme.dir .. "/icons/mail_on.png"
theme.widget_task                               = theme.dir .. "/icons/task.png"
theme.widget_scissors                           = theme.dir .. "/icons/scissors.png"
theme.tasklist_plain_task_name                  = true
theme.tasklist_disable_icon                     = true
theme.master_count                              = 1
theme.useless_gap                               = 12
theme.titlebar_close_button_focus               = theme.dir .. "/icons/titlebar/close_focus.png"
theme.titlebar_close_button_normal              = theme.dir .. "/icons/titlebar/close_normal.png"
theme.titlebar_ontop_button_focus_active        = theme.dir .. "/icons/titlebar/ontop_focus_active.png"
theme.titlebar_ontop_button_normal_active       = theme.dir .. "/icons/titlebar/ontop_normal_active.png"
theme.titlebar_ontop_button_focus_inactive      = theme.dir .. "/icons/titlebar/ontop_focus_inactive.png"
theme.titlebar_ontop_button_normal_inactive     = theme.dir .. "/icons/titlebar/ontop_normal_inactive.png"
theme.titlebar_sticky_button_focus_active       = theme.dir .. "/icons/titlebar/sticky_focus_active.png"
theme.titlebar_sticky_button_normal_active      = theme.dir .. "/icons/titlebar/sticky_normal_active.png"
theme.titlebar_sticky_button_focus_inactive     = theme.dir .. "/icons/titlebar/sticky_focus_inactive.png"
theme.titlebar_sticky_button_normal_inactive    = theme.dir .. "/icons/titlebar/sticky_normal_inactive.png"
theme.titlebar_floating_button_focus_active     = theme.dir .. "/icons/titlebar/floating_focus_active.png"
theme.titlebar_floating_button_normal_active    = theme.dir .. "/icons/titlebar/floating_normal_active.png"
theme.titlebar_floating_button_focus_inactive   = theme.dir .. "/icons/titlebar/floating_focus_inactive.png"
theme.titlebar_floating_button_normal_inactive  = theme.dir .. "/icons/titlebar/floating_normal_inactive.png"
theme.titlebar_maximized_button_focus_active    = theme.dir .. "/icons/titlebar/maximized_focus_active.png"
theme.titlebar_maximized_button_normal_active   = theme.dir .. "/icons/titlebar/maximized_normal_active.png"
theme.titlebar_maximized_button_focus_inactive  = theme.dir .. "/icons/titlebar/maximized_focus_inactive.png"
theme.titlebar_maximized_button_normal_inactive = theme.dir .. "/icons/titlebar/maximized_normal_inactive.png"

local markup = lain.util.markup
local separators = lain.util.separators

--[[ Binary clock
local binclock = require("themes.powerarrow.binclock"){
    height = 16,
    show_seconds = true,
    color_active = theme.fg_normal,
    color_inactive = theme.bg_focus
}
--]]

--local textclock = wibox.widget.textclock(" %a %b %d, %I:%M%p  ", 15)
--local clockicon = wibox.widget.imagebox(theme.widget_clock)
local textclock = awful.widget.watch(
    "date +'%a %b %d  %I:%M%p'", 60,
    function(widget, stdout)
        widget:set_markup(" " .. markup.fontfg(theme.font, theme.fg_normal, stdout))
    end
)

-- Calendar
theme.cal = lain.widget.cal({
    --cal = "cal --color=always",
    attach_to = { textclock },
    notification_preset = {
        font = theme.monospace_font,
        fg   = theme.fg_normal,
        bg   = theme.bg_normal
    }
})

--[[ Taskwarrior
local task = wibox.widget.imagebox(theme.widget_task)
lain.widget.contrib.task.attach(task, {
    -- do not colorize output
    show_cmd = "task | sed -r 's/\\x1B\\[([0-9]{1,2}(;[0-9]{1,2})?)?[mGK]//g'"
})
task:buttons(my_table.join(awful.button({}, 1, lain.widget.contrib.task.prompt)))
--]]

--[[ Scissors (xsel copy and paste)
local scissors = wibox.widget.imagebox(theme.widget_scissors)
scissors:buttons(my_table.join(awful.button({}, 1, function() awful.spawn.with_shell("xsel | xsel -i -b") end)))
--]]

-- Mail IMAP check
--[[ commented because it needs to be set before use
local mailicon = wibox.widget.imagebox(theme.widget_mail)
mailicon:buttons(my_table.join(awful.button({ }, 1, function () awful.spawn(mail) end)))
theme.mail = lain.widget.imap({
    timeout  = 180,
    server   = "server",
    mail     = "mail",
    password = "keyring get mail",
    settings = function()
        if mailcount > 0 then
            widget:set_text(" " .. mailcount .. " ")
            mailicon:set_image(theme.widget_mail_on)
        else
            widget:set_text("")
            mailicon:set_image(theme.widget_mail)
        end
    end
})
--]]

--[[ ALSA volume
theme.volume = lain.widget.alsabar({
    --togglechannel = "IEC958,3",
    notification_preset = { font = theme.font, fg = theme.fg_normal },
})
--]]

-- MPD
local musicplr = awful.util.terminal .. " -title Music -g 130x34-320+16 -e ncmpcpp"
local mpdicon = wibox.widget.imagebox(theme.widget_music)
mpdicon:buttons(my_table.join(
    awful.button({ modkey }, 1, function () awful.spawn.with_shell(musicplr) end),
    awful.button({ }, 1, function ()
        os.execute("mpc prev")
        theme.mpd.update()
    end),
    awful.button({ }, 2, function ()
        os.execute("mpc toggle")
        theme.mpd.update()
    end),
    awful.button({ }, 3, function ()
        os.execute("mpc next")
        theme.mpd.update()
    end)))
theme.mpd = lain.widget.mpd({
    settings = function()
        if mpd_now.state == "play" or mpd_now.state == "pause" then
            if mpd_now.state == "play" then
                mpdicon:set_image(theme.widget_music_on)
            else
                mpdicon:set_image(theme.widget_music_pause)
            end
            artist = " " .. mpd_now.artist .. " "
            title  = mpd_now.title  .. " "
            widget:set_markup(markup.fontfg(theme.font, theme.arrow_fg_normal, markup(theme.arrow_fg_alt, artist) .. " " .. title))
        else
            widget:set_text("")
            mpdicon:set_image(theme.widget_music)
        end
    end
})


-- MEM
local memicon = wibox.widget.imagebox(theme.widget_mem)
local mem = lain.widget.mem({
    settings = function()
        widget:set_markup(markup.fontfg(theme.font, theme.arrow_fg_normal, string.format(" %4dMB ", mem_now.used)))
    end
})

-- CPU
local cpuicon = wibox.widget.imagebox(theme.widget_cpu)
local cpu = lain.widget.cpu({
    settings = function()
        widget:set_markup(markup.fontfg(theme.font, theme.arrow_fg_normal, string.format(" %3d%% ", cpu_now.usage)))
    end
})

-- Coretemp (lm_sensors, per core)
local tempwidget = awful.widget.watch({awful.util.shell, '-c', 'sensors | grep Core'}, 30,
function(widget, stdout)
    local temps = ""
    for line in stdout:gmatch("[^\r\n]+") do
        temps = temps .. line:match("+(%d+).*°C")  .. "° " -- in Celsius
    end
    widget:set_markup(markup.fontfg(theme.font, theme.arrow_fg_normal, " " .. temps))
end)

--[[ Coretemp (lain, average)
local temp = lain.widget.temp({
    settings = function()
        widget:set_markup(markup.fontfg(theme.font, theme.arrow_fg_normal, " " .. coretemp_now .. "°C "))
    end,
    tempfile = "/sys/class/thermal/thermal_zone0/temp"
})
--]]
--local tempicon = wibox.widget.imagebox(theme.widget_temp)

-- / fs
local fsicon = wibox.widget.imagebox(theme.widget_hdd)
-- commented because it needs Gio/Glib >= 2.54
local fs = lain.widget.fs({
    notification_preset = { fg = theme.fg_normal, bg = theme.bg_normal, font = theme.monospace_font },
    settings = function()
        local fsp = string.format(" %3.2f / %3.2f %s (%2.0f%%) ", fs_now["/home"].used, fs_now["/home"].size, fs_now["/home"].units, fs_now["/home"].percentage)
        widget:set_markup(markup.fontfg(theme.font, theme.arrow_fg_normal, fsp))
    end,
    partition 
})


-- Battery
local baticon = wibox.widget.imagebox(theme.widget_battery)
local bat = lain.widget.bat({
    settings = function()
        if bat_now.status and bat_now.status ~= "N/A" then
            if bat_now.ac_status == 1 then
                widget:set_markup(markup.fontfg(theme.font, theme.arrow_fg_normal, " AC "))
                baticon:set_image(theme.widget_ac)
                return
            elseif not bat_now.perc and tonumber(bat_now.perc) <= 5 then
                baticon:set_image(theme.widget_battery_empty)
            elseif not bat_now.perc and tonumber(bat_now.perc) <= 15 then
                baticon:set_image(theme.widget_battery_low)
            else
                baticon:set_image(theme.widget_battery)
            end
            widget:set_markup(markup.fontfg(theme.font, theme.arrow_fg_normal, " " .. bat_now.perc .. "% "))
        else
            widget:set_markup()
            baticon:set_image(theme.widget_ac)
        end
    end
})

-- Net
local neticon = wibox.widget.imagebox(theme.widget_net)
local net = lain.widget.net({
    notification_preset = { fg = theme.fg_normal, bg = theme.bg_normal, font = theme.monospace_font },
    settings = function()
        widget:set_markup(markup.fontfg(theme.font, theme.arrow_fg_normal, string.format(" %4.1fMbps↓ %4.1fMbps↑ ", net_now.received/128, net_now.sent/128)))
    end
})


-- Separators
local arrow = separators.arrow_left

function theme.powerline_rl(cr, width, height)
    local arrow_depth, offset = height/2, 0

    -- Avoid going out of the (potential) clip area
    if arrow_depth < 0 then
        width  =  width + 2*arrow_depth
        offset = -arrow_depth
    end

    cr:move_to(offset + arrow_depth         , 0        )
    cr:line_to(offset + width               , 0        )
    cr:line_to(offset + width - arrow_depth , height/2 )
    cr:line_to(offset + width               , height   )
    cr:line_to(offset + arrow_depth         , height   )
    cr:line_to(offset                       , height/2 )

    cr:close_path()
end

local function pl(widget, bgcolor, padding)
    return wibox.container.background(wibox.container.margin(widget, 16, 16), bgcolor, theme.powerline_rl)
end

function theme.at_screen_connect(s)
    -- Quake application
    s.quake = lain.util.quake({ app = awful.util.terminal })

    -- If wallpaper is a function, call it with the screen
    local wallpaper = theme.wallpaper
    if type(wallpaper) == "function" then
        wallpaper = wallpaper(s)
    end
    gears.wallpaper.maximized(wallpaper, s, true)

    -- Tags
    --awful.tag(awful.util.tagnames, s, awful.layout.layouts[1])
    for i=1, #awful.util.tagnames do
        awful.tag.add(awful.util.tagnames[i], {
            layout = awful.layout.layouts[1],
            screen = s,
            selected = (i == 1),
            master_count = theme.master_count
        })
    end

    -- Create a promptbox for each screen
    s.mypromptbox = awful.widget.prompt()
    -- Create an imagebox widget which will contains an icon indicating which layout we're using.
    -- We need one layoutbox per screen.
    s.mylayoutbox = awful.widget.layoutbox(s)
    s.mylayoutbox:buttons(my_table.join(
                           awful.button({ }, 1, function () awful.layout.inc( 1) end),
                           awful.button({ }, 3, function () awful.layout.inc(-1) end),
                           awful.button({ }, 4, function () awful.layout.inc( 1) end),
                           awful.button({ }, 5, function () awful.layout.inc(-1) end)))
    -- Create a taglist widget
    s.mytaglist = awful.widget.taglist(s, awful.widget.taglist.filter.all, awful.util.taglist_buttons)

    -- Create a tasklist widget
    s.mytasklist = awful.widget.tasklist(s, awful.widget.tasklist.filter.minimizedcurrenttags, awful.util.tasklist_buttons)

    -- Create the wibox
    s.mywibox = awful.wibar({ position = "top", screen = s, height = 24, bg = theme.bg_normal, fg = theme.fg_normal })

    -- Add widgets to the wibox
    s.mywibox:setup {
        layout = wibox.layout.align.horizontal,
        { -- Left widgets
            layout = wibox.layout.fixed.horizontal,
            s.mylayoutbox,
            --spr,
            s.mytaglist,
            s.mypromptbox,
            spr,
        },
        s.mytasklist, -- Middle widget
        { -- Right widgets
            layout = wibox.layout.fixed.horizontal,
            --wibox.container.margin(scissors, 4, 8),
            --[[ using shapes
            pl(wibox.widget { mpdicon, theme.mpd.widget, layout = wibox.layout.align.horizontal }, "#343434"),
            pl(task, "#343434"),
            --pl(wibox.widget { mailicon, mail and theme.mail.widget, layout = wibox.layout.align.horizontal }, "#343434"),
            pl(wibox.widget { memicon, mem.widget, layout = wibox.layout.align.horizontal }, "#777E76"),
            pl(wibox.widget { cpuicon, cpu.widget, layout = wibox.layout.align.horizontal }, "#4B696D"),
            pl(wibox.widget { tempicon, temp.widget, layout = wibox.layout.align.horizontal }, "#4B3B51"),
            --pl(wibox.widget { fsicon, fs and fs.widget, layout = wibox.layout.align.horizontal }, "#CB755B"),
            pl(wibox.widget { baticon, bat.widget, layout = wibox.layout.align.horizontal }, "#8DAA9A"),
            pl(wibox.widget { neticon, net.widget, layout = wibox.layout.align.horizontal }, "#C0C0A2"),
            pl(binclock.widget, "#777E76"),
            --]]
            -- using separators
            --arrow(theme.bg_normal, "#343434"),
            arrow(theme.bg_normal, wal_colors[wal_color_start]),
            --wibox.container.background(wibox.container.margin(wibox.widget { mailicon, theme.mail and theme.mail.widget, layout = wibox.layout.align.horizontal }, 4, 7), "#343434"),
            --arrow("#343434", theme.bg_normal),
            wibox.container.background(wibox.container.margin(wibox.widget { mpdicon, theme.mpd.widget, layout = wibox.layout.align.horizontal }, 3, 6), wal_colors[wal_color_start]),
            --arrow(theme.bg_normal, "#343434"),
            --wibox.container.background(wibox.container.margin(task, 3, 7), "#343434"),
            --arrow("#343434", "#777E76"),
            arrow(wal_colors[wal_color_start], wal_colors[wal_color_start+1]),
            wibox.container.background(wibox.container.margin(wibox.widget { memicon, mem.widget, layout = wibox.layout.align.horizontal }, 2, 3), wal_colors[wal_color_start+1]),
            arrow(wal_colors[wal_color_start+1], wal_colors[wal_color_start+2]),
            wibox.container.background(wibox.container.margin(wibox.widget { cpuicon, cpu.widget, layout = wibox.layout.align.horizontal }, 3, 4), wal_colors[wal_color_start+2]),
            --arrow("#1d3033", "#1b1420"),
            --wibox.container.background(wibox.container.margin(wibox.widget { tempicon, temp.widget, layout = wibox.layout.align.horizontal }, 4, 4), "#1b1420"),
            arrow(wal_colors[wal_color_start+2], wal_colors[wal_color_start+3]),
            wibox.container.background(wibox.container.margin(wibox.widget { fsicon, fs and fs.widget, layout = wibox.layout.align.horizontal }, 3, 3), wal_colors[wal_color_start+3]),
            --arrow("#CB755B", "#8DAA9A"),
            --wibox.container.background(wibox.container.margin(wibox.widget { baticon, bat.widget, layout = wibox.layout.align.horizontal }, 3, 3), "#8DAA9A"),
            --arrow("#8DAA9A", "#C0C0A2"),
            arrow(wal_colors[wal_color_start+3], wal_colors[wal_color_start+4]),
            wibox.container.background(wibox.container.margin(wibox.widget { nil, neticon, net.widget, layout = wibox.layout.align.horizontal }, 3, 3), wal_colors[wal_color_start+4]),
            arrow(wal_colors[wal_color_start+4], wal_colors[1]),
            --wibox.container.background(wibox.container.margin(binclock.widget, 4, 8), "#777E76"),
            wibox.container.background(wibox.container.margin(textclock, 4, 8), wal_colors[1]),
            arrow(wal_colors[1], "alpha"),
            wibox.widget.systray(),
            --]]
        },
    }
end

return theme

RGB LED colors via OpenRGB

RGB hardware is neat, but man it really bothers me that the default setting is usually to cycle through a rainbow of colors. Especially if you have multiple pieces of RGB hardware cycling at different rates it can just about give you a seizure. So the last part of the script calls the CLI client for OpenRGB for each piece of hardware it can detect and assigns a Pywal color (forked in a subshell because it's very slow). Most RGB hardware doesn't match the colors that appear on the screen very well, but it's way more sane than the psychedelic color show.

OpenRGB colors from Pywal - 1 of 3 OpenRGB colors from Pywal - 2 of 3 OpenRGB colors from Pywal - 3 of 3

Other Integrations

I'd love to have Pywal generate a GTK and Qt color scheme, but alas that's outside of my skill level and patience right now. I did play around with wpgtk a bit which does this using Pywal's template system, but it's a bit clunky and you have to use one particular included GTK theme for it to work. Plus I'd really like a solution that doesn't require a GUI (so that it can be integrated with my script). I ended up choosing a GTK theme called "Groot" that came pre-installed in Archcraft and works well with most of my generated color schemes.