summaryrefslogtreecommitdiff
path: root/examples
diff options
context:
space:
mode:
Diffstat (limited to 'examples')
-rw-r--r--examples/gtk3/js/.gitignore (renamed from examples/js/.gitignore)0
-rw-r--r--examples/gtk3/js/applauncher/README.md (renamed from examples/js/applauncher/README.md)0
-rw-r--r--examples/gtk3/js/applauncher/app.ts (renamed from examples/js/applauncher/app.ts)0
-rw-r--r--examples/gtk3/js/applauncher/style.scss (renamed from examples/js/applauncher/style.scss)0
-rw-r--r--examples/gtk3/js/applauncher/widget/Applauncher.scss (renamed from examples/js/applauncher/widget/Applauncher.scss)0
-rw-r--r--examples/gtk3/js/applauncher/widget/Applauncher.tsx (renamed from examples/js/applauncher/widget/Applauncher.tsx)10
-rw-r--r--examples/gtk3/js/media-player/README.md (renamed from examples/js/media-player/README.md)0
-rw-r--r--examples/gtk3/js/media-player/app.ts (renamed from examples/js/media-player/app.ts)0
-rw-r--r--examples/gtk3/js/media-player/style.scss (renamed from examples/js/media-player/style.scss)0
-rw-r--r--examples/gtk3/js/media-player/widget/MediaPlayer.scss (renamed from examples/js/media-player/widget/MediaPlayer.scss)0
-rw-r--r--examples/gtk3/js/media-player/widget/MediaPlayer.tsx (renamed from examples/js/media-player/widget/MediaPlayer.tsx)0
-rw-r--r--examples/gtk3/js/notifications/README.md (renamed from examples/js/notifications/README.md)0
-rw-r--r--examples/gtk3/js/notifications/app.ts (renamed from examples/js/notifications/app.ts)0
-rw-r--r--examples/gtk3/js/notifications/notifications/Notification.scss (renamed from examples/js/notifications/notifications/Notification.scss)0
-rw-r--r--examples/gtk3/js/notifications/notifications/Notification.tsx (renamed from examples/js/notifications/notifications/Notification.tsx)0
-rw-r--r--examples/gtk3/js/notifications/notifications/NotificationPopups.tsx (renamed from examples/js/notifications/notifications/NotificationPopups.tsx)2
-rw-r--r--examples/gtk3/js/notifications/style.scss (renamed from examples/js/notifications/style.scss)0
-rw-r--r--examples/gtk3/js/osd/README.md7
-rw-r--r--examples/gtk3/js/osd/app.ts11
-rw-r--r--examples/gtk3/js/osd/osd/OSD.scss30
-rw-r--r--examples/gtk3/js/osd/osd/OSD.tsx69
-rw-r--r--examples/gtk3/js/osd/osd/brightness.ts45
-rw-r--r--examples/gtk3/js/osd/style.scss1
-rw-r--r--examples/gtk3/js/popover/Popover.tsx90
-rw-r--r--examples/gtk3/js/popover/Popover2.tsx66
-rw-r--r--examples/gtk3/js/popover/app.tsx72
-rw-r--r--examples/gtk3/js/simple-bar/README.md (renamed from examples/js/simple-bar/README.md)0
-rw-r--r--examples/gtk3/js/simple-bar/app.ts (renamed from examples/js/simple-bar/app.ts)0
-rw-r--r--examples/gtk3/js/simple-bar/style.scss (renamed from examples/lua/simple-bar/style.scss)50
-rw-r--r--examples/gtk3/js/simple-bar/widget/Bar.tsx (renamed from examples/js/simple-bar/widget/Bar.tsx)43
-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/README.md (renamed from examples/lua/simple-bar/README.md)0
-rw-r--r--examples/gtk3/lua/simple-bar/init.lua (renamed from examples/lua/simple-bar/init.lua)0
-rw-r--r--examples/gtk3/lua/simple-bar/lib.lua (renamed from examples/lua/simple-bar/lib.lua)8
-rw-r--r--examples/gtk3/lua/simple-bar/style.scss (renamed from examples/js/simple-bar/style.scss)50
-rw-r--r--examples/gtk3/lua/simple-bar/widget/Bar.lua (renamed from examples/lua/simple-bar/widget/Bar.lua)135
-rw-r--r--examples/gtk3/lua/stylua.toml4
-rw-r--r--examples/gtk3/py/.gitignore (renamed from examples/py/.gitignore)0
-rw-r--r--examples/gtk3/py/simple-bar/README.md (renamed from examples/py/simple-bar/README.md)0
-rw-r--r--examples/gtk3/py/simple-bar/__init__.py (renamed from examples/py/simple-bar/__init__.py)0
-rwxr-xr-xexamples/gtk3/py/simple-bar/app.py (renamed from examples/py/simple-bar/app.py)6
-rw-r--r--examples/gtk3/py/simple-bar/style.scss (renamed from examples/vala/simple-bar/style.scss)50
-rw-r--r--examples/gtk3/py/simple-bar/versions.py (renamed from examples/py/simple-bar/versions.py)0
-rw-r--r--examples/gtk3/py/simple-bar/widget/Bar.py (renamed from examples/py/simple-bar/widget/Bar.py)38
-rw-r--r--examples/gtk3/py/simple-bar/widget/__init__.py (renamed from examples/py/simple-bar/widget/__init__.py)0
-rw-r--r--examples/gtk3/vala/simple-bar/README.md (renamed from examples/vala/simple-bar/README.md)0
-rw-r--r--examples/gtk3/vala/simple-bar/app.in.vala (renamed from examples/vala/simple-bar/app.in.vala)0
-rw-r--r--examples/gtk3/vala/simple-bar/flake.nix (renamed from examples/vala/simple-bar/flake.nix)0
-rw-r--r--examples/gtk3/vala/simple-bar/meson.build (renamed from examples/vala/simple-bar/meson.build)19
-rw-r--r--examples/gtk3/vala/simple-bar/style.scss (renamed from examples/py/simple-bar/style.scss)50
-rw-r--r--examples/gtk3/vala/simple-bar/widget/Bar.vala (renamed from examples/vala/simple-bar/widget/Bar.vala)40
69 files changed, 1558 insertions, 226 deletions
diff --git a/examples/js/.gitignore b/examples/gtk3/js/.gitignore
index d53b85b..d53b85b 100644
--- a/examples/js/.gitignore
+++ b/examples/gtk3/js/.gitignore
diff --git a/examples/js/applauncher/README.md b/examples/gtk3/js/applauncher/README.md
index 682adf1..682adf1 100644
--- a/examples/js/applauncher/README.md
+++ b/examples/gtk3/js/applauncher/README.md
diff --git a/examples/js/applauncher/app.ts b/examples/gtk3/js/applauncher/app.ts
index d6c9e1c..d6c9e1c 100644
--- a/examples/js/applauncher/app.ts
+++ b/examples/gtk3/js/applauncher/app.ts
diff --git a/examples/js/applauncher/style.scss b/examples/gtk3/js/applauncher/style.scss
index ba13eed..ba13eed 100644
--- a/examples/js/applauncher/style.scss
+++ b/examples/gtk3/js/applauncher/style.scss
diff --git a/examples/js/applauncher/widget/Applauncher.scss b/examples/gtk3/js/applauncher/widget/Applauncher.scss
index ae2453d..ae2453d 100644
--- a/examples/js/applauncher/widget/Applauncher.scss
+++ b/examples/gtk3/js/applauncher/widget/Applauncher.scss
diff --git a/examples/js/applauncher/widget/Applauncher.tsx b/examples/gtk3/js/applauncher/widget/Applauncher.tsx
index c7bac68..8206250 100644
--- a/examples/js/applauncher/widget/Applauncher.tsx
+++ b/examples/gtk3/js/applauncher/widget/Applauncher.tsx
@@ -35,6 +35,7 @@ function AppButton({ app }: { app: Apps.Application }) {
export default function Applauncher() {
const { CENTER } = Gtk.Align
const apps = new Apps.Apps()
+ const width = Variable(1000)
const text = Variable("")
const list = text(text => apps.fuzzy_query(text).slice(0, MAX_ITEMS))
@@ -49,13 +50,16 @@ export default function Applauncher() {
exclusivity={Astal.Exclusivity.IGNORE}
keymode={Astal.Keymode.ON_DEMAND}
application={App}
- onShow={() => text.set("")}
+ onShow={(self) => {
+ text.set("")
+ width.set(self.get_current_monitor().workarea.width)
+ }}
onKeyPressEvent={function (self, event: Gdk.Event) {
if (event.get_keyval()[1] === Gdk.KEY_Escape)
self.hide()
}}>
<box>
- <eventbox widthRequest={4000} expand onClick={hide} />
+ <eventbox widthRequest={width(w => w / 2)} expand onClick={hide} />
<box hexpand={false} vertical>
<eventbox heightRequest={100} onClick={hide} />
<box widthRequest={500} className="Applauncher" vertical>
@@ -81,7 +85,7 @@ export default function Applauncher() {
</box>
<eventbox expand onClick={hide} />
</box>
- <eventbox widthRequest={4000} expand onClick={hide} />
+ <eventbox widthRequest={width(w => w / 2)} expand onClick={hide} />
</box>
</window>
}
diff --git a/examples/js/media-player/README.md b/examples/gtk3/js/media-player/README.md
index 4e3d237..4e3d237 100644
--- a/examples/js/media-player/README.md
+++ b/examples/gtk3/js/media-player/README.md
diff --git a/examples/js/media-player/app.ts b/examples/gtk3/js/media-player/app.ts
index 5b7558a..5b7558a 100644
--- a/examples/js/media-player/app.ts
+++ b/examples/gtk3/js/media-player/app.ts
diff --git a/examples/js/media-player/style.scss b/examples/gtk3/js/media-player/style.scss
index 2e2f625..2e2f625 100644
--- a/examples/js/media-player/style.scss
+++ b/examples/gtk3/js/media-player/style.scss
diff --git a/examples/js/media-player/widget/MediaPlayer.scss b/examples/gtk3/js/media-player/widget/MediaPlayer.scss
index e1597c2..e1597c2 100644
--- a/examples/js/media-player/widget/MediaPlayer.scss
+++ b/examples/gtk3/js/media-player/widget/MediaPlayer.scss
diff --git a/examples/js/media-player/widget/MediaPlayer.tsx b/examples/gtk3/js/media-player/widget/MediaPlayer.tsx
index 06c7e77..06c7e77 100644
--- a/examples/js/media-player/widget/MediaPlayer.tsx
+++ b/examples/gtk3/js/media-player/widget/MediaPlayer.tsx
diff --git a/examples/js/notifications/README.md b/examples/gtk3/js/notifications/README.md
index 60dad60..60dad60 100644
--- a/examples/js/notifications/README.md
+++ b/examples/gtk3/js/notifications/README.md
diff --git a/examples/js/notifications/app.ts b/examples/gtk3/js/notifications/app.ts
index ed53292..ed53292 100644
--- a/examples/js/notifications/app.ts
+++ b/examples/gtk3/js/notifications/app.ts
diff --git a/examples/js/notifications/notifications/Notification.scss b/examples/gtk3/js/notifications/notifications/Notification.scss
index a32f08b..a32f08b 100644
--- a/examples/js/notifications/notifications/Notification.scss
+++ b/examples/gtk3/js/notifications/notifications/Notification.scss
diff --git a/examples/js/notifications/notifications/Notification.tsx b/examples/gtk3/js/notifications/notifications/Notification.tsx
index 5149d5b..5149d5b 100644
--- a/examples/js/notifications/notifications/Notification.tsx
+++ b/examples/gtk3/js/notifications/notifications/Notification.tsx
diff --git a/examples/js/notifications/notifications/NotificationPopups.tsx b/examples/gtk3/js/notifications/notifications/NotificationPopups.tsx
index 9b84d84..13fdd88 100644
--- a/examples/js/notifications/notifications/NotificationPopups.tsx
+++ b/examples/gtk3/js/notifications/notifications/NotificationPopups.tsx
@@ -98,7 +98,7 @@ export default function NotificationPopups(gdkmonitor: Gdk.Monitor) {
gdkmonitor={gdkmonitor}
exclusivity={Astal.Exclusivity.EXCLUSIVE}
anchor={TOP | RIGHT}>
- <box vertical>
+ <box vertical noImplicitDestroy>
{bind(notifs)}
</box>
</window>
diff --git a/examples/js/notifications/style.scss b/examples/gtk3/js/notifications/style.scss
index 7ef0168..7ef0168 100644
--- a/examples/js/notifications/style.scss
+++ b/examples/gtk3/js/notifications/style.scss
diff --git a/examples/gtk3/js/osd/README.md b/examples/gtk3/js/osd/README.md
new file mode 100644
index 0000000..ee1d497
--- /dev/null
+++ b/examples/gtk3/js/osd/README.md
@@ -0,0 +1,7 @@
+# On Screen Display
+
+![osd](https://github.com/user-attachments/assets/08e0e118-6b07-4cac-8ebc-08262594cee7)
+
+A simple widget that pops up when screen brightness or audio changes
+
+Uses the [WirePlumber library](https://aylur.github.io/astal/guide/libraries/wireplumber).
diff --git a/examples/gtk3/js/osd/app.ts b/examples/gtk3/js/osd/app.ts
new file mode 100644
index 0000000..50e314e
--- /dev/null
+++ b/examples/gtk3/js/osd/app.ts
@@ -0,0 +1,11 @@
+import { App } from "astal/gtk3"
+import style from "./style.scss"
+import OSD from "./osd/OSD"
+
+App.start({
+ instanceName: "osd-example",
+ css: style,
+ main() {
+ App.get_monitors().map(OSD)
+ },
+})
diff --git a/examples/gtk3/js/osd/osd/OSD.scss b/examples/gtk3/js/osd/osd/OSD.scss
new file mode 100644
index 0000000..d0fe4d1
--- /dev/null
+++ b/examples/gtk3/js/osd/osd/OSD.scss
@@ -0,0 +1,30 @@
+$fg-color: #{"@theme_fg_color"};
+$bg-color: #{"@theme_bg_color"};
+
+window.OSD {
+ box.OSD {
+ border-radius: 100px;
+ background-color: $bg-color;
+ padding: 13px 16px;
+ margin: 13px;
+ box-shadow: 3px 3px 7px 0 rgba(0,0,0,.4);
+ }
+
+ icon {
+ font-size: 4rem;
+ }
+
+ label {
+ font-size: 2.4rem;
+ }
+
+ levelbar {
+ trough {
+ margin: 1 .6rem;
+ }
+
+ block {
+ min-height: 2rem;
+ }
+ }
+}
diff --git a/examples/gtk3/js/osd/osd/OSD.tsx b/examples/gtk3/js/osd/osd/OSD.tsx
new file mode 100644
index 0000000..df28da5
--- /dev/null
+++ b/examples/gtk3/js/osd/osd/OSD.tsx
@@ -0,0 +1,69 @@
+import { App, Astal, Gdk, Gtk } from "astal/gtk3"
+import { timeout } from "astal/time"
+import Variable from "astal/variable"
+import Brightness from "./brightness"
+import Wp from "gi://AstalWp"
+
+function OnScreenProgress({ visible }: { visible: Variable<boolean> }) {
+ const brightness = Brightness.get_default()
+ const speaker = Wp.get_default()!.get_default_speaker()
+
+ const iconName = Variable("")
+ const value = Variable(0)
+
+ let count = 0
+ function show(v: number, icon: string) {
+ visible.set(true)
+ value.set(v)
+ iconName.set(icon)
+ count++
+ timeout(2000, () => {
+ count--
+ if (count === 0) visible.set(false)
+ })
+ }
+
+ return (
+ <revealer
+ setup={(self) => {
+ self.hook(brightness, "notify::screen", () =>
+ show(brightness.screen, "display-brightness-symbolic"),
+ )
+
+ if (speaker) {
+ self.hook(speaker, "notify::volume", () =>
+ show(speaker.volume, speaker.volumeIcon),
+ )
+ }
+ }}
+ revealChild={visible()}
+ transitionType={Gtk.RevealerTransitionType.SLIDE_UP}
+ >
+ <box className="OSD">
+ <icon icon={iconName()} />
+ <levelbar valign={Gtk.Align.CENTER} widthRequest={100} value={value()} />
+ <label label={value(v => `${Math.floor(v * 100)}%`)} />
+ </box>
+ </revealer>
+ )
+}
+
+export default function OSD(monitor: Gdk.Monitor) {
+ const visible = Variable(false)
+
+ return (
+ <window
+ gdkmonitor={monitor}
+ className="OSD"
+ namespace="osd"
+ application={App}
+ layer={Astal.Layer.OVERLAY}
+ keymode={Astal.Keymode.ON_DEMAND}
+ anchor={Astal.WindowAnchor.BOTTOM}
+ >
+ <eventbox onClick={() => visible.set(false)}>
+ <OnScreenProgress visible={visible} />
+ </eventbox>
+ </window>
+ )
+}
diff --git a/examples/gtk3/js/osd/osd/brightness.ts b/examples/gtk3/js/osd/osd/brightness.ts
new file mode 100644
index 0000000..cf5060a
--- /dev/null
+++ b/examples/gtk3/js/osd/osd/brightness.ts
@@ -0,0 +1,45 @@
+import GObject, { register, property } from "astal/gobject"
+import { monitorFile, readFileAsync } from "astal/file"
+import { exec, execAsync } from "astal/process"
+
+const get = (args: string) => Number(exec(`brightnessctl ${args}`))
+const screen = exec(`bash -c "ls -w1 /sys/class/backlight | head -1"`)
+
+@register({ GTypeName: "Brightness" })
+export default class Brightness extends GObject.Object {
+ static instance: Brightness
+ static get_default() {
+ if (!this.instance)
+ this.instance = new Brightness()
+
+ return this.instance
+ }
+
+ #screenMax = get("max")
+ #screen = get("get") / (get("max") || 1)
+
+ @property(Number)
+ get screen() { return this.#screen }
+
+ set screen(percent) {
+ if (percent < 0)
+ percent = 0
+
+ if (percent > 1)
+ percent = 1
+
+ execAsync(`brightnessctl set ${Math.floor(percent * 100)}% -q`).then(() => {
+ this.#screen = percent
+ this.notify("screen")
+ })
+ }
+
+ constructor() {
+ super()
+ monitorFile(`/sys/class/backlight/${screen}/brightness`, async f => {
+ const v = await readFileAsync(f)
+ this.#screen = Number(v) / this.#screenMax
+ this.notify("screen")
+ })
+ }
+}
diff --git a/examples/gtk3/js/osd/style.scss b/examples/gtk3/js/osd/style.scss
new file mode 100644
index 0000000..ba6f06d
--- /dev/null
+++ b/examples/gtk3/js/osd/style.scss
@@ -0,0 +1 @@
+@use "./osd/OSD.scss";
diff --git a/examples/gtk3/js/popover/Popover.tsx b/examples/gtk3/js/popover/Popover.tsx
new file mode 100644
index 0000000..38ea01e
--- /dev/null
+++ b/examples/gtk3/js/popover/Popover.tsx
@@ -0,0 +1,90 @@
+import { Astal, Gdk, Gtk, Widget } from "astal/gtk3"
+
+const { TOP, BOTTOM, LEFT, RIGHT } = Astal.WindowAnchor
+
+type PopoverProps = Pick<
+ Widget.WindowProps,
+ | "name"
+ | "namespace"
+ | "className"
+ | "visible"
+ | "child"
+ | "marginBottom"
+ | "marginTop"
+ | "marginLeft"
+ | "marginRight"
+ | "halign"
+ | "valign"
+> & {
+ onClose?(self: Widget.Window): void
+}
+
+/**
+ * Full screen window widget where you can space the child widget
+ * using margins and alignment properties.
+ *
+ * NOTE: Child widgets will assume they can span across the full window width
+ * this means that setting `wrap` or `ellipsize` on labels for example will not work
+ * without explicitly setting its `max_width_chars` property.
+ * For a workaround see Popover2.tsx
+ */
+export default function Popover({
+ child,
+ marginBottom,
+ marginTop,
+ marginLeft,
+ marginRight,
+ halign = Gtk.Align.CENTER,
+ valign = Gtk.Align.CENTER,
+ onClose,
+ ...props
+}: PopoverProps) {
+ return (
+ <window
+ {...props}
+ css="background-color: transparent"
+ keymode={Astal.Keymode.EXCLUSIVE}
+ anchor={TOP | BOTTOM | LEFT | RIGHT}
+ exclusivity={Astal.Exclusivity.IGNORE}
+ onNotifyVisible={(self) => {
+ if (!self.visible) onClose?.(self)
+ }}
+ // close when click occurs otside of child
+ onButtonPressEvent={(self, event) => {
+ const [, _x, _y] = event.get_coords()
+ const { x, y, width, height } = self
+ .get_child()!
+ .get_allocation()
+
+ const xOut = _x < x || _x > x + width
+ const yOut = _y < y || _y > y + height
+
+ // clicked outside
+ if (xOut || yOut) {
+ self.visible = false
+ }
+ }}
+ // close when hitting Escape
+ onKeyPressEvent={(self, event: Gdk.Event) => {
+ if (event.get_keyval()[1] === Gdk.KEY_Escape) {
+ self.visible = false
+ }
+ }}
+ >
+ <box
+ // make sure click event does not bubble up
+ onButtonPressEvent={() => true}
+ // child can be positioned with `halign` `valign` and margins
+ expand
+ halign={halign}
+ valign={valign}
+ marginBottom={marginBottom}
+ marginTop={marginTop}
+ marginStart={marginLeft}
+ marginEnd={marginRight}
+ >
+ {child}
+ </box>
+ </window>
+ )
+}
diff --git a/examples/gtk3/js/popover/Popover2.tsx b/examples/gtk3/js/popover/Popover2.tsx
new file mode 100644
index 0000000..e058079
--- /dev/null
+++ b/examples/gtk3/js/popover/Popover2.tsx
@@ -0,0 +1,66 @@
+import { Astal, Gdk, Widget } from "astal/gtk3"
+import Variable from "astal/variable"
+
+type Popover2Props = Pick<
+ Widget.WindowProps,
+ | "name"
+ | "namespace"
+ | "className"
+ | "visible"
+ | "child"
+> & {
+ onClose?(self: Widget.Window): void
+}
+
+/**
+ * Full screen window where the child is positioned to center.
+ *
+ * NOTE: Workaround for the label wrap issue by padding the window
+ * with eventboxes and only anchoring to TOP | BOTTOM.
+ */
+export default function Popover2({
+ child,
+ onClose,
+ ...props
+}: Popover2Props) {
+ let win: Widget.Window
+
+ const width = Variable(1000)
+ const hide = () => (win.visible = false)
+
+ return (
+ <window
+ {...props}
+ setup={self => win = self}
+ css="background-color: transparent"
+ keymode={Astal.Keymode.EXCLUSIVE}
+ anchor={Astal.WindowAnchor.TOP | Astal.WindowAnchor.BOTTOM}
+ exclusivity={Astal.Exclusivity.IGNORE}
+ onNotifyVisible={(self) => {
+ // instead of anchoring to all sides we set the width explicitly
+ // otherwise label wrapping won't work correctly without setting their width
+ if (self.visible) {
+ width.set(self.get_current_monitor().workarea.width)
+ } else {
+ onClose?.(self)
+ }
+ }}
+ // close when hitting Escape
+ onKeyPressEvent={(self, event: Gdk.Event) => {
+ if (event.get_keyval()[1] === Gdk.KEY_Escape) {
+ self.visible = false
+ }
+ }}
+ >
+ <box>
+ <eventbox widthRequest={width(w => w / 2)} expand onClick={hide} />
+ <box hexpand={false} vertical>
+ <eventbox expand onClick={hide} />
+ {child}
+ <eventbox expand onClick={hide} />
+ </box>
+ <eventbox widthRequest={width(w => w / 2)} expand onClick={hide} />
+ </box>
+ </window>
+ )
+}
diff --git a/examples/gtk3/js/popover/app.tsx b/examples/gtk3/js/popover/app.tsx
new file mode 100644
index 0000000..5386b66
--- /dev/null
+++ b/examples/gtk3/js/popover/app.tsx
@@ -0,0 +1,72 @@
+import { App, Astal, Gtk } from "astal/gtk3"
+import { Variable } from "astal"
+import Popover from "./Popover"
+import Popover2 from "./Popover2"
+const { TOP, RIGHT, LEFT } = Astal.WindowAnchor
+
+const lorem = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean quis semper risus."
+
+App.start({
+ instanceName: "popup-example",
+ css: `
+ .popup {
+ background-color: @theme_bg_color;
+ box-shadow: 2px 3px 7px 0 rgba(0,0,0,0.4);
+ border-radius: 12px;
+ padding: 12px;
+ }
+ `,
+ main() {
+ const visible1 = Variable(false);
+ const visible2 = Variable(false);
+
+ const _popover1 = <Popover
+ className="Popup"
+ onClose={() => visible1.set(false)}
+ visible={visible1()}
+ marginTop={36}
+ marginRight={60}
+ valign={Gtk.Align.START}
+ halign={Gtk.Align.END}
+ >
+ <box className="popup" vertical>
+ {/* maxWidthChars is needed to make wrap work */}
+ <label label={lorem} wrap maxWidthChars={8} />
+ <button onClicked={() => visible1.set(false)}>
+ Click me to close the popup
+ </button>
+ </box>
+ </Popover>
+
+
+ const _popover2 = <Popover2
+ className="Popup"
+ onClose={() => visible2.set(false)}
+ visible={visible2()}
+ >
+ <box className="popup" vertical>
+ {/* maxWidthChars is needed, wrap will work as intended */}
+ <label label={lorem} wrap />
+ <button onClicked={() => visible2.set(false)}>
+ Click me to close the popup
+ </button>
+ </box>
+ </Popover2>
+
+ return (
+ <window
+ anchor={TOP | LEFT | RIGHT}
+ exclusivity={Astal.Exclusivity.EXCLUSIVE}
+ >
+ <box halign={Gtk.Align.END}>
+ <button onClicked={() => visible2.set(true)} halign={Gtk.Align.END}>
+ Click to open popover2
+ </button>
+ <button onClicked={() => visible1.set(true)} halign={Gtk.Align.END}>
+ Click to open popover
+ </button>
+ </box>
+ </window>
+ )
+ },
+})
diff --git a/examples/js/simple-bar/README.md b/examples/gtk3/js/simple-bar/README.md
index f92b20e..f92b20e 100644
--- a/examples/js/simple-bar/README.md
+++ b/examples/gtk3/js/simple-bar/README.md
diff --git a/examples/js/simple-bar/app.ts b/examples/gtk3/js/simple-bar/app.ts
index 4b7ea48..4b7ea48 100644
--- a/examples/js/simple-bar/app.ts
+++ b/examples/gtk3/js/simple-bar/app.ts
diff --git a/examples/lua/simple-bar/style.scss b/examples/gtk3/js/simple-bar/style.scss
index 1dcf729..f5f771a 100644
--- a/examples/lua/simple-bar/style.scss
+++ b/examples/gtk3/js/simple-bar/style.scss
@@ -13,31 +13,45 @@ window.Bar {
font-size: 1.1em;
font-weight: bold;
- button {
- all: unset;
- background-color: transparent;
+ 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);
+ &: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)
+ }
}
- &: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;
}
- }
- label {
- transition: 200ms;
- padding: 0 8px;
- margin: 2px;
- border-radius: $radius;
- border: 1pt solid transparent;
+ .focused label {
+ color: $accent;
+ border-color: $accent;
+ }
}
- .Workspaces .focused label {
- color: $accent;
- border-color: $accent;
+ .SysTray {
+ margin-right: 8px;
+
+ button {
+ padding: 0 4px;
+ }
}
.FocusedClient {
diff --git a/examples/js/simple-bar/widget/Bar.tsx b/examples/gtk3/js/simple-bar/widget/Bar.tsx
index 8a0126e..6592f6a 100644
--- a/examples/js/simple-bar/widget/Bar.tsx
+++ b/examples/gtk3/js/simple-bar/widget/Bar.tsx
@@ -11,33 +11,33 @@ import Tray from "gi://AstalTray"
function SysTray() {
const tray = Tray.get_default()
- return <box>
- {bind(tray, "items").as(items => items.map(item => {
- if (item.iconThemePath)
- App.add_icons(item.iconThemePath)
-
- const menu = item.create_menu()
-
- return <button
+ return <box className="SysTray">
+ {bind(tray, "items").as(items => items.map(item => (
+ <menubutton
tooltipMarkup={bind(item, "tooltipMarkup")}
- onDestroy={() => menu?.destroy()}
- onClickRelease={self => {
- menu?.popup_at_widget(self, Gdk.Gravity.SOUTH, Gdk.Gravity.NORTH, null)
- }}>
- <icon gIcon={bind(item, "gicon")} />
- </button>
- }))}
+ usePopover={false}
+ actionGroup={bind(item, "actionGroup").as(ag => ["dbusmenu", ag])}
+ menuModel={bind(item, "menuModel")}>
+ <icon gicon={bind(item, "gicon")} />
+ </menubutton>
+ )))}
</box>
}
function Wifi() {
- const { wifi } = Network.get_default()
+ const network = Network.get_default()
+ const wifi = bind(network, "wifi")
+
+ return <box visible={wifi.as(Boolean)}>
+ {wifi.as(wifi => wifi && (
+ <icon
+ tooltipText={bind(wifi, "ssid").as(String)}
+ className="Wifi"
+ icon={bind(wifi, "iconName")}
+ />
+ ))}
+ </box>
- return <icon
- tooltipText={bind(wifi, "ssid").as(String)}
- className="Wifi"
- icon={bind(wifi, "iconName")}
- />
}
function AudioSlider() {
@@ -95,6 +95,7 @@ function Workspaces() {
return <box className="Workspaces">
{bind(hypr, "workspaces").as(wss => wss
+ .filter(ws => !(ws.id >= -99 && ws.id <= -2)) // filter out special workspaces
.sort((a, b) => a.id - b.id)
.map(ws => (
<button
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/lua/simple-bar/README.md b/examples/gtk3/lua/simple-bar/README.md
index 48cc27c..48cc27c 100644
--- a/examples/lua/simple-bar/README.md
+++ b/examples/gtk3/lua/simple-bar/README.md
diff --git a/examples/lua/simple-bar/init.lua b/examples/gtk3/lua/simple-bar/init.lua
index 8c412fb..8c412fb 100644
--- a/examples/lua/simple-bar/init.lua
+++ b/examples/gtk3/lua/simple-bar/init.lua
diff --git a/examples/lua/simple-bar/lib.lua b/examples/gtk3/lua/simple-bar/lib.lua
index d94db5c..6f2dcea 100644
--- a/examples/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/js/simple-bar/style.scss b/examples/gtk3/lua/simple-bar/style.scss
index 1dcf729..f5f771a 100644
--- a/examples/js/simple-bar/style.scss
+++ b/examples/gtk3/lua/simple-bar/style.scss
@@ -13,31 +13,45 @@ window.Bar {
font-size: 1.1em;
font-weight: bold;
- button {
- all: unset;
- background-color: transparent;
+ 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);
+ &: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)
+ }
}
- &: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;
}
- }
- label {
- transition: 200ms;
- padding: 0 8px;
- margin: 2px;
- border-radius: $radius;
- border: 1pt solid transparent;
+ .focused label {
+ color: $accent;
+ border-color: $accent;
+ }
}
- .Workspaces .focused label {
- color: $accent;
- border-color: $accent;
+ .SysTray {
+ margin-right: 8px;
+
+ button {
+ padding: 0 4px;
+ }
}
.FocusedClient {
diff --git a/examples/lua/simple-bar/widget/Bar.lua b/examples/gtk3/lua/simple-bar/widget/Bar.lua
index bf230bb..3f685a2 100644
--- a/examples/lua/simple-bar/widget/Bar.lua
+++ b/examples/gtk3/lua/simple-bar/widget/Bar.lua
@@ -1,8 +1,6 @@
local astal = require("astal")
-local App = require("astal.gtk3.app")
local Widget = require("astal.gtk3.widget")
local Variable = astal.Variable
-local Gdk = astal.require("Gdk", "3.0")
local GLib = astal.require("GLib")
local bind = astal.bind
local Mpris = astal.require("AstalMpris")
@@ -17,28 +15,18 @@ 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)
- if item.icon_theme_path ~= nil then
- App:add_icons(item.icon_theme_path)
- end
-
- local menu = item:create_menu()
-
- return Widget.Button({
+ return Widget.MenuButton({
tooltip_markup = bind(item, "tooltip_markup"),
- on_destroy = function()
- if menu ~= nil then
- menu:destroy()
- end
- end,
- on_click_release = function(self)
- if menu ~= nil then
- menu:popup_at_widget(self, Gdk.Gravity.SOUTH, Gdk.Gravity.NORTH, nil)
- end
- end,
+ use_popover = false,
+ menu_model = bind(item, "menu-model"),
+ action_group = bind(item, "action-group"):as(
+ function(ag) return { "dbusmenu", ag } end
+ ),
Widget.Icon({
- g_icon = bind(item, "gicon"),
+ gicon = bind(item, "gicon"),
}),
})
end)
@@ -53,21 +41,32 @@ 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
local function Wifi()
- local wifi = Network.get_default().wifi
+ local network = Network.get_default()
+ local wifi = bind(network, "wifi")
- return Widget.Icon({
- tooltip_text = bind(wifi, "ssid"):as(tostring),
- class_name = "Wifi",
- icon = bind(wifi, "icon-name"),
+ 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
@@ -82,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"),
}),
})
@@ -100,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
@@ -116,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
@@ -134,50 +137,50 @@ 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)
- 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),
- })
+ 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)
+ 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",
@@ -189,10 +192,10 @@ return function(gdkmonitor)
}),
Widget.Box({
halign = "END",
+ SysTray(),
Wifi(),
AudioSlider(),
BatteryLevel(),
- SysTray(),
Time("%H:%M - %A %e."),
}),
}),
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/examples/py/.gitignore b/examples/gtk3/py/.gitignore
index c18dd8d..c18dd8d 100644
--- a/examples/py/.gitignore
+++ b/examples/gtk3/py/.gitignore
diff --git a/examples/py/simple-bar/README.md b/examples/gtk3/py/simple-bar/README.md
index 48cc27c..48cc27c 100644
--- a/examples/py/simple-bar/README.md
+++ b/examples/gtk3/py/simple-bar/README.md
diff --git a/examples/py/simple-bar/__init__.py b/examples/gtk3/py/simple-bar/__init__.py
index e69de29..e69de29 100644
--- a/examples/py/simple-bar/__init__.py
+++ b/examples/gtk3/py/simple-bar/__init__.py
diff --git a/examples/py/simple-bar/app.py b/examples/gtk3/py/simple-bar/app.py
index 17b6782..d95dc0e 100755
--- a/examples/py/simple-bar/app.py
+++ b/examples/gtk3/py/simple-bar/app.py
@@ -1,6 +1,7 @@
#!/usr/bin/env python3
import sys
import versions
+import subprocess
from gi.repository import AstalIO, Astal, Gio
from widget.Bar import Bar
from pathlib import Path
@@ -18,9 +19,8 @@ class App(Astal.Application):
def do_activate(self) -> None:
self.hold()
- AstalIO.Process.execv(["sass", scss, css])
+ subprocess.run(["sass", scss, css])
self.apply_css(css, True)
- print("hello")
for mon in self.get_monitors():
self.add_window(Bar(mon))
@@ -30,7 +30,7 @@ app = App(instance_name=instance_name)
if __name__ == "__main__":
try:
- print(app.acquire_socket())
+ app.acquire_socket()
app.run(None)
except Exception as e:
print(AstalIO.send_message(instance_name, "".join(sys.argv[1:])))
diff --git a/examples/vala/simple-bar/style.scss b/examples/gtk3/py/simple-bar/style.scss
index 1dcf729..f5f771a 100644
--- a/examples/vala/simple-bar/style.scss
+++ b/examples/gtk3/py/simple-bar/style.scss
@@ -13,31 +13,45 @@ window.Bar {
font-size: 1.1em;
font-weight: bold;
- button {
- all: unset;
- background-color: transparent;
+ 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);
+ &: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)
+ }
}
- &: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;
}
- }
- label {
- transition: 200ms;
- padding: 0 8px;
- margin: 2px;
- border-radius: $radius;
- border: 1pt solid transparent;
+ .focused label {
+ color: $accent;
+ border-color: $accent;
+ }
}
- .Workspaces .focused label {
- color: $accent;
- border-color: $accent;
+ .SysTray {
+ margin-right: 8px;
+
+ button {
+ padding: 0 4px;
+ }
}
.FocusedClient {
diff --git a/examples/py/simple-bar/versions.py b/examples/gtk3/py/simple-bar/versions.py
index 0e57708..0e57708 100644
--- a/examples/py/simple-bar/versions.py
+++ b/examples/gtk3/py/simple-bar/versions.py
diff --git a/examples/py/simple-bar/widget/Bar.py b/examples/gtk3/py/simple-bar/widget/Bar.py
index 3b09dce..555ab85 100644
--- a/examples/py/simple-bar/widget/Bar.py
+++ b/examples/gtk3/py/simple-bar/widget/Bar.py
@@ -32,7 +32,8 @@ class Workspaces(Gtk.Box):
child.destroy()
for ws in hypr.get_workspaces():
- self.add(self.button(ws))
+ if not (ws.get_id() >= -99 and ws.get_id() <= -2): # filter out special workspaces
+ self.add(self.button(ws))
def button(self, ws):
hypr = Hyprland.get_default()
@@ -108,6 +109,7 @@ class Media(Gtk.Box):
class SysTray(Gtk.Box):
def __init__(self) -> None:
super().__init__()
+ Astal.widget_set_class_names(self, ["SysTray"])
self.items = {}
tray = Tray.get_default()
tray.connect("item_added", self.add_item)
@@ -118,33 +120,22 @@ class SysTray(Gtk.Box):
return
item = Tray.get_default().get_item(id)
- theme = item.get_icon_theme_path()
-
- if theme is not None:
- from app import app
-
- app.add_icons(theme)
-
- menu = item.create_menu()
- btn = Astal.Button(visible=True)
+ btn = Gtk.MenuButton(use_popover=False, visible=True)
icon = Astal.Icon(visible=True)
- def on_clicked(btn):
- if menu:
- menu.popup_at_widget(btn, Gdk.Gravity.SOUTH, Gdk.Gravity.NORTH, None)
+ item.bind_property("tooltip-markup", btn, "tooltip-markup", SYNC)
+ item.bind_property("gicon", icon, "gicon", SYNC)
+ item.bind_property("menu-model", btn, "menu-model", SYNC)
+ btn.insert_action_group("dbusmenu", item.get_action_group())
- def on_destroy(btn):
- if menu:
- menu.destroy()
+ def on_action_group(*args):
+ btn.insert_action_group("dbusmenu", item.get_action_group())
- btn.connect("clicked", on_clicked)
- btn.connect("destroy", on_destroy)
+ item.connect("notify::action-group", on_action_group)
- item.bind_property("tooltip-markup", btn, "tooltip-markup", SYNC)
- item.bind_property("gicon", icon, "gicon", SYNC)
+ btn.add(icon)
self.add(btn)
self.items[id] = btn
- self.show_all()
def remove_item(self, _: Tray.Tray, id: str):
if id in self.items:
@@ -156,8 +147,9 @@ class Wifi(Astal.Icon):
super().__init__()
Astal.widget_set_class_names(self, ["Wifi"])
wifi = Network.get_default().get_wifi()
- wifi.bind_property("ssid", self, "tooltip-text", SYNC)
- wifi.bind_property("icon-name", self, "icon", SYNC)
+ if wifi:
+ wifi.bind_property("ssid", self, "tooltip-text", SYNC)
+ wifi.bind_property("icon-name", self, "icon", SYNC)
class AudioSlider(Gtk.Box):
diff --git a/examples/py/simple-bar/widget/__init__.py b/examples/gtk3/py/simple-bar/widget/__init__.py
index e69de29..e69de29 100644
--- a/examples/py/simple-bar/widget/__init__.py
+++ b/examples/gtk3/py/simple-bar/widget/__init__.py
diff --git a/examples/vala/simple-bar/README.md b/examples/gtk3/vala/simple-bar/README.md
index 48cc27c..48cc27c 100644
--- a/examples/vala/simple-bar/README.md
+++ b/examples/gtk3/vala/simple-bar/README.md
diff --git a/examples/vala/simple-bar/app.in.vala b/examples/gtk3/vala/simple-bar/app.in.vala
index b04a1fa..b04a1fa 100644
--- a/examples/vala/simple-bar/app.in.vala
+++ b/examples/gtk3/vala/simple-bar/app.in.vala
diff --git a/examples/vala/simple-bar/flake.nix b/examples/gtk3/vala/simple-bar/flake.nix
index d13c649..d13c649 100644
--- a/examples/vala/simple-bar/flake.nix
+++ b/examples/gtk3/vala/simple-bar/flake.nix
diff --git a/examples/vala/simple-bar/meson.build b/examples/gtk3/vala/simple-bar/meson.build
index 10f5dd2..5a0ef4c 100644
--- a/examples/vala/simple-bar/meson.build
+++ b/examples/gtk3/vala/simple-bar/meson.build
@@ -21,22 +21,15 @@ pkgconfig_deps = [
# needed for GLib.Math
deps = pkgconfig_deps + meson.get_compiler('c').find_library('m')
-custom_target(
- 'style.css',
- command: [
- find_program('sass'),
- meson.project_source_root() / 'style.scss',
- '@OUTPUT@',
- ],
- output: 'style.css',
- install: true,
- install_dir: libdir,
-)
-
main = configure_file(
input: 'app.in.vala',
output: 'app.vala',
- configuration: {'STYLE': libdir / 'style.css'},
+ configuration: {
+ 'STYLE': run_command(
+ find_program('sass'),
+ meson.project_source_root() / 'style.scss',
+ ).stdout(),
+ },
)
sources = files(
diff --git a/examples/py/simple-bar/style.scss b/examples/gtk3/vala/simple-bar/style.scss
index 1dcf729..f5f771a 100644
--- a/examples/py/simple-bar/style.scss
+++ b/examples/gtk3/vala/simple-bar/style.scss
@@ -13,31 +13,45 @@ window.Bar {
font-size: 1.1em;
font-weight: bold;
- button {
- all: unset;
- background-color: transparent;
+ 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);
+ &: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)
+ }
}
- &: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;
}
- }
- label {
- transition: 200ms;
- padding: 0 8px;
- margin: 2px;
- border-radius: $radius;
- border: 1pt solid transparent;
+ .focused label {
+ color: $accent;
+ border-color: $accent;
+ }
}
- .Workspaces .focused label {
- color: $accent;
- border-color: $accent;
+ .SysTray {
+ margin-right: 8px;
+
+ button {
+ padding: 0 4px;
+ }
}
.FocusedClient {
diff --git a/examples/vala/simple-bar/widget/Bar.vala b/examples/gtk3/vala/simple-bar/widget/Bar.vala
index ba4062c..28b32ef 100644
--- a/examples/vala/simple-bar/widget/Bar.vala
+++ b/examples/gtk3/vala/simple-bar/widget/Bar.vala
@@ -10,8 +10,12 @@ class Workspaces : Gtk.Box {
foreach (var child in get_children())
child.destroy();
- foreach (var ws in hypr.workspaces)
- add(button(ws));
+ foreach (var ws in hypr.workspaces) {
+ // filter out special workspaces
+ if (!(ws.id >= -99 && ws.id <= -2)) {
+ add(button(ws));
+ }
+ }
}
Gtk.Button button(AstalHyprland.Workspace ws) {
@@ -103,6 +107,7 @@ class SysTray : Gtk.Box {
AstalTray.Tray tray = AstalTray.get_default();
public SysTray() {
+ Astal.widget_set_class_names(this, { "SysTray" });
tray.item_added.connect(add_item);
tray.item_removed.connect(remove_item);
}
@@ -112,26 +117,19 @@ class SysTray : Gtk.Box {
return;
var item = tray.get_item(id);
+ var btn = new Gtk.MenuButton() { use_popover = false, visible = true };
+ var icon = new Astal.Icon() { visible = true };
- var menu = item.create_menu();
- var btn = new Astal.Button();
- var icon = new Astal.Icon();
-
- btn.clicked.connect(() => {
- if (menu != null)
- menu.popup_at_widget(this, Gdk.Gravity.SOUTH, Gdk.Gravity.NORTH, null);
- });
-
- btn.destroy.connect(() => {
- if (menu != null)
- menu.destroy();
+ item.bind_property("tooltip-markup", btn, "tooltip-markup", BindingFlags.SYNC_CREATE);
+ item.bind_property("gicon", icon, "gicon", BindingFlags.SYNC_CREATE);
+ item.bind_property("menu-model", btn, "menu-model", BindingFlags.SYNC_CREATE);
+ btn.insert_action_group("dbusmenu", item.action_group);
+ item.notify["action-group"].connect(() => {
+ btn.insert_action_group("dbusmenu", item.action_group);
});
- item.bind_property("tooltip-markup", btn, "tooltip-markup", BindingFlags.SYNC_CREATE);
- item.bind_property("gicon", icon, "g-icon", BindingFlags.SYNC_CREATE);
btn.add(icon);
add(btn);
- btn.show_all();
items.set(id, btn);
}
@@ -145,9 +143,11 @@ class SysTray : Gtk.Box {
class Wifi : Astal.Icon {
public Wifi() {
Astal.widget_set_class_names(this, {"Wifi"});
- var wifi = AstalNetwork.get_default().wifi;
- wifi.bind_property("ssid", this, "tooltip-text", BindingFlags.SYNC_CREATE);
- wifi.bind_property("icon-name", this, "icon", BindingFlags.SYNC_CREATE);
+ var wifi = AstalNetwork.get_default().get_wifi();
+ if (wifi != null) {
+ wifi.bind_property("ssid", this, "tooltip-text", BindingFlags.SYNC_CREATE);
+ wifi.bind_property("icon-name", this, "icon", BindingFlags.SYNC_CREATE);
+ }
}
}