diff options
Diffstat (limited to 'examples/gtk3/lua')
25 files changed, 1263 insertions, 0 deletions
diff --git a/examples/gtk3/lua/applauncher/README.md b/examples/gtk3/lua/applauncher/README.md new file mode 100644 index 0000000..682adf1 --- /dev/null +++ b/examples/gtk3/lua/applauncher/README.md @@ -0,0 +1,5 @@ +# Applauncher + + + +Using [Apps](https://aylur.github.io/astal/guide/libraries/apps). diff --git a/examples/gtk3/lua/applauncher/init.lua b/examples/gtk3/lua/applauncher/init.lua new file mode 100644 index 0000000..0cd6db5 --- /dev/null +++ b/examples/gtk3/lua/applauncher/init.lua @@ -0,0 +1,16 @@ +local astal = require("astal") +local App = require("astal.gtk3.app") + +local AppLauncher = require("widget.Applauncher") +local src = require("lib").src + +local scss = src("style.scss") +local css = "/tmp/style.css" + +astal.exec("sass " .. scss .. " " .. css) + +App:start({ + instance_name = "launcher", + css = css, + main = AppLauncher, +}) diff --git a/examples/gtk3/lua/applauncher/lib.lua b/examples/gtk3/lua/applauncher/lib.lua new file mode 100644 index 0000000..8a50bdd --- /dev/null +++ b/examples/gtk3/lua/applauncher/lib.lua @@ -0,0 +1,38 @@ +local M = {} + +function M.src(path) + local str = debug.getinfo(2, "S").source:sub(2) + local src = str:match("(.*/)") or str:match("(.*\\)") or "./" + return src .. path +end + +---@generic T, R +---@param array T[] +---@param func fun(T, i: integer): R +---@return R[] +function M.map(array, func) + local new_arr = {} + for i, v in ipairs(array) do + new_arr[i] = func(v, i) + end + return new_arr +end + +---@generic T +---@param array T[] +---@param start integer +---@param stop? integer +---@return T[] +function M.slice(array, start, stop) + local new_arr = {} + + stop = stop or #array + + for i = start, stop do + table.insert(new_arr, array[i]) + end + + return new_arr +end + +return M diff --git a/examples/gtk3/lua/applauncher/style.scss b/examples/gtk3/lua/applauncher/style.scss new file mode 100644 index 0000000..ba13eed --- /dev/null +++ b/examples/gtk3/lua/applauncher/style.scss @@ -0,0 +1 @@ +@use "./widget/Applauncher.scss" diff --git a/examples/gtk3/lua/applauncher/widget/Applauncher.lua b/examples/gtk3/lua/applauncher/widget/Applauncher.lua new file mode 100644 index 0000000..78f7fa5 --- /dev/null +++ b/examples/gtk3/lua/applauncher/widget/Applauncher.lua @@ -0,0 +1,118 @@ +local astal = require("astal") + +local Apps = astal.require("AstalApps") +local App = require("astal.gtk3.app") +local Astal = require("astal.gtk3").Astal +local Gdk = require("astal.gtk3").Gdk +local Variable = astal.Variable +local Widget = require("astal.gtk3.widget") + +local slice = require("lib").slice +local map = require("lib").map + +local MAX_ITEMS = 8 + +local function hide() + local launcher = App:get_window("launcher") + if launcher then launcher:hide() end +end + +local function AppButton(app) + return Widget.Button({ + class_name = "AppButton", + on_clicked = function() + hide() + app:launch() + end, + Widget.Box({ + Widget.Icon({ icon = app.icon_name }), + Widget.Box({ + valign = "CENTER", + vertical = true, + Widget.Label({ + class_name = "name", + wrap = true, + xalign = 0, + label = app.name, + }), + app.description and Widget.Label({ + class_name = "description", + wrap = true, + xalign = 0, + label = app.description, + }), + }), + }), + }) +end + +return function() + local apps = Apps.Apps() + + local text = Variable("") + local list = text( + function(t) return slice(apps:fuzzy_query(t), 1, MAX_ITEMS) end + ) + + local on_enter = function() + local found = apps:fuzzy_query(text:get())[1] + if found then + found:launch() + hide() + end + end + + return Widget.Window({ + name = "launcher", + anchor = Astal.WindowAnchor.TOP + Astal.WindowAnchor.BOTTOM, + exclusivity = "IGNORE", + keymode = "ON_DEMAND", + application = App, + on_show = function() text:set("") end, + on_key_press_event = function(self, event) + if event.keyval == Gdk.KEY_Escape then self:hide() end + end, + Widget.Box({ + Widget.EventBox({ + expand = true, + on_click = hide, + width_request = 4000, + }), + Widget.Box({ + hexpand = false, + vertical = true, + Widget.EventBox({ on_click = hide, height_request = 100 }), + Widget.Box({ + vertical = true, + width_request = 500, + class_name = "Applauncher", + Widget.Entry({ + placeholder_text = "Search", + text = text(), + on_changed = function(self) text:set(self.text) end, + on_activate = on_enter, + }), + Widget.Box({ + spacing = 6, + vertical = true, + list:as(function(l) return map(l, AppButton) end), + }), + Widget.Box({ + halign = "CENTER", + class_name = "not-found", + vertical = true, + visible = list:as(function(l) return #l == 0 end), + Widget.Icon({ icon = "system-search-symbolic" }), + Widget.Label({ label = "No match found" }), + }), + }), + Widget.EventBox({ expand = true, on_click = hide }), + }), + Widget.EventBox({ + width_request = 4000, + expand = true, + on_click = hide, + }), + }), + }) +end diff --git a/examples/gtk3/lua/applauncher/widget/Applauncher.scss b/examples/gtk3/lua/applauncher/widget/Applauncher.scss new file mode 100644 index 0000000..38b5be1 --- /dev/null +++ b/examples/gtk3/lua/applauncher/widget/Applauncher.scss @@ -0,0 +1,59 @@ +@use 'sass:string'; + +@function gtkalpha($c, $a) { + @return string.unquote('alpha(#{$c},#{$a})'); +} + +// https://gitlab.gnome.org/GNOME/gtk/-/blob/gtk-3-24/gtk/theme/Adwaita/_colors-public.scss +$fg-color: #{'@theme_fg_color'}; +$bg-color: #{'@theme_bg_color'}; + +window#launcher { + all: unset; + + box.Applauncher { + background-color: $bg-color; + border-radius: 11px; + margin: 1rem; + padding: 0.8rem; + box-shadow: 2px 3px 8px 0 gtkalpha(black, 0.4); + + entry { + margin-bottom: 0.8rem; + } + + button { + min-width: 0; + min-height: 0; + padding: 0.5rem; + + icon { + font-size: 3em; + margin-right: 0.3rem; + } + + label.name { + font-weight: bold; + font-size: 1.1em; + } + + label.description { + color: gtkalpha($fg-color, 0.8); + } + } + + box.not-found { + padding: 1rem; + + icon { + font-size: 6em; + color: gtkalpha($fg-color, 0.7); + } + + label { + color: gtkalpha($fg-color, 0.9); + font-size: 1.2em; + } + } + } +} diff --git a/examples/gtk3/lua/media-player/README.md b/examples/gtk3/lua/media-player/README.md new file mode 100644 index 0000000..4e3d237 --- /dev/null +++ b/examples/gtk3/lua/media-player/README.md @@ -0,0 +1,5 @@ +# Media Player + + + +Using [Mpris](https://aylur.github.io/astal/guide/libraries/mpris). diff --git a/examples/gtk3/lua/media-player/init.lua b/examples/gtk3/lua/media-player/init.lua new file mode 100644 index 0000000..16ccbfb --- /dev/null +++ b/examples/gtk3/lua/media-player/init.lua @@ -0,0 +1,19 @@ +local astal = require("astal") +local App = require("astal.gtk3.app") +local Widget = require("astal.gtk3.widget") + +local MprisPlayers = require("widget.MediaPlayer") +local src = require("lib").src + +local scss = src("style.scss") +local css = "/tmp/style.css" + +astal.exec("sass " .. scss .. " " .. css) + +App:start({ + instance_name = "lua", + css = css, + main = function() + Widget.Window({ MprisPlayers() }) + end, +}) diff --git a/examples/gtk3/lua/media-player/lib.lua b/examples/gtk3/lua/media-player/lib.lua new file mode 100644 index 0000000..8a50bdd --- /dev/null +++ b/examples/gtk3/lua/media-player/lib.lua @@ -0,0 +1,38 @@ +local M = {} + +function M.src(path) + local str = debug.getinfo(2, "S").source:sub(2) + local src = str:match("(.*/)") or str:match("(.*\\)") or "./" + return src .. path +end + +---@generic T, R +---@param array T[] +---@param func fun(T, i: integer): R +---@return R[] +function M.map(array, func) + local new_arr = {} + for i, v in ipairs(array) do + new_arr[i] = func(v, i) + end + return new_arr +end + +---@generic T +---@param array T[] +---@param start integer +---@param stop? integer +---@return T[] +function M.slice(array, start, stop) + local new_arr = {} + + stop = stop or #array + + for i = start, stop do + table.insert(new_arr, array[i]) + end + + return new_arr +end + +return M diff --git a/examples/gtk3/lua/media-player/style.scss b/examples/gtk3/lua/media-player/style.scss new file mode 100644 index 0000000..be398dd --- /dev/null +++ b/examples/gtk3/lua/media-player/style.scss @@ -0,0 +1 @@ +@use './widget/MediaPlayer.scss'; diff --git a/examples/gtk3/lua/media-player/widget/MediaPlayer.lua b/examples/gtk3/lua/media-player/widget/MediaPlayer.lua new file mode 100644 index 0000000..fbad3e0 --- /dev/null +++ b/examples/gtk3/lua/media-player/widget/MediaPlayer.lua @@ -0,0 +1,144 @@ +local astal = require("astal") + +local Astal = astal.require("Astal", "3.0") + +local bind = astal.bind +local Widget = require("astal.gtk3.widget") +local lookup_icon = Astal.Icon.lookup_icon + +local map = require("lib").map + +local Mpris = astal.require("AstalMpris") + +---@param length integer +local function length_str(length) + local min = math.floor(length / 60) + local sec = math.floor(length % 60) + + return string.format("%d:%s%d", min, sec < 10 and "0" or "", sec) +end + +local function MediaPlayer(player) + local title = bind(player, "title"):as( + function(t) return t or "Unknown Track" end + ) + + local artist = bind(player, "artist"):as( + function(a) return a or "Unknown Artist" end + ) + + local cover_art = bind(player, "cover-art"):as( + function(c) return string.format("background-image: url('%s');", c) end + ) + + local player_icon = bind(player, "entry"):as( + function(e) return lookup_icon(e) and e or "audio-x-generic-symbolic" end + ) + + local position = bind(player, "position"):as( + function(p) return player.length > 0 and p / player.length or 0 end + ) + + local play_icon = bind(player, "playback-status"):as( + function(s) + return s == "PLAYING" and "media-playback-pause-symbolic" + or "media-playback-start-symbolic" + end + ) + + return Widget.Box({ + class_name = "MediaPlayer", + Widget.Box({ + class_name = "cover-art", + css = cover_art, + }), + Widget.Box({ + vertical = true, + Widget.Box({ + class_name = "title", + Widget.Label({ + ellipsize = "END", + hexpand = true, + halign = "START", + label = title, + }), + Widget.Icon({ + icon = player_icon, + }), + }), + Widget.Label({ + halign = "START", + valign = "START", + vexpand = true, + wrap = true, + label = artist, + }), + Widget.Slider({ + visible = bind(player, "length"):as( + function(l) return l > 0 end + ), + on_dragged = function(event) + player.position = event.value * player.length + end, + value = position, + }), + Widget.CenterBox({ + class_name = "actions", + Widget.Label({ + hexpand = true, + class_name = "position", + halign = "START", + visible = bind(player, "length"):as( + function(l) return l > 0 end + ), + label = bind(player, "position"):as(length_str), + }), + Widget.Box({ + Widget.Button({ + on_clicked = function() player:previous() end, + visible = bind(player, "can-go-previous"), + Widget.Icon({ + icon = "media-skip-backward-symbolic", + }), + }), + Widget.Button({ + on_clicked = function() player:play_pause() end, + visible = bind(player, "can-control"), + Widget.Icon({ + icon = play_icon, + }), + }), + Widget.Button({ + on_clicked = function() player:next() end, + visible = bind(player, "can-go-next"), + Widget.Icon({ + icon = "media-skip-forward-symbolic", + }), + }), + }), + Widget.Label({ + class_name = "length", + hexpand = true, + halign = "END", + visible = bind(player, "length"):as( + function(l) return l > 0 end + ), + label = bind(player, "length"):as( + function(l) return l > 0 and length_str(l) or "0:00" end + ), + }), + }), + }), + }) +end + +return function() + local mpris = Mpris.get_default() + + return Widget.Box({ + vertical = true, + bind(mpris, "players"):as( + function(players) return map(players, MediaPlayer) end + ), + }) +end diff --git a/examples/gtk3/lua/media-player/widget/MediaPlayer.scss b/examples/gtk3/lua/media-player/widget/MediaPlayer.scss new file mode 100644 index 0000000..e1597c2 --- /dev/null +++ b/examples/gtk3/lua/media-player/widget/MediaPlayer.scss @@ -0,0 +1,56 @@ +// https://gitlab.gnome.org/GNOME/gtk/-/blob/gtk-3-24/gtk/theme/Adwaita/_colors-public.scss +$fg-color: #{"@theme_fg_color"}; +$bg-color: #{"@theme_bg_color"}; + +window { + all: unset; +} + +box.MediaPlayer { + padding: .6rem; + background-color: $bg-color; + + box.cover-art { + min-width: 120px; + min-height: 120px; + border-radius: 9px; + margin-right: .6rem; + background-size: contain; + background-position: center; + } + + box.title { + label { + font-weight: bold; + font-size: 1.1em; + } + } + + scale { + padding: 0; + margin: .4rem 0; + + trough { + min-height: 8px; + } + + highlight { + background-color: $fg-color; + } + + slider { + all: unset; + } + } + + centerbox.actions { + min-width: 220px; + + button { + min-width: 0; + min-height: 0; + padding: .4rem; + margin: 0 .2rem; + } + } +} diff --git a/examples/gtk3/lua/notifications/README.md b/examples/gtk3/lua/notifications/README.md new file mode 100644 index 0000000..60dad60 --- /dev/null +++ b/examples/gtk3/lua/notifications/README.md @@ -0,0 +1,5 @@ +# Notifications Popups + + + +A replacement for dunst and other daemons using [Notifd](https://aylur.github.io/astal/guide/libraries/notifd). diff --git a/examples/gtk3/lua/notifications/init.lua b/examples/gtk3/lua/notifications/init.lua new file mode 100644 index 0000000..886e9ab --- /dev/null +++ b/examples/gtk3/lua/notifications/init.lua @@ -0,0 +1,20 @@ +local astal = require("astal") +local App = require("astal.gtk3.app") + +local NotificationPopups = require("notifications.NotificationPopups") +local src = require("lib").src + +local scss = src("style.scss") +local css = "/tmp/style.css" + +astal.exec("sass " .. scss .. " " .. css) + +App:start({ + instance_name = "notifications", + css = css, + main = function() + for _, mon in pairs(App.monitors) do + NotificationPopups(mon) + end + end, +}) diff --git a/examples/gtk3/lua/notifications/lib.lua b/examples/gtk3/lua/notifications/lib.lua new file mode 100644 index 0000000..289fc7e --- /dev/null +++ b/examples/gtk3/lua/notifications/lib.lua @@ -0,0 +1,74 @@ +local astal = require("astal") +local Variable = require("astal").Variable +local Gtk = require("astal.gtk3").Gtk +local GLib = astal.require("GLib") + +local M = {} + +function M.src(path) + local str = debug.getinfo(2, "S").source:sub(2) + local src = str:match("(.*/)") or str:match("(.*\\)") or "./" + return src .. path +end + +---@generic T, R +---@param array T[] +---@param func fun(T, i: integer): R +---@return R[] +function M.map(array, func) + local new_arr = {} + for i, v in ipairs(array) do + new_arr[i] = func(v, i) + end + return new_arr +end + +---@param path string +---@return boolean +function M.file_exists(path) return GLib.file_test(path, "EXISTS") end + +function M.varmap(initial) + local map = initial + local var = Variable() + + local function notify() + local arr = {} + for _, value in pairs(map) do + table.insert(arr, value) + end + var:set(arr) + end + + local function delete(key) + if Gtk.Widget:is_type_of(map[key]) then map[key]:destroy() end + + map[key] = nil + end + + notify() + + return setmetatable({ + set = function(key, value) + delete(key) + map[key] = value + notify() + end, + delete = function(key) + delete(key) + notify() + end, + get = function() return var:get() end, + subscribe = function(callback) return var:subscribe(callback) end, + }, { + __call = function() return var() end, + }) +end + +---@param time number +---@param format? string +function M.time(time, format) + format = format or "%H:%M" + return GLib.DateTime.new_from_unix_local(time):format(format) +end + +return M diff --git a/examples/gtk3/lua/notifications/notifications/Notification.lua b/examples/gtk3/lua/notifications/notifications/Notification.lua new file mode 100644 index 0000000..39d36f5 --- /dev/null +++ b/examples/gtk3/lua/notifications/notifications/Notification.lua @@ -0,0 +1,105 @@ +local Widget = require("astal.gtk3").Widget +local Gtk = require("astal.gtk3").Gtk +local Astal = require("astal.gtk3").Astal + +local map = require("lib").map +local time = require("lib").time +local file_exists = require("lib").file_exists + +local function is_icon(icon) return Astal.Icon.lookup_icon(icon) ~= nil end + +---@param props { setup?: function, on_hover_lost?: function, notification: any } +return function(props) + local n = props.notification + + local header = Widget.Box({ + class_name = "header", + (n.app_icon or n.desktop_entry) and Widget.Icon({ + class_name = "app-icon", + icon = n.app_icon or n.desktop_entry, + }), + Widget.Label({ + class_name = "app-name", + halign = "START", + ellipsize = "END", + label = n.app_name or "Unknown", + }), + Widget.Label({ + class_name = "time", + hexpand = true, + halign = "END", + label = time(n.time), + }), + Widget.Button({ + on_clicked = function() n:dismiss() end, + Widget.Icon({ icon = "window-close-symbolic" }), + }), + }) + + local content = Widget.Box({ + class_name = "content", + (n.image and file_exists(n.image)) and Widget.Box({ + valign = "START", + class_name = "image", + css = string.format("background-image: url('%s')", n.image), + }), + n.image and is_icon(n.image) and Widget.Box({ + valign = "START", + class_name = "icon-image", + Widget.Icon({ + icon = n.image, + hexpand = true, + vexpand = true, + halign = "CENTER", + valign = "CENTER", + }), + }), + Widget.Box({ + vertical = true, + Widget.Label({ + class_name = "summary", + halign = "START", + xalign = 0, + ellipsize = "END", + label = n.summary, + }), + Widget.Label({ + class_name = "body", + wrap = true, + use_markup = true, + halign = "START", + xalign = 0, + justify = "FILL", + label = n.body, + }), + }), + }) + + return Widget.EventBox({ + class_name = string.format("Notification %s", string.lower(n.urgency)), + setup = props.setup, + on_hover_lost = props.on_hover_lost, + Widget.Box({ + vertical = true, + header, + Gtk.Separator({ visible = true }), + content, + #n.actions > 0 and Widget.Box({ + class_name = "actions", + map(n.actions, function(action) + local label, id = action.label, action.id + + return Widget.Button({ + hexpand = true, + on_clicked = function() return n:invoke(id) end, + Widget.Label({ + label = label, + halign = "CENTER", + hexpand = true, + }), + }) + end), + }), + }), + }) +end diff --git a/examples/gtk3/lua/notifications/notifications/Notification.scss b/examples/gtk3/lua/notifications/notifications/Notification.scss new file mode 100644 index 0000000..089d587 --- /dev/null +++ b/examples/gtk3/lua/notifications/notifications/Notification.scss @@ -0,0 +1,126 @@ +@use "sass:string"; + +@function gtkalpha($c, $a) { + @return string.unquote("alpha(#{$c},#{$a})"); +} + +// https://gitlab.gnome.org/GNOME/gtk/-/blob/gtk-3-24/gtk/theme/Adwaita/_colors-public.scss +$fg-color: #{"@theme_fg_color"}; +$bg-color: #{"@theme_bg_color"}; +$error: red; + +window.NotificationPopups { + all: unset; +} + +eventbox.Notification { + + &:first-child>box { + margin-top: 1rem; + } + + &:last-child>box { + margin-bottom: 1rem; + } + + // eventboxes can not take margins so we style its inner box instead + >box { + min-width: 400px; + border-radius: 13px; + background-color: $bg-color; + margin: .5rem 1rem .5rem 1rem; + box-shadow: 2px 3px 8px 0 gtkalpha(black, .4); + border: 1pt solid gtkalpha($fg-color, .03); + } + + &.critical>box { + border: 1pt solid gtkalpha($error, .4); + + .header { + + .app-name { + color: gtkalpha($error, .8); + + } + + .app-icon { + color: gtkalpha($error, .6); + } + } + } + + .header { + padding: .5rem; + color: gtkalpha($fg-color, 0.5); + + .app-icon { + margin: 0 .4rem; + } + + .app-name { + margin-right: .3rem; + font-weight: bold; + + &:first-child { + margin-left: .4rem; + } + } + + .time { + margin: 0 .4rem; + } + + button { + padding: .2rem; + min-width: 0; + min-height: 0; + } + } + + separator { + margin: 0 .4rem; + background-color: gtkalpha($fg-color, .1); + } + + .content { + margin: 1rem; + margin-top: .5rem; + + .summary { + font-size: 1.2em; + color: $fg-color; + } + + .body { + color: gtkalpha($fg-color, 0.8); + } + + .image { + border: 1px solid gtkalpha($fg-color, .02); + margin-right: .5rem; + border-radius: 9px; + min-width: 100px; + min-height: 100px; + background-size: cover; + background-position: center; + } + } + + .actions { + margin: 1rem; + margin-top: 0; + padding: .2rem; + + button { + margin: 0 .3rem; + + &:first-child { + margin-left: 0; + } + + &:last-child { + margin-right: 0; + } + } + } +} diff --git a/examples/gtk3/lua/notifications/notifications/NotificationPopups.lua b/examples/gtk3/lua/notifications/notifications/NotificationPopups.lua new file mode 100644 index 0000000..c5f9e1b --- /dev/null +++ b/examples/gtk3/lua/notifications/notifications/NotificationPopups.lua @@ -0,0 +1,57 @@ +local astal = require("astal") +local Widget = require("astal.gtk3").Widget + +local Notifd = astal.require("AstalNotifd") +local Notification = require("notifications.Notification") +local timeout = astal.timeout + +local TIMEOUT_DELAY = 5000 + +local varmap = require("lib").varmap +local notifd = Notifd.get_default() + +local function NotificationMap() + local notif_map = varmap({}) + + notifd.on_notified = function(_, id) + notif_map.set( + id, + Notification({ + notification = notifd:get_notification(id), + -- once hovering over the notification is done + -- destroy the widget without calling notification.dismiss() + -- so that it acts as a "popup" and we can still display it + -- in a notification center like widget + -- but clicking on the close button will close it + on_hover_lost = function() notif_map.delete(id) end, + setup = function() + timeout(TIMEOUT_DELAY, function() + -- uncomment this if you want to "hide" the notifications + -- after TIMEOUT_DELAY + + -- NotificationMap.delete(id) + end) + end, + }) + ) + end + + notifd.on_resolved = function(_, id) notif_map.delete(id) end + + return notif_map +end + +return function(gdkmonitor) + local Anchor = astal.require("Astal").WindowAnchor + local notifs = NotificationMap() + + return Widget.Window({ + class_name = "NotificationPopups", + gdkmonitor = gdkmonitor, + anchor = Anchor.TOP + Anchor.RIGHT, + Widget.Box({ + vertical = true, + notifs(), + }), + }) +end diff --git a/examples/gtk3/lua/notifications/style.scss b/examples/gtk3/lua/notifications/style.scss new file mode 100644 index 0000000..7ef0168 --- /dev/null +++ b/examples/gtk3/lua/notifications/style.scss @@ -0,0 +1 @@ +@use "./notifications/Notification.scss"; diff --git a/examples/gtk3/lua/simple-bar/README.md b/examples/gtk3/lua/simple-bar/README.md new file mode 100644 index 0000000..48cc27c --- /dev/null +++ b/examples/gtk3/lua/simple-bar/README.md @@ -0,0 +1,13 @@ +# Simple Bar Example + + + +A simple bar for Hyprland using + +- [Battery library](https://aylur.github.io/astal/guide/libraries/battery). +- [Hyprland library](https://aylur.github.io/astal/guide/libraries/hyprland). +- [Mpris library](https://aylur.github.io/astal/guide/libraries/mpris). +- [Network library](https://aylur.github.io/astal/guide/libraries/network). +- [Tray library](https://aylur.github.io/astal/guide/libraries/tray). +- [WirePlumber library](https://aylur.github.io/astal/guide/libraries/wireplumber). +- [dart-sass](https://sass-lang.com/dart-sass/) as the css precompiler diff --git a/examples/gtk3/lua/simple-bar/init.lua b/examples/gtk3/lua/simple-bar/init.lua new file mode 100644 index 0000000..8c412fb --- /dev/null +++ b/examples/gtk3/lua/simple-bar/init.lua @@ -0,0 +1,24 @@ +local astal = require("astal") +local App = require("astal.gtk3.app") + +local Bar = require("widget.Bar") +local src = require("lib").src + +local scss = src("style.scss") +local css = "/tmp/style.css" + +astal.exec("sass " .. scss .. " " .. css) + +App:start({ + instance_name = "lua", + css = css, + request_handler = function(msg, res) + print(msg) + res("ok") + end, + main = function() + for _, mon in pairs(App.monitors) do + Bar(mon) + end + end, +}) diff --git a/examples/gtk3/lua/simple-bar/lib.lua b/examples/gtk3/lua/simple-bar/lib.lua new file mode 100644 index 0000000..6f2dcea --- /dev/null +++ b/examples/gtk3/lua/simple-bar/lib.lua @@ -0,0 +1,25 @@ +local Variable = require("astal").Variable + +local M = {} + +function M.src(path) + local str = debug.getinfo(2, "S").source:sub(2) + local src = str:match("(.*/)") or str:match("(.*\\)") or "./" + return src .. path +end + +---@generic T, R +---@param array T[] +---@param func fun(T, i: integer): R +---@return R[] +function M.map(array, func) + local new_arr = {} + for i, v in ipairs(array) do + new_arr[i] = func(v, i) + end + return new_arr +end + +M.date = Variable(""):poll(1000, "date") + +return M diff --git a/examples/gtk3/lua/simple-bar/style.scss b/examples/gtk3/lua/simple-bar/style.scss new file mode 100644 index 0000000..f5f771a --- /dev/null +++ b/examples/gtk3/lua/simple-bar/style.scss @@ -0,0 +1,106 @@ +@use "sass:color"; + +$bg: #212223; +$fg: #f1f1f1; +$accent: #378DF7; +$radius: 7px; + +window.Bar { + border: none; + box-shadow: none; + background-color: $bg; + color: $fg; + font-size: 1.1em; + font-weight: bold; + + label { + margin: 0 8px; + } + + .Workspaces { + button { + all: unset; + background-color: transparent; + + &:hover label { + background-color: color.adjust($fg, $alpha: -0.84); + border-color: color.adjust($accent, $alpha: -0.8); + } + + &:active label { + background-color: color.adjust($fg, $alpha: -0.8) + } + } + + label { + transition: 200ms; + padding: 0 8px; + margin: 2px; + border-radius: $radius; + border: 1pt solid transparent; + } + + .focused label { + color: $accent; + border-color: $accent; + } + } + + .SysTray { + margin-right: 8px; + + button { + padding: 0 4px; + } + } + + .FocusedClient { + color: $accent; + } + + .Media .Cover { + min-height: 1.2em; + min-width: 1.2em; + border-radius: $radius; + background-position: center; + background-size: contain; + } + + .Battery label { + padding-left: 0; + margin-left: 0; + } + + .AudioSlider { + * { + all: unset; + } + + icon { + margin-right: .6em; + } + + & { + margin: 0 1em; + } + + trough { + background-color: color.adjust($fg, $alpha: -0.8); + border-radius: $radius; + } + + highlight { + background-color: $accent; + min-height: .8em; + border-radius: $radius; + } + + slider { + background-color: $fg; + border-radius: $radius; + min-height: 1em; + min-width: 1em; + margin: -.2em; + } + } +} diff --git a/examples/gtk3/lua/simple-bar/widget/Bar.lua b/examples/gtk3/lua/simple-bar/widget/Bar.lua new file mode 100644 index 0000000..3f685a2 --- /dev/null +++ b/examples/gtk3/lua/simple-bar/widget/Bar.lua @@ -0,0 +1,203 @@ +local astal = require("astal") +local Widget = require("astal.gtk3.widget") +local Variable = astal.Variable +local GLib = astal.require("GLib") +local bind = astal.bind +local Mpris = astal.require("AstalMpris") +local Battery = astal.require("AstalBattery") +local Wp = astal.require("AstalWp") +local Network = astal.require("AstalNetwork") +local Tray = astal.require("AstalTray") +local Hyprland = astal.require("AstalHyprland") +local map = require("lib").map + +local function SysTray() + local tray = Tray.get_default() + + return Widget.Box({ + class_name = "SysTray", + bind(tray, "items"):as(function(items) + return map(items, function(item) + return Widget.MenuButton({ + tooltip_markup = bind(item, "tooltip_markup"), + use_popover = false, + menu_model = bind(item, "menu-model"), + action_group = bind(item, "action-group"):as( + function(ag) return { "dbusmenu", ag } end + ), + Widget.Icon({ + gicon = bind(item, "gicon"), + }), + }) + end) + end), + }) +end + +local function FocusedClient() + local hypr = Hyprland.get_default() + local focused = bind(hypr, "focused-client") + + return Widget.Box({ + class_name = "Focused", + visible = focused, + focused:as( + function(client) + return client + and Widget.Label({ + label = bind(client, "title"):as(tostring), + }) + end + ), + }) +end + +local function Wifi() + local network = Network.get_default() + local wifi = bind(network, "wifi") + + return Widget.Box({ + visible = wifi:as(function(v) return v ~= nil end), + wifi:as( + function(w) + return Widget.Icon({ + tooltip_text = bind(w, "ssid"):as(tostring), + class_name = "Wifi", + icon = bind(w, "icon-name"), + }) + end + ), + }) +end + +local function AudioSlider() + local speaker = Wp.get_default().audio.default_speaker + + return Widget.Box({ + class_name = "AudioSlider", + css = "min-width: 140px;", + Widget.Icon({ + icon = bind(speaker, "volume-icon"), + }), + Widget.Slider({ + hexpand = true, + on_dragged = function(self) speaker.volume = self.value end, + value = bind(speaker, "volume"), + }), + }) +end + +local function BatteryLevel() + local bat = Battery.get_default() + + return Widget.Box({ + class_name = "Battery", + visible = bind(bat, "is-present"), + Widget.Icon({ + icon = bind(bat, "battery-icon-name"), + }), + Widget.Label({ + label = bind(bat, "percentage"):as( + function(p) return tostring(math.floor(p * 100)) .. " %" end + ), + }), + }) +end + +local function Media() + local player = Mpris.Player.new("spotify") + + return Widget.Box({ + class_name = "Media", + visible = bind(player, "available"), + Widget.Box({ + class_name = "Cover", + valign = "CENTER", + css = bind(player, "cover-art"):as( + function(cover) + return "background-image: url('" .. (cover or "") .. "');" + end + ), + }), + Widget.Label({ + label = bind(player, "metadata"):as( + function() + return (player.title or "") + .. " - " + .. (player.artist or "") + end + ), + }), + }) +end + +local function Workspaces() + local hypr = Hyprland.get_default() + + return Widget.Box({ + class_name = "Workspaces", + bind(hypr, "workspaces"):as(function(wss) + table.sort(wss, function(a, b) return a.id < b.id end) + + return map(wss, function(ws) + if not (ws.id >= -99 and ws.id <= -2) then -- filter out special workspaces + return Widget.Button({ + class_name = bind(hypr, "focused-workspace"):as( + function(fw) return fw == ws and "focused" or "" end + ), + on_clicked = function() ws:focus() end, + label = bind(ws, "id"):as( + function(v) + return type(v) == "number" + and string.format("%.0f", v) + or v + end + ), + }) + end + end) + end), + }) +end + +local function Time(format) + local time = Variable(""):poll( + 1000, + function() return GLib.DateTime.new_now_local():format(format) end + ) + + return Widget.Label({ + class_name = "Time", + on_destroy = function() time:drop() end, + label = time(), + }) +end + +return function(gdkmonitor) + local Anchor = astal.require("Astal").WindowAnchor + + return Widget.Window({ + class_name = "Bar", + gdkmonitor = gdkmonitor, + anchor = Anchor.TOP + Anchor.LEFT + Anchor.RIGHT, + exclusivity = "EXCLUSIVE", + Widget.CenterBox({ + Widget.Box({ + halign = "START", + Workspaces(), + FocusedClient(), + }), + Widget.Box({ + Media(), + }), + Widget.Box({ + halign = "END", + SysTray(), + Wifi(), + AudioSlider(), + BatteryLevel(), + Time("%H:%M - %A %e."), + }), + }), + }) +end diff --git a/examples/gtk3/lua/stylua.toml b/examples/gtk3/lua/stylua.toml new file mode 100644 index 0000000..4141934 --- /dev/null +++ b/examples/gtk3/lua/stylua.toml @@ -0,0 +1,4 @@ +indent_type = "Tabs" +indent_width = 4 +column_width = 80 +collapse_simple_statement = "Always" |