summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/default.nix16
-rw-r--r--flake.nix1
-rw-r--r--lang/gjs/meson.build3
-rw-r--r--lang/gjs/package.json2
-rw-r--r--lang/gjs/src/_app.ts122
-rw-r--r--lang/gjs/src/gobject.ts10
-rw-r--r--lang/gjs/src/gtk3/app.ts104
-rw-r--r--lang/gjs/src/gtk4/app.ts8
-rw-r--r--lang/gjs/src/gtk4/astalify.ts2
-rw-r--r--lang/gjs/src/gtk4/index.ts10
-rw-r--r--lang/gjs/src/gtk4/jsx-runtime.ts2
-rw-r--r--lang/gjs/src/gtk4/widget.ts1
-rw-r--r--lang/gjs/tsconfig.json2
-rw-r--r--lib/astal/gtk3/src/widget/window.vala17
-rw-r--r--lib/astal/gtk4/gir.py58
-rw-r--r--lib/astal/gtk4/meson.build18
-rw-r--r--lib/astal/gtk4/src/application.vala241
-rw-r--r--lib/astal/gtk4/src/config.vala.in6
-rw-r--r--lib/astal/gtk4/src/meson.build89
-rw-r--r--lib/astal/gtk4/src/widget/window.vala261
-rw-r--r--lib/astal/gtk4/version1
-rw-r--r--nix/devshell.nix1
22 files changed, 857 insertions, 118 deletions
diff --git a/docs/default.nix b/docs/default.nix
index 1370fc6..ff7e8c9 100644
--- a/docs/default.nix
+++ b/docs/default.nix
@@ -58,6 +58,11 @@
description = "The GTK toolkit";
docs_url = "https://docs.gtk.org/gtk3/";
};
+ "Gtk-4.0" = {
+ name = "Gtk";
+ description = "The GTK toolkit";
+ docs_url = "https://docs.gtk.org/gtk4/";
+ };
"AstalIO-0.1" = {
name = "AstalIO";
description = "Astal Core library";
@@ -101,7 +106,9 @@ in
json-glib
gobject-introspection
gtk3
+ gtk4
gtk-layer-shell
+ gtk4-layer-shell
gdk-pixbuf
libdbusmenu-gtk3
wireplumber
@@ -129,6 +136,15 @@ in
dependencies = {inherit (dependency) "AstalIO-0.1" "Gtk-3.0";};
}}
${genLib {
+ flakepkg = "astal4";
+ gir = "";
+ api-ver = "4.0";
+ browse = "astal/gtk4";
+ description = "Astal GTK4 widget library";
+ version = ../lib/astal/gtk4/version;
+ dependencies = {inherit (dependency) "AstalIO-0.1" "Gtk-4.0";};
+ }}
+ ${genLib {
flakepkg = "apps";
gir = "Apps";
description = "Application query library";
diff --git a/flake.nix b/flake.nix
index 2f7189b..734a110 100644
--- a/flake.nix
+++ b/flake.nix
@@ -49,6 +49,7 @@
io = mkPkg "astal" ./lib/astal/io [];
astal3 = mkPkg "astal" ./lib/astal/gtk3 [self.packages.${system}.io gtk3 gtk-layer-shell];
+ astal4 = mkPkg "astal" ./lib/astal/gtk4 [self.packages.${system}.io gtk4 gtk4-layer-shell];
apps = mkPkg "astal-apps" ./lib/apps [json-glib];
auth = mkPkg "astal-auth" ./lib/auth [pam];
battery = mkPkg "astal-battery" ./lib/battery [json-glib];
diff --git a/lang/gjs/meson.build b/lang/gjs/meson.build
index 388b301..7134dc0 100644
--- a/lang/gjs/meson.build
+++ b/lang/gjs/meson.build
@@ -14,9 +14,10 @@ install_data(
'src/process.ts',
'src/time.ts',
'src/variable.ts',
+ 'src/_app.ts',
],
install_dir: dest,
)
install_subdir('src/gtk3', install_dir: dest)
-# install_subdir('src/gtk4', install_dir: dest)
+install_subdir('src/gtk4', install_dir: dest)
diff --git a/lang/gjs/package.json b/lang/gjs/package.json
index 9f44388..da88d90 100644
--- a/lang/gjs/package.json
+++ b/lang/gjs/package.json
@@ -17,7 +17,7 @@
"exports": {
".": "./index.ts",
"./gtk3": "./src/gtk3/index.ts",
- "./gtk4": "./src/gtk3/index.ts",
+ "./gtk4": "./src/gtk4/index.ts",
"./binding": "./src/binding.ts",
"./file": "./src/file.ts",
"./gobject": "./src/gobject.ts",
diff --git a/lang/gjs/src/_app.ts b/lang/gjs/src/_app.ts
new file mode 100644
index 0000000..82e8bb5
--- /dev/null
+++ b/lang/gjs/src/_app.ts
@@ -0,0 +1,122 @@
+import { setConsoleLogDomain } from "console"
+import { exit, programArgs } from "system"
+import IO from "gi://AstalIO"
+import GObject from "gi://GObject"
+import Gio from "gi://Gio?version=2.0"
+import type Astal3 from "gi://Astal?version=3.0"
+import type Astal4 from "gi://Astal?version=4.0"
+
+type Config = Partial<{
+ instanceName: string
+ css: string
+ icons: string
+ gtkTheme: string
+ iconTheme: string
+ cursorTheme: string
+ hold: boolean
+ requestHandler(request: string, res: (response: any) => void): void
+ main(...args: string[]): void
+ client(message: (msg: string) => string, ...args: string[]): void
+}>
+
+interface Astal3JS extends Astal3.Application {
+ eval(body: string): Promise<any>
+ requestHandler: Config["requestHandler"]
+ apply_css(style: string, reset?: boolean): void
+ quit(code?: number): void
+ start(config?: Config): void
+}
+
+interface Astal4JS extends Astal4.Application {
+ eval(body: string): Promise<any>
+ requestHandler?: Config["requestHandler"]
+ apply_css(style: string, reset?: boolean): void
+ quit(code?: number): void
+ start(config?: Config): void
+}
+
+type App3 = typeof Astal3.Application
+type App4 = typeof Astal4.Application
+
+export function mkApp<App extends App3>(App: App): Astal3JS
+export function mkApp<App extends App4>(App: App): Astal4JS
+
+export function mkApp(App: App3 | App4) {
+ return new (class AstalJS extends App {
+ static { GObject.registerClass({ GTypeName: "AstalJS" }, this as any) }
+
+ eval(body: string): Promise<any> {
+ return new Promise((res, rej) => {
+ try {
+ const fn = Function(`return (async function() {
+ ${body.includes(";") ? body : `return ${body};`}
+ })`)
+ fn()().then(res).catch(rej)
+ }
+ catch (error) {
+ rej(error)
+ }
+ })
+ }
+
+ requestHandler?: Config["requestHandler"]
+
+ vfunc_request(msg: string, conn: Gio.SocketConnection): void {
+ if (typeof this.requestHandler === "function") {
+ this.requestHandler(msg, (response) => {
+ IO.write_sock(conn, String(response), (_, res) =>
+ IO.write_sock_finish(res),
+ )
+ })
+ }
+ else {
+ super.vfunc_request(msg, conn)
+ }
+ }
+
+ apply_css(style: string, reset = false) {
+ super.apply_css(style, reset)
+ }
+
+ quit(code?: number): void {
+ super.quit()
+ exit(code ?? 0)
+ }
+
+ start({ requestHandler, css, hold, main, client, icons, ...cfg }: Config = {}) {
+ const app = this as unknown as InstanceType<App3 | App4>
+
+ client ??= () => {
+ print(`Astal instance "${app.instanceName}" already running`)
+ exit(1)
+ }
+
+ Object.assign(this, cfg)
+ setConsoleLogDomain(app.instanceName)
+
+ this.requestHandler = requestHandler
+ app.connect("activate", () => {
+ main?.(...programArgs)
+ })
+
+ try {
+ app.acquire_socket()
+ }
+ catch (error) {
+ return client(msg => IO.send_message(app.instanceName, msg)!, ...programArgs)
+ }
+
+ if (css)
+ this.apply_css(css, false)
+
+ if (icons)
+ app.add_icons(icons)
+
+ hold ??= true
+ if (hold)
+ app.hold()
+
+ app.runAsync([])
+ }
+ })
+}
diff --git a/lang/gjs/src/gobject.ts b/lang/gjs/src/gobject.ts
index aeb3d9e..59dd62a 100644
--- a/lang/gjs/src/gobject.ts
+++ b/lang/gjs/src/gobject.ts
@@ -40,6 +40,12 @@ type MetaInfo = GObject.MetaInfo<never, Array<{ $gtype: GObject.GType }>, never>
export function register(options: MetaInfo = {}) {
return function (cls: GObjectConstructor) {
+ const t = options.Template
+ if (typeof t === "string" && !t.startsWith("resource://") && !t.startsWith("file://")) {
+ // assume xml template
+ options.Template = new TextEncoder().encode(t)
+ }
+
GObject.registerClass({
Signals: { ...cls[meta]?.Signals },
Properties: { ...cls[meta]?.Properties },
@@ -95,10 +101,10 @@ export function property(declaration: PropertyDeclaration = Object) {
}
export function signal(...params: Array<{ $gtype: GObject.GType } | typeof Object>):
- (target: any, signal: any, desc?: PropertyDescriptor) => void
+(target: any, signal: any, desc?: PropertyDescriptor) => void
export function signal(declaration?: SignalDeclaration):
- (target: any, signal: any, desc?: PropertyDescriptor) => void
+(target: any, signal: any, desc?: PropertyDescriptor) => void
export function signal(
declaration?: SignalDeclaration | { $gtype: GObject.GType } | typeof Object,
diff --git a/lang/gjs/src/gtk3/app.ts b/lang/gjs/src/gtk3/app.ts
index 1191dc4..aa43641 100644
--- a/lang/gjs/src/gtk3/app.ts
+++ b/lang/gjs/src/gtk3/app.ts
@@ -1,105 +1,7 @@
-import IO from "gi://AstalIO"
-import GObject from "gi://GObject"
-import Astal from "gi://Astal?version=3.0"
-import Gio from "gi://Gio?version=2.0"
import Gtk from "gi://Gtk?version=3.0"
+import Astal from "gi://Astal?version=3.0"
+import { mkApp } from "../_app"
Gtk.init(null)
-type RequestHandler = {
- (request: string, res: (response: any) => void): void
-}
-
-type Config = Partial<{
- icons: string
- instanceName: string
- gtkTheme: string
- iconTheme: string
- cursorTheme: string
- css: string
- requestHandler: RequestHandler
- main(...args: string[]): void
- client(message: (msg: string) => string, ...args: string[]): void
- hold: boolean
-}>
-
-import { setConsoleLogDomain } from "console"
-import { exit, programArgs } from "system"
-
-export default new (class AstalJS extends Astal.Application {
- static { GObject.registerClass({ GTypeName: "AstalJS" }, this) }
-
- eval(body: string): Promise<any> {
- return new Promise((res, rej) => {
- try {
- const fn = Function(`return (async function() {
- ${body.includes(";") ? body : `return ${body};`}
- })`)
- fn()()
- .then(res)
- .catch(rej)
- }
- catch (error) {
- rej(error)
- }
- })
- }
-
- requestHandler?: RequestHandler
-
- vfunc_request(msg: string, conn: Gio.SocketConnection): void {
- if (typeof this.requestHandler === "function") {
- this.requestHandler(msg, (response) => {
- IO.write_sock(conn, String(response), (_, res) =>
- IO.write_sock_finish(res),
- )
- })
- }
- else {
- super.vfunc_request(msg, conn)
- }
- }
-
- apply_css(style: string, reset = false) {
- super.apply_css(style, reset)
- }
-
- quit(code?: number): void {
- super.quit()
- exit(code ?? 0)
- }
-
- start({ requestHandler, css, hold, main, client, icons, ...cfg }: Config = {}) {
- client ??= () => {
- print(`Astal instance "${this.instanceName}" already running`)
- exit(1)
- }
-
- Object.assign(this, cfg)
- setConsoleLogDomain(this.instanceName)
-
- this.requestHandler = requestHandler
- this.connect("activate", () => {
- main?.(...programArgs)
- })
-
- try {
- this.acquire_socket()
- }
- catch (error) {
- return client(msg => IO.send_message(this.instanceName, msg)!, ...programArgs)
- }
-
- if (css)
- this.apply_css(css, false)
-
- if (icons)
- this.add_icons(icons)
-
- hold ??= true
- if (hold)
- this.hold()
-
- this.runAsync([])
- }
-})
+export default mkApp(Astal.Application)
diff --git a/lang/gjs/src/gtk4/app.ts b/lang/gjs/src/gtk4/app.ts
index d931f73..1c51772 100644
--- a/lang/gjs/src/gtk4/app.ts
+++ b/lang/gjs/src/gtk4/app.ts
@@ -1 +1,7 @@
-// TODO: gtk4
+import Gtk from "gi://Gtk?version=4.0"
+import Astal from "gi://Astal?version=4.0"
+import { mkApp } from "../_app"
+
+Gtk.init()
+
+export default mkApp(Astal.Application)
diff --git a/lang/gjs/src/gtk4/astalify.ts b/lang/gjs/src/gtk4/astalify.ts
index d931f73..6c8ea4d 100644
--- a/lang/gjs/src/gtk4/astalify.ts
+++ b/lang/gjs/src/gtk4/astalify.ts
@@ -1 +1 @@
-// TODO: gtk4
+// TODO:
diff --git a/lang/gjs/src/gtk4/index.ts b/lang/gjs/src/gtk4/index.ts
index d931f73..3b1f737 100644
--- a/lang/gjs/src/gtk4/index.ts
+++ b/lang/gjs/src/gtk4/index.ts
@@ -1 +1,9 @@
-// TODO: gtk4
+import Astal from "gi://Astal?version=4.0"
+import Gtk from "gi://Gtk?version=4.0"
+import Gdk from "gi://Gdk?version=4.0"
+// import astalify, { type ConstructProps } from "./astalify.js"
+
+export { Astal, Gtk, Gdk }
+export { default as App } from "./app.js"
+// export { astalify, ConstructProps }
+// export * as Widget from "./widget.js"
diff --git a/lang/gjs/src/gtk4/jsx-runtime.ts b/lang/gjs/src/gtk4/jsx-runtime.ts
index d931f73..6c8ea4d 100644
--- a/lang/gjs/src/gtk4/jsx-runtime.ts
+++ b/lang/gjs/src/gtk4/jsx-runtime.ts
@@ -1 +1 @@
-// TODO: gtk4
+// TODO:
diff --git a/lang/gjs/src/gtk4/widget.ts b/lang/gjs/src/gtk4/widget.ts
new file mode 100644
index 0000000..6c8ea4d
--- /dev/null
+++ b/lang/gjs/src/gtk4/widget.ts
@@ -0,0 +1 @@
+// TODO:
diff --git a/lang/gjs/tsconfig.json b/lang/gjs/tsconfig.json
index 171e75b..4e57e37 100644
--- a/lang/gjs/tsconfig.json
+++ b/lang/gjs/tsconfig.json
@@ -11,6 +11,8 @@
"include": [
"@girs",
"src/*.ts",
+ // "src/gtk3/*",
+ // "src/gtk4/*",
"index.ts",
]
}
diff --git a/lib/astal/gtk3/src/widget/window.vala b/lib/astal/gtk3/src/widget/window.vala
index 9287200..11d542d 100644
--- a/lib/astal/gtk3/src/widget/window.vala
+++ b/lib/astal/gtk3/src/widget/window.vala
@@ -47,28 +47,27 @@ public enum Astal.Keymode {
* Subclass of [[email protected]] which integrates GtkLayerShell as class fields.
*/
public class Astal.Window : Gtk.Window {
- private static bool check(string action) {
+ private InhibitManager? inhibit_manager;
+ private Inhibitor? inhibitor;
+
+ private bool check(string action) {
if (!is_supported()) {
critical(@"can not $action on window: layer shell not supported");
print("tip: running from an xwayland terminal can cause this, for example VsCode");
return true;
}
+ if (!is_layer_window(this)) {
+ init_for_window(this);
+ }
return false;
}
- private InhibitManager? inhibit_manager;
- private Inhibitor? inhibitor;
-
construct {
- if (check("initialize layer shell"))
- return;
-
// If the window has no size allocatoted when it gets mapped.
// It won't show up later either when it size changes by adding children.
height_request = 1;
width_request = 1;
-
- init_for_window(this);
+ check("initialize layer shell");
inhibit_manager = InhibitManager.get_default();
}
diff --git a/lib/astal/gtk4/gir.py b/lib/astal/gtk4/gir.py
new file mode 100644
index 0000000..9ef680f
--- /dev/null
+++ b/lib/astal/gtk4/gir.py
@@ -0,0 +1,58 @@
+"""
+Vala's generated gir does not contain comments,
+so we use valadoc to generate them. However, they are formatted
+for valadoc and not gi-docgen so we need to fix it.
+"""
+
+import xml.etree.ElementTree as ET
+import html
+import sys
+import subprocess
+
+
+def fix_gir(name: str, gir: str, out: str):
+ namespaces = {
+ "": "http://www.gtk.org/introspection/core/1.0",
+ "c": "http://www.gtk.org/introspection/c/1.0",
+ "glib": "http://www.gtk.org/introspection/glib/1.0",
+ }
+ for prefix, uri in namespaces.items():
+ ET.register_namespace(prefix, uri)
+
+ tree = ET.parse(gir)
+ root = tree.getroot()
+
+ for doc in root.findall(".//doc", namespaces):
+ if doc.text:
+ doc.text = (
+ html.unescape(doc.text).replace("<para>", "").replace("</para>", "")
+ )
+
+ if (inc := root.find("c:include", namespaces)) is not None:
+ inc.set("name", f"{name}.h")
+ else:
+ print("no c:include tag found", file=sys.stderr)
+ exit(1)
+
+ tree.write(out, encoding="utf-8", xml_declaration=True)
+
+
+def valadoc(name: str, gir: str, args: list[str]):
+ cmd = ["valadoc", "-o", "docs", "--package-name", name, "--gir", gir, *args]
+ try:
+ subprocess.run(cmd, check=True, text=True, capture_output=True)
+ except subprocess.CalledProcessError as e:
+ print(e.stderr, file=sys.stderr)
+ exit(1)
+
+
+if __name__ == "__main__":
+ name = sys.argv[1]
+ in_out = sys.argv[2].split(":")
+ args = sys.argv[3:]
+
+ gir = in_out[0]
+ out = in_out[1] if len(in_out) > 1 else gir
+
+ valadoc(name, gir, args)
+ fix_gir(name, gir, out)
diff --git a/lib/astal/gtk4/meson.build b/lib/astal/gtk4/meson.build
new file mode 100644
index 0000000..48d3058
--- /dev/null
+++ b/lib/astal/gtk4/meson.build
@@ -0,0 +1,18 @@
+project(
+ 'astal',
+ 'vala',
+ 'c',
+ version: run_command('cat', join_paths(meson.project_source_root(), 'version')).stdout().strip(),
+ meson_version: '>= 0.62.0',
+ default_options: [
+ 'warning_level=2',
+ 'werror=false',
+ 'c_std=gnu11',
+ ],
+)
+
+libdir = get_option('prefix') / get_option('libdir')
+pkgdatadir = get_option('prefix') / get_option('datadir') / 'astal'
+girpy = files('gir.py')
+
+subdir('src')
diff --git a/lib/astal/gtk4/src/application.vala b/lib/astal/gtk4/src/application.vala
new file mode 100644
index 0000000..fadf705
--- /dev/null
+++ b/lib/astal/gtk4/src/application.vala
@@ -0,0 +1,241 @@
+[DBus (name="io.Astal.Application")]
+public class Astal.Application : Gtk.Application, AstalIO.Application {
+ private List<Gtk.CssProvider> css_providers = new List<Gtk.CssProvider>();
+ private SocketService service;
+ private DBusConnection conn;
+ private string _instance_name = "astal";
+ private string socket_path { get; private set; }
+
+ /**
+ * Emitted when a window that has been added using
+ * [[email protected]_window] changes its visibility .
+ */
+ [DBus (visible=false)]
+ public signal void window_toggled(Gtk.Window window);
+
+ /**
+ * Get all monitors from [[email protected]].
+ */
+ [DBus (visible=false)]
+ public List<weak Gdk.Monitor> monitors {
+ owned get {
+ var mons = Gdk.Display.get_default().get_monitors();
+ var list = new List<weak Gdk.Monitor>();
+ for (var i = 0; i <= mons.get_n_items(); ++i) {
+ var mon = (Gdk.Monitor)mons.get_item(i);
+ if (mon != null) {
+ list.append(mon);
+ }
+ }
+ return list;
+ }
+ }
+
+ /**
+ * A unique instance name.
+ *
+ * This is the identifier used by the AstalIO package and the CLI.
+ */
+ [DBus (visible=false)]
+ public string instance_name {
+ owned get { return _instance_name; }
+ construct set {
+ _instance_name = value != null ? value : "astal";
+ application_id = @"io.Astal.$_instance_name";
+ }
+ }
+
+ /**
+ * Windows that has been added to this app using [[email protected]_window].
+ */
+ [DBus (visible=false)]
+ public List<Gtk.Window> windows {
+ get { return get_windows(); }
+ }
+
+ private Gtk.Settings settings {
+ get { return Gtk.Settings.get_default(); }
+ }
+
+ private Gdk.Display display {
+ get { return Gdk.Display.get_default(); }
+ }
+
+ /**
+ * Shortcut for [[email protected]:gtk_theme_name]
+ */
+ [DBus (visible=false)]
+ public string gtk_theme {
+ owned get { return settings.gtk_theme_name; }
+ set { settings.gtk_theme_name = value; }
+ }
+
+ /**
+ * Shortcut for [[email protected]:gtk_icon_theme_name]
+ */
+ [DBus (visible=false)]
+ public string icon_theme {
+ owned get { return settings.gtk_icon_theme_name; }
+ set { settings.gtk_icon_theme_name = value; }
+ }
+
+ /**
+ * Shortcut for [[email protected]:gtk_cursor_theme_name]
+ */
+ [DBus (visible=false)]
+ public string cursor_theme {
+ owned get { return settings.gtk_cursor_theme_name; }
+ set { settings.gtk_cursor_theme_name = value; }
+ }
+
+ /**
+ * Remove all [[email protected]] providers.
+ */
+ [DBus (visible=false)]
+ public void reset_css() {
+ foreach(var provider in css_providers) {
+ Gtk.StyleContext.remove_provider_for_display(display, provider);
+ }
+ css_providers = new List<Gtk.CssProvider>();
+ }
+
+ /**
+ * Shortcut for [[email protected]_interactive_debugging].
+ */
+ public void inspector() throws DBusError, IOError {
+ Gtk.Window.set_interactive_debugging(true);
+ }
+
+ /**
+ * Get a window by its [[email protected]:name] that has been added to this app
+ * using [[email protected]_window].
+ */
+ [DBus (visible=false)]
+ public Gtk.Window? get_window(string name) {
+ foreach(var win in windows) {
+ if (win.name == name)
+ return win;
+ }
+
+ critical("no window with name \"%s\"".printf(name));
+ return null;
+ }
+
+ /**
+ * Toggle the visibility of a window by its [[email protected]:name]
+ * that has been added to this app using [[email protected]_window].
+ */
+ public void toggle_window(string window) throws Error {
+ var win = get_window(window);
+ if (win != null) {
+ win.visible = !win.visible;
+ } else {
+ throw new IOError.FAILED("window not found");
+ }
+ }
+
+ /**
+ * Add a new [[email protected]] provider.
+ *
+ * @param style Css string or a path to a css file.
+ */
+ [DBus (visible=false)]
+ public void apply_css(string style, bool reset = false) {
+ var provider = new Gtk.CssProvider();
+
+ if (reset)
+ reset_css();
+
+ try {
+ if (FileUtils.test(style, FileTest.EXISTS))
+ provider.load_from_path(style);
+ else
+ provider.load_from_string(style);
+ } catch (Error err) {
+ critical(err.message);
+ }
+
+ Gtk.StyleContext.add_provider_for_display(
+ display, provider, Gtk.STYLE_PROVIDER_PRIORITY_USER);
+
+ css_providers.append(provider);
+ }
+
+ /**
+ * Shortcut for [[email protected]_search_path].
+ */
+ [DBus (visible=false)]
+ public void add_icons(string? path) {
+ if (path != null) {
+ Gtk.IconTheme.get_for_display(display).add_search_path(path);
+ }
+ }
+
+ /**
+ * Handler for an incoming request.
+ *
+ * @param msg Body of the message
+ * @param conn The connection which expects the response.
+ */
+ [DBus (visible=false)]
+ public virtual void request(string msg, SocketConnection conn) {
+ AstalIO.write_sock.begin(conn, @"missing response implementation on $application_id");
+ }
+
+ /**
+ * Attempt to acquire the astal socket for this app identified by its [[email protected]:instance_name].
+ * If the socket is in use by another app with the same name an [[email protected]_OCCUPIED] is thrown.
+ */
+ [DBus (visible=false)]
+ public void acquire_socket() throws Error {
+ string path;
+ service = AstalIO.acquire_socket(this, out path);
+ socket_path = path;
+
+ Bus.own_name(
+ BusType.SESSION,
+ application_id,
+ BusNameOwnerFlags.NONE,
+ (conn) => {
+ try {
+ this.conn = conn;
+ conn.register_object("/io/Astal/Application", this);
+ } catch (Error err) {
+ critical(err.message);
+ }
+ },
+ () => {},
+ () => {}
+ );
+ }
+
+ /**
+ * Quit and stop the socket if it was acquired.
+ */
+ public new void quit() throws DBusError, IOError {
+ if (service != null) {
+ service.stop();
+ service.close();
+ }
+
+ base.quit();
+ }
+
+ construct {
+ window_added.connect((window) => {
+ ulong id1, id2;
+ id1 = window.notify["visible"].connect(() => window_toggled(window));
+ id2 = window_removed.connect((removed) => {
+ if (removed == window) {
+ window.disconnect(id1);
+ this.disconnect(id2);
+ }
+ });
+ });
+
+ shutdown.connect(() => { try { quit(); } catch(Error err) {} });
+ Unix.signal_add(1, () => { try { quit(); } catch(Error err) {} }, Priority.HIGH);
+ Unix.signal_add(2, () => { try { quit(); } catch(Error err) {} }, Priority.HIGH);
+ Unix.signal_add(15, () => { try { quit(); } catch(Error err) {} }, Priority.HIGH);
+ }
+}
diff --git a/lib/astal/gtk4/src/config.vala.in b/lib/astal/gtk4/src/config.vala.in
new file mode 100644
index 0000000..88bfe9c
--- /dev/null
+++ b/lib/astal/gtk4/src/config.vala.in
@@ -0,0 +1,6 @@
+namespace Astal {
+ public const int MAJOR_VERSION = @MAJOR_VERSION@;
+ public const int MINOR_VERSION = @MINOR_VERSION@;
+ public const int MICRO_VERSION = @MICRO_VERSION@;
+ public const string VERSION = "@VERSION@";
+}
diff --git a/lib/astal/gtk4/src/meson.build b/lib/astal/gtk4/src/meson.build
new file mode 100644
index 0000000..8aac969
--- /dev/null
+++ b/lib/astal/gtk4/src/meson.build
@@ -0,0 +1,89 @@
+version_split = meson.project_version().split('.')
+api_version = version_split[0] + '.' + version_split[1]
+gir = 'Astal-' + api_version + '.gir'
+typelib = 'Astal-' + api_version + '.typelib'
+
+vapi_dir = meson.current_source_dir() / 'vapi'
+add_project_arguments(['--vapidir', vapi_dir], language: 'vala')
+
+config = configure_file(
+ input: 'config.vala.in',
+ output: 'config.vala',
+ configuration: {
+ 'VERSION': meson.project_version(),
+ 'MAJOR_VERSION': version_split[0],
+ 'MINOR_VERSION': version_split[1],
+ 'MICRO_VERSION': version_split[2],
+ },
+)
+
+deps = [
+ dependency('astal-io-0.1'),
+ dependency('glib-2.0'),
+ dependency('gtk4'),
+ dependency('gtk4-layer-shell-0'),
+]
+
+sources = [config] + files(
+ 'widget/window.vala',
+ 'application.vala',
+)
+
+lib = library(
+ meson.project_name(),
+ sources,
+ dependencies: deps,
+ vala_args: ['--vapi-comments'],
+ vala_header: meson.project_name() + '.h',
+ vala_vapi: meson.project_name() + '-' + api_version + '.vapi',
+ version: meson.project_version(),
+ install: true,
+ install_dir: [true, true, true],
+)
+
+pkgs = []
+foreach dep : deps
+ pkgs += ['--pkg=' + dep.name()]
+endforeach
+
+gir_tgt = custom_target(
+ gir,
+ command: [
+ find_program('python3'),
+ girpy,
+ meson.project_name(),
+ gir + ':src/' + gir,
+ ]
+ + pkgs
+ + sources,
+ input: sources,
+ depends: lib,
+ output: gir,
+ install: true,
+ install_dir: get_option('datadir') / 'gir-1.0',
+)
+
+custom_target(
+ typelib,
+ command: [
+ find_program('g-ir-compiler'),
+ '--output', '@OUTPUT@',
+ '--shared-library', libdir / '@PLAINNAME@',
+ meson.current_build_dir() / gir,
+ ],
+ input: lib,
+ output: typelib,
+ depends: [lib, gir_tgt],
+ install: true,
+ install_dir: libdir / 'girepository-1.0',
+)
+
+import('pkgconfig').generate(
+ lib,
+ name: meson.project_name(),
+ filebase: meson.project_name() + '-' + api_version,
+ version: meson.project_version(),
+ subdirs: meson.project_name(),
+ requires: deps,
+ install_dir: libdir / 'pkgconfig',
+)
diff --git a/lib/astal/gtk4/src/widget/window.vala b/lib/astal/gtk4/src/widget/window.vala
new file mode 100644
index 0000000..0cf3d11
--- /dev/null
+++ b/lib/astal/gtk4/src/widget/window.vala
@@ -0,0 +1,261 @@
+using GtkLayerShell;
+
+[Flags]
+public enum Astal.WindowAnchor {
+ NONE,
+ TOP,
+ RIGHT,
+ LEFT,
+ BOTTOM,
+}
+
+public enum Astal.Exclusivity {
+ NORMAL,
+ /**
+ * Request the compositor to allocate space for this window.
+ */
+ EXCLUSIVE,
+ /**
+ * Request the compositor to stack layers on top of each other.
+ */
+ IGNORE,
+}
+
+public enum Astal.Layer {
+ BACKGROUND = 0, // GtkLayerShell.Layer.BACKGROUND
+ BOTTOM = 1, // GtkLayerShell.Layer.BOTTOM
+ TOP = 2, // GtkLayerShell.Layer.TOP
+ OVERLAY = 3, // GtkLayerShell.Layer.OVERLAY
+}
+
+public enum Astal.Keymode {
+ /**
+ * Window should not receive keyboard events.
+ */
+ NONE = 0, // GtkLayerShell.KeyboardMode.NONE
+ /**
+ * Window should have exclusive focus if it is on the top or overlay layer.
+ */
+ EXCLUSIVE = 1, // GtkLayerShell.KeyboardMode.EXCLUSIVE
+ /**
+ * Focus and Unfocues the window as needed.
+ */
+ ON_DEMAND = 2, // GtkLayerShell.KeyboardMode.ON_DEMAND
+}
+
+/**
+ * Subclass of [[email protected]] which integrates GtkLayerShell as class fields.
+ */
+public class Astal.Window : Gtk.Window {
+ private bool check(string action) {
+ if (!is_supported()) {
+ critical(@"can not $action on window: layer shell not supported");
+ print("tip: running from an xwayland terminal can cause this, for example VsCode");
+ return true;
+ }
+ if (!is_layer_window(this)) {
+ init_for_window(this);
+ }
+ return false;
+ }
+
+ construct {
+ // If the window has no size allocatoted when it gets mapped.
+ // It won't show up later either when it size changes by adding children.
+ height_request = 1;
+ width_request = 1;
+ check("initialize layer shell");
+ }
+
+ /**
+ * Namespace of this window. This can be used to target the layer in compositor rules.
+ */
+ public string namespace {
+ get { return get_namespace(this); }
+ set { set_namespace(this, value); }
+ }
+
+ /**
+ * Edges to anchor the window to.
+ *
+ * If two perpendicular edges are anchored, the surface will be anchored to that corner.
+ * If two opposite edges are anchored, the window will be stretched across the screen in that direction.
+ */
+ public WindowAnchor anchor {
+ set {
+ if (check("set anchor"))
+ return;
+
+ set_anchor(this, Edge.TOP, WindowAnchor.TOP in value);
+ set_anchor(this, Edge.BOTTOM, WindowAnchor.BOTTOM in value);
+ set_anchor(this, Edge.LEFT, WindowAnchor.LEFT in value);
+ set_anchor(this, Edge.RIGHT, WindowAnchor.RIGHT in value);
+ }
+ get {
+ var a = WindowAnchor.NONE;
+ if (get_anchor(this, Edge.TOP))
+ a = a | WindowAnchor.TOP;
+
+ if (get_anchor(this, Edge.RIGHT))
+ a = a | WindowAnchor.RIGHT;
+
+ if (get_anchor(this, Edge.LEFT))
+ a = a | WindowAnchor.LEFT;
+
+ if (get_anchor(this, Edge.BOTTOM))
+ a = a | WindowAnchor.BOTTOM;
+
+ return a;
+ }
+ }
+
+ /**
+ * Exclusivity of this window.
+ */
+ public Exclusivity exclusivity {
+ set {
+ if (check("set exclusivity"))
+ return;
+
+ switch (value) {
+ case Exclusivity.NORMAL:
+ set_exclusive_zone(this, 0);
+ break;
+ case Exclusivity.EXCLUSIVE:
+ auto_exclusive_zone_enable(this);
+ break;
+ case Exclusivity.IGNORE:
+ set_exclusive_zone(this, -1);
+ break;
+ }
+ }
+ get {
+ if (auto_exclusive_zone_is_enabled(this))
+ return Exclusivity.EXCLUSIVE;
+
+ if (get_exclusive_zone(this) == -1)
+ return Exclusivity.IGNORE;
+
+ return Exclusivity.NORMAL;
+ }
+ }
+
+ /**
+ * Which layer to appear this window on.
+ */
+ public Layer layer {
+ get { return (Layer)get_layer(this); }
+ set {
+ if (check("set layer"))
+ return;
+
+ set_layer(this, (GtkLayerShell.Layer)value);
+ }
+ }
+
+ /**
+ * Keyboard mode of this window.
+ */
+ public Keymode keymode {
+ get { return (Keymode)get_keyboard_mode(this); }
+ set {
+ if (check("set keymode"))
+ return;
+
+ set_keyboard_mode(this, (GtkLayerShell.KeyboardMode)value);
+ }
+ }
+
+ /**
+ * Which monitor to appear this window on.
+ */
+ public Gdk.Monitor gdkmonitor {
+ get { return get_monitor(this); }
+ set {
+ if (check("set gdkmonitor"))
+ return;
+
+ set_monitor (this, value);
+ }
+ }
+
+ public new int margin_top {
+ get { return GtkLayerShell.get_margin(this, Edge.TOP); }
+ set {
+ if (check("set margin_top"))
+ return;
+
+ GtkLayerShell.set_margin(this, Edge.TOP, value);
+ }
+ }
+
+ public new int margin_bottom {
+ get { return GtkLayerShell.get_margin(this, Edge.BOTTOM); }
+ set {
+ if (check("set margin_bottom"))
+ return;
+
+ GtkLayerShell.set_margin(this, Edge.BOTTOM, value);
+ }
+ }
+
+ public new int margin_left {
+ get { return GtkLayerShell.get_margin(this, Edge.LEFT); }
+ set {
+ if (check("set margin_left"))
+ return;
+
+ GtkLayerShell.set_margin(this, Edge.LEFT, value);
+ }
+ }
+
+ public new int margin_right {
+ get { return GtkLayerShell.get_margin(this, Edge.RIGHT); }
+ set {
+ if (check("set margin_right"))
+ return;
+
+ GtkLayerShell.set_margin(this, Edge.RIGHT, value);
+ }
+ }
+
+ public new int margin {
+ set {
+ if (check("set margin"))
+ return;
+
+ margin_top = value;
+ margin_right = value;
+ margin_bottom = value;
+ margin_left = value;
+ }
+ }
+
+ /**
+ * Which monitor to appear this window on.
+ *
+ * CAUTION: the id might not be the same mapped by the compositor.
+ */
+ public int monitor {
+ set {
+ if (check("set monitor"))
+ return;
+
+ if (value < 0)
+ set_monitor(this, (Gdk.Monitor)null);
+
+ var m = (Gdk.Monitor)Gdk.Display.get_default().get_monitors().get_item(value);
+ set_monitor(this, m);
+ }
+ get {
+ var m = get_monitor(this);
+ var mons = Gdk.Display.get_default().get_monitors();
+ for (var i = 0; i < mons.get_n_items(); ++i) {
+ if (m == mons.get_item(i))
+ return i;
+ }
+
+ return -1;
+ }
+ }
+}
diff --git a/lib/astal/gtk4/version b/lib/astal/gtk4/version
new file mode 100644
index 0000000..fcdb2e1
--- /dev/null
+++ b/lib/astal/gtk4/version
@@ -0,0 +1 @@
+4.0.0
diff --git a/nix/devshell.nix b/nix/devshell.nix
index db52681..66c46e5 100644
--- a/nix/devshell.nix
+++ b/nix/devshell.nix
@@ -39,6 +39,7 @@
wireplumber
libdbusmenu-gtk3
wayland
+ blueprint-compiler
dart-sass
lua