├── .envrc ├── .gitattributes ├── .gitignore ├── Makefile ├── api └── lawful.fnl ├── async-testing.fnl ├── awesome-macros.fnl ├── commands ├── tag.fnl └── window.fnl ├── daemons ├── battery.fnl ├── cpu.fnl ├── pulseaudio.fnl ├── ram.fnl └── workspace-env.fnl ├── features ├── alt-tab.fnl ├── broom.fnl ├── familiar.fnl ├── hydra.fnl ├── motion.fnl ├── nrepl.fnl ├── persistence.fnl ├── titlebar.fnl ├── wallpaper.fnl └── workspaces.fnl ├── fennel.lua ├── icons ├── _macros.fnl ├── box.fnl ├── layouts.fnl ├── loader.fnl ├── shtwzrd.fnl └── tabler.fnl ├── init.fnl ├── keybindings.fnl ├── manifest.scm ├── patches └── fix-snid.lua ├── rc.lua ├── rules.fnl ├── test ├── hotfuzz.fnl ├── icons.fnl ├── init.fnl └── xml.fnl ├── themes └── dracula │ ├── theme.fnl │ └── titlebar │ ├── close_focus.svg │ ├── close_focus_hover.svg │ ├── close_normal.svg │ ├── close_normal_hover.svg │ ├── floating_focus_active.svg │ ├── floating_focus_active_hover.svg │ ├── floating_focus_inactive.svg │ ├── floating_focus_inactive_hover.svg │ ├── floating_normal_active.svg │ ├── floating_normal_active_hover.svg │ ├── floating_normal_inactive.svg │ ├── floating_normal_inactive_hover.svg │ ├── maximized_focus_active.svg │ ├── maximized_focus_active_hover.svg │ ├── maximized_focus_inactive.svg │ ├── maximized_focus_inactive_hover.svg │ ├── maximized_normal_active.svg │ ├── maximized_normal_active_hover.svg │ ├── maximized_normal_inactive.svg │ ├── maximized_normal_inactive_hover.svg │ ├── minimize_focus.svg │ ├── minimize_focus_hover.svg │ ├── minimize_normal.svg │ ├── minimize_normal_hover.svg │ ├── ontop_focus_active.svg │ ├── ontop_focus_active_hover.svg │ ├── ontop_focus_inactive.svg │ ├── ontop_focus_inactive_hover.svg │ ├── ontop_normal_active.svg │ ├── ontop_normal_active_hover.svg │ ├── ontop_normal_inactive.svg │ ├── ontop_normal_inactive_hover.svg │ ├── sticky_focus_active.svg │ ├── sticky_focus_active_hover.svg │ ├── sticky_focus_inactive.svg │ ├── sticky_focus_inactive_hover.svg │ ├── sticky_normal_active.svg │ ├── sticky_normal_active_hover.svg │ ├── sticky_normal_inactive.svg │ └── sticky_normal_inactive_hover.svg ├── todo.md ├── user.fnl ├── utils ├── async.fnl ├── hotfuzz.fnl ├── identicon.fnl ├── input.fnl ├── oleander.fnl ├── pulseaudio.fnl ├── screens.fnl ├── tags.fnl ├── widgets.fnl └── xml.fnl ├── vendor ├── bencode.lua ├── jeejah.lua ├── jeejah │ └── fenneleval.lua ├── json.lua ├── lume.lua ├── lunatest.lua └── serpent.lua └── widgets ├── layout-switcher.fnl └── workspace-switcher.fnl /.envrc: -------------------------------------------------------------------------------- 1 | use_guix --load ./manifest.scm 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | vendor/* linguist-vendored 2 | fennel.lua linguist-vendored 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .nrepl-port 2 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL := /bin/bash 2 | 3 | awesome_lib_path := $(shell echo "${GUIX_ENVIRONMENT}/share/awesome/lib/?.lua;${GUIX_ENVIRONMENT}/share/awesome/lib/?/init.lua") 4 | lua_share_path := $(shell echo "${GUIX_ENVIRONMENT}/share/lua/5.1/?.lua;${GUIX_ENVIRONMENT}/share/lua/5.1/init.lua") 5 | lua_lib_path := $(shell echo "${GUIX_ENVIRONMENT}/lib/lua/5.1/?.lua;${GUIX_ENVIRONMENT}/lib/lua/5.1/init.lua") 6 | 7 | awesome_local_path := $(shell echo "${HOME}/.config/awesome/?.lua;${HOME}/.config/awesome/?/?.lua") 8 | lua_path := "$(shell lua -e 'print(package.path)');$(awesome_lib_path);$(awesome_local_path);$(lua_share_path);$(lua_lib_path)" 9 | 10 | lua_cpath := "$(shell lua -e 'print(package.path)');${GUIX_ENVIRONMENT}/lib/lua/5.1/?.so" 11 | 12 | run: 13 | Xephyr :1 -name xephyr_awesome -screen "1366x768" >/dev/null 2>&1 & 14 | sleep 1 15 | DISPLAY=:1.0 awesome -c rc.lua & 16 | 17 | stop: 18 | kill `pgrep Xephyr` >/dev/null 2>&1 19 | 20 | test: 21 | LUA_PATH=${lua_path} LUA_CPATH=${lua_cpath} fennel --globals awesome,client --correlate --metadata --lua luajit test/init.fnl 22 | 23 | .PHONY: run stop test 24 | -------------------------------------------------------------------------------- /api/lawful.fnl: -------------------------------------------------------------------------------- 1 | (require "patches.fix-snid") 2 | (local lgi (require :lgi)) 3 | (local cairo lgi.cairo) 4 | (local rsvg lgi.Rsvg) 5 | (local awful (require :awful)) 6 | (local naughty (require :naughty)) 7 | (local gears (require :gears)) 8 | (local dump gears.debug.dump_return) 9 | (local join gears.table.join) 10 | (local unpack (or table.unpack _G.unpack)) 11 | (import-macros {: async : await} :utils.async) 12 | 13 | (var lawful {:ext {} 14 | :geo awful.placement ;; passthrough 15 | :util {:join join} 16 | :fs {} 17 | :img {} 18 | :notify {}}) 19 | 20 | (fn lawful.fs.home-dir [] 21 | (os.getenv "HOME")) 22 | 23 | (lambda lawful.fs.config-dir [] 24 | (.. (lawful.fs.home-dir) "/.config/awesome/")) 25 | 26 | (lambda lawful.fs.cache-dir [] 27 | (.. (lawful.fs.home-dir) "/.cache/awesome/")) 28 | 29 | (lambda lawful.fs.icon-dir [] 30 | (.. (lawful.fs.home-dir) "/.cache/awesome/icons/")) 31 | 32 | (lambda lawful.img.load-svg [svg-file-name width height] 33 | (let [surf (cairo.ImageSurface cairo.Format.ARGB32 width height) 34 | cr (cairo.Context surf) 35 | handle (assert (rsvg.Handle.new_from_file svg-file-name)) 36 | dim (handle:get_dimensions) 37 | aspect (math.min (/ width dim.width) (/ height dim.height))] 38 | (cr:scale aspect aspect) 39 | (handle:render_cairo cr) 40 | surf)) 41 | 42 | (lambda lawful.notify.msg [?message ?args] 43 | (let [args (or ?args {}) 44 | message (or ?message nil) 45 | tv (type message)] 46 | (if message 47 | (match tv 48 | :string (naughty.notify (join args {:text message})) 49 | :number (naughty.notify (join args {:text (.. "" message)})) 50 | _ (naughty.notify (join args {:text (dump message)}))) 51 | (naughty.notify (join args {:text "nil"}))))) 52 | 53 | (lambda lawful.notify.info [?message ?timeout] 54 | (lawful.notify.msg ?message {:timeout (or ?timeout 10)})) 55 | 56 | (fn lawful.notify.error [?message] 57 | (let [err-preset {:timeout 0 :bg "#000000" :fg "#ff0000" :max_height 1080}] 58 | (lawful.notify.msg ?message err-preset))) 59 | 60 | (lambda lawful.ext.shellout [cmd] 61 | (async 62 | (let [[out err reason code] (await awful.spawn.easy_async_with_shell cmd)] 63 | {:stdout out 64 | :stderr err 65 | :reason reason 66 | :code code}))) 67 | 68 | (lambda lawful.ext.shellout! [cmd] 69 | (let [{: stdout : stderr : code} (lawful.ext.shellout cmd)] 70 | (if (= code 0) 71 | stdout 72 | (lawful.notify.error stderr)))) 73 | 74 | (set lawful.ext.spawnbuf {}) 75 | 76 | (lambda lawful.ext.spawn [cmd ?props ?cb] 77 | "Spawn application with CMD and set PROPS on resulting client. 78 | Execute callback ?CB if provided, passing the client as the only argument." 79 | ;; launch everything through an interactive bash so aliases can resolve 80 | (let [(pid snid) (awesome.spawn (.. "bash -ic " cmd) true)] 81 | (when snid 82 | (tset lawful.ext.spawnbuf snid [(or ?props {}) ?cb])) 83 | (values pid snid))) 84 | 85 | (client.connect_signal 86 | :manage 87 | (fn [c] 88 | (when c.startup_id 89 | (let [snid c.startup_id 90 | snid-data (. lawful.ext.spawnbuf snid)] 91 | (when snid-data 92 | (let [props (. snid-data 1) 93 | ?cb (. snid-data 2)] 94 | (each [k v (pairs props)] 95 | (tset c k v)) 96 | (when (= (. props :titlebars_enabled) false) 97 | (awful.titlebar.hide c)) 98 | (when (. props :placement) ; apply placement 99 | ((. props :placement) c)) 100 | (when ?cb 101 | (?cb c)) 102 | (tset lawful.ext.spawnbuf snid nil))))))) 103 | 104 | lawful 105 | -------------------------------------------------------------------------------- /async-testing.fnl: -------------------------------------------------------------------------------- 1 | (fn test-simple [] 2 | (async 3 | (output.notify "1") 4 | (let [[stdout stderr reason code] (await awful.spawn.easy_async_with_shell "sleep 2 && echo 2")] 5 | (output.notify (or stdout "failed"))))) 6 | 7 | (fn test-nested [] 8 | (async 9 | (var thing 10 | (do 11 | (output.notify "1") 12 | (output.notify "2") 13 | (async 14 | (let [[stdout stderr reason code] (await awful.spawn.easy_async_with_shell "sleep 2 && echo 3")] 15 | stdout)))) 16 | 17 | (output.notify (or thing "failed")))) 18 | 19 | (fn test-nested-serial [] 20 | (async 21 | (var thing 22 | (do 23 | (output.notify "1") 24 | (output.notify "2") 25 | (async 26 | (let [[stdout stderr reason code] (await awful.spawn.easy_async_with_shell "sleep 2 && echo 3")] 27 | stdout)) 28 | (async 29 | (let [[stdout stderr reason code] (await awful.spawn.easy_async_with_shell "sleep 2 && echo 4")] 30 | stdout)))) 31 | 32 | (output.notify (or thing "failed")))) 33 | 34 | (fn test-nested-serial-return [] 35 | (async 36 | (var thing 37 | (do 38 | (output.notify "1") 39 | (output.notify "2") 40 | (async 41 | (let [[stdout1 stderr1 reason1 code1] (await awful.spawn.easy_async_with_shell "sleep 2 && echo 4") 42 | [nothin] (output.notify stdout1) 43 | [stdout2 stderr2 reason2 code2] (await awful.spawn.easy_async_with_shell (.. "sleep 2 && echo 4 and " stdout1))] 44 | stdout2)))) 45 | 46 | (output.notify (or thing "failed")))) 47 | -------------------------------------------------------------------------------- /awesome-macros.fnl: -------------------------------------------------------------------------------- 1 | (fn /< [...] 2 | "Create a 'mixed' table like in standard Lua. 3 | The table can contain both sequential and non-sequential keys. 4 | Like: 5 | (/< (awful.titlebar.widget.iconwidget c) 6 | :buttons buttons 7 | :layout wibox.layout.fixed.horizontal) 8 | 9 | Useful for wibox declarations." 10 | (let [tbl {}] 11 | (var skip 0) 12 | (each [i v (ipairs [...])] 13 | (when (~= i skip) 14 | (let [tv (type v)] 15 | (match tv 16 | "string" (do 17 | (set skip (+ i 1)) 18 | (tset tbl v (. [...] skip))) 19 | "table" (table.insert tbl v) 20 | _ (error (.. tv " key literal in mixed table")))))) 21 | tbl)) 22 | 23 | (fn with-current-screen [name body ...] 24 | "Do something with the currently selected screen." 25 | `(let [,name (awful.screen.focused)] 26 | (when ,name 27 | ,body 28 | ,...))) 29 | 30 | (fn with-current-tag [name body ...] 31 | "Do something with the currently 'active' tag (tag where focus is)." 32 | `(let [,name (. (awful.screen.focused) :selected_tag)] 33 | (when ,name 34 | ,body 35 | ,...))) 36 | 37 | {:/< /< 38 | :with-current-tag with-current-tag 39 | :with-current-screen with-current-screen} 40 | -------------------------------------------------------------------------------- /commands/tag.fnl: -------------------------------------------------------------------------------- 1 | ;; Functions meant to be called interactively on tags 2 | (local awful (require "awful")) 3 | (local beautiful (require "beautiful")) 4 | (local xresources (require "beautiful.xresources")) 5 | (local dpi xresources.apply_dpi) 6 | (local lume (require "vendor.lume")) 7 | (local tags (require "utils.tags")) 8 | 9 | (local gap-step 4) 10 | 11 | (local tagcmd {}) 12 | 13 | (lambda tagcmd.layout-fn [layout] 14 | "Return a 0-arity function which changes the layout of current tag to LAYOUT" 15 | (fn [] 16 | (let [s (awful.screen.focused) 17 | ct s.selected_tag] 18 | (tset ct :layout layout)))) 19 | 20 | (fn tagcmd.destroy-current [] 21 | "Delete the currently focused tag" 22 | (tags.destroy (. (awful.screen.focused) :selected_tag))) 23 | 24 | (fn tagcmd.go-right [] 25 | "Unmap current tag and map next tag, creating a new tag if none exists" 26 | (tags.view-next (awful.screen.focused))) 27 | 28 | (fn tagcmd.go-left [] 29 | "Unmap current tag and map previous tag" 30 | (tags.view-prev (awful.screen.focused))) 31 | 32 | (fn tagcmd.inc-gap [] 33 | "Grow the gap between clients" 34 | (let [s (awful.screen.focused) 35 | ct s.selected_tag] 36 | (tset ct :gap (+ gap-step ct.gap)))) 37 | 38 | (fn tagcmd.dec-gap [] 39 | "Shrink the gap between clients" 40 | (let [s (awful.screen.focused) 41 | ct s.selected_tag 42 | cgap (- ct.gap gap-step)] 43 | (tset ct :gap (if (> cgap 0) cgap 0)))) 44 | 45 | (fn tagcmd.inc-masters [] 46 | "Increase the number of windows that share the master area" 47 | (awful.tag.incnmaster 1)) 48 | 49 | (fn tagcmd.dec-masters [] 50 | "Decrease the number of windows that share the master area" 51 | (awful.tag.incnmaster -1)) 52 | 53 | (fn tagcmd.inc-master-width [] 54 | "Increase the master width factor, if the layout permits" 55 | (awful.tag.incmwfact 0.05)) 56 | 57 | (fn tagcmd.dec-master-width [] 58 | "Decrease the master width factor, if the layout permits" 59 | (awful.tag.incmwfact -0.05)) 60 | 61 | (fn tagcmd.inc-cols [] 62 | "Increase the number of columns, if the layout permits" 63 | (let [s (awful.screen.focused) 64 | ct s.selected_tag] 65 | (tset ct :column_count (+ 1 ct.column_count)))) 66 | 67 | (fn tagcmd.dec-cols [] 68 | "Decrease the number of columns, if the layout permits" 69 | (let [s (awful.screen.focused) 70 | ct s.selected_tag] 71 | (tset ct :column_count (- ct.column_count 1)))) 72 | 73 | (fn tagcmd.toggle-fill-policy [] 74 | "Toggle size fill policy for master client(s)" 75 | (let [s (awful.screen.focused) 76 | ct s.selected_tag] 77 | (awful.tag.togglemfpol ct))) 78 | 79 | tagcmd 80 | -------------------------------------------------------------------------------- /daemons/battery.fnl: -------------------------------------------------------------------------------- 1 | ;; Provides signals -- 2 | ;; battery::capacity 3 | ;; battery::status 4 | ;; Requires -- 5 | ;; upower 6 | 7 | (local awful (require "awful")) 8 | (local lume (require "vendor.lume")) 9 | (local {: notify } (require :api.lawful)) 10 | 11 | (local listen-script "bash -c 'upower --monitor'") 12 | (local capacity-script "bash -c 'cat /sys/class/power_supply/BAT*/capacity'") 13 | (local status-script "upower -i '/org/freedesktop/UPower/devices/battery_BAT0'") 14 | 15 | (lambda average-capacity [line-output] 16 | "Take a string of new-line delimited numbers and average them" 17 | (var t {}) 18 | (each [l (: line-output :gmatch "([^\n]*)\n?")] 19 | (table.insert t (tonumber l))) 20 | (let [sum (lume.reduce t (fn [a b] (+ a b)))] 21 | (/ sum (# t)))) 22 | 23 | (lambda get-field [str key] 24 | (lume.trim (or (: str :match (.. key "(.-)\n")) ""))) 25 | 26 | (lambda get-capacity [callback] 27 | "Asynchronously get the averaged capacity of all connected batteries" 28 | (awful.spawn.easy_async capacity-script 29 | (fn [out] 30 | (let [capacity (average-capacity out)] 31 | (callback capacity))))) 32 | 33 | (lambda get-status [callback] 34 | "Asynchronously get the status of the first connected battery. 35 | Returns map with keys :time-to-empty, :state, :time-to-full" 36 | (awful.spawn.easy_async status-script 37 | (fn [out] 38 | (let [state (get-field out "state:") 39 | toe (get-field out "time to empty:") 40 | tof (get-field out "time to full:")] 41 | (callback {:state state 42 | :time-to-empty toe 43 | :time-to-full tof}))))) 44 | 45 | (fn emit [] 46 | (get-capacity (fn [cap] (awesome.emit_signal "battery::percentage" cap) 47 | (get-status (fn [stats] (awesome.emit_signal "battery::status" stats)))))) 48 | 49 | (lambda listen [callback] 50 | (awful.spawn.with_line_callback listen-script { 51 | :stdout callback 52 | :stderr (fn [err] (notify.error err)) 53 | })) 54 | 55 | (listen emit) 56 | -------------------------------------------------------------------------------- /daemons/cpu.fnl: -------------------------------------------------------------------------------- 1 | (local awful (require "awful")) 2 | (local gears (require "gears")) 3 | (local lume (require "vendor.lume")) 4 | 5 | (local interval 3) 6 | (local cpu-script 7 | "bash -c 'vmstat 1 2 | tail -1 | tr -s [:blank:] | cut -d \" \" -f16'") 8 | 9 | (lambda get-cpu [callback] 10 | (awful.spawn.easy_async cpu-script 11 | (fn [res] 12 | (let [sanitized (lume.trim res) 13 | cpu-idle (tonumber sanitized) 14 | cpu-used (- 100 (or cpu-idle 0))] 15 | (callback cpu-used))))) 16 | 17 | (fn emit [] 18 | (get-cpu (fn [usage] (awesome.emit_signal "cpu::usage" usage)))) 19 | 20 | (lambda listen [callback] 21 | (gears.timer {:autostart true 22 | :call_now true 23 | :timeout interval 24 | :callback callback})) 25 | 26 | (listen emit) 27 | -------------------------------------------------------------------------------- /daemons/pulseaudio.fnl: -------------------------------------------------------------------------------- 1 | (local awful (require "awful")) 2 | (local pa (require "utils.pulseaudio")) 3 | (local {: notify } (require :api.lawful)) 4 | 5 | (fn get-volume [?callback] 6 | (let [cb (or ?callback (fn [] nil))] 7 | (awful.spawn.easy_async_with_shell pa.get-volume-script cb))) 8 | 9 | (fn get-muted [?callback] 10 | (let [cb (or ?callback (fn [] nil))] 11 | (awful.spawn.easy_async_with_shell pa.get-muted-script cb))) 12 | 13 | (fn get-mic-muted [?callback] 14 | (let [cb (or ?callback (fn [] nil))] 15 | (awful.spawn.easy_async_with_shell pa.get-mic-muted-script cb))) 16 | 17 | (fn emit-output-data [] 18 | (get-volume (fn [vol] 19 | (awesome.emit_signal 20 | "pulseaudio::output::volume" 21 | (tonumber vol)))) 22 | (get-muted (fn [muted?] 23 | (awesome.emit_signal 24 | "pulseaudio::output::muted" 25 | (if (string.find muted? "no") false true))))) 26 | 27 | (fn emit-input-data [] 28 | (get-mic-muted (fn [muted?] 29 | (awesome.emit_signal 30 | "pulseaudio::input::muted" 31 | (if (string.find muted? "no") false true))))) 32 | 33 | (lambda listen-volume [callback] 34 | (awful.spawn.with_line_callback pa.volume-listener-script 35 | { 36 | :stdout callback 37 | :stderr (fn [err] (notify.error err)) 38 | })) 39 | 40 | (lambda listen-mic [callback] 41 | (awful.spawn.with_line_callback pa.mic-listener-script 42 | { 43 | :stdout callback 44 | :stderr (fn [err] (notify.error err)) 45 | })) 46 | 47 | ;; run once on start-up to push out an initial value 48 | (emit-output-data) 49 | (emit-input-data) 50 | 51 | ;; find any orphaned listeners and kill them before spawning a new one 52 | (awful.spawn.easy_async_with_shell 53 | pa.kill-listeners-script 54 | (fn [] 55 | (listen-volume emit-output-data) 56 | (listen-mic emit-input-data))) 57 | -------------------------------------------------------------------------------- /daemons/ram.fnl: -------------------------------------------------------------------------------- 1 | (local awful (require "awful")) 2 | (local gears (require "gears")) 3 | (local lume (require "vendor.lume")) 4 | 5 | (local interval 15) 6 | (local ram-script 7 | "bash -c \"free | grep Mem | awk '{print $3/$2 * 100.0}'\"") 8 | 9 | (lambda get-ram-usage [callback] 10 | (awful.spawn.easy_async ram-script 11 | (fn [res] 12 | (let [ram-used (tonumber res)] 13 | (callback ram-used))))) 14 | 15 | (fn emit [] 16 | (get-ram-usage (fn [usage] (awesome.emit_signal "ram::usage" usage)))) 17 | 18 | (lambda listen [callback] 19 | (gears.timer {:autostart true 20 | :call_now true 21 | :timeout interval 22 | :callback callback})) 23 | 24 | (listen emit) 25 | -------------------------------------------------------------------------------- /daemons/workspace-env.fnl: -------------------------------------------------------------------------------- 1 | (local workspaces (require :features.workspaces)) 2 | (local { : notify : ext } (require :api.lawful)) 3 | (import-macros {: async : await} :utils.async) 4 | 5 | (fn inject-bash-workspace-env [] 6 | (with-open [bashrc (io.open (.. (os.getenv "HOME") "/.bashrc") :a+)] 7 | (let [source-line "source ~/.cache/awesome/wsenv" 8 | content (bashrc:read "*all")] 9 | (when (not (string.match content source-line)) 10 | (bashrc:write source-line))))) 11 | 12 | (fn prepare-firefox-profile [workspace-name] 13 | (let [config-path (.. (os.getenv "HOME") "/.config/firefox") 14 | profile-path (.. (os.getenv "HOME") "/.mozilla/firefox/" workspace-name)] 15 | (ext.shellout (.. "firefox -CreateProfile \"" workspace-name " " profile-path "\"")))) 16 | ; (ext.shellout (.. "ln -f -s " config-path "/* " profile-path)))) 17 | 18 | (fn spawn-workspace-daemons [workspace-name] 19 | (prepare-firefox-profile workspace-name) 20 | (when (not (= workspace-name :default)) 21 | (do 22 | (ext.shellout (.. "emacs --daemon=" workspace-name))))) 23 | 24 | (fn generate-bash-aliases [workspace-name] 25 | (let [filepath (.. (os.getenv "HOME") "/.cache/awesome" "/wsenv") 26 | lines (if (= workspace-name :default) 27 | [(.. "alias emacsclient='emacsclient -c'") 28 | (.. "alias e='emacsclient'") 29 | (.. "alias firefox='firefox -new-instance -P " workspace-name "'")] 30 | [(.. "alias emacsclient='emacsclient -c -s " workspace-name "'") 31 | (.. "alias e='emacsclient'") 32 | (.. "alias firefox='firefox -new-instance -P " workspace-name "'")] 33 | )] 34 | (with-open [outfile (io.open filepath :w)] 35 | (outfile:write 36 | (table.concat lines "\n"))))) 37 | 38 | (awesome.connect_signal 39 | "startup" 40 | (fn [] 41 | (async 42 | (inject-bash-workspace-env)))) 43 | 44 | (awesome.connect_signal 45 | "workspaces::applied" 46 | (fn [workspace-name] 47 | (async 48 | (doto workspace-name 49 | (spawn-workspace-daemons) 50 | (generate-bash-aliases))))) 51 | -------------------------------------------------------------------------------- /features/alt-tab.fnl: -------------------------------------------------------------------------------- 1 | ;; Not functional yet 2 | ;; Waiting for gears.history API 3 | ;; https://www.reddit.com/r/awesomewm/comments/ams2gt/keygrabber_example_in_documentation/ 4 | 5 | (local awful (require "awful")) 6 | (local input (require "utils.input")) 7 | 8 | (local bindings 9 | [ 10 | ; [[:Mod1 ] :Tab awful.client.focus.history.select_previous ] 11 | ; [[:Mod1 :Shift] :Tab awful.client.focus.history.select_next ] 12 | ]) 13 | 14 | (local alt-tab-config 15 | { 16 | :keybindings bindings 17 | :stop_key :Mod1 18 | :stop_event :release 19 | :start_callback awful.client.focus.history.disable_tracking 20 | :stop_callback awful.client.focus.history.enable_tracking 21 | :export_keybindings true 22 | }) 23 | 24 | (local grabber-fn (awful.keygrabber alt-tab-config)) 25 | 26 | (input.keybind "client" [:alt :shift] :Tab grabber-fn "cycle focus backward") 27 | 28 | (local 29 | alt-tab 30 | (input.keybind "client" [:alt] :Tab grabber-fn "cycle focus forward")) 31 | 32 | 33 | alt-tab 34 | -------------------------------------------------------------------------------- /features/familiar.fnl: -------------------------------------------------------------------------------- 1 | ; ██ 2 | ; ░░████░░ 3 | ; ▒▒ ▒▒████ ▒▒ 4 | ; ██ ██████▒▒ ██ 5 | ; ██████░░▒▒████████▓▓░░ ▓▓▒▒ 6 | ; ██ ▒▒████████████████████ ▒▒▓▓░░████▒▒ 7 | ; ░░████ ██████████████████ ██████████████ 8 | ; ████████████████████████ ██████████████ 9 | ; ▒▒░░ ████████████████████ ██████████████▒▒ 10 | ; ████████████████████████░░ ▒▒██████████████ 11 | ; ██████████████████ ██████████████ 12 | ; ░░██████████████████████████ 13 | ; ░░████████████████████████ 14 | ; ██████████████████████████ 15 | ; ▒▒████████████████████████ 16 | ; ██████████████████████░░ 17 | ; ████████████ ██▓▓▒▒ 18 | ; ▒▒██████████ ████ 19 | ; ██████████ ░░▒▒ 20 | ; ▒▒████████ ▒▒ 21 | ; ██████████ ▓▓░░██░░▒▒ 22 | ; ████████ ▒▒▒▒ 23 | ; ▒▒████▒▒ 24 | ; ░░██▓▓ 25 | ; ██ 26 | 27 | ;; “You see, a witch has to have a familiar, some little animal like a cat or 28 | ;; toad. He helps her somehow. When the witch dies the familiar is supposed to 29 | ;; die too, but sometimes it doesn't.” 30 | ;; -- Henry Kuttner, “Before I Wake” 31 | 32 | (local awful (require "awful")) 33 | (local lume (require "vendor.lume")) 34 | (local input (require "utils.input")) 35 | (local persistence (require "features.persistence")) 36 | (local {: ext : geo} (require "api.lawful")) 37 | (local {: concat } (require "utils.oleander")) 38 | 39 | (local fam {}) 40 | 41 | (set fam.default-config { 42 | :placement :left 43 | :screen-ratio 0.33 44 | }) 45 | 46 | (set fam.collection {}) 47 | 48 | (lambda tag-familiar [conf] 49 | "Return a function for capturing a familiar's window ID after it has mapped" 50 | (lambda [client] 51 | (tset fam.collection 52 | (.. "" client.window) 53 | (lume.merge conf {:wid client.window})))) 54 | 55 | (lambda axis-from-dir [dir] 56 | (match dir 57 | :left :vertical 58 | :right :vertical 59 | :top :horizontal 60 | :bottom :horizontal 61 | _ (error (.. "Unknown direction '" 62 | dir 63 | "': expected top, left, right or bottom")))) 64 | 65 | (lambda gen-placement [conf] 66 | "Generate a placement function based on a familiar's config" 67 | (fn [c] 68 | (let [axis (axis-from-dir conf.placement) 69 | p (+ geo.scale 70 | (. geo conf.placement) 71 | (. geo (.. "maximize_" axis "ly")))] 72 | (p c {:to_percent conf.screen-ratio :honor_workarea false})))) 73 | 74 | (lambda summon-familiar [conf] 75 | "Summon familiar with configuration CONF" 76 | (let [placement (gen-placement conf) 77 | (pid snid) (ext.spawn conf.command 78 | { 79 | :is_familiar true 80 | :familiar_name conf.name 81 | :floating true 82 | :ontop true 83 | :sticky true 84 | :placement placement 85 | :border_width 0 86 | :skip_taskbar true 87 | :hidden false 88 | :size_hints_honor false 89 | :requests_no_titlebar true 90 | :titlebars_enabled false} 91 | (tag-familiar conf) 92 | )] 93 | nil)) 94 | 95 | (lambda find-familiar [name] 96 | "Enumerate current clients and return the familiar named NAME" 97 | ((awful.client.iterate (fn [c] (= c.familiar_name name))))) 98 | 99 | (lambda find-or-summon-familiar [conf] 100 | "Find familiar named CONF.NAME, summoning it if it does not exist" 101 | (let [f (find-familiar conf.name)] 102 | (or f (summon-familiar conf)))) 103 | 104 | (lambda toggle-familiar [conf] 105 | "Call or dismiss familiar named CONF.NAME" 106 | (let [f (find-or-summon-familiar conf)] 107 | (when f 108 | (tset f :hidden (not f.hidden)) 109 | (when (not f.hidden) 110 | (: f :emit_signal "request::activate"))))) 111 | 112 | (fn fam.save [] 113 | "Return current state for persistence" 114 | {:collection fam.collection}) 115 | 116 | (lambda fam.load [state] 117 | "Restore contents of STATE and apply it" 118 | (set fam.collection (or state.collection fam.collection)) 119 | (each [_ c (ipairs (client.get))] 120 | (let [wid (.. "" c.window)] 121 | (when (. fam.collection wid) 122 | (let [conf (. fam.collection wid) 123 | placement (gen-placement conf)] 124 | (tset c :familiar_name conf.name) 125 | (tset c :is_familiar true) 126 | (tset c :border_width 0) 127 | (tset c :ontop true) 128 | (tset c :sticky true) 129 | (tset c :hidden true) 130 | (awful.titlebar.hide c) 131 | (placement c)))))) 132 | 133 | (persistence.register "familiars" fam.save fam.load) 134 | 135 | (lambda fam.def [conf] 136 | "Define a familiar with properties map CONF. 137 | 138 | CONF has properties -- 139 | :name -- every familiar deserves a unique and fitting name 140 | :key -- key for summoning familiar, in form [[MODIFIERS] KEY-CODE] 141 | :command -- command to execute, along with any arguments 142 | :screen-ratio -- 0 < decimal < 1 representing how much of the screen it covers 143 | :placement -- portion of screen (left, right, top, bottom) it calls home" 144 | (let [category "familiars" 145 | description (.. "⏼ " conf.name " visible") 146 | merge-conf (lume.merge fam.default-config conf) 147 | toggle-fn (fn [] (toggle-familiar merge-conf)) 148 | [mods key] conf.key 149 | binding (input.keybind category mods key toggle-fn description)] 150 | (root.keys (concat (root.keys) binding)))) 151 | 152 | fam.def 153 | -------------------------------------------------------------------------------- /features/motion.fnl: -------------------------------------------------------------------------------- 1 | (local lume (require "lume")) 2 | ;[:g my.gap.function "gaps" "misc section"] 3 | 4 | (fn parse 5 | [_ _ _ sequence] 6 | (let [tokens (lume.split (: sequence :gsub "%D" " %1 "))] 7 | -------------------------------------------------------------------------------- /features/nrepl.fnl: -------------------------------------------------------------------------------- 1 | (local lawful (require :api.lawful)) 2 | (local gears (require :gears)) 3 | (local jeejah (require :vendor.jeejah)) 4 | 5 | (local nrepl-port-filepath (.. (lawful.fs.config-dir) ".nrepl-port")) 6 | 7 | (var _coro nil) 8 | 9 | (lambda spawn-nrepl [?port] 10 | (let [port (or ?port 7888)] 11 | (set _coro (jeejah.start port {:debug true :fennel true})) 12 | (with-open [nrepl-port-file (io.open nrepl-port-filepath :w)] 13 | (nrepl-port-file:write port)) 14 | (lawful.notify.info (.. "nREPL server listening on " port ".")) 15 | (gears.timer {:autostart true 16 | :call_now true 17 | :timeout 0.100 18 | :callback (fn [] (coroutine.resume _coro))}))) 19 | 20 | (lambda kill-nrepl [] 21 | (jeejah.stop _coro) 22 | (os.remove nrepl-port-filepath) 23 | (lawful.notify.info "nREPL server disconnected.")) 24 | 25 | {:spawn-nrepl spawn-nrepl 26 | :kill-nrepl kill-nrepl} 27 | -------------------------------------------------------------------------------- /features/persistence.fnl: -------------------------------------------------------------------------------- 1 | (local awful (require "awful")) 2 | (local json (require "vendor.json")) 3 | (local lume (require "vendor.lume")) 4 | (local screen-utils (require "utils.screens")) 5 | (local { : notify} (require :api.lawful)) 6 | 7 | (local persistence {}) 8 | 9 | (local signame "persistence::") 10 | (local cachedir (awful.util.get_cache_dir)) 11 | (local statefile "state.json") 12 | (local permafile "persisted.json") 13 | 14 | (local subscribers []) 15 | 16 | ;; initialize as disabled 17 | (set persistence.enabled false) 18 | 19 | (lambda write-file [path filename content] 20 | (let [f (io.open (.. path "/" filename) "w")] 21 | (: f :write content) 22 | (: f :close))) 23 | 24 | (lambda read-file [path filename] 25 | (match (io.open (.. path "/" filename) "r") 26 | [nil err] (do (write-file path filename "{}") 27 | (match (io.open (.. path "/" filename) "r") 28 | [nil err] (notify.err err) 29 | f (: f :read "*all"))) 30 | f (: f :read "*all"))) 31 | 32 | (lambda delete-file [path filename] 33 | (os.remove (.. path filename))) 34 | 35 | (lambda persistence.register [name save on-load ?persist-across-reboot?] 36 | "Register hooks for saving and loading state" 37 | (table.insert subscribers {:name name 38 | :save save 39 | :on-load on-load 40 | :perma ?persist-across-reboot?})) 41 | 42 | (fn persistence.save-all [] 43 | (var new-state {}) 44 | (var new-perma {}) 45 | (awesome.emit_signal (.. signame "saving")) 46 | (each [_ sub (ipairs subscribers)] 47 | (let [s (if sub.perma new-perma new-state)] 48 | (awesome.emit_signal (.. signame sub.name "::" "saving")) 49 | (tset s sub.name (sub.save)) 50 | (awesome.emit_signal (.. signame sub.name "::" "saved")))) 51 | (when persistence.enabled 52 | (write-file cachedir statefile (json.encode new-state)) 53 | (write-file cachedir permafile (json.encode new-perma))) 54 | (awesome.emit_signal (.. signame "saved"))) 55 | 56 | (fn persistence.load-all [] 57 | (awesome.emit_signal (.. signame "loading")) 58 | (let [state-file (read-file cachedir statefile) 59 | perma-file (read-file cachedir permafile) 60 | s (if persistence.enabled 61 | (if state-file (json.decode state-file) {}) 62 | {}) 63 | p (if persistence.enabled 64 | (if perma-file (json.decode perma-file) {}) 65 | {})] 66 | (each [_ sub (ipairs subscribers)] 67 | (let [data (if sub.perma p s)] 68 | (when data 69 | (awesome.emit_signal 70 | (.. signame sub.name "::" "loading")) 71 | (sub.on-load (or (. data sub.name) {})) 72 | (awesome.emit_signal 73 | (.. signame sub.name "::" "loaded"))))) 74 | (awesome.emit_signal (.. signame "loaded")))) 75 | 76 | (fn layout-name [layout] 77 | (let [tv (type layout)] 78 | (match tv 79 | "function" (. (layout) :name) 80 | "table" layout.name 81 | _ (error (.. "Layout should be a function or table, got " tv))))) 82 | 83 | (fn save-tags [] 84 | (let [current-tags (root.tags)] 85 | (lume.map 86 | current-tags 87 | (fn [t] { 88 | :name t.name 89 | :selected t.selected 90 | :activated t.activated 91 | :index t.index 92 | :screen-index t.screen.index 93 | :master-width-factor t.master_width_factor 94 | :layout (layout-name t.layout) 95 | :volatile t.volatile 96 | :gap t.gap 97 | :gap-single-client t.gap_single_client 98 | :master-fill-policy t.master_fill_policy 99 | :master-count t.master_count 100 | :icon t.icon 101 | :column-count t.column_count 102 | :window-ids (lume.map (: t :clients) (fn [c] c.window)) 103 | })))) 104 | 105 | (fn load-tags [map] 106 | (let [screens (screen-utils.get-screens) 107 | clients (client.get)] 108 | (each [_ t (pairs map)] 109 | (let [scr (or (. screens t.screen-index) screen.primary) 110 | lay (-> awful.layout.layouts 111 | (lume.filter (fn [l] (= l.name t.layout))) 112 | (lume.first))] 113 | (local tag (awful.tag.add 114 | t.name 115 | { 116 | :selected t.selected 117 | :activated t.activated 118 | :hide t.hide 119 | :index t.index 120 | :screen scr 121 | :master_width_factor t.master-width-factor 122 | :layout lay 123 | :volatile t.volatile 124 | :gap_single_client t.gap-single-client 125 | :master_fill_policy t.master-fill-policy 126 | :master_count t.master-count 127 | :icon t.icon 128 | :column_count t.column-count 129 | })) 130 | (each [_ c (ipairs clients)] 131 | (when (lume.match t.window-ids (fn [wid] (= wid c.window))) 132 | (: c :move_to_tag tag))))))) 133 | 134 | (persistence.register "tags" save-tags load-tags) 135 | 136 | (fn persistence.enable [] 137 | (set persistence.enabled true)) 138 | 139 | ;; other features can depend on persistence's signals, 140 | ;; even if persistence is not enabled 141 | (awesome.connect_signal 142 | "exit" 143 | (fn [restart?] 144 | (if restart? 145 | (persistence.save-all) 146 | (persistence.delete-file cachedir statefile)))) 147 | 148 | (awesome.connect_signal "startup" (fn [] (persistence.load-all))) 149 | 150 | persistence 151 | -------------------------------------------------------------------------------- /features/titlebar.fnl: -------------------------------------------------------------------------------- 1 | (local wibox (require :wibox)) 2 | (local awful (require :awful)) 3 | (local lawful (require :api.lawful)) 4 | (local {: map} (require :lume)) 5 | (import-macros {: /< : with-current-tag } :awesome-macros) 6 | 7 | (var titlebars? false) 8 | (var enabled? false) 9 | 10 | (fn add-titlebar [c] 11 | "Decorate client C with a titlebar." 12 | (let [buttons (lawful.util.join 13 | (awful.button [] 1 (fn [] 14 | (: c :emit_signal :request::activate :titlebar {:raise true}) 15 | (awful.mouse.client.move c))) 16 | (awful.button [] 3 (fn [] 17 | (: c :emit_signal :request::activate :titlebar {:raise true}) 18 | (awful.mouse.client.resize c)))) 19 | titlebar (awful.titlebar c)] 20 | (: titlebar :setup (/< 21 | (/< ;; Left 22 | (awful.titlebar.widget.iconwidget c) 23 | :buttons buttons 24 | :layout wibox.layout.fixed.horizontal) 25 | (/< ;; Middle 26 | ;; (/< ;; Title 27 | ;; :align "center" 28 | ;; :widget (awful.titlebar.widget.titlewidget c)) 29 | :buttons buttons 30 | :layout wibox.layout.flex.horizontal) 31 | (/< ;; Right 32 | (awful.titlebar.widget.floatingbutton c) 33 | (awful.titlebar.widget.maximizedbutton c) 34 | (awful.titlebar.widget.stickybutton c) 35 | (awful.titlebar.widget.ontopbutton c) 36 | (awful.titlebar.widget.closebutton c) 37 | :layout (wibox.layout.fixed.horizontal)) 38 | :layout wibox.layout.align.horizontal)) 39 | (when (not titlebars?) 40 | (awful.titlebar.hide c)))) 41 | 42 | (fn enable [] 43 | "Enable titlebars for all windows in current tag." 44 | (set titlebars? true) 45 | (with-current-tag t 46 | (map (t:clients) (fn [c] 47 | (awful.titlebar.show c))))) 48 | 49 | (fn disable [] 50 | "Disable titlebars for all windows in current tag." 51 | (set titlebars? false) 52 | (with-current-tag t 53 | (map (t:clients) (fn [c] 54 | (awful.titlebar.hide c))))) 55 | 56 | (fn toggle [] 57 | "Toggle titlebars for all windows in current tag." 58 | (if titlebars? 59 | (disable) 60 | (enable))) 61 | 62 | (client.connect_signal :request::titlebars add-titlebar) 63 | 64 | {:toggle toggle 65 | :disable disable 66 | :enable enable} 67 | -------------------------------------------------------------------------------- /features/wallpaper.fnl: -------------------------------------------------------------------------------- 1 | (local awful (require "awful")) 2 | (local gears (require "gears")) 3 | (local beautiful (require "beautiful")) 4 | 5 | (local wallpaper {}) 6 | 7 | (lambda wallpaper.set [s] 8 | (when beautiful.wallpaper 9 | (var wallpaper beautiful.wallpaper) 10 | ;; If wallpaper is a function, call it with the screen 11 | (when (= (type wallpaper) "function") 12 | (set wallpaper (wallpaper s))) 13 | ;; workaround memleak when called frequently eg. by a timer 14 | ;; https://github.com/awesomeWM/awesome/issues/2858 15 | (collectgarbage "step" 4000) 16 | (gears.wallpaper.maximized wallpaper s true))) 17 | 18 | (fn wallpaper.enable [] 19 | (awful.screen.connect_for_each_screen wallpaper.set) 20 | ;; Reset wallpaper when a screen's geometry changes (eg. resolution change) 21 | (screen.connect_signal "property::geometry" wallpaper.set)) 22 | 23 | wallpaper 24 | -------------------------------------------------------------------------------- /features/workspaces.fnl: -------------------------------------------------------------------------------- 1 | (local awful (require "awful")) 2 | (local lume (require "vendor.lume")) 3 | (local tag-utils (require "utils.tags")) 4 | (local screen-utils (require "utils.screens")) 5 | (local identicon (require "utils.identicon")) 6 | (local user (require "user")) 7 | (local persistence (require "features.persistence")) 8 | 9 | (local workspaces {}) 10 | 11 | (set workspaces.map {:default {:tags [] :selected []}}) 12 | (set workspaces.current :default) 13 | 14 | (set workspaces.name-cache []) 15 | 16 | (fn workspaces.get [?name] 17 | "Get the current workspace, or the workspace with name ?NAME" 18 | (let [name (or ?name workspaces.current)] 19 | (. workspaces.map name))) 20 | 21 | (fn workspaces.get-icon [name] 22 | "Get the icon for workspace with NAME" 23 | (if (= name :default) 24 | user.avatar 25 | (identicon.create name 7 32))) 26 | 27 | (fn workspaces.list [] 28 | "List active workspaces" 29 | (lume.keys workspaces.map)) 30 | 31 | (lambda workspaces.create [name] 32 | "Create a new workspace with name NAME, if it does not already exist" 33 | (when (not (workspaces.get name)) 34 | (tset workspaces.map name {:tags [] :selected []}))) 35 | 36 | (fn workspaces.reverse-tag-lookup [] 37 | "Get a table where keys are tag names and values are workspace names" 38 | (let [concat (fn [a b] (lume.merge a b))] 39 | (-> (lume.keys workspaces.map) 40 | (lume.map 41 | (fn [k] 42 | (let [v (. workspaces.map k) 43 | kv (lume.map v.tags (fn [t] {t k}))] 44 | (lume.reduce kv concat {})))) 45 | (lume.reduce concat)))) 46 | 47 | (lambda workspaces.attach-tag [tag ?client ?workspace] 48 | "Associate TAG with ?WORKSPACE or current workspace, if it has no connections" 49 | (let [lookup (workspaces.reverse-tag-lookup)] 50 | (let [wsname (or (. lookup tag.name) ?workspace workspaces.current) 51 | ws (workspaces.get wsname)] 52 | (when (not (lume.find (or ws.tags []) tag.name)) 53 | (table.insert (. ws :tags) tag.name)) 54 | (tset tag :workspace wsname)))) 55 | 56 | (fn workspaces.save-selected-tags [] 57 | "Find all selected tags across all screens and persist them" 58 | (var selection []) 59 | (each [scr (screen-utils.screen-iter)] 60 | (set selection 61 | (lume.concat selection 62 | (lume.map (. scr :selected_tags) (fn [t] t.name))))) 63 | (let [selected-tags (. (. workspaces.map workspaces.current) :tags) 64 | filtered (lume.filter selection (fn [s] (lume.find selected-tags s)))] 65 | (tset (. workspaces.map workspaces.current) :selected filtered))) 66 | 67 | (fn workspaces.restore-selected-tags [] 68 | "Re-select the last known selected tags for the current workspace" 69 | (lume.each (. (. workspaces.map workspaces.current) :selected) 70 | (fn [name] (let [t (awful.tag.find_by_name nil name)] 71 | (tset t :selected true))))) 72 | 73 | (lambda workspaces.clear-tag-ref [tagname] 74 | "Walk over the workspaces map, eliminating references to TAGNAME" 75 | (each [wsname (pairs workspaces.map)] 76 | (let [ws (. workspaces.map wsname)] 77 | (lume.remove (. ws :tags) tagname) 78 | (lume.remove (. ws :selected) tagname)))) 79 | 80 | (lambda workspaces.apply [?name] 81 | "Activate ?NAME or current workspace, deactivating tags not attached to it" 82 | (let [ws (or ?name workspaces.current) 83 | tags (root.tags) 84 | predicate (fn [t] (= t.workspace ws)) 85 | active (lume.filter tags predicate) 86 | inactive (lume.reject tags predicate)] 87 | 88 | (awesome.emit_signal "workspaces::applying" ws) 89 | (workspaces.save-selected-tags) 90 | (set workspaces.current ws) 91 | (each [_ tag (ipairs active)] 92 | (tag-utils.activate tag)) 93 | (each [_ tag (ipairs inactive)] 94 | (tag-utils.deactivate tag)) 95 | (workspaces.restore-selected-tags) 96 | ;; if there's no active tags on a screen, create one 97 | (each [scr (screen-utils.screen-iter)] 98 | (when (= (# (tag-utils.list-visible scr)) 0) 99 | (tag-utils.create scr nil {:workspace ws}))) 100 | (awesome.emit_signal "workspaces::applied" ws))) 101 | 102 | (fn workspaces.save [] 103 | "Return current state for persistence" 104 | {:current workspaces.current 105 | :map workspaces.map}) 106 | 107 | (lambda workspaces.load [state] 108 | "Restore contents of STATE and apply it" 109 | (set workspaces.current (or state.current workspaces.current)) 110 | (set workspaces.map (or state.map workspaces.map)) 111 | (let [lookup (workspaces.reverse-tag-lookup)] 112 | (each [_ t (ipairs (root.tags))] 113 | ;; if a tag has no workspace (orphaned), attach it to the current workspace 114 | (workspaces.attach-tag t nil (or (. lookup tag.name) workspaces.current))) 115 | (awful.tag.attached_connect_signal nil "tagged" workspaces.attach-tag) 116 | (workspaces.apply))) 117 | 118 | (fn workspaces.save-names [] 119 | workspaces.name-cache) 120 | 121 | (lambda workspaces.load-names [l] 122 | (set workspaces.name-cache l)) 123 | 124 | (fn workspaces.enable [] 125 | (awesome.emit_signal "workspaces::init") 126 | (awesome.connect_signal "tag::deleted" workspaces.clear-tag-ref) 127 | (persistence.register "workspaces" workspaces.save workspaces.load) 128 | (persistence.register "workspace-names" 129 | workspaces.save-names 130 | workspaces.load-names 131 | true)) 132 | 133 | workspaces 134 | -------------------------------------------------------------------------------- /icons/_macros.fnl: -------------------------------------------------------------------------------- 1 | (local icon-macros {}) 2 | 3 | (fn icon-macros.deficon [name tbl-def] 4 | `(fn ,name [options#] 5 | (local xml# (require "utils.xml")) 6 | (let [width# (or (. options# :width) 24) 7 | height# (or (. options# :height) 24) 8 | viewBox# (or (. options# :viewBox) "0 0 24 24") 9 | fill# (or (. options# :fill) "none") 10 | shape-rendering# (or (. options# :shape-rendering) :auto) 11 | stroke-color# (or (. options# :stroke-color) "#fff") 12 | stroke-width# (or (. options# :stroke-width) 2) 13 | stroke-linecap# (or (. options# :stroke-linecap) :round) 14 | stroke-linejoin# (or (. options# :stroke-linejoin) :round)] 15 | ((. xml# :create-element) :svg 16 | {:xmlns "http://www.w3.org/2000/svg" 17 | :shape-rendering shape-rendering# 18 | :height height# 19 | :width width# 20 | :viewBox (or viewBox# (.. "0 0 " height# " " width#)) 21 | :stroke-width stroke-width# 22 | :stroke stroke-color# 23 | :stroke-linecap stroke-linecap# 24 | :stroke-linejoin stroke-linejoin# 25 | :fill fill#} 26 | ,tbl-def)))) 27 | 28 | icon-macros 29 | -------------------------------------------------------------------------------- /icons/box.fnl: -------------------------------------------------------------------------------- 1 | (require-macros :icons._macros) 2 | 3 | (deficon box 4 | [[:path {:stroke :none :d "M0 0h24v24H0z"}] 5 | [:polyline {:points "12 3 20 7.5 20 16.5 12 21 4 16.5 4 7.5 12 3"}] 6 | [:line {:x1 12 :y1 12 :x2 20 :y2 7.5}] 7 | [:line {:x1 12 :y1 12 :x2 12 :y2 21}] 8 | [:line {:x1 12 :y1 12 :x2 4 :y2 7.5}]]) 9 | -------------------------------------------------------------------------------- /icons/layouts.fnl: -------------------------------------------------------------------------------- 1 | (require-macros :icons._macros) 2 | 3 | (local layouts {}) 4 | 5 | (deficon layouts.tile 6 | [[:path {:stroke :none :fill :none :d "M0 0h24v24H0z"}] 7 | [:rect {:x 4 :y 4 :width 4 :height 16 :stroke-width 1}] 8 | [:rect {:x 10 :y 4 :width 4 :height 4 :stroke-width 1}] 9 | [:rect {:x 10 :y 10 :width 4 :height 4 :stroke-width 1}] 10 | [:rect {:x 10 :y 16 :width 4 :height 4 :stroke-width 1}] 11 | [:rect {:x 16 :y 4 :width 4 :height 4 :stroke-width 1}] 12 | [:rect {:x 16 :y 10 :width 4 :height 4 :stroke-width 1}] 13 | [:rect {:x 16 :y 16 :width 4 :height 4 :stroke-width 1}]]) 14 | 15 | (deficon layouts.tilebottom 16 | [[:path {:stroke :none :fill :none :d "M0 0h24v24H0z"}] 17 | [:rect {:x 4 :y 4 :width 16 :height 4 :stroke-width 1}] 18 | [:rect {:x 4 :y 10 :width 4 :height 4 :stroke-width 1}] 19 | [:rect {:x 4 :y 16 :width 4 :height 4 :stroke-width 1}] 20 | [:rect {:x 10 :y 10 :width 4 :height 4 :stroke-width 1}] 21 | [:rect {:x 10 :y 16 :width 4 :height 4 :stroke-width 1}] 22 | [:rect {:x 16 :y 10 :width 4 :height 4 :stroke-width 1}] 23 | [:rect {:x 16 :y 16 :width 4 :height 4 :stroke-width 1}]]) 24 | 25 | (deficon layouts.tiletop 26 | [[:path {:stroke :none :fill :none :d "M0 0h24v24H0z"}] 27 | [:rect {:x 4 :y 16 :width 16 :height 4 :stroke-width 1}] 28 | [:rect {:x 4 :y 10 :width 4 :height 4 :stroke-width 1}] 29 | [:rect {:x 4 :y 4 :width 4 :height 4 :stroke-width 1}] 30 | [:rect {:x 10 :y 10 :width 4 :height 4 :stroke-width 1}] 31 | [:rect {:x 10 :y 4 :width 4 :height 4 :stroke-width 1}] 32 | [:rect {:x 16 :y 10 :width 4 :height 4 :stroke-width 1}] 33 | [:rect {:x 16 :y 4 :width 4 :height 4 :stroke-width 1}]]) 34 | 35 | (deficon layouts.tileleft 36 | [[:path {:stroke :none :fill :none :d "M0 0h24v24H0z"}] 37 | [:rect {:x 4 :y 4 :width 4 :height 4 :stroke-width 1}] 38 | [:rect {:x 4 :y 10 :width 4 :height 4 :stroke-width 1}] 39 | [:rect {:x 4 :y 16 :width 4 :height 4 :stroke-width 1}] 40 | [:rect {:x 16 :y 4 :width 4 :height 16 :stroke-width 1}] 41 | [:rect {:x 10 :y 4 :width 4 :height 4 :stroke-width 1}] 42 | [:rect {:x 10 :y 10 :width 4 :height 4 :stroke-width 1}] 43 | [:rect {:x 10 :y 16 :width 4 :height 4 :stroke-width 1}]]) 44 | 45 | (deficon layouts.spiral 46 | [[:path {:stroke :none :fill :none :d "M0 0h24v24H0z"}] 47 | [:rect {:x 4 :y 4 :width 8 :height 16 :stroke-width 1}] 48 | [:rect {:x 14 :y 4 :width 8 :height 8 :stroke-width 1}] 49 | [:rect {:x 18 :y 14 :width 4 :height 6 :stroke-width 1}] 50 | [:rect {:x 14 :y 18 :width 2 :height 2 :stroke-width 1}] 51 | [:rect {:x 14 :y 14 :width 2 :height 2 :stroke-width 1}]]) 52 | 53 | (deficon layouts.dwindle 54 | [[:path {:stroke :none :fill :none :d "M0 0h24v24H0z"}] 55 | [:rect {:x 4 :y 4 :width 8 :height 16 :stroke-width 1}] 56 | [:rect {:x 14 :y 4 :width 8 :height 8 :stroke-width 1}] 57 | [:rect {:x 14 :y 14 :width 4 :height 6 :stroke-width 1}] 58 | [:rect {:x 20 :y 18 :width 2 :height 2 :stroke-width 1}] 59 | [:rect {:x 20 :y 14 :width 2 :height 2 :stroke-width 1}]]) 60 | 61 | (deficon layouts.max 62 | [[:path {:stroke :none :fill :none :d "M0 0h24v24H0z"}] 63 | [:rect {:x 8 :y 8 :width 8 :height 8 :stroke-width 1}] 64 | [:polygon {:points "12,4 10,6 14,6"}] 65 | [:polygon {:points "12,20 10,18 14,18"}] 66 | [:polygon {:points "4,12 6,10 6,14"}] 67 | [:polygon {:points "20,12 18,10 18,14"}]]) 68 | 69 | (deficon layouts.fullscreen 70 | [[:path {:stroke :none :fill :none :d "M0 0h24v24H0z"}] 71 | [:polygon {:points "12,4 10,6 14,6"}] 72 | [:polygon {:points "12,20 10,18 14,18"}] 73 | [:polygon {:points "4,12 6,10 6,14"}] 74 | [:polygon {:points "20,12 18,10 18,14"}]]) 75 | 76 | (deficon layouts.magnifier 77 | [[:path {:stroke :none :fill :none :d "M0 0h24v24H0z"}] 78 | [:rect {:x 6 :y 6 :width 12 :height 12}] 79 | [:line {:x1 12 :y1 4 :x2 12 :y2 6}] 80 | [:line {:x1 12 :y1 18 :x2 12 :y2 20}] 81 | [:line {:x1 4 :y1 12 :x2 6 :y2 12}] 82 | [:line {:x1 18 :y1 12 :x2 20 :y2 12}]]) 83 | 84 | (deficon layouts.fairv 85 | [[:path {:stroke :none :fill :none :d "M0 0h24v24H0z"}] 86 | [:rect {:x 4 :y 4 :width 8 :height 4 :stroke-width 1}] 87 | [:rect {:x 4 :y 10 :width 8 :height 4 :stroke-width 1}] 88 | [:rect {:x 4 :y 16 :width 8 :height 4 :stroke-width 1}] 89 | [:rect {:x 14 :y 4 :width 8 :height 4 :stroke-width 1}] 90 | [:rect {:x 14 :y 10 :width 8 :height 4 :stroke-width 1}] 91 | [:rect {:x 14 :y 16 :width 8 :height 4 :stroke-width 1}]]) 92 | 93 | (deficon layouts.fairh 94 | [[:path {:stroke :none :fill :none :d "M0 0h24v24H0z"}] 95 | [:rect {:x 4 :y 4 :width 4 :height 8 :stroke-width 1}] 96 | [:rect {:x 10 :y 4 :width 4 :height 8 :stroke-width 1}] 97 | [:rect {:x 16 :y 4 :width 4 :height 8 :stroke-width 1}] 98 | [:rect {:x 4 :y 14 :width 4 :height 8 :stroke-width 1}] 99 | [:rect {:x 10 :y 14 :width 4 :height 8 :stroke-width 1}] 100 | [:rect {:x 16 :y 14 :width 4 :height 8 :stroke-width 1}]]) 101 | 102 | (deficon layouts.floating 103 | [[:path {:stroke :none :fill :none :d "M0 0h24v24H0z"}] 104 | [:rect {:x 2 :y 2 :width 16 :height 12 :stroke-width 1}] 105 | [:line {:x1 6 :y1 14 :x2 6 :y2 20 :stroke-width 1}] 106 | [:line {:x1 6 :y1 20 :x2 22 :y2 20 :stroke-width 1}] 107 | [:line {:x1 22 :y1 20 :x2 22 :y2 8 :stroke-width 1}] 108 | [:line {:x1 22 :y1 8 :x2 18 :y2 8 :stroke-width 1}]]) 109 | 110 | (deficon layouts.cornernw 111 | [[:path {:stroke :none :fill :none :d "M0 0h24v24H0z"}] 112 | [:rect {:x 2 :y 2 :width 14 :height 14 :stroke-width 1}] 113 | [:rect {:x 18 :y 2 :width 4 :height 5 :stroke-width 1}] 114 | [:rect {:x 18 :y 10 :width 4 :height 5 :stroke-width 1}] 115 | [:rect {:x 18 :y 18 :width 4 :height 4 :stroke-width 1}] 116 | [:rect {:x 10 :y 18 :width 6 :height 4 :stroke-width 1}] 117 | [:rect {:x 2 :y 18 :width 6 :height 4 :stroke-width 1}]]) 118 | 119 | (deficon layouts.cornersw 120 | [[:path {:stroke :none :fill :none :d "M0 0h24v24H0z"}] 121 | [:rect {:x 2 :y 8 :width 14 :height 14 :stroke-width 1}] 122 | [:rect {:x 18 :y 10 :width 4 :height 5 :stroke-width 1}] 123 | [:rect {:x 18 :y 17 :width 4 :height 5 :stroke-width 1}] 124 | [:rect {:x 18 :y 2 :width 4 :height 5 :stroke-width 1}] 125 | [:rect {:x 10 :y 2 :width 6 :height 4 :stroke-width 1}] 126 | [:rect {:x 2 :y 2 :width 6 :height 4 :stroke-width 1}]]) 127 | 128 | (deficon layouts.cornerse 129 | [[:path {:stroke :none :fill :none :d "M0 0h24v24H0z"}] 130 | [:rect {:x 8 :y 8 :width 14 :height 14 :stroke-width 1}] 131 | [:rect {:x 2 :y 10 :width 4 :height 5 :stroke-width 1}] 132 | [:rect {:x 2 :y 17 :width 4 :height 5 :stroke-width 1}] 133 | [:rect {:x 2 :y 2 :width 4 :height 5 :stroke-width 1}] 134 | [:rect {:x 8 :y 2 :width 6 :height 4 :stroke-width 1}] 135 | [:rect {:x 16 :y 2 :width 6 :height 4 :stroke-width 1}]]) 136 | 137 | (deficon layouts.cornerne 138 | [[:path {:stroke :none :fill :none :d "M0 0h24v24H0z"}] 139 | [:rect {:x 8 :y 2 :width 14 :height 14 :stroke-width 1}] 140 | [:rect {:x 2 :y 2 :width 4 :height 5 :stroke-width 1}] 141 | [:rect {:x 2 :y 10 :width 4 :height 5 :stroke-width 1}] 142 | [:rect {:x 18 :y 18 :width 4 :height 4 :stroke-width 1}] 143 | [:rect {:x 10 :y 18 :width 6 :height 4 :stroke-width 1}] 144 | [:rect {:x 2 :y 18 :width 6 :height 4 :stroke-width 1}]]) 145 | 146 | layouts 147 | -------------------------------------------------------------------------------- /icons/loader.fnl: -------------------------------------------------------------------------------- 1 | (local json (require "vendor.json")) 2 | (local oleander (require "utils.oleander")) 3 | (local {: notify : ext : fs } (require :api.lawful)) 4 | 5 | (local icon-loader {}) 6 | 7 | (set icon-loader.cachedir (fs.icon-dir)) 8 | (tset icon-loader :initialized false) 9 | 10 | (lambda loader [module name ?options] 11 | (let [iconpkg (require (.. "icons." module)) 12 | icon-fn (. iconpkg name) 13 | opts (or ?options {}) 14 | opt-hash (oleander.bsd-checksum (json.encode opts)) 15 | filename (.. module "-" name "-" opt-hash ".svg") 16 | filepath (.. icon-loader.cachedir filename) 17 | exists (io.open filepath :r)] 18 | (if (= exists nil) 19 | (with-open [outfile (io.open filepath :w)] 20 | (outfile:write (icon-fn opts))) 21 | (exists:close)) 22 | filepath)) 23 | 24 | (lambda icon-loader.load [module name ?options] 25 | (when (not icon-loader.initialized) 26 | (ext.shellout (.. "mkdir " icon-loader.cachedir " || true")) 27 | (tset icon-loader :initialized true)) 28 | (loader module name ?options)) 29 | 30 | (fn icon-loader.clear-cache [] 31 | (ext.shellout (.. "rm " icon-loader.cachedir "*.svg")) 32 | (tset icon-loader :initialized false)) 33 | 34 | icon-loader 35 | -------------------------------------------------------------------------------- /icons/shtwzrd.fnl: -------------------------------------------------------------------------------- 1 | (require-macros :icons._macros) 2 | 3 | (local shtwzrd {}) 4 | 5 | (deficon shtwzrd.avatar8bit 6 | [[:path {:stroke "#413331" :d "M1 0h5M0 1h3M6 1h1M0 2h1M0 3h2M0 4h1M6 4h1M0 5h1M3 5h2M6 5h1M0 6h1M2 6h5M0 7h2M5 7h2"}] 7 | [:path {:stroke "#e1c0ac" :d "M3 1h3M4 3h1M1 4h5M1 5h2M5 5h1M1 6h1M2 7h3"}] 8 | [:path {:stroke "#91bdcd" :d "M1 2h1M6 2h1M6 3h1"}] 9 | [:path {:stroke "#b4d1dc" :d "M2 2h4M2 3h2M5 3h1"}]]) 10 | 11 | shtwzrd 12 | -------------------------------------------------------------------------------- /icons/tabler.fnl: -------------------------------------------------------------------------------- 1 | (require-macros :icons._macros) 2 | 3 | (local tabler {}) 4 | 5 | (deficon tabler.box 6 | [[:path {:stroke :none :d "M0 0h24v24H0z"}] 7 | [:polyline {:points "12 3 20 7.5 20 16.5 12 21 4 16.5 4 7.5 12 3"}] 8 | [:line {:x1 12 :y1 12 :x2 20 :y2 7.5}] 9 | [:line {:x1 12 :y1 12 :x2 12 :y2 21}] 10 | [:line {:x1 12 :y1 12 :x2 4 :y2 7.5}]]) 11 | 12 | (deficon tabler.grid 13 | [[:path {:stroke :none :d "M0 0h24v24H0z"}] 14 | [:circle {:cx 5 :cy 5 :r 1}] 15 | [:circle {:cx 12 :cy 5 :r 1}] 16 | [:circle {:cx 19 :cy 5 :r 1}] 17 | [:circle {:cx 5 :cy 12 :r 1}] 18 | [:circle {:cx 12 :cy 12 :r 1}] 19 | [:circle {:cx 19 :cy 12 :r 1}] 20 | [:circle {:cx 5 :cy 19 :r 1}] 21 | [:circle {:cx 12 :cy 19 :r 1}] 22 | [:circle {:cx 19 :cy 19 :r 1}]]) 23 | 24 | (deficon tabler.terminal 25 | [[:path {:stroke :none :fill :none :d "M0 0h24v24H0z"}] 26 | [:path {:d "M8 9l3 3l-3 3"}] 27 | [:line {:x1 13 :y1 15 :x2 16 :y2 15}] 28 | [:rect {:x 3 :y 4 :width 18 :height 16 :rx 2}]]) 29 | 30 | (local battery-path 31 | (.. "M6 7h11a2 2 0 0 1 2 2v.5a0.5 .5 0 0 0 .5 .5a0.5 .5 0 0 1 .5" 32 | " .5v3a0.5 .5 0 0 1 -.5 .5a0.5 .5 0 0 0 -.5 .5v.5a2 2 0 0 1 -2" 33 | " 2h-11a2 2 0 0 1 -2 -2v-6a2 2 0 0 1 2 -2")) 34 | 35 | (deficon tabler.battery-empty 36 | [[:path {:stroke :none :fill :none :d "M0 0h24v24H0z"}] 37 | [:path {:d battery-path}]]) 38 | 39 | (deficon tabler.battery-low 40 | [[:path {:stroke :none :fill :none :d "M0 0h24v24H0z"}] 41 | [:path {:d battery-path}] 42 | [:line {:x1 7 :y1 10 :x2 7 :y2 14}]]) 43 | 44 | (deficon tabler.battery-mid 45 | [[:path {:stroke :none :fill :none :d "M0 0h24v24H0z"}] 46 | [:path {:d battery-path}] 47 | [:line {:x1 7 :y1 10 :x2 7 :y2 14}] 48 | [:line {:x1 10 :y1 10 :x2 10 :y2 14}]]) 49 | 50 | (deficon tabler.battery-high 51 | [[:path {:stroke :none :fill :none :d "M0 0h24v24H0z"}] 52 | [:path {:d battery-path}] 53 | [:line {:x1 7 :y1 10 :x2 7 :y2 14}] 54 | [:line {:x1 10 :y1 10 :x2 10 :y2 14}] 55 | [:line {:x1 13 :y1 10 :x2 13 :y2 14}]]) 56 | 57 | (deficon tabler.battery-full 58 | [[:path {:stroke :none :fill :none :d "M0 0h24v24H0z"}] 59 | [:path {:d battery-path}] 60 | [:line {:x1 7 :y1 10 :x2 7 :y2 14}] 61 | [:line {:x1 10 :y1 10 :x2 10 :y2 14}] 62 | [:line {:x1 13 :y1 10 :x2 13 :y2 14}] 63 | [:line {:x1 16 :y1 10 :x2 16 :y2 14}]]) 64 | 65 | (deficon tabler.battery-charging 66 | [[:path {:stroke :none :fill :none :d "M0 0h24v24H0z"}] 67 | [:path {:d (.. 68 | "M16 7h1a2 2 0 0 1 2 2v.5a0.5 .5 0 0 0 .5 .5a0.5 .5 0 0" 69 | " 1 .5 .5v3a0.5 .5 0 0 1 -.5 .5a0.5 .5 0 0 0 -.5 .5v.5a2" 70 | " 2 0 0 1 -2 2h-2")}] 71 | [:path {:d "M8 7h-2a2 2 0 0 0 -2 2v6a2 2 0 0 0 2 2h1"}] 72 | [:path {:d "M12 8l-2 4h3l-2 4"}]]) 73 | 74 | (deficon tabler.battery-disabled 75 | [[:path {:stroke :none :fill :none :d "M0 0h24v24H0z"}] 76 | [:path {:d (.. 77 | "M11 7h6a2 2 0 0 1 2 2v.5a0.5 .5 0 0 0 .5 .5a0.5 .5" 78 | " 0 0 1 .5 .5v3a0.5 .5 0 0 1 -.5 .5a0.5 .5 0 0 0" 79 | " -.5 .5v.5m-2 2h-11a2 2 0 0 1 -2 -2v-6a2 2 0 0 1 2 -2h1" 80 | )}] 81 | [:line {:x1 3 :y1 3 :x2 21 :y2 21}]]) 82 | 83 | (local speaker-path 84 | (.. "M6 15h-2a1 1 0 0 1 -1 -1v-4a1 1 0 0 1 1 -1h2l3.5 -4.5a0.8" 85 | " .8 0 0 1 1.5 .5v14a0.8 .8 0 0 1 -1.5 .5l-3.5 -4.5")) 86 | 87 | (deficon tabler.volume-low 88 | [[:path {:stroke :none :fill :none :d "M0 0h24v24H0z"}] 89 | [:path {:d speaker-path }] 90 | [:path {:d "M15 8a5 5 0 0 1 0 8"}]]) 91 | 92 | (deficon tabler.volume-high 93 | [[:path {:stroke :none :fill :none :d "M0 0h24v24H0z"}] 94 | [:path {:d speaker-path }] 95 | [:path {:d "M15 8a5 5 0 0 1 0 8"}] 96 | [:path {:d "M17.7 5a9 9 0 0 1 0 14"}]]) 97 | 98 | (deficon tabler.volume-muted 99 | [[:path {:stroke :none :fill :none :d "M0 0h24v24H0z"}] 100 | [:path {:d speaker-path }] 101 | [:path {:d "M16 10l4 4m0 -4l-4 4"}]]) 102 | 103 | (deficon tabler.wifi-low 104 | [[:path {:stroke :none :fill :none :d "M0 0h24v24H0z"}] 105 | [:path {:d "M9.172 15.172a4 4 0 0 1 5.656 0" }] 106 | [:line {:x1 12 :y1 18 :x2 12.01 :y2 18}]]) 107 | 108 | (deficon tabler.wifi-mid 109 | [[:path {:stroke :none :fill :none :d "M0 0h24v24H0z"}] 110 | [:path {:d "M9.172 15.172a4 4 0 0 1 5.656 0" }] 111 | [:path {:d "M6.343 12.343a8 8 0 0 1 11.314 0" }] 112 | [:line {:x1 12 :y1 18 :x2 12.01 :y2 18}]]) 113 | 114 | (deficon tabler.wifi-high 115 | [[:path {:stroke :none :fill :none :d "M0 0h24v24H0z"}] 116 | [:path {:d "M9.172 15.172a4 4 0 0 1 5.656 0" }] 117 | [:path {:d "M6.343 12.343a8 8 0 0 1 11.314 0" }] 118 | [:path {:d "M3.515 9.515c4.686 -4.687 12.284 -4.687 17 0" }] 119 | [:line {:x1 12 :y1 18 :x2 12.01 :y2 18}]]) 120 | 121 | (deficon tabler.wifi-disabled 122 | [[:path {:stroke :none :fill :none :d "M0 0h24v24H0z"}] 123 | [:path {:d "M9.172 15.172a4 4 0 0 1 5.656 0" }] 124 | [:path {:d "M6.343 12.343a7.963 7.963 0 0 1 3.864 -2.14m4.163 .155a7.965 7.965 0 0 1 3.287 2" }] 125 | [:path {:d "M3.515 9.515a12 12 0 0 1 3.544 -2.455m3.101 -.92a12 12 0 0 1 10.325 3.374" }] 126 | [:line {:x1 12 :y1 18 :x2 12.01 :y2 18}] 127 | [:line {:x1 3 :y1 3 :x2 21 :y2 21}]]) 128 | 129 | (deficon tabler.desktop 130 | [[:path {:stroke :none :fill :none :d "M0 0h24v24H0z"}] 131 | [:rect {:x 3 :y 4 :width 18 :height 12 :rx 1}] 132 | [:line {:x1 7 :y1 20 :x2 17 :y2 20}] 133 | [:line {:x1 9 :y1 16 :x2 9 :y2 20}] 134 | [:line {:x1 15 :y1 16 :x2 15 :y2 20}]]) 135 | 136 | tabler 137 | -------------------------------------------------------------------------------- /keybindings.fnl: -------------------------------------------------------------------------------- 1 | ;; Keyboard & Mouse bindings declarations -- all in one place 2 | (local awful (require "awful")) 3 | (local gears (require "gears")) 4 | (local hotkeys_popup (require "awful.hotkeys_popup")) 5 | (local lume (require "vendor.lume")) 6 | (local input (require "utils.input")) 7 | (local pa (require "utils.pulseaudio")) 8 | (local wincmd (require "commands.window")) 9 | (local tagcmd (require "commands.tag")) 10 | (local defhydra (require "features.hydra")) 11 | (local {: spawn-nrepl : kill-nrepl } (require :features.nrepl)) 12 | (local {: notify} (require :api.lawful)) 13 | (local titlebar (require :features.titlebar)) 14 | 15 | (local layouts awful.layout.suit) 16 | 17 | (local window-hydra 18 | (defhydra {:name "Windows 🐲" :take-counts true :key [[:mod] :w] } 19 | ["Movement" 20 | [:h wincmd.move-left "move left"] 21 | [:j wincmd.move-down "move down"] 22 | [:k wincmd.move-up "move up"] 23 | [:l wincmd.move-right "move right"] 24 | [:c wincmd.cycle-clockwise "cycle clockwise"] 25 | [:C wincmd.cycle-counter-clockwise "cycle counter-clockwise"] 26 | [:Left wincmd.transfer-to-prev-tag "to previous tag" {:exit true}] 27 | [:Right wincmd.transfer-to-next-tag "to next tag" {:exit true}]] 28 | ["Snap" 29 | [:H wincmd.snap-left "snap to left edge"] 30 | [:J wincmd.snap-bottom "snap to bottom edge"] 31 | [:K wincmd.snap-top "snap to top edge"] 32 | [:L wincmd.snap-right "snap to right edge"] 33 | [:Y wincmd.snap-top-left-corner "snap to top left"] 34 | [:U wincmd.snap-top-right-corner "snap to top right"] 35 | [:N wincmd.snap-bottom-left-corner "snap to bottom left"] 36 | [:B wincmd.snap-bottom-right-corner "snap to bottom right"]] 37 | ["State" 38 | [:m wincmd.minimize "minimize" {:exit true}] 39 | [:q wincmd.close "close" {:exit true}] 40 | [:space wincmd.toggle-floating "⏼ floating"] 41 | [:s wincmd.toggle-sticky "⏼ sticky"] 42 | [:f wincmd.toggle-fullscreen "⏼ fullscreen" {:exit true}]])) 43 | 44 | (local tag-hydra 45 | (defhydra {:name "Tags 🐲" :take-counts true :key [[:mod] :t] } 46 | ["Layout" 47 | [:space (tagcmd.layout-fn layouts.floating) "floating"] 48 | [:= (tagcmd.layout-fn layouts.fair) "fair"] 49 | [:/ (tagcmd.layout-fn layouts.fair.horizontal) "fairv"] 50 | [:t (tagcmd.layout-fn layouts.tile) "tile"] 51 | [:s (tagcmd.layout-fn layouts.spiral) "spiral"] 52 | [:S (tagcmd.layout-fn layouts.spiral.dwindle) "dwindle"] 53 | [:f (tagcmd.layout-fn layouts.max) "max"] 54 | [:F (tagcmd.layout-fn layouts.max.fullscreen) "fullscreen"] 55 | [:+ (tagcmd.layout-fn layouts.magnifier) "magnifier"]] 56 | ["Spacing" 57 | [:c tagcmd.inc-cols "add column"] 58 | [:C tagcmd.dec-cols "remove column"] 59 | [:m tagcmd.inc-masters "increase masters"] 60 | [:M tagcmd.dec-masters "decrease masters"] 61 | [:j tagcmd.dec-master-width "shrink master area"] 62 | [:k tagcmd.inc-master-width "grow master area"] 63 | [:p tagcmd.toggle-fill-policy "⏼ fill policy"] 64 | [:g tagcmd.inc-gap "increase gaps"] 65 | [:G tagcmd.dec-gap "decrease gaps"]] 66 | ["Transfer" 67 | [:q tagcmd.destroy-current "destroy" {:exit true}] 68 | [:h (fn [] (notify.msg "TODO")) "move to screen left"] 69 | [:l (fn [] (notify.msg "TODO")) "move to screen right"]])) 70 | 71 | (local 72 | bindings 73 | { 74 | :global-keys 75 | (gears.table.join 76 | 77 | (input.key-group 78 | "awesome" 79 | [[:mod] :s hotkeys_popup.show_help "show help"] 80 | [[:mod :shift] :r awesome.restart "reload config"] 81 | [[:mod :shift] :t titlebar.toggle "⏼ titlebars"] 82 | ) 83 | 84 | (input.key-group 85 | "exec" 86 | [[:mod] :space (fn [] (awful.util.spawn "rofi -show run")) "app launcher"] 87 | [[:mod :shift] :n (fn [] (spawn-nrepl)) "spawn nrepl"] 88 | [[:mod :shift] :z (fn [] (kill-nrepl)) "kill nrepl"] 89 | [[:mod] :e (fn [] (awful.util.spawn "emacsclient -c -n -e '(switch-to-buffer nil)'")) "emacs"] 90 | [[:mod] :v (fn [] 91 | (let [option "gopass ls --flat" 92 | prompt "rofi -dmenu -p pass" 93 | action "xargs --no-run-if-empty gopass show -c" 94 | cmd (.. option " | " prompt " | " action)] 95 | (awful.spawn.easy_async_with_shell cmd (fn [])))) 96 | "secret vault"] 97 | [[:mod] :Return (fn [] (awful.spawn "alacritty")) "open terminal"] 98 | [[] :XF86AudioMute pa.toggle-muted "⏼ audio muted"] 99 | [[] :XF86AudioMicMute pa.toggle-mic-mute "⏼ mic muted"] 100 | [[] :XF86AudioRaiseVolume (fn [] (pa.adjust-volume 5)) "raise volume"] 101 | [[] :XF86AudioLowerVolume (fn [] (pa.adjust-volume -5)) "lower volume"] 102 | ) 103 | 104 | (input.key-group 105 | "tags" 106 | [[:mod :shift] :l tagcmd.go-right "switch to next tag"] 107 | [[:mod :shift] :h tagcmd.go-left "switch to previous tag"] 108 | ) 109 | tag-hydra 110 | 111 | (input.key-group 112 | "client" 113 | [[:mod] :h (fn [] (awful.client.focus.bydirection :left)) "focus left"] 114 | [[:mod] :j (fn [] (awful.client.focus.bydirection :down)) "focus down"] 115 | [[:mod] :k (fn [] (awful.client.focus.bydirection :up)) "focus up"] 116 | [[:mod] :l (fn [] (awful.client.focus.bydirection :right)) "focus right"] 117 | [[:mod] :n (fn [] (awful.client.focus.byidx 1)) "focus next"] 118 | [[:mod] :p (fn [] (awful.client.focus.byidx -1)) "focus previous"] 119 | [[:mod :shift] :j (fn [] (awful.client.swap.byidx 1)) "swap next with previous"] 120 | [[:mod :shift] :k (fn [] (awful.client.swap.byidx -1)) "swap next with previous"] 121 | ) 122 | window-hydra 123 | ) 124 | 125 | 126 | :client-keys 127 | (gears.table.join 128 | (input.key-group 129 | "client" 130 | [[:mod] :m wincmd.minimize "minimize"] 131 | [[:mod] :t wincmd.toggle-ontop "⏼ keep on top"] 132 | [[:mod :shift] :M wincmd.toggle-maximized "⏼ maximize"] 133 | [[:mod] :f wincmd.toggle-fullscreen "⏼ fullscreen"] 134 | [[:mod :ctrl] :space awful.client.floating.toggle "⏼ floating"] 135 | [[:mod :ctrl] :Return wincmd.move-to-master "move to master"] 136 | [[:mod :ctrl] :m wincmd.toggle-maximize-vertical "⏼ max vertically"] 137 | [[:mod :shift] :m wincmd.toggle-maximize-horizontal "⏼ max horizontally"] 138 | [[:mod :shift] :c wincmd.close "close"] 139 | )) 140 | 141 | :client-buttons 142 | (gears.table.join 143 | (input.mousebind [] input.left-click client.mouse-raise) 144 | (input.mousebind [:mod] input.left-click client.mouse-drag-move) 145 | (input.mousebind [:mod] input.right-click client.mouse-drag-resize) 146 | ) 147 | }) 148 | 149 | bindings 150 | -------------------------------------------------------------------------------- /manifest.scm: -------------------------------------------------------------------------------- 1 | (use-modules (guix transformations)) 2 | 3 | (packages->manifest 4 | (list 5 | (specification->package "coreutils") 6 | (specification->package "make") 7 | (specification->package "lua@5.1") 8 | (specification->package "lua5.1-socket") 9 | (specification->package "lua5.1-lgi") 10 | 11 | ((options->transformation 12 | '((with-input . "lua=lua@5.1") 13 | (with-commit . "fennel=03c1c95f"))) 14 | (specification->package "fennel")) 15 | 16 | (specification->package "luajit") 17 | (specification->package "awesome-next-luajit") 18 | (specification->package "gobject-introspection") 19 | (specification->package "xorg-server") 20 | (specification->package "xterm"))) 21 | 22 | -------------------------------------------------------------------------------- /patches/fix-snid.lua: -------------------------------------------------------------------------------- 1 | local awful = require("awful") 2 | local blacklisted_snid = setmetatable({}, {__mode = "v" }) 3 | 4 | --- Make startup notification work for some clients like XTerm. This is ugly 5 | -- but works often enough to be useful. 6 | local function fix_startup_id(c) 7 | -- Prevent "broken" sub processes created by c to inherit its SNID 8 | if c.startup_id then 9 | blacklisted_snid[c.startup_id] = blacklisted_snid[c.startup_id] or c 10 | return 11 | end 12 | 13 | if not c.pid then return end 14 | 15 | -- Read the process environment variables 16 | local f = io.open("/proc/"..c.pid.."/environ", "rb") 17 | 18 | -- It will only work on Linux, that's already 99% of the userbase. 19 | if not f then return end 20 | 21 | local value = "%d+\0" 22 | 23 | local snid = f:read("*all"):match("STARTUP_ID=" .. value) 24 | f:close() 25 | 26 | -- If there is already a client using this SNID, it means it's either a 27 | -- subprocess or another window for the same process. While it makes sense 28 | -- in some case to apply the same rules, it is not always the case, so 29 | -- better doing nothing rather than something stupid. 30 | if blacklisted_snid[snid] then return end 31 | 32 | if snid then 33 | c.startup_id = snid 34 | blacklisted_snid[snid] = c 35 | end 36 | end 37 | 38 | awful.rules.add_rule_source( 39 | "snid", fix_startup_id, {}, {"awful.spawn", "awful.rules"} 40 | ) 41 | -------------------------------------------------------------------------------- /rc.lua: -------------------------------------------------------------------------------- 1 | -- add guix lua packages to package.path 2 | guixEnv = os.getenv("GUIX_ENVIRONMENT") 3 | if guixEnv then 4 | package.path = package.path .. ";" .. guixEnv .. "/share/lua/5.1/?.lua" .. 5 | ";" .. guixEnv .. "/share/lua/5.1/?/init.lua" .. 6 | ";" .. guixEnv .. "/lib/lua/5.1/?.lua" .. 7 | ";" .. guixEnv .. "/lib/lua/5.1/?/init.lua" 8 | 9 | package.cpath = package.cpath .. ";" .. guixEnv .. "/lib/lua/5.1/?.so" 10 | end 11 | 12 | cfgDir = os.getenv("HOME") .. "/.config/awesome/" 13 | 14 | -- add the vendor dir 15 | package.path = package.path .. ";" .. cfgDir .. "/vendor/?.lua" 16 | package.path = package.path .. ";" .. cfgDir .. "/vendor/?/?.lua" 17 | 18 | fennel = require("fennel") 19 | 20 | fennel.path = fennel.path .. ";" .. cfgDir .. "?.fnl;" .. cfgDir .. "?/init.fnl" 21 | 22 | searcher = fennel.makeSearcher({ 23 | correlate = true, 24 | useMetadata = true, 25 | -- disable strict checking. 26 | -- TODO: assemble a full list of globals so we can enable this 27 | allowedGlobals = false 28 | }) 29 | 30 | table.insert(package.loaders or package.searchers, searcher) 31 | debug.traceback = fennel.traceback 32 | require("init") -- load ~/.config/awesome/init.fnl 33 | -------------------------------------------------------------------------------- /rules.fnl: -------------------------------------------------------------------------------- 1 | (local awful (require "awful")) 2 | (local beautiful (require "beautiful")) 3 | (local keybindings (require "keybindings")) 4 | 5 | (local 6 | rules 7 | [ 8 | ;; All clients will match this rule. 9 | { 10 | :rule { } 11 | :properties { 12 | :border_width beautiful.border_width 13 | :border_color beautiful.border_normal 14 | :focus awful.client.focus.filter 15 | :raise true 16 | :size_hints_honor false 17 | :keys keybindings.client-keys 18 | :buttons keybindings.client-buttons 19 | :screen awful.screen.preferred 20 | } 21 | } 22 | 23 | ;; Floating clients. 24 | { 25 | :rule_any { 26 | :instance [ 27 | "DTA" ;; Firefox addon DownThemAll. 28 | "copyq" ;; Includes session name in class. 29 | "pinentry" 30 | ] 31 | :class [ 32 | "Arandr" 33 | "Blueman-manager" 34 | "Gpick" 35 | "Kruler" 36 | "MessageWin" ;; kalarm. 37 | "Sxiv" 38 | "Tor Browser" ;; Needs a fixed window size to avoid fingerprinting by screen size. 39 | "Wpa_gui" 40 | "veromix" 41 | "xtightvncviewer" 42 | ] 43 | ;; Note that the name property shown in xprop might be set slightly after creation of the client 44 | ;; and the name shown there might not match defined rules here. 45 | :name [ 46 | "Event Tester" ;; xev 47 | ] 48 | :role [ 49 | "AlarmWindow" ;; Thunderbird's calendar. 50 | "ConfigManager" ;; Thunderbird's about:config. 51 | "pop-up" ;; e.g. Google Chrome's (detached) Developer Tools. 52 | ] 53 | } 54 | :properties {:floating true }} 55 | 56 | ;; Add titlebars to normal clients and dialogs 57 | { 58 | :rule_any {:type [ "normal" "dialog" ] } 59 | :properties {:titlebars_enabled true } 60 | } 61 | 62 | { 63 | :rule_any {:role [ "PictureInPicture" ] } 64 | :properties { 65 | :placement awful.placement.bottom_right 66 | } 67 | } 68 | { 69 | :rule_any {:name ["*Minibuf-0*"]} 70 | :properties { 71 | :placement awful.placement.bottom_left 72 | :height 20 73 | :width 1920 74 | :floating true 75 | :honor_padding true 76 | :honor_workarea true 77 | :requests_no_titlebar true 78 | :titlebars_enabled false 79 | } 80 | } 81 | 82 | ;; Set Firefox to always map on the tag named "2" on screen 1. 83 | ;; { :rule { :class "Firefox" } 84 | ;; :properties { :screen 1 :tag "2" } } 85 | ]) 86 | 87 | rules 88 | -------------------------------------------------------------------------------- /test/hotfuzz.fnl: -------------------------------------------------------------------------------- 1 | (local t (require "vendor.lunatest")) 2 | (local pp (require "fennel.view")) 3 | (local hotfuzz (require "utils.hotfuzz")) 4 | (local lume (require "vendor.lume")) 5 | 6 | (local m {}) 7 | 8 | (fn m.test-seller-matrix [] 9 | (let [result-matrix (hotfuzz.init-seller-matrix 3 3) 10 | expected-matrix [[0 0 0] [1 0 0] [2 0 0]]] 11 | (t.assert_equal (pp expected-matrix) (pp result-matrix)))) 12 | 13 | (fn m.test-core-levenshtein [] 14 | (let [expect [[[0 0 0 0 0 0 0 0] [1 1 1 1 1 1 1 1] [2 2 1 2 2 1 2 2] [3 3 2 1 2 2 2 3] [4 4 3 2 1 2 3 3] [5 5 4 3 2 2 3 4] [6 6 5 4 3 3 2 3]]] 15 | inputs [["kitten" "sitting"]]] 16 | (each [i p (ipairs inputs)] 17 | (let [[term cand] p] 18 | (var mat (hotfuzz.init-seller-matrix (+ 1 (length term)) (+ 1 (length cand)))) 19 | (for [n 2 (+ 1 (length term))] 20 | (for [m 2 (+ 1 (length cand))] 21 | (hotfuzz.levenshtein-dist term cand mat n m))) 22 | (t.assert_equal (pp (. expect i)) (pp mat)))))) 23 | 24 | (fn m.test-init-search-trie [] 25 | (let [trie (hotfuzz.init-search-trie ["ha" "hat" "cat"]) 26 | expected {:candidates {} :children {:c {:candidates {} :children {:a {:candidates {} :children {:t {:candidates [{:index 3 :value "cat"}] :children {} :depth 0}} :depth 1}} :depth 2} :h {:candidates {} :children {:a {:candidates [{:index 1 :value "ha"}] :children {:t {:candidates [{:index 2 :value "hat"}] :children {} :depth 0}} :depth 1}} :depth 2}} :depth 3}] 27 | (t.assert_equal (pp expected) (pp trie)))) 28 | 29 | (fn m.test-compare-results [] 30 | (let [perfect {:score 1 :hit {:start 0} :length-diff 0 :index 0} 31 | good-score {:score .9 :hit {:start 3} :length-diff 3 :index 1} 32 | good-hit {:score .6 :hit {:start 0} :length-diff 3 :index 2} 33 | good-len {:score .6 :hit {:start 3} :length-diff 0 :index 3} 34 | awful {:score .6 :hit {:start 3} :length-diff 3 :index 4} 35 | even-worse {:score .6 :hit {:start 3} :length-diff 3 :index 5}] 36 | (t.assert_lt 0 (hotfuzz.compare-results perfect good-score) "perfect match") 37 | (t.assert_gt 0 (hotfuzz.compare-results good-score perfect) "can't beat perfect") 38 | (t.assert_lt 0 (hotfuzz.compare-results good-score good-hit) "score > hit") 39 | (t.assert_lt 0 (hotfuzz.compare-results good-hit good-len) "hit > length") 40 | (t.assert_lt 0 (hotfuzz.compare-results good-len awful) "length > nothing") 41 | (t.assert_lt 0 (hotfuzz.compare-results awful even-worse) "index final deciding factor"))) 42 | 43 | (fn m.test-add-result [] 44 | (let [candidate {:value "cat" :index 1} 45 | hit {:start 0} 46 | score 1 47 | len 2 48 | expected {:hit hit :length-diff len :score score :value candidate.value :index candidate.index}] 49 | (var results []) 50 | (var result-map {}) 51 | (hotfuzz.add-result results result-map candidate score hit len) 52 | (t.assert_equal 1 (length results)) 53 | (t.assert_equal 1 (length result-map)) 54 | (t.assert_equal (pp [expected]) (pp results)))) 55 | 56 | (fn m.test-search-order [] 57 | "search should return values in expected order (perfect match first)" 58 | (let [search-candidates ["rat" "matt" "cat" "catepillar" "bobcat"] 59 | search-results (. (hotfuzz.search search-candidates "cat") :results) 60 | expected-order ["cat" "catepillar" "bobcat" "rat" "matt"] 61 | resulting-order (lume.map search-results (fn [x] x.value))] 62 | (t.assert_equal (pp expected-order) (pp resulting-order)))) 63 | 64 | (fn m.test-search-threshold [] 65 | "candidate `space` should be excluded from results for scoring too low" 66 | (let [search-candidates ["moon" "moonman" "moonpie" "space"] 67 | search-results (hotfuzz.search search-candidates "moo")] 68 | (t.assert_equal 3 (length search-results.results)))) 69 | 70 | (fn m.test-search-hit [] 71 | "result should return a hit that gives offset and length of the match" 72 | (let [search-candidates ["witch's broom" "moon-witch"] 73 | results (. (hotfuzz.search search-candidates "witch") :results) 74 | broom-hit (-> results (lume.filter (fn [x] (= x.value "witch's broom"))) (lume.first)) 75 | moon-hit (-> results (lume.filter (fn [x] (= x.value "moon-witch"))) (lume.first))] 76 | (t.assert_equal "witch" (string.sub "moon-witch" moon-hit.hit.start moon-hit.hit.end)) 77 | (t.assert_equal "witch" (string.sub "witch's broom" broom-hit.hit.start broom-hit.hit.end)))) 78 | 79 | (fn m.test-search-hit-single-char [] 80 | "result should return a hit that gives offset and length of the match, no off-by-one errors" 81 | (let [search-candidates ["awesome" "something" "default" "set" "ime" "ton"] 82 | results (. (hotfuzz.search search-candidates "some" {:threshold 0.0}) :results) 83 | awesome-hit (-> results (lume.filter (fn [x] (= x.value "awesome"))) (lume.first)) 84 | set-hit (-> results (lume.filter (fn [x] (= x.value "set"))) (lume.first)) 85 | ime-hit (-> results (lume.filter (fn [x] (= x.value "ime"))) (lume.first)) 86 | ton-hit (-> results (lume.filter (fn [x] (= x.value "ton"))) (lume.first)) 87 | something-hit (-> results (lume.filter (fn [x] (= x.value "something"))) (lume.first)) 88 | default-hit (-> results (lume.filter (fn [x] (= x.value "default"))) (lume.first))] 89 | (t.assert_equal "some" (string.sub "something" something-hit.hit.start something-hit.hit.end)) 90 | (t.assert_equal "some" (string.sub "awesome" awesome-hit.hit.start awesome-hit.hit.end)) 91 | (t.assert_equal "me" (string.sub "ime" ime-hit.hit.start ime-hit.hit.end)) 92 | (t.assert_equal "e" (string.sub "set" set-hit.hit.start set-hit.hit.end)) 93 | (t.assert_equal "o" (string.sub "ton" ton-hit.hit.start ton-hit.hit.end)) 94 | (t.assert_equal "e" (string.sub "default" default-hit.hit.start default-hit.hit.end)))) 95 | 96 | m 97 | -------------------------------------------------------------------------------- /test/icons.fnl: -------------------------------------------------------------------------------- 1 | (local t (require "vendor.lunatest")) 2 | (local icon-loader (require "icons.loader")) 3 | (local tabler-icons (require "icons.tabler")) 4 | 5 | (local m {}) 6 | 7 | (fn m.test-can-load-icon [] 8 | (let [tabler-grid (icon-loader.load :tabler :grid {:viewBox "0 0 24 24"})] 9 | ; can we load an icon without getting exceptions? 10 | (t.assert_equal 1 1))) 11 | 12 | m 13 | -------------------------------------------------------------------------------- /test/init.fnl: -------------------------------------------------------------------------------- 1 | (local t (require "vendor.lunatest")) 2 | 3 | ;(t.suite :test.icons) 4 | (t.suite :test.hotfuzz) 5 | (t.suite :test.xml) 6 | 7 | (t.run) 8 | -------------------------------------------------------------------------------- /test/xml.fnl: -------------------------------------------------------------------------------- 1 | (local t (require "vendor.lunatest")) 2 | (local pp (require "fennel.view")) 3 | (local xml (require "utils.xml")) 4 | (local lume (require "vendor.lume")) 5 | 6 | (local m {}) 7 | 8 | (fn m.test-create-empty-element [] 9 | (let [result (xml.create-element :span)] 10 | (t.assert_equal "" result))) 11 | 12 | (fn m.test-create-simple-element [] 13 | (let [result (xml.create-element :span {} "hello world")] 14 | (t.assert_equal "hello world" result))) 15 | 16 | (fn m.test-create-multiple-child-element [] 17 | (let [result (xml.create-element :span {} "rain" "bow")] 18 | (t.assert_equal "rainbow" result))) 19 | 20 | (fn m.test-create-element-with-attributes [] 21 | (let [result (xml.create-element :span {:foreground :red} "hello world")] 22 | (t.assert_equal "hello world" result))) 23 | 24 | (fn m.test-create-nested-element [] 25 | (let [result (xml.create-element 26 | :span {:text :big} 27 | [[:div {} "l. 1"] 28 | [:span {} [:div {} "l. 2"]] 29 | [:span {} [[:div {} "l. 3a"] 30 | [:span {} "l. 3b"]]]])] 31 | (t.assert_equal 32 | "
l. 1
l. 2
l. 3a
l. 3b
" 33 | result))) 34 | 35 | m 36 | -------------------------------------------------------------------------------- /themes/dracula/titlebar/close_focus.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 39 | 41 | 42 | 44 | image/svg+xml 45 | 47 | 48 | 49 | 50 | 51 | 55 | 60 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /themes/dracula/titlebar/close_focus_hover.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 39 | 41 | 42 | 44 | image/svg+xml 45 | 47 | 48 | 49 | 50 | 51 | 55 | 60 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /themes/dracula/titlebar/close_normal.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 39 | 41 | 42 | 44 | image/svg+xml 45 | 47 | 48 | 49 | 50 | 51 | 55 | 60 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /themes/dracula/titlebar/close_normal_hover.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 39 | 41 | 42 | 44 | image/svg+xml 45 | 47 | 48 | 49 | 50 | 51 | 55 | 60 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /themes/dracula/titlebar/floating_focus_active.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 39 | 41 | 42 | 44 | image/svg+xml 45 | 47 | 48 | 49 | 50 | 51 | 56 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /themes/dracula/titlebar/floating_focus_active_hover.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 39 | 41 | 42 | 44 | image/svg+xml 45 | 47 | 48 | 49 | 50 | 51 | 56 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /themes/dracula/titlebar/floating_focus_inactive.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 39 | 41 | 42 | 44 | image/svg+xml 45 | 47 | 48 | 49 | 50 | 51 | 56 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /themes/dracula/titlebar/floating_focus_inactive_hover.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 39 | 41 | 42 | 44 | image/svg+xml 45 | 47 | 48 | 49 | 50 | 51 | 56 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /themes/dracula/titlebar/floating_normal_active.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 39 | 41 | 42 | 44 | image/svg+xml 45 | 47 | 48 | 49 | 50 | 51 | 56 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /themes/dracula/titlebar/floating_normal_active_hover.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 39 | 41 | 42 | 44 | image/svg+xml 45 | 47 | 48 | 49 | 50 | 51 | 56 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /themes/dracula/titlebar/floating_normal_inactive.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 39 | 41 | 42 | 44 | image/svg+xml 45 | 47 | 48 | 49 | 50 | 51 | 56 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /themes/dracula/titlebar/floating_normal_inactive_hover.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 39 | 41 | 42 | 44 | image/svg+xml 45 | 47 | 48 | 49 | 50 | 51 | 56 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /themes/dracula/titlebar/maximized_focus_active.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 39 | 41 | 42 | 44 | image/svg+xml 45 | 47 | 48 | 49 | 50 | 51 | 56 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /themes/dracula/titlebar/maximized_focus_active_hover.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 39 | 41 | 42 | 44 | image/svg+xml 45 | 47 | 48 | 49 | 50 | 51 | 56 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /themes/dracula/titlebar/maximized_focus_inactive.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 39 | 41 | 42 | 44 | image/svg+xml 45 | 47 | 48 | 49 | 50 | 51 | 56 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /themes/dracula/titlebar/maximized_focus_inactive_hover.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 39 | 41 | 42 | 44 | image/svg+xml 45 | 47 | 48 | 49 | 50 | 51 | 56 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /themes/dracula/titlebar/maximized_normal_active.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 39 | 41 | 42 | 44 | image/svg+xml 45 | 47 | 48 | 49 | 50 | 51 | 56 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /themes/dracula/titlebar/maximized_normal_active_hover.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 39 | 41 | 42 | 44 | image/svg+xml 45 | 47 | 48 | 49 | 50 | 51 | 56 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /themes/dracula/titlebar/maximized_normal_inactive.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 39 | 41 | 42 | 44 | image/svg+xml 45 | 47 | 48 | 49 | 50 | 51 | 56 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /themes/dracula/titlebar/maximized_normal_inactive_hover.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 39 | 41 | 42 | 44 | image/svg+xml 45 | 47 | 48 | 49 | 50 | 51 | 56 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /themes/dracula/titlebar/minimize_focus.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 39 | 41 | 42 | 44 | image/svg+xml 45 | 47 | 48 | 49 | 50 | 51 | 56 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /themes/dracula/titlebar/minimize_focus_hover.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 39 | 41 | 42 | 44 | image/svg+xml 45 | 47 | 48 | 49 | 50 | 51 | 56 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /themes/dracula/titlebar/minimize_normal.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 39 | 41 | 42 | 44 | image/svg+xml 45 | 47 | 48 | 49 | 50 | 51 | 56 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /themes/dracula/titlebar/minimize_normal_hover.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 39 | 41 | 42 | 44 | image/svg+xml 45 | 47 | 48 | 49 | 50 | 51 | 56 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /themes/dracula/titlebar/ontop_focus_active.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 39 | 41 | 42 | 44 | image/svg+xml 45 | 47 | 48 | 49 | 50 | 51 | 55 | 60 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /themes/dracula/titlebar/ontop_focus_active_hover.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 39 | 41 | 42 | 44 | image/svg+xml 45 | 47 | 48 | 49 | 50 | 51 | 55 | 60 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /themes/dracula/titlebar/ontop_focus_inactive.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 39 | 41 | 42 | 44 | image/svg+xml 45 | 47 | 48 | 49 | 50 | 51 | 55 | 60 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /themes/dracula/titlebar/ontop_focus_inactive_hover.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 39 | 41 | 42 | 44 | image/svg+xml 45 | 47 | 48 | 49 | 50 | 51 | 55 | 60 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /themes/dracula/titlebar/ontop_normal_active.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 39 | 41 | 42 | 44 | image/svg+xml 45 | 47 | 48 | 49 | 50 | 51 | 55 | 60 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /themes/dracula/titlebar/ontop_normal_active_hover.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 39 | 41 | 42 | 44 | image/svg+xml 45 | 47 | 48 | 49 | 50 | 51 | 55 | 60 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /themes/dracula/titlebar/ontop_normal_inactive.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 39 | 41 | 42 | 44 | image/svg+xml 45 | 47 | 48 | 49 | 50 | 51 | 55 | 60 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /themes/dracula/titlebar/ontop_normal_inactive_hover.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 39 | 41 | 42 | 44 | image/svg+xml 45 | 47 | 48 | 49 | 50 | 51 | 55 | 60 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /themes/dracula/titlebar/sticky_focus_active.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 20 | 22 | 25 | 29 | 30 | 31 | 49 | 51 | 52 | 54 | image/svg+xml 55 | 57 | 58 | 59 | 60 | 61 | 66 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /themes/dracula/titlebar/sticky_focus_active_hover.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 20 | 22 | 25 | 29 | 30 | 31 | 49 | 51 | 52 | 54 | image/svg+xml 55 | 57 | 58 | 59 | 60 | 61 | 66 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /themes/dracula/titlebar/sticky_focus_inactive.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 20 | 22 | 25 | 29 | 30 | 31 | 49 | 51 | 52 | 54 | image/svg+xml 55 | 57 | 58 | 59 | 60 | 61 | 66 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /themes/dracula/titlebar/sticky_focus_inactive_hover.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 20 | 22 | 25 | 29 | 30 | 31 | 49 | 51 | 52 | 54 | image/svg+xml 55 | 57 | 58 | 59 | 60 | 61 | 66 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /themes/dracula/titlebar/sticky_normal_active.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 20 | 22 | 25 | 29 | 30 | 31 | 49 | 51 | 52 | 54 | image/svg+xml 55 | 57 | 58 | 59 | 60 | 61 | 66 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /themes/dracula/titlebar/sticky_normal_active_hover.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 20 | 22 | 25 | 29 | 30 | 31 | 49 | 51 | 52 | 54 | image/svg+xml 55 | 57 | 58 | 59 | 60 | 61 | 66 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /themes/dracula/titlebar/sticky_normal_inactive.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 20 | 22 | 25 | 29 | 30 | 31 | 49 | 51 | 52 | 54 | image/svg+xml 55 | 57 | 58 | 59 | 60 | 61 | 66 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /themes/dracula/titlebar/sticky_normal_inactive_hover.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 20 | 22 | 25 | 29 | 30 | 31 | 49 | 51 | 52 | 54 | image/svg+xml 55 | 57 | 58 | 59 | 60 | 61 | 66 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /todo.md: -------------------------------------------------------------------------------- 1 | * ~~wmii style tag management~~ 2 | - ~~dynamic~~ 3 | - ~~volatile~~ 4 | - ~~automatically create new tag when going -> past existing tags~~ 5 | * ~~tag hydra~~ 6 | - ~~manage properties like layout, gaps, columns, master width factor~~ 7 | * workspaces 8 | - ~~hide tags not part of current workspace~~ 9 | - spawn programs like Firefox and chrome differently when in a workspace (profiles) 10 | - improve workspace switcher 11 | - ~~persistence~~ 12 | - ~~identicons for each workspace~~ 13 | - ~~persist previously used workspace names in the perma.json file~~ 14 | * ~~sticky clients/familiars~~ 15 | - ~~fix layout issues (want to dictate via :direction and :fill-percentage)~~ 16 | - ~~prevent titlebars~~ 17 | - ~~persist info about familiars in state.json~~ 18 | - ~~slide-in terminal for one-off commands~~ 19 | - ~~apps like spotify, teams, a browser~~ 20 | - ~~persistent, workspace-agnostic tag for these?~~ sticky works across workspaces 21 | - ~~autofocus on open~~ 22 | - ~~always on top~~ 23 | * notification center 24 | - inspiration 25 | - https://www.reddit.com/r/unixporn/comments/dwiyca/awesome_notification_center/ 26 | * daemons 27 | - ~~battery~~ 28 | - ~~cpu~~ 29 | - ~~memory~~ 30 | - network status 31 | - ~~pulseaudio status (vol, sinks?)~~ 32 | * a e s t h e t i c s (especially the bar omg ew) 33 | - ~~svg handling~~ 34 | - ~~loader~~ 35 | - ~~identicons~~ 36 | - ~~deficon macro~~ 37 | - ~~caching ?~~ 38 | - more svgs 39 | - ~~popups need to constrain to screen edges~~ 40 | - ~~maybe use the default placement functions or steal their logic?~~ 41 | - inspiration 42 | - https://www.reddit.com/r/unixporn/comments/atkn5b/awesome_skyfall/ 43 | - https://www.reddit.com/r/unixporn/comments/caiad9/awesomewm_nighty_nordic/ 44 | - https://www.youtube.com/watch?v=_1M1Wv64JGA 45 | - https://www.reddit.com/r/unixporn/comments/bgfeoh/bspwm_dracula_all_the_things/ 46 | - https://www.reddit.com/r/unixporn/comments/dnsiiq/bspwm_dracula_revamped_for_a_newer_laptop/ 47 | * ~~password integration (gopass + rofi, need some keyring integration i guess?)~~ 48 | * screenshot shortcut 49 | - just select an area and copy to clipboard 50 | - optionally save to a dir 51 | * ssh bastion-style prompt (via rofi ?) 52 | * ~~rofi emoji picker 🍆 (used rofimoji)~~ 53 | * screen setup scripts/ui ? 54 | - move focus between screens implicitly (requires getting physical scr order) 55 | - allow moving clients between tags in a similar way 56 | * script for bringing a random window into current tag via rofi 57 | - see https://github.com/elenapan/dotfiles/blob/997650801a7f6a60b1c1753f40e5f610669a1b2c/config/awesome/helpers.lua#L104 58 | - yoink and yeet 59 | * pulseaudio ui/config ? 60 | - single output device across all streams pls 61 | - make WF1000MX3 stop connecting as a damm headset 62 | - priority setting? Bluetooth > HDMI > Analog > everything else ? 63 | * lockscreen (xsecurelock?) 64 | * emacs org and calendar integration? 65 | - emacs org capture keybind? 66 | * toggle-able - keypress echo widget 67 | * more tag movement commands (between screens, merge tags, swap tags) ? 68 | * figure out how I want floating/tiling clients inside floating/tiling layouts to work 69 | 70 | * consider using machi, or making something similar (particularly draft mode) 71 | - https://github.com/xinhaoyuan/layout-machi 72 | 73 | * virt stuff? 74 | - https://github.com/virt-lightning/virt-lightning 75 | 76 | $dracula-superdark:e#191A21 77 | $dracula-background: #282a36; 78 | $dracula-current-line: #44475a; 79 | $dracula-selection: #44475a; 80 | $dracula-foreground: #f8f8f2; 81 | $dracula-comment: #6272a4; 82 | $dracula-cyan: #8be9fd; 83 | $dracula-green: #50fa7b; 84 | $dracula-orange: #ffb86c; 85 | $dracula-pink: #ff79c6; 86 | $dracula-purple: #bd93f9; 87 | $dracula-red: #ff5555; 88 | $dracula-yellow: #f1fa8c; 89 | -------------------------------------------------------------------------------- /user.fnl: -------------------------------------------------------------------------------- 1 | (local icon-loader (require "icons.loader")) 2 | 3 | (local user {}) 4 | 5 | (set user.avatar 6 | (icon-loader.load :shtwzrd :avatar8bit { 7 | :viewBox "0 -0.5 8 8" 8 | :height 8 9 | :width 8 10 | :stroke-width 1 11 | :stroke-linecap :none 12 | :stroke-linejoin :none 13 | :shape-rendering :crispEdges})) 14 | 15 | user 16 | -------------------------------------------------------------------------------- /utils/async.fnl: -------------------------------------------------------------------------------- 1 | (fn async [...] 2 | "Transform inclosed expressions into a self-executing coroutine." 3 | `(if (coroutine.running) ; only create the outermost coro, no nesting 4 | (do ,... ) 5 | (do ((coroutine.wrap (fn [] ,... )))))) 6 | 7 | (fn make-async [func] 8 | "Transform function FUNC with last arg as a callback into a function 9 | which utilizes coroutines. 10 | It will yield the current coroutine, and resume it when FUNC returns. 11 | Must be called within a coroutine." 12 | `(fn [...] 13 | (when (not= (type ,func) "function") 14 | (error (.. "await: Expected function, got " (type ,func)))) 15 | (var co# nil) 16 | (let [len# (+ (select "#" ...) 1) ;; callback is left nil, so length++ 17 | arg# [...] 18 | tv# (type (. arg# len#))] 19 | (when (not= tv# (type nil)) 20 | (error (.. "await: Last arg (callback) should be nil, but got " tv#))) 21 | (tset arg# len# (fn [...] 22 | (if (= co# nil) 23 | (set co# [...]) 24 | (coroutine.resume co# [...]) 25 | [...]))) 26 | (,func (unpack arg# 1 len#)) 27 | (if (= co# nil) 28 | (do 29 | (set co# (coroutine.running)) 30 | (coroutine.yield)) 31 | (unpack co#))))) 32 | 33 | (fn await [func ...] 34 | "Transform function FUNC with last arg as a callback into a coroutine. 35 | Yield the current coroutine, which will get resumed when FUNC returns. 36 | Must be called within a coroutine, or from within an `async` block." 37 | (let [af# (make-async func)] 38 | `(,af# ,...))) 39 | 40 | {:async async 41 | :make-async make-async 42 | :await await} 43 | -------------------------------------------------------------------------------- /utils/hotfuzz.fnl: -------------------------------------------------------------------------------- 1 | (local pp (require "fennel.view")) 2 | (local lume (require "vendor.lume")) 3 | (local {: concat : range : fill } (require "utils.oleander")) 4 | (local m {}) 5 | 6 | (fn m.init-seller-matrix [n m] 7 | "Return an initialized N-by-M matrix for doing Seller's algorithm." 8 | (var matrix []) 9 | (for [i 1 n] 10 | (tset matrix i (fill 1 m 0)) 11 | (tset (. matrix i) 1 (- i 1))) 12 | matrix) 13 | 14 | (fn m.matrix-pp [matrix] 15 | "Print MATRIX out in rows and cols typical for displaying edit distance calcs" 16 | (for [i 1 (length matrix)] 17 | (for [j 1 (length (. matrix 1))] 18 | (io.write (. (. matrix i) j) " ")) 19 | (io.write "\n"))) 20 | 21 | (fn m.levenshtein-dist [str-a str-b matrix i j] 22 | "Core levenshtein distance function. 23 | Write the cost at position (I,J) in the given MATRIX for STR-A and STR-B" 24 | (let [sub-cost (if (= (str-a:byte (- i 1)) (str-b:byte (- j 1))) 0 1) 25 | del-dist (+ (. (. matrix (- i 1)) j) 1); deletion 26 | ins-dist (+ (. (. matrix i) (- j 1)) 1); insertion 27 | sub-dist (+ (. (. matrix (- i 1)) (- j 1)) sub-cost) 28 | dist (math.min del-dist ins-dist sub-dist)]; substitution 29 | (tset (. matrix i) j dist)) 30 | matrix) 31 | 32 | (fn m.levenshtein-col [str-a str-b matrix j] 33 | "Calculate the levenshtein distance between STR-A and STR-B on column J" 34 | (let [len-a (string.len str-a)] 35 | (for [i 2 (+ 1 len-a) 1] 36 | (m.levenshtein-dist str-a str-b matrix i j)) 37 | matrix)) 38 | 39 | (lambda m.trie-insert [trie text item] 40 | (var walker trie) 41 | (for [i 1 (length text)] 42 | (let [c (text:sub i i)] 43 | (when (not (. walker.children c)) 44 | (tset walker.children c {:children {} :candidates [] :depth 0})) 45 | (set walker.depth (math.max walker.depth (- (length text) (- i 1)))) 46 | (set walker (. walker.children c)))) 47 | (table.insert walker.candidates item)) 48 | 49 | (lambda m.init-search-trie [items ?trie] 50 | (var t (or ?trie {:candidates [] :children {} :depth 0})) 51 | (each [i c (ipairs items)] 52 | (m.trie-insert t c {:index i :value c})) 53 | t) 54 | 55 | (fn m.compare-results [a b] 56 | "Compare A to B and return a numeric score. 57 | If negative, A is better. If positive, B is better." 58 | (let [non-zero (fn [a] (~= a 0))] 59 | (if (non-zero (- b.score a.score)) ; score-diff 60 | (- b.score a.score) 61 | (non-zero (- a.hit.start b.hit.start)) ; earlier-match 62 | (- a.hit.start b.hit.start) 63 | (non-zero (- a.length-diff b.length-diff)) ; closer-length 64 | (- a.length-diff b.length-diff) 65 | (- a.index b.index)))) ; if nothing else, earlier insertion order wins 66 | 67 | (fn m.result-comparator [a b] 68 | "Compare A to B and return a boolean value. 69 | If true, A is better. If false, B is better." 70 | (> 0 (m.compare-results a b))) 71 | 72 | (fn m.add-result [results result-map candidate score hit len] 73 | "Add CANDIDATE with SCORE, HIT and LEN to RESULTS and RESULT-MAP in a 74 | normalized way." 75 | (local item {:value candidate.value 76 | :score score 77 | :hit hit 78 | :length-diff len 79 | :index candidate.index}) 80 | (if (= nil (. result-map candidate.index)) 81 | (do 82 | (tset result-map candidate.index (length results)) 83 | (table.insert results item)) 84 | (< (m.compare-results item (. results (. result-map candidate.index))) 0) 85 | (tset results (. result-map candidate.index) item))) 86 | 87 | (fn m.should-continue [node term opts score-value end-value] 88 | (let [best-potential (math.min score-value (- end-value (+ 1 node.depth)))] 89 | (>= (- 1 (/ best-potential (length term))) opts.threshold))) 90 | 91 | (fn m.back-track [matrix score-idx] 92 | "Walk back up MATRIX, returning the hit start and end from SCORE-IDX" 93 | (if (= 0 score-idx) 94 | {:start 0 :end 0} 95 | (do 96 | (var start score-idx) 97 | (for [i (- (length matrix) 2) 1 -1] 98 | (let [row (. matrix i)] 99 | (when (> start 1) 100 | (when (>= (. row start) (. row (- start 1))) 101 | (set start (- start 1)))))) 102 | {:start (if (= start 1) 2 (- start 1)) 103 | :end (- score-idx 1)}))) 104 | 105 | (fn m.search-recursively [trie term matrix results result-map opts] 106 | (var stack []) 107 | (each [c child (pairs trie.children)] 108 | (table.insert stack [child 2 c 0 (length term)])) 109 | 110 | (var acc []) 111 | (while (> (length stack) 0) 112 | (do 113 | (var [node len char start-idx start-value] (table.remove stack)) 114 | (tset acc (- len 1) char) 115 | (while (> (length acc) (- len 1)) 116 | (table.remove acc)) 117 | 118 | (m.levenshtein-col term (table.concat acc) matrix len) 119 | 120 | ;; update best score and position 121 | (local end-idx len) 122 | (local end-value (-> matrix (. (length matrix) (. end-idx)))) 123 | (var [score-idx score-value] [start-idx start-value]) 124 | (when (< end-value start-value) 125 | (do 126 | (set score-idx end-idx) 127 | (set score-value end-value))) 128 | 129 | ;; populate result 130 | (when (> (length node.candidates) 0) 131 | (do 132 | (let [l (length term) 133 | score (- 1.0 (/ score-value l))] 134 | (when (>= score opts.threshold) 135 | (do 136 | (let [hit (m.back-track matrix score-idx) 137 | ldiff (math.abs (- len (length term)))] 138 | (each [_ cand (pairs node.candidates)] 139 | (m.add-result results result-map cand score hit ldiff)))))))) 140 | 141 | ;; push children onto stack to keep iterating 142 | (each [c child (pairs node.children)] 143 | (when (m.should-continue child term opts score-value end-value) 144 | (let [frame [child (+ 1 len) c score-idx score-value]] 145 | (table.insert stack frame))))))) 146 | 147 | (lambda m.search [items term ?opts ?trie] 148 | "Fuzzy-search ITEMS for TERM with given options ?OPTS. 149 | Returns a table with keys: 150 | :results -- sequential table of items with keys: 151 | :hit -- {:start :end} indices for matching portion of string 152 | :value -- provided text from ITEMS string table 153 | :index -- results's index in ITEMS table 154 | :score -- value from 0..1 scoring how close the result matches TERM 155 | :trie -- the prefix-tree generated for performing the search 156 | 157 | Possible ?OPTS: 158 | :threshold -- score (0..1) below which we discard results 159 | 160 | If calling multiple times with same ITEMS, cache :trie and pass it as ?TRIE to 161 | avoid recalculating the entire prefix-tree." 162 | (let [trie (or ?trie (m.init-search-trie items)) 163 | defaults {:threshold .6} 164 | rows (+ (length term) 1) 165 | cols (+ trie.depth 1) 166 | matrix (m.init-seller-matrix rows cols) 167 | opts (concat defaults (or ?opts {}))] 168 | (var results []) 169 | (var result-map {}) 170 | (when (or (>= opts.threshold 0) 171 | (= (length term) 0)) 172 | (each [i cand (pairs trie.candidates)] 173 | (m.add-result 174 | results 175 | result-map 176 | cand 177 | 0 178 | {:start 1 :end (length cand)} 179 | (length term)))) 180 | 181 | (m.search-recursively trie term matrix results result-map opts) 182 | 183 | {:trie trie 184 | :results (lume.sort results m.result-comparator)})) 185 | 186 | m 187 | -------------------------------------------------------------------------------- /utils/identicon.fnl: -------------------------------------------------------------------------------- 1 | (local lawful (require "api.lawful")) 2 | (local xresources (require "beautiful.xresources")) 3 | (local dpi xresources.apply_dpi) 4 | (local lume (require "vendor.lume")) 5 | (local xml (require "utils.xml")) 6 | (local oleander (require "utils.oleander")) 7 | (local range oleander.range) 8 | (local bsd-checksum oleander.bsd-checksum) 9 | 10 | (local identicon {}) 11 | 12 | (fn identicon.color-from-hash [hash] 13 | (math.randomseed hash) 14 | (let [r (math.random 0 255) 15 | g (math.random 0 255) 16 | b (math.random 0 255)] 17 | (.. "rgb(" r ", " g ", " b ")"))) 18 | 19 | (lambda generate-squares [hash squares square-length color] 20 | "Generate squares in a SQUARESxSQUARES grid" 21 | (var seq []) 22 | (math.randomseed hash) 23 | (let [mirror (+ (/ squares 2) 1) 24 | l square-length] 25 | (for [i 0 (- mirror 1)] 26 | (for [j 0 (- squares 1)] 27 | (when (<= 0.5 (math.random)) 28 | (let [x (* i l) 29 | y (* j l) 30 | x2 (* (- squares i 1) l)] ; x position reflected over center 31 | (table.insert 32 | seq 33 | [:rect {:x x :y y :width l :height l :fill color}]) 34 | (when (~= i (- mirror 1)) ; no need to reflect the center column 35 | (table.insert 36 | seq 37 | [:rect {:x x2 :y y :width l :height l :fill color}]))))))) 38 | seq) 39 | 40 | (lambda identicon.generate-svg 41 | [str squares dimension ?color-list?] 42 | "Get SVG string for an identicon using seed STR. 43 | 44 | Use SQUARES to determine the resolution of the identicon (must be odd). 45 | Use DIMENSION as a size hint. 46 | If ?COLOR-LIST? is provided, restrict output to that palette" 47 | (let [hash (bsd-checksum str) 48 | size (/ dimension squares) 49 | canvas (* squares size) 50 | mirror (+ (/ squares 2) 1) 51 | color (if ?color-list? 52 | (. ?color-list? (+ (% hash (length ?color-list?)) 1)) 53 | (identicon.color-from-hash hash))] 54 | (xml.create-element 55 | :svg {:xmlns "http://www.w3.org/2000/svg" 56 | :shape-rendering :crispEdges ; turns off anti-aliasing 57 | :viewBox (.. "0 0 " canvas " " canvas)} 58 | (lume.concat [[:rect {:x 0 :y 0 59 | :width canvas :height canvas 60 | :stroke :none :fill :none}]] 61 | (generate-squares hash squares size color))))) 62 | 63 | (lambda identicon.create 64 | [str squares dimension ?color-list?] 65 | "Generate an identicon and return the filepath to the resulting SVG file. 66 | 67 | Use SQUARES to determine the resolution of the identicon (must be odd). 68 | Use DIMENSION as a size hint. 69 | If ?COLOR-LIST? is provided, restrict output to that palette" 70 | (let [filename (.. str ".svg") 71 | filepath (.. (lawful.fs.cache-dir) filename) 72 | exists (io.open filepath :r)] 73 | (if (= exists nil) 74 | (with-open [outfile (io.open filepath :w)] 75 | (let [svg (identicon.generate-svg str squares dimension ?color-list?)] 76 | (outfile:write svg))) 77 | (exists:close)) 78 | (lawful.img.load-svg filepath dimension dimension))) 79 | 80 | identicon 81 | -------------------------------------------------------------------------------- /utils/input.fnl: -------------------------------------------------------------------------------- 1 | (local awful (require "awful")) 2 | (local lume (require "vendor.lume")) 3 | (local unpack (or table.unpack _G.unpack)) 4 | 5 | (var input {}) 6 | 7 | ;; X11 Mouse Buttons 8 | (set input.left-click 1) 9 | (set input.middle-click 2) 10 | (set input.right-click 3) 11 | (set input.scroll-up 4) 12 | (set input.scroll-down 5) 13 | (set input.scroll-left 6) 14 | (set input.scroll-right 7) 15 | (set input.thumb-back-click 8) 16 | (set input.thumb-next-click 9) 17 | 18 | (set input.modkey "Mod4") 19 | 20 | (set input.modifiers 21 | {:mod input.modkey 22 | :shift "Shift" 23 | :ctrl "Control" 24 | :alt "Alt"}) 25 | 26 | (fn input.map-mods 27 | ;; convert short modifier names to ones Awesome/X11 understands 28 | [mods] 29 | (-> mods 30 | (lume.map (partial . input.modifiers)))) 31 | 32 | (fn input.keybind 33 | ;; describe a keybinding and assign it to a group 34 | [group mods key-code fun desc] 35 | (awful.key (input.map-mods mods) key-code fun {:description desc :group group})) 36 | 37 | (fn input.mousebind 38 | ;; describe a mouse button binding, short-modifier-aware 39 | [mods btn-code fun] 40 | (awful.button (input.map-mods mods) btn-code fun)) 41 | 42 | (fn input.key-group 43 | ;; Given a group name, and any additional number of arguments shaped as follows: 44 | ;; [[MODIFIERS] KEY-CODE FUNCTION DESCRIPTION] 45 | ;; return an `awful.key` for each additional argument (multi-valued return) 46 | [group ...] 47 | (let [map-key-group (partial input.keybind group)] 48 | (-> [...] 49 | (lume.map (fn [k] (map-key-group (unpack k)))) 50 | (lume.reduce (fn [a b] (lume.concat a b)) []) 51 | (values)))) 52 | 53 | input 54 | -------------------------------------------------------------------------------- /utils/oleander.fnl: -------------------------------------------------------------------------------- 1 | ;; Oleander is a dumping bed for various algorithms, functions and other little 2 | ;; flowers too precious to leave strewn about this garden. 3 | 4 | (local m {}) 5 | 6 | (fn m.require? [...] 7 | "Optionally require a module, returning nil if it could not be resolved." 8 | (let [(ok mod) (pcall require ...)] 9 | (if ok mod nil))) 10 | 11 | (lambda m.range 12 | [start end ?step] 13 | "Create a sequential table from START to END, with a stride of ?STEP or 1" 14 | (local s (if (= ?step nil) 1 ?step)) 15 | (var seq []) 16 | (for [i start end s] 17 | (table.insert seq i)) 18 | seq) 19 | 20 | (lambda m.fill 21 | [start end value] 22 | "Create a table with keys from START to END where each value is VALUE" 23 | (var seq []) 24 | (for [i start end] 25 | (table.insert seq i value)) 26 | seq) 27 | 28 | ;; try to 'polyfill' the best available bitwise operators 29 | (local bit (m.require? "bitop")) 30 | (local blshift (when bit bit.lshift)) 31 | (local brshift (when bit bit.rshift)) 32 | (local llshift (when (>= _G._VERSION "Lua 5.3") _G.lshift)) 33 | (local lrshift (when (>= _G._VERSION "Lua 5.3") _G.rshift)) 34 | 35 | (fn shim-lshift [n bits] 36 | (* (math.floor n) (^ 2 bits))) 37 | 38 | (fn shim-rshift [n bits] 39 | (math.floor (/ (math.floor n) (^ 2 bits)))) 40 | 41 | (set m.lshift (or llshift blshift shim-lshift)) 42 | (set m.rshift (or lrshift brshift shim-rshift)) 43 | 44 | (fn m.bsd-checksum 45 | [str] 46 | "Simple checksum. Returns a 32-bit numeric hash of input STR." 47 | (var sum 0) 48 | (let [bytes [(string.byte str 1 -1)]] 49 | (each [_ value (ipairs bytes)] 50 | (set sum (+ (m.rshift sum 1) (m.lshift sum 31))) 51 | (set sum (+ sum value)) 52 | (set sum (% sum 2147483647))) ; clamp to expected range 53 | sum)) 54 | 55 | (fn m.concat [...] 56 | "Concatenate all tables into a new table." 57 | (let [ret []] 58 | (for [i 1 (select "#" ...) 1] 59 | (local t (select i ...)) 60 | (let [tv (type t)] 61 | (if (= tv :table) 62 | (do 63 | (each [k v (pairs t)] 64 | (if (= (type k) "number") 65 | (table.insert ret v) 66 | (tset ret k v)))) 67 | (error (.. "Expected table, got " tv))))) 68 | ret)) 69 | 70 | m 71 | -------------------------------------------------------------------------------- /utils/pulseaudio.fnl: -------------------------------------------------------------------------------- 1 | ;; a set of helper functions for interacting with PulseAudio 2 | ;; requires: pactl 3 | (local awful (require "awful")) 4 | 5 | (local pa {}) 6 | 7 | (set 8 | pa.get-volume-script 9 | "pactl list sinks | grep -m1 'Volume:' | awk '{print $5}' | cut -d '%' -f1") 10 | 11 | (set 12 | pa.get-muted-script 13 | "pactl list sinks | grep -m 1 'Mute:' | awk '{printf \"%s\", $2}'") 14 | 15 | (set 16 | pa.get-mic-muted-script 17 | "pactl list sources | grep -m 1 'Mute:' | awk '{printf \"%s\", $2}'") 18 | 19 | (set 20 | pa.volume-listener-script 21 | "bash -c 'pactl subscribe 2> /dev/null | grep --line-buffered \"sink\"'") 22 | 23 | (set 24 | pa.mic-listener-script 25 | "bash -c 'pactl subscribe 2> /dev/null | grep --line-buffered \"source\"'") 26 | 27 | (set 28 | pa.kill-listeners-script 29 | "ps x | grep \"pactl subscribe\" | grep -v grep | awk '{print $1}' | xargs kill") 30 | 31 | (lambda pa.toggle-muted-script-tmpl [?sink] 32 | (let [sink (or ?sink "@DEFAULT_SINK@")] 33 | (.. "pactl set-sink-mute " sink " toggle"))) 34 | 35 | (lambda pa.toggle-mute-mic-script-tmpl [?source] 36 | (let [source (or ?source "@DEFAULT_SOURCE@")] 37 | (.. "pactl set-source-mute " source " toggle"))) 38 | 39 | (lambda pa.adjust-volume-script-tmpl [amount ?sink] 40 | (let [sink (or ?sink "@DEFAULT_SINK@") 41 | sign (if (> amount 0) "+" "")] 42 | (.. "pactl set-sink-volume " sink " " sign amount "%"))) 43 | 44 | (lambda pa.adjust-volume [step ?callback] 45 | (let [cmd (pa.adjust-volume-script-tmpl step) 46 | cb (or ?callback (fn [] nil))] 47 | (awful.spawn.easy_async_with_shell cmd cb))) 48 | 49 | (lambda pa.toggle-muted [?sink ?callback] 50 | (let [cmd (pa.toggle-muted-script-tmpl ?sink) 51 | cb (or ?callback (fn [] nil))] 52 | (awful.spawn.easy_async_with_shell cmd cb))) 53 | 54 | (lambda pa.toggle-mic-mute [?source ?callback] 55 | (let [cmd (pa.toggle-mute-mic-script-tmpl ?source) 56 | cb (or ?callback (fn [] nil))] 57 | (awful.spawn.easy_async_with_shell cmd cb))) 58 | 59 | (lambda pa.get-volume [?callback] 60 | (let [cb (or ?callback (fn [] nil))] 61 | (awful.spawn.easy_async_with_shell pa.get-volume-script cb))) 62 | 63 | pa 64 | -------------------------------------------------------------------------------- /utils/screens.fnl: -------------------------------------------------------------------------------- 1 | (local screen-utils {}) 2 | 3 | (fn screen-utils.screen-iter [] 4 | "Return an iterator for enumerating the currently attached screens" 5 | (var i 0) 6 | (var n (: screen :count)) 7 | (fn [] 8 | (set i (+ i 1)) 9 | (when (<= i n) 10 | (. screen i)))) 11 | 12 | (fn screen-utils.get-screens [] 13 | "Return an array containing all current screens" 14 | (var ss []) 15 | (each [s (screen-utils.screen-iter)] 16 | (table.insert ss s)) 17 | ss) 18 | 19 | screen-utils 20 | -------------------------------------------------------------------------------- /utils/tags.fnl: -------------------------------------------------------------------------------- 1 | (local awful (require "awful")) 2 | (local beautiful (require "beautiful")) 3 | (local lume (require "vendor.lume")) 4 | (local identicon (require "utils.identicon")) 5 | 6 | (local tags {}) 7 | 8 | (lambda tags.list-visible [?scr] 9 | "Return all active (visible) tags in-order" 10 | (let [s (or ?scr (awful.screen.focused))] 11 | (-> s.tags 12 | (lume.reject (fn [t] t.hide)) 13 | (lume.sort (fn [a b] (< a.name b.name)))))) 14 | 15 | (lambda tags.create [?scr ?layout ?props] 16 | "Create a new tag on ?SCR or focused screen with ?LAYOUT. 17 | Passthrough ?PROPS will merge, overwriting the defaults" 18 | (let [s (or ?scr (awful.screen.focused)) 19 | id (.. (os.time) "-" (os.clock))] ; sequential name for filter/sort 20 | (awful.tag.add 21 | id 22 | (lume.merge 23 | {:screen s 24 | :layout (or ?layout awful.layout.suit.tile) 25 | :icon (identicon.create id 5 128) 26 | :selected true 27 | :gap beautiful.useless_gap} 28 | (or ?props {}))))) 29 | 30 | (lambda tags.destroy [tag] 31 | "Delete TAG" 32 | (let [name tag.name] 33 | (: tag :delete) 34 | (awesome.emit_signal "tag::deleted" name))) 35 | 36 | (lambda tags.get-next [?scr] 37 | "Return the tag after current tag, creating a new tag if there is no next" 38 | (let [s (or ?scr (awful.screen.focused)) 39 | visible (tags.list-visible s) 40 | ct (or 41 | (. (awful.screen.focused) :selected_tag) 42 | (lume.last visible)) 43 | ct-index (lume.find visible ct) 44 | nt (. visible (+ (or ct-index (# visible)) 1))] 45 | (or nt (tags.create s ct.layout)))) 46 | 47 | (lambda tags.view-next [?scr] 48 | "Unmap current tag and map next tag, creating a new tag if none exists" 49 | (let [s (or ?scr (awful.screen.focused)) 50 | visible (tags.list-visible s) 51 | ct (or 52 | (. (awful.screen.focused) :selected_tag) 53 | (lume.last visible)) 54 | nt (tags.get-next s)] 55 | (tset nt :selected true) 56 | (when ct 57 | (tset ct :selected false)))) 58 | 59 | (lambda tags.get-prev [?scr] 60 | "Return the tag before current tag, or first visible tag" 61 | (let [s (or ?scr (awful.screen.focused)) 62 | visible (tags.list-visible s) 63 | ct (or 64 | (. (awful.screen.focused) :selected_tag) 65 | (lume.last visible)) 66 | ct-index (lume.find visible ct) 67 | pt-index (- (or ct-index (# visible)) 1)] 68 | (or (. visible pt-index) (. visible 1)))) 69 | 70 | (lambda tags.view-prev [?scr] 71 | "Unmap current tag and map previous tag" 72 | (let [s (or ?scr (awful.screen.focused)) 73 | visible (tags.list-visible s) 74 | ct (or 75 | (. (awful.screen.focused) :selected_tag) 76 | (lume.last visible)) 77 | pt (tags.get-prev s)] 78 | (when pt 79 | (do 80 | (tset ct :selected false) 81 | (tset pt :selected true) 82 | (when (= (# (: ct :clients)) 0) 83 | (when (~= ct pt) 84 | (tags.destroy ct))))))) 85 | 86 | (lambda tags.activate [tag] 87 | "Idempotently activate a tag" 88 | (tset tag :hide false)) 89 | 90 | (lambda tags.deactivate [tag] 91 | "Idempotently deactivate a tag" 92 | (tset tag :hide true) 93 | (tset tag :selected false)) 94 | 95 | (fn tags.get-or-create 96 | [scr tag-prop] 97 | (let [{:pos pos :workspace workspace} tag-prop 98 | tag-by-props (lume.filter scr.tags (fn [t] 99 | (and 100 | (= t.pos pos) 101 | (= t.workspace workspace))))] 102 | (if (= (# tag-by-props) 0) 103 | (awful.tag.add pos {:layout (. awful.layout.layouts 1) 104 | :screen scr 105 | :pos pos 106 | :workspace workspace }) 107 | (. tag-by-props 1)))) 108 | 109 | (fn tags.get-by-prop 110 | [key value ?invert] 111 | (lume.filter (root.tags) (fn [t] (if ?invert 112 | (~= (. t key) value)) 113 | (= (. t key) value)))) 114 | 115 | tags 116 | -------------------------------------------------------------------------------- /utils/widgets.fnl: -------------------------------------------------------------------------------- 1 | (local awful (require "awful")) 2 | (local beautiful (require "beautiful")) 3 | 4 | (local widget-utils {}) 5 | 6 | (lambda widget-utils.buttonize [target on-click? ?props] 7 | "Attach ON-CLICK? callback and stylize TARGET as a button. 8 | 9 | PROPS are possible overrides to styles defined in `beautiful.button_*` 10 | 11 | PROPS.on-hover-in and PROPS.on-hover-out are optional callbacks" 12 | (let [props (or ?props {}) 13 | bg-hover (or props.bg-hover beautiful.button_bg_hover "#222") 14 | fg-hover (or props.fg-hover beautiful.button_fg_hover "#eee") 15 | bg-click (or props.bg-click beautiful.button_bg_click "#222") 16 | fg-click (or props.fg-click beautiful.button_fg_click "#eee") 17 | color-in-fn (fn [t key color] 18 | (fn [] 19 | (when (~= (. t key) color) 20 | (tset t (.. key :_backup) (. t key)) 21 | (tset t (.. :has_ key :_backup) true)) 22 | (tset t key color))) 23 | color-out-fn (fn [t key] 24 | (fn [] 25 | (when (. t (.. :has_ key :_backup)) 26 | (tset t key (. t (.. key :_backup))))))] 27 | (when props.on-hover-out 28 | (: target :connect_signal :mouse::leave props.on-hover-out)) 29 | (when props.on-hover-in 30 | (: target :connect_signal :mouse::enter props.on-hover-in)) 31 | (: target :connect_signal :mouse::enter (color-in-fn target :bg bg-hover)) 32 | (: target :connect_signal :mouse::enter (color-in-fn target :fg fg-hover)) 33 | (: target :connect_signal :mouse::leave (color-out-fn target :bg)) 34 | (: target :connect_signal :mouse::leave (color-out-fn target :fg)) 35 | (: target :connect_signal :button::press (color-in-fn target :bg bg-click)) 36 | (: target :connect_signal :button::press (color-in-fn target :fg fg-click)) 37 | (: target :connect_signal :button::release (color-out-fn target :bg)) 38 | (: target :connect_signal :button::release (color-out-fn target :fg)) 39 | (when on-click? 40 | (: target :connect_signal :button::release on-click?)) 41 | target)) 42 | 43 | (lambda widget-utils.popoverize [target popover-widget ?props] 44 | "Transform TARGET into a popover button which displays given POPOVER-WIDGET. 45 | 46 | PROPS contains: 47 | :preferred-positions -- priority array like [:bottom :right :left :top] 48 | :preferred-anchors -- priority array like [:middle :back :front] 49 | overrides to styles defined in `beautiful.button_*` 50 | :on-hover-in, :on-hover-out -- optional callbacks to mouse events on TARGET" 51 | (let [props (or ?props {}) 52 | ppos (or props.preferred-positions [:bottom :right :left :top]) 53 | panc (or props.preferred-anchors [:middle :back :front])] 54 | (widget-utils.buttonize 55 | target 56 | (fn [] 57 | (awful.placement.next_to 58 | popover-widget 59 | {:mode :geometry 60 | :preferred_positions ppos 61 | :preferred_anchors panc 62 | :geometry mouse.current_widget_geometry}) 63 | (tset popover-widget :visible (not popover-widget.visible))) 64 | props))) 65 | 66 | widget-utils 67 | -------------------------------------------------------------------------------- /utils/xml.fnl: -------------------------------------------------------------------------------- 1 | (local {: map : reduce} (require :vendor.lume)) 2 | (local unpack (or table.unpack _G.unpack)) 3 | 4 | ;; hiccup-inspired library for creating XML from Fennel tables 5 | (local xml {}) 6 | 7 | (fn xml.escape [str] 8 | "Substitute XML special characters with their escape sequences." 9 | (-> (or str "") 10 | (string.gsub "&" "&") 11 | (string.gsub "<" "<") 12 | (string.gsub ">" ">") 13 | (string.gsub "\"" """))) 14 | 15 | (fn xml.format-attr [key value] 16 | "Transform key-value pair into pair of attribute-value strings." 17 | (match value 18 | true [key "true"] 19 | false [key "false"] 20 | nil nil 21 | _ [key (xml.escape value)])) 22 | 23 | (fn xml.render-attrs [attrs] 24 | "Turn a map into a string of XML attributes." 25 | (var str "") 26 | (when attrs 27 | (each [attr value (pairs attrs)] 28 | (let [[k v] (xml.format-attr attr value)] 29 | (when k 30 | (set str (.. str " " k "=\"" v "\"")))))) 31 | str) 32 | 33 | (lambda xml.create-element [tag ?attrs ...] 34 | "Render TAG with attributes ?ATTRS and restargs as children." 35 | (.. 36 | "<" tag (xml.render-attrs ?attrs) ">" 37 | (-> (if (= ... nil) [""] [...]) 38 | (map 39 | (fn [v] 40 | (match (type v) 41 | "table" (let [ftv (type (. v 1))] 42 | (match ftv 43 | "string" (let [[t a c] v] (xml.create-element t a c)) 44 | "table" (-> v 45 | (map (fn [tbl] 46 | (let [[t a c] tbl] 47 | (xml.create-element t a c)))) 48 | (reduce (fn [a b] (.. a b)) "")) 49 | nil "" 50 | _ (error 51 | (.. 52 | "Found " ftv ", expected string or table")))) 53 | _ (or v "")))) 54 | (reduce (fn [a b] (.. a b)))) 55 | "")) 56 | 57 | xml 58 | -------------------------------------------------------------------------------- /vendor/bencode.lua: -------------------------------------------------------------------------------- 1 | local unpack = unpack or table.unpack 2 | local encode, decode 3 | 4 | local function decode_list(str, t, total_len) 5 | -- print(" list", str, lume.serialize(t)) 6 | if(str:sub(1,1) == "e") then return t, total_len + 1 end 7 | local value, v_len = decode(str) 8 | if(not value) then return value, v_len end 9 | table.insert(t, value) 10 | total_len = total_len + v_len 11 | return decode_list(str:sub(v_len + 1), t, total_len) 12 | end 13 | 14 | local function decode_table(str, t, total_len) 15 | -- print(" table", str, lume.serialize(t)) 16 | if(str:sub(1,1) == "e") then return t, total_len + 1 end 17 | local key, k_len = decode(str) 18 | if(not key) then return key, k_len end 19 | local value, v_len = decode(str:sub(k_len+1)) 20 | if(not value) then return value, v_len end 21 | local end_pos = 1 + k_len + v_len 22 | t[key] = value 23 | total_len = total_len + k_len + v_len 24 | return decode_table(str:sub(end_pos), t, total_len) 25 | end 26 | 27 | function decode(str) 28 | -- print("decoding", str) 29 | if(str:sub(1,1) == "l") then 30 | return decode_list(str:sub(2), {}, 1) 31 | elseif(str:sub(1,1) == "d") then 32 | return decode_table(str:sub(2), {}, 1) 33 | elseif(str:sub(1,1) == "i") then 34 | -- print(" number", tonumber(str:sub(2, str:find("e") - 1))) 35 | return(tonumber(str:sub(2, str:find("e") - 1))), str:find("e") 36 | elseif(str:match("[0-9]+")) then 37 | local num_str = str:match("[0-9]+") 38 | local beginning_of_string = #num_str + 2 39 | local str_len = tonumber(num_str) 40 | local total_len = beginning_of_string + str_len - 1 41 | -- print(" string", str:sub(beginning_of_string)) 42 | return str:sub(beginning_of_string, total_len), total_len 43 | else 44 | return false, "Could not parse "..str 45 | end 46 | end 47 | 48 | local function decode_all(str, already) 49 | local decoded, len_or_err = decode(str) 50 | if(not decoded) then return decoded, len_or_err, already end 51 | already = already or {} 52 | table.insert(already, decoded) 53 | if(decoded and #str == len_or_err) then 54 | return already 55 | elseif(decoded) then 56 | return decode_all(str:sub(len_or_err + 1), already) 57 | else 58 | return false, len_or_err 59 | end 60 | end 61 | 62 | local function encode_str(s) return #s .. ":" .. s end 63 | local function encode_int(n) return "i" .. tostring(n) .. "e" end 64 | 65 | local function encode_table(t) 66 | local s, keys = "d", {} 67 | for k in pairs(t) do table.insert(keys, k) end 68 | table.sort(keys) 69 | for _,k in ipairs(keys) do s = s .. encode(k) .. encode(t[k]) end 70 | return s .. "e" 71 | end 72 | 73 | local function encode_list(l) 74 | local s = "l" 75 | for _,x in ipairs(l) do s = s .. encode(x) end 76 | return s .. "e" 77 | end 78 | 79 | local function count(tbl) 80 | local i = 0 81 | for _ in pairs(tbl) do i = i + 1 end 82 | return i 83 | end 84 | 85 | function encode(x) 86 | if(type(x) == "table" and select("#", unpack(x)) == count(x)) then 87 | return encode_list(x) 88 | elseif(type(x) == "table") then 89 | return encode_table(x) 90 | elseif(type(x) == "number" and math.floor(x) == x) then 91 | return encode_int(x) 92 | elseif(type(x) == "string") then 93 | return encode_str(x) 94 | else 95 | return false, "Could not encode " .. type(x) .. ": " .. tostring(x) 96 | end 97 | end 98 | 99 | return {decode=decode, decode_all=decode_all, encode=encode} 100 | -------------------------------------------------------------------------------- /vendor/jeejah/fenneleval.lua: -------------------------------------------------------------------------------- 1 | local fennel = require("fennel") 2 | -- fall back to pre-0.8.0 if necessary 3 | local fennelview = fennel.view or require("fennelview") 4 | 5 | local d = os.getenv("DEBUG") and print or function(_) end 6 | 7 | local repls = {} 8 | 9 | local print_for = function(write) 10 | return function(...) 11 | local args = {...} 12 | for i,x in ipairs(args) do args[i] = tostring(x) end 13 | table.insert(args, "\n") 14 | write(table.concat(args, " ")) 15 | end 16 | end 17 | 18 | local make_repl = function(session, repls) 19 | local on_values = function(xs) 20 | session.values(xs) 21 | session.done({status={"done"}}) 22 | end 23 | local read = function() 24 | -- If we skip empty input, it confuses the client. 25 | local input = coroutine.yield() 26 | if(input:find("^%s*$")) then return "nil\n" else return input end 27 | end 28 | local err = function(errtype, msg) 29 | session.write(table.concat({errtype, msg}, ": ")) session.done() 30 | end 31 | 32 | local env = session.sandbox 33 | if not env then 34 | env = {} 35 | for k, v in pairs(_G) do env[k] = v end 36 | env.io = {} 37 | end 38 | env.print = print_for(session.write) 39 | env.io.write = session.write 40 | env.io.read = function() 41 | session.needinput() 42 | local input, done = coroutine.yield() 43 | done() 44 | return input 45 | end 46 | 47 | local f = function() 48 | return fennel.repl({readChunk = read, 49 | onValues = on_values, 50 | onError = err, 51 | env = env, 52 | pp = fennelview}) 53 | end 54 | repls[session.id] = coroutine.wrap(f) 55 | repls[session.id]() 56 | return repls[session.id] 57 | end 58 | 59 | return function(conn, msg, session, send, response_for) 60 | local repl = repls[session.id] or make_repl(session, repls) 61 | if msg.op == "eval" then 62 | d("Evaluating", msg.code) 63 | session.values = function(xs) 64 | send(conn, response_for(msg, {value=table.concat(xs, "\n") .. "\n"})) 65 | end 66 | session.done = function() 67 | send(conn, response_for(msg, {status={"done"}})) 68 | end 69 | session.needinput = function() 70 | send(conn, response_for(msg, {status={"need-input"}})) 71 | end 72 | repl(msg.code .. "\n") 73 | elseif msg.op == "stdin" then 74 | d("Evaluating", msg.code) 75 | repl(msg.stdin, 76 | function() send(conn, response_for(msg, {status={"done"}})) end) 77 | elseif msg.op == "completions" then 78 | d("Completions", msg.prefix) 79 | session.values = function(xs) 80 | local result = {} 81 | for _, v in pairs(xs) do 82 | table.insert(result, {candidate = v}) 83 | end 84 | send(conn, response_for(msg, {completions = result})) 85 | end 86 | session.done = function() 87 | send(conn, response_for(msg, {status={"done"}})) 88 | end 89 | session.needinput = function() 90 | send(conn, response_for(msg, {status={"need-input"}})) 91 | end 92 | repl(",complete " .. msg.prefix .. "\n") 93 | elseif msg.op == "lookup" then 94 | d("Lookup", msg.sym) 95 | session.values = function(xs) 96 | if #xs == 0 or 97 | string.find(xs[1], "#") or 98 | xs[1] == msg.sym .. " not found" then 99 | -- nREPL spec has no error-signalling for this, just empty map 100 | send(conn, response_for(msg, {info = {}})) 101 | else 102 | local i = string.find(xs[1], "\n") 103 | local top_line = string.sub(xs[1], 1, i) 104 | local j = string.find(top_line, " ") + 1 105 | local k = string.find(top_line, "%)") - 1 106 | local info = {name = msg.sym, 107 | arglists = "([" .. string.sub(top_line, j, k) .. "])", 108 | doc = string.sub(xs[1], i + 1)} 109 | send(conn, response_for(msg, {info = info})) 110 | end 111 | end 112 | session.done = function() 113 | send(conn, response_for(msg, {status={"done"}})) 114 | end 115 | session.needinput = function() 116 | send(conn, response_for(msg, {status={"need-input"}})) 117 | end 118 | repl(",doc " .. msg.sym .. "\n") 119 | end 120 | end 121 | -------------------------------------------------------------------------------- /widgets/layout-switcher.fnl: -------------------------------------------------------------------------------- 1 | (local wibox (require "wibox")) 2 | (local awful (require "awful")) 3 | (local beautiful (require "beautiful")) 4 | (local icon-loader (require "icons.loader")) 5 | (local layout-icons (require "icons.layouts")) 6 | (local widget-utils (require "utils.widgets")) 7 | 8 | (local ls {}) 9 | 10 | (set ls.reset 11 | (fn [] 12 | (let [img (icon-loader.load 13 | :layouts 14 | (awful.layout.getname) 15 | {:stroke-color beautiful.fg})] 16 | (tset ls.switcher :image img)))) 17 | 18 | (set ls.on-click 19 | (fn [] 20 | (awful.layout.inc 1 (awful.screen.focused) awful.layout.layouts) 21 | (ls.on-hover-in))) 22 | 23 | (set ls.on-hover-in 24 | (fn [] 25 | (let [img (icon-loader.load 26 | :layouts 27 | (awful.layout.getname) 28 | {:stroke-color beautiful.fg 29 | :fill beautiful.fg})] 30 | (tset ls.switcher :image img)))) 31 | 32 | (set ls.switcher 33 | (widget-utils.buttonize 34 | (wibox.widget {:widget wibox.widget.imagebox}) 35 | ls.on-click 36 | {:on-hover-in ls.on-hover-in 37 | :on-hover-out ls.reset})) 38 | 39 | (awful.tag.attached_connect_signal nil "property::layout" ls.reset) 40 | (awful.tag.attached_connect_signal nil "property::selected" ls.reset) 41 | 42 | (awesome.connect_signal "persistence::loaded" ls.reset) 43 | 44 | ls 45 | -------------------------------------------------------------------------------- /widgets/workspace-switcher.fnl: -------------------------------------------------------------------------------- 1 | (local wibox (require "wibox")) 2 | (local workspaces (require :features.workspaces)) 3 | (local { : notify } (require :api.lawful)) 4 | (require-macros :awesome-macros) 5 | 6 | (local ws {}) 7 | 8 | (set ws.indicator 9 | (wibox.widget 10 | (/< 11 | :widget wibox.widget.imagebox 12 | :resize true 13 | :image (workspaces.get-icon workspaces.current)))) 14 | 15 | (awesome.connect_signal 16 | "workspaces::applied" 17 | (fn [sig] 18 | (notify.msg (.. "Changed to " sig " workspace.")) 19 | (tset ws.indicator :image (workspaces.get-icon sig)))) 20 | 21 | ws 22 | --------------------------------------------------------------------------------