summaryrefslogtreecommitdiff
path: root/docs/guide/ags/faq.md
diff options
context:
space:
mode:
Diffstat (limited to 'docs/guide/ags/faq.md')
-rw-r--r--docs/guide/ags/faq.md374
1 files changed, 374 insertions, 0 deletions
diff --git a/docs/guide/ags/faq.md b/docs/guide/ags/faq.md
new file mode 100644
index 0000000..2758ef0
--- /dev/null
+++ b/docs/guide/ags/faq.md
@@ -0,0 +1,374 @@
+# Frequently asked question, common issues, tips and tricks
+
+## Monitor id does not match compositor
+
+The monitor property that windows expect is mapped by Gdk, which is not always
+the same as the compositor. Instead use the `gdkmonitor` property which expects
+a `Gdk.Monitor` object which you can get from compositor libraries.
+
+Example with Hyprland
+
+```tsx
+import Hyprland from "gi://AstalHyprland"
+
+function Bar(gdkmonitor) {
+ return <window gdkmonitor={gdkmonitor} />
+}
+
+function main() {
+ for (const m of Hyprland.get_default().get_monitors()) {
+ Bar(m.gdk_monitor)
+ }
+}
+
+App.start({ main })
+```
+
+## Environment variables
+
+JavaScript is **not** an bash.
+
+```ts
+const HOME = exec("echo $HOME") // does not work
+```
+
+`exec` and `execAsync` runs the passed program as is, its **not** run in a
+shell environment, so the above example just passes `$HOME` as a string literal
+to the `echo` program.
+
+:::danger Please don't do this
+You could pass it to bash, but that is a horrible approach.
+
+```ts
+const HOME = exec("bash -c 'echo $HOME'")
+```
+
+:::
+
+You can read environment variables with [GLib.getenv](https://gjs-docs.gnome.org/glib20~2.0/glib.getenv).
+
+```ts
+import GLib from "gi://GLib"
+
+const HOME = GLib.getenv("HOME")
+```
+
+## Custom svg symbolic icons
+
+Put the svgs in a directory, named `<icon-name>-symbolic.svg`
+and use `App.add_icons` or `icons` parameter in `App.start`
+
+:::code-group
+
+```ts [app.ts]
+App.start({
+ icons: `${SRC}/icons`,
+ main() {
+ Widget.Icon({
+ icon: "custom-symbolic", // custom-symbolic.svg
+ css: "color: green;", // can be colored, like other named icons
+ })
+ },
+})
+```
+
+:::
+
+:::info
+If there is a name clash with an icon from your current icon pack
+the icon pack will take precedence
+:::
+
+## Logging
+
+The `console` API in gjs uses glib logging functions.
+If you just want to print some text as is to stdout
+use the globally available `print` function or `printerr` for stderr.
+
+```ts
+print("print this line to stdout")
+printerr("print this line to stderr")
+```
+
+## Binding custom structures
+
+The `bind` function can take two types of objects.
+
+```ts
+interface Subscribable<T = unknown> {
+ subscribe(callback: (value: T) => void): () => void
+ get(): T
+}
+
+interface Connectable {
+ connect(signal: string, callback: (...args: any[]) => unknown): number
+ disconnect(id: number): void
+}
+```
+
+`Connectable` is for mostly gobjects, while `Subscribable` is for `Variables`
+and custom objects.
+
+For example you can compose `Variables` in using a class.
+
+```ts
+type MyVariableValue = {
+ number: number
+ string: string
+}
+
+class MyVariable {
+ number = Variable(0)
+ string = Variable("")
+
+ get(): MyVariableValue {
+ return {
+ number: this.number.get(),
+ string: this.string.get(),
+ }
+ }
+
+ subscribe(callback: (v: MyVariableValue) => void) {
+ const unsub1 = this.number.subscribe((value) => {
+ callback({ string: value, number: this.number.get() })
+ })
+
+ const unsub2 = this.string.subscribe((value) => {
+ callback({ number: value, string: this.string.get() })
+ })
+
+ return () => {
+ unsub1()
+ unsub2()
+ }
+ }
+}
+```
+
+Then it can be used with `bind`.
+
+```tsx
+function MyWidget() {
+ const myvar = new MyVariable()
+ const label = bind(myvar).as(({ string, number }) => {
+ return `${string} ${number}`
+ })
+
+ return <label label={label} />
+}
+```
+
+## Populate the global scope with frequently accessed variables
+
+It might be annoying to always import Gtk only for `Gtk.Align` enums.
+
+:::code-group
+
+```ts [globals.ts]
+import Gtk from "gi://Gtk"
+
+declare global {
+ const START: number
+ const CENTER: number
+ const END: number
+ const FILL: number
+}
+
+Object.assign(globalThis, {
+ START: Gtk.Align.START,
+ CENTER: Gtk.Align.CENTER,
+ END: Gtk.Align.END,
+ FILL: Gtk.Align.FILL,
+})
+```
+
+:::
+
+:::code-group
+
+```tsx [Bar.tsx]
+export default function Bar() {
+ return <window>
+ <box halign={START} />
+ </window>
+}
+```
+
+:::
+
+:::code-group
+
+```ts [app.ts]
+import "./globals"
+import Bar from "./Bar"
+
+App.start({
+ main: Bar
+})
+```
+
+:::
+
+:::info
+It is considered bad practice to populate the global scope, but its your code, not a public library.
+:::
+
+## Auto create Window for each Monitor
+
+To have Window widgets appear on a monitor when its plugged in, listen to `App.monitor_added`.
+
+:::code-group
+
+```tsx [Bar.tsx]
+export default function Bar(gdkmonitor: Gdk.Monitor) {
+ return <window gdkmonitor={gdkmonitor} />
+}
+```
+
+:::
+
+:::code-group
+
+```ts [app.ts]
+import { Gdk, Gtk } from "astal"
+import Bar from "./Bar"
+
+function main() {
+ const bars = new Map<Gdk.Monitor, Gtk.Widget>()
+
+ // initialize
+ for (const gdkmonitor of App.get_monitors()) {
+ bars.set(gdkmonitor, Bar(gdkmonitor))
+ }
+
+ App.connect("monitor-added", (_, gdkmonitor) => {
+ bars.set(gdkmonitor, Bar(gdkmonitor))
+ })
+
+ App.connect("monitor-removed", (_, gdkmonitor) => {
+ bars.get(gdkmonitor)?.destroy()
+ bars.delete(gdkmonitor)
+ })
+}
+
+App.start({ main })
+```
+
+:::
+
+## Error: Can't convert non-null pointer to JS value
+
+These happen when accessing list type properties. Gjs fails to correctly bind
+`List` and other array like types of Vala as a property.
+
+```ts
+import Notifd from "gi://AstalNotifd"
+const notifd = Notifd.get_default()
+
+notifd.notifications // ❌ // [!code error]
+
+notifd.get_notifications() // ✅
+```
+
+## How to create regular floating windows
+
+Use `Gtk.Window` with [Widget.astalify](/guide/ags/widget#how-to-use-non-builtin-gtk-widgets).
+
+By default `Gtk.Window` is destroyed on close. To prevent this add a handler for `delete-event`.
+
+```tsx {4-7}
+const RegularWindow = Widget.astalify(Gtk.Window)
+
+return <RegularWindow
+ onDeleteEvent={(self) => {
+ self.hide()
+ return true
+ }}
+>
+ {child}
+</RegularWindow>
+```
+
+## Avoiding unnecessary re-rendering
+
+As mentioned before, any object can be bound that implements the `Subscribable` interface.
+
+```ts
+interface Subscribable<T = unknown> {
+ subscribe(callback: (value: T) => void): () => void
+ get(): T
+}
+```
+
+This can be used to our advantage to create a reactive `Map` object.
+
+```ts
+import { type Subscribable } from "astal/binding"
+import { Gtk } from "astal"
+
+export class VarMap<K, T = Gtk.Widget> implements Subscribable {
+ #subs = new Set<(v: Array<[K, T]>) => void>()
+ #map: Map<K, T>
+
+ #notifiy() {
+ const value = this.get()
+ for (const sub of this.#subs) {
+ sub(value)
+ }
+ }
+
+ #delete(key: K) {
+ const v = this.#map.get(key)
+
+ if (v instanceof Gtk.Widget) {
+ v.destroy()
+ }
+
+ this.#map.delete(key)
+ }
+
+ constructor(initial?: Iterable<[K, T]>) {
+ this.#map = new Map(initial)
+ }
+
+ add(key: K, value: T) {
+ this.#delete(key)
+ this.#map.set(key, value)
+ this.#notifiy()
+ }
+
+ delete(key: K) {
+ this.#delete(key)
+ this.#notifiy()
+ }
+
+ get() {
+ return [...this.#map.entries()]
+ }
+
+ subscribe(callback: (v: Array<[K, T]>) => void) {
+ this.#subs.add(callback)
+ return () => this.#subs.delete(callback)
+ }
+}
+```
+
+And this `VarMap<key, Widget>` can be used as an alternative to `Variable<Array<Widget>>`.
+
+```tsx
+function MappedBox() {
+ const map = new VarMap([
+ [1, <MyWidget id={id} />]
+ [2, <MyWidget id={id} />]
+ ])
+
+ const conns = [
+ gobject.connect("added", (_, id) => map.set(id, MyWidget({ id }))),
+ gobject.connect("removed", (_, id) => map.delete(id, MyWidget({ id }))),
+ ]
+
+ return <box onDestroy={() => conns.map(id => gobject.disconnect(id))}>
+ {bind(map).as(arr => arr.sort(([a], [b]) => a - b).map(([,w]) => w))}
+ </box>
+}
+```