diff options
Diffstat (limited to 'docs/guide/ags/faq.md')
-rw-r--r-- | docs/guide/ags/faq.md | 374 |
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> +} +``` |