summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKevin <[email protected]>2025-01-23 09:34:46 -0300
committerGitHub <[email protected]>2025-01-23 13:34:46 +0100
commit907230e479ba6c9489463797f81c7348ed754302 (patch)
tree4f12e013cebcff9097ae6a0e23885f1236b8b3d7
parent4d6d85562be7fe25cf659f1f1898244e1bdb44ca (diff)
add: lua gtk3 examples (#156)
-rw-r--r--docs/guide/getting-started/supported-languages.md9
-rw-r--r--docs/guide/lua/binding.md21
-rw-r--r--docs/guide/lua/examples.md9
-rw-r--r--examples/gtk3/lua/applauncher/README.md5
-rw-r--r--examples/gtk3/lua/applauncher/init.lua16
-rw-r--r--examples/gtk3/lua/applauncher/lib.lua38
-rw-r--r--examples/gtk3/lua/applauncher/style.scss1
-rw-r--r--examples/gtk3/lua/applauncher/widget/Applauncher.lua118
-rw-r--r--examples/gtk3/lua/applauncher/widget/Applauncher.scss59
-rw-r--r--examples/gtk3/lua/media-player/README.md5
-rw-r--r--examples/gtk3/lua/media-player/init.lua19
-rw-r--r--examples/gtk3/lua/media-player/lib.lua38
-rw-r--r--examples/gtk3/lua/media-player/style.scss1
-rw-r--r--examples/gtk3/lua/media-player/widget/MediaPlayer.lua144
-rw-r--r--examples/gtk3/lua/media-player/widget/MediaPlayer.scss56
-rw-r--r--examples/gtk3/lua/notifications/README.md5
-rw-r--r--examples/gtk3/lua/notifications/init.lua20
-rw-r--r--examples/gtk3/lua/notifications/lib.lua74
-rw-r--r--examples/gtk3/lua/notifications/notifications/Notification.lua105
-rw-r--r--examples/gtk3/lua/notifications/notifications/Notification.scss126
-rw-r--r--examples/gtk3/lua/notifications/notifications/NotificationPopups.lua57
-rw-r--r--examples/gtk3/lua/notifications/style.scss1
-rw-r--r--examples/gtk3/lua/simple-bar/lib.lua8
-rw-r--r--examples/gtk3/lua/simple-bar/widget/Bar.lua107
-rw-r--r--examples/gtk3/lua/stylua.toml4
-rw-r--r--lang/lua/astal/gtk3/astalify.lua23
26 files changed, 1000 insertions, 69 deletions
diff --git a/docs/guide/getting-started/supported-languages.md b/docs/guide/getting-started/supported-languages.md
index 0dc3f62..c38a001 100644
--- a/docs/guide/getting-started/supported-languages.md
+++ b/docs/guide/getting-started/supported-languages.md
@@ -39,6 +39,15 @@ Examples:
- [Simple Bar](https://github.com/Aylur/astal/tree/main/examples/gtk3/lua/simple-bar)
![simple-bar](https://github.com/user-attachments/assets/a306c864-56b7-44c4-8820-81f424f32b9b)
+- [Notification Popups](https://github.com/Aylur/astal/tree/main/examples/lua/notifications)
+![notification-popups](https://github.com/user-attachments/assets/0df0eddc-5c74-4af0-a694-48dc8ec6bb44)
+
+- [Applauncher](https://github.com/Aylur/astal/tree/main/examples/lua/applauncher)
+![launcher](https://github.com/user-attachments/assets/2695e3bb-dff4-478a-b392-279fe638bfd3)
+
+- [Media Player](https://github.com/Aylur/astal/tree/main/examples/lua/media-player)
+![media-player](https://github.com/user-attachments/assets/891e9706-74db-4505-bd83-c3628d7b4fd0)
+
## Python
There is a WIP [package for python](https://github.com/aylur/astal/tree/feat/python),
diff --git a/docs/guide/lua/binding.md b/docs/guide/lua/binding.md
index f4d5f0b..f9957b5 100644
--- a/docs/guide/lua/binding.md
+++ b/docs/guide/lua/binding.md
@@ -68,12 +68,13 @@ local Gtk = require("astal.gtk3").Gtk
local Variable = require("astal.variable")
---@param initial table
+---@return varmap
return function(initial)
local map = initial
- local var = Variable()
+ local var = Variable.new({})
local function notify()
- local arr
+ local arr = {}
for _, value in pairs(map) do
table.insert(arr, value)
end
@@ -90,7 +91,13 @@ return function(initial)
notify() -- init
- return {
+ ---@class varmap
+ ---@field set fun(key: any, value: any): nil
+ ---@field delete fun(key: any): nil
+ ---@field get fun(): any
+ ---@field subscribe fun(callback: function): function
+ ---@overload fun(): Binding
+ return setmetatable({
set = function(key, value)
delete(key)
map[key] = value
@@ -106,7 +113,11 @@ return function(initial)
subscribe = function(callback)
return var:subscribe(callback)
end,
- }
+ }, {
+ __call = function()
+ return var()
+ end,
+ })
end
```
@@ -130,7 +141,7 @@ function MappedBox()
map.delete(id)
end)
end,
- bind(map):as(function (arr)
+ map():as(function(arr)
-- can be sorted here
return arr
end),
diff --git a/docs/guide/lua/examples.md b/docs/guide/lua/examples.md
index c2dd30c..464f917 100644
--- a/docs/guide/lua/examples.md
+++ b/docs/guide/lua/examples.md
@@ -2,3 +2,12 @@
## [Simple Bar](https://github.com/Aylur/astal/tree/main/examples/gtk3/lua/simple-bar)
![simple-bar](https://github.com/user-attachments/assets/a306c864-56b7-44c4-8820-81f424f32b9b)
+
+### [Notification Popups](https://github.com/Aylur/astal/tree/main/examples/gtk3/lua/notifications)
+![notification-popups](https://github.com/user-attachments/assets/0df0eddc-5c74-4af0-a694-48dc8ec6bb44)
+
+### [Applauncher](https://github.com/Aylur/astal/tree/main/examples/gtk3/lua/applauncher)
+![launcher](https://github.com/user-attachments/assets/2695e3bb-dff4-478a-b392-279fe638bfd3)
+
+### [Media Player](https://github.com/Aylur/astal/tree/main/examples/gtk3/lua/media-player)
+![media-player](https://github.com/user-attachments/assets/891e9706-74db-4505-bd83-c3628d7b4fd0)
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
+
+![launcher](https://github.com/user-attachments/assets/2695e3bb-dff4-478a-b392-279fe638bfd3)
+
+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
+
+![mpris](https://github.com/user-attachments/assets/891e9706-74db-4505-bd83-c3628d7b4fd0)
+
+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
+
+![notifs](https://github.com/user-attachments/assets/0df0eddc-5c74-4af0-a694-48dc8ec6bb44)
+
+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/lib.lua b/examples/gtk3/lua/simple-bar/lib.lua
index d94db5c..6f2dcea 100644
--- a/examples/gtk3/lua/simple-bar/lib.lua
+++ b/examples/gtk3/lua/simple-bar/lib.lua
@@ -9,12 +9,12 @@ function M.src(path)
end
---@generic T, R
----@param arr T[]
----@param func fun(T, integer): R
+---@param array T[]
+---@param func fun(T, i: integer): R
---@return R[]
-function M.map(arr, func)
+function M.map(array, func)
local new_arr = {}
- for i, v in ipairs(arr) do
+ for i, v in ipairs(array) do
new_arr[i] = func(v, i)
end
return new_arr
diff --git a/examples/gtk3/lua/simple-bar/widget/Bar.lua b/examples/gtk3/lua/simple-bar/widget/Bar.lua
index 155b9b8..3f685a2 100644
--- a/examples/gtk3/lua/simple-bar/widget/Bar.lua
+++ b/examples/gtk3/lua/simple-bar/widget/Bar.lua
@@ -22,9 +22,9 @@ local function SysTray()
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),
+ action_group = bind(item, "action-group"):as(
+ function(ag) return { "dbusmenu", ag } end
+ ),
Widget.Icon({
gicon = bind(item, "gicon"),
}),
@@ -41,11 +41,14 @@ local function FocusedClient()
return Widget.Box({
class_name = "Focused",
visible = focused,
- focused:as(function(client)
- return client and Widget.Label({
- label = bind(client, "title"):as(tostring),
- })
- end),
+ focused:as(
+ function(client)
+ return client
+ and Widget.Label({
+ label = bind(client, "title"):as(tostring),
+ })
+ end
+ ),
})
end
@@ -54,16 +57,16 @@ local function Wifi()
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),
+ 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
@@ -78,9 +81,7 @@ local function AudioSlider()
}),
Widget.Slider({
hexpand = true,
- on_dragged = function(self)
- speaker.volume = self.value
- end,
+ on_dragged = function(self) speaker.volume = self.value end,
value = bind(speaker, "volume"),
}),
})
@@ -96,9 +97,9 @@ local function BatteryLevel()
icon = bind(bat, "battery-icon-name"),
}),
Widget.Label({
- label = bind(bat, "percentage"):as(function(p)
- return tostring(math.floor(p * 100)) .. " %"
- end),
+ label = bind(bat, "percentage"):as(
+ function(p) return tostring(math.floor(p * 100)) .. " %" end
+ ),
}),
})
end
@@ -112,14 +113,20 @@ local function Media()
Widget.Box({
class_name = "Cover",
valign = "CENTER",
- css = bind(player, "cover-art"):as(function(cover)
- return "background-image: url('" .. (cover or "") .. "');"
- end),
+ 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),
+ label = bind(player, "metadata"):as(
+ function()
+ return (player.title or "")
+ .. " - "
+ .. (player.artist or "")
+ end
+ ),
}),
})
end
@@ -130,22 +137,22 @@ local function Workspaces()
return Widget.Box({
class_name = "Workspaces",
bind(hypr, "workspaces"):as(function(wss)
- table.sort(wss, function(a, b)
- return a.id < b.id
- end)
+ 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),
+ 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)
@@ -154,28 +161,26 @@ local function Workspaces()
end
local function Time(format)
- local time = Variable(""):poll(1000, function()
- return GLib.DateTime.new_now_local():format(format)
- end)
+ 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,
+ on_destroy = function() time:drop() end,
label = time(),
})
end
return function(gdkmonitor)
- local WindowAnchor = astal.require("Astal", "3.0").WindowAnchor
+ local Anchor = astal.require("Astal").WindowAnchor
return Widget.Window({
class_name = "Bar",
gdkmonitor = gdkmonitor,
- anchor = WindowAnchor.TOP + WindowAnchor.LEFT + WindowAnchor.RIGHT,
+ anchor = Anchor.TOP + Anchor.LEFT + Anchor.RIGHT,
exclusivity = "EXCLUSIVE",
-
Widget.CenterBox({
Widget.Box({
halign = "START",
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"
diff --git a/lang/lua/astal/gtk3/astalify.lua b/lang/lua/astal/gtk3/astalify.lua
index 5bf3c1d..f1a280b 100644
--- a/lang/lua/astal/gtk3/astalify.lua
+++ b/lang/lua/astal/gtk3/astalify.lua
@@ -52,15 +52,20 @@ local function includes(tbl, elem)
end
local function set_children(parent, children)
- children = map(flatten(children), function(item)
- if Gtk.Widget:is_type_of(item) then
- return item
- end
- return Gtk.Label({
- visible = true,
- label = tostring(item),
- })
- end)
+ children = map(
+ filter(flatten(children), function(item)
+ return not not item
+ end),
+ function(item)
+ if Gtk.Widget:is_type_of(item) then
+ return item
+ end
+ return Gtk.Label({
+ visible = true,
+ label = tostring(item),
+ })
+ end
+ )
-- remove
if Gtk.Bin:is_type_of(parent) then