(local {: Gtk : GtkLayerShell : Gdk : GdkPixbuf : Gio : GLib : cairo } (require :lgi)) (local posix (require :posix)) (local {: view} (require :fennel)) (local icon-theme (Gtk.IconTheme.get_default)) (local HEIGHT 48) (local css-provider (let [p (Gtk.CssProvider)] (Gtk.StyleContext.add_provider_for_screen (Gdk.Screen.get_default) p Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION) p)) (fn load-styles [pathname] (let [(success err) (css-provider:load_from_path pathname)] (or success (print "failed to load stylesheet" err)))) (var file-monitor nil) (fn watch-stylesheet [pathname] (let [f (Gio.File.new_for_path pathname) (monitor err) (Gio.File.monitor f Gio.FileMonitorFlags.NONE nil)] (load-styles pathname) (if (not monitor) (print :watch-stylesheet err)) (set file-monitor monitor) (doto monitor (tset :on_changed #(load-styles (: $2 :get_path)))))) (fn resolve [f] (match (type f) "string" f "function" (f))) (fn find-icon-pixbuf [name] (var found nil) (each [_ res (pairs [HEIGHT 128 64 48]) :until found] (let [pixbuf (icon-theme:load_icon name res (+ Gtk.IconLookupFlags.FORCE_SVG Gtk.IconLookupFlags.USE_BUILTIN))] (when pixbuf (set found (pixbuf:scale_simple HEIGHT (* pixbuf.width (/ HEIGHT pixbuf.height)) GdkPixbuf.InterpType.BILINEAR))))) found) (local found-icons {}) (macro check-err [form] `(let [(result# err#) ,form] (or result# (error err#)))) (fn load-icon [name] (let [pixbuf (if (= (name:sub 1 1) "/") ;; From a direct path (check-err (GdkPixbuf.Pixbuf.new_from_file_at_scale name HEIGHT -1 true)) ;; From icon theme (find-icon-pixbuf name))] (Gtk.Image.new_from_pixbuf pixbuf))) (fn find-icon [name] (let [icon (. found-icons name)] (or icon (let [(icon err) (load-icon name)] (if (not icon) (print err)) (tset found-icons name icon) icon)))) (fn add-css-classes [widget classes] (let [context (widget:get_style_context)] (each [_ c (ipairs classes)] (context:add_class c)))) (fn clear-css-classes [widget] (let [context (widget:get_style_context)] (each [_ c (ipairs (context:list_classes))] (context:remove_class c)))) (fn indicator [{: wait-for : refresh : on-click}] (let [button (Gtk.EventBox { })] (fn update-indicator [] (let [content (resolve refresh)] (when content (match (button:get_child) it (button:remove it)) (match content {:icon icon} (button:add (find-icon icon)) {:text text} (button:add (Gtk.Label {:label text}))) (clear-css-classes button) (add-css-classes button ["indicator"]) (match content {:classes classes} (add-css-classes button classes)) (button:show_all)))) (update-indicator) { : button :update update-indicator :inputs (or wait-for.input []) :interval wait-for.interval })) (fn make-layer-shell [window layer exclusive? anchors] (let [s GtkLayerShell] (s.init_for_window window) (s.set_layer window (. { :top GtkLayerShell.Layer.TOP } layer)) (if exclusive? (s.auto_exclusive_zone_enable window)) (each [edge margin (pairs anchors)] (let [edge (. {:top GtkLayerShell.Edge.TOP :bottom GtkLayerShell.Edge.BOTTOM :left GtkLayerShell.Edge.LEFT :right GtkLayerShell.Edge.RIGHT} edge)] (GtkLayerShell.set_margin window edge margin) (GtkLayerShell.set_anchor window edge 1))))) (local bars []) (fn bar [{: anchor : orientation : indicators : gravity : classes }] (let [window (Gtk.Window {} ) orientation (match orientation :vertical Gtk.Orientation.VERTICAL :horizontal Gtk.Orientation.HORIZONTAL) box (Gtk.Box { :orientation orientation})] (doto box (add-css-classes ["bar"]) (add-css-classes (or classes []))) (if (= gravity :end) (box:pack_start (Gtk.EventBox) true true 0)) (table.insert bars { : window : anchor : indicators }) (each [_ i (ipairs indicators)] (box:pack_start i.button false false 0)) (window:add box))) (fn gsource-for-file-input [file cb] (let [fd file.fileno] (doto (GLib.unix_fd_source_new fd GLib.IOCondition.IN) (: :set_callback cb)))) (fn ready-to-update? [indicator now update-times] (if indicator.interval (> now (or (. update-times indicator) 0)))) (fn hcf [a b] (let [remainder (% a b)] (if (= remainder 0) b (hcf b remainder)))) (assert (= (hcf 198 360) 18)) (assert (= (hcf 10 15) 5)) (fn minimum-interval [intervals] (accumulate [min (. intervals 1) _ interval (ipairs intervals)] (hcf min interval))) (assert (= (minimum-interval [ 350 1000 5000 ]) 50)) (fn run [] (var intervals []) (each [_ bar (ipairs bars)] (each [_ indicator (ipairs bar.indicators)] (if indicator.interval (table.insert intervals indicator.interval)) (each [_ file (ipairs indicator.inputs)] (GLib.Source.attach (gsource-for-file-input file #(or (indicator:update) true)))))) (let [update-times {} interval (minimum-interval intervals)] (when (< interval 100) (print (.. "required refresh interval is " interval "ms"))) (GLib.timeout_add 0 (minimum-interval intervals) (fn [] (let [now (/ (GLib.get_monotonic_time) 1000)] (each [_ bar (ipairs bars)] (each [_ indicator (ipairs bar.indicators)] (when (ready-to-update? indicator now update-times) (indicator:update) (tset update-times indicator (+ now indicator.interval)))))) true))) (each [_ b (ipairs bars)] (make-layer-shell b.window :top true (collect [_ edge (ipairs b.anchor)] edge 0)) (b.window:show_all)) (Gtk.main)) { : bar : indicator : run :stylesheet watch-stylesheet : file-monitor ;don't let this get GCed }