diff options
Diffstat (limited to 'docs/guide/ags')
-rw-r--r-- | docs/guide/ags/binding.md | 234 | ||||
-rw-r--r-- | docs/guide/ags/cli-app.md | 167 | ||||
-rw-r--r-- | docs/guide/ags/faq.md | 266 | ||||
-rw-r--r-- | docs/guide/ags/first-widgets.md | 402 | ||||
-rw-r--r-- | docs/guide/ags/gobject.md | 166 | ||||
-rw-r--r-- | docs/guide/ags/installation.md | 65 | ||||
-rw-r--r-- | docs/guide/ags/theming.md | 161 | ||||
-rw-r--r-- | docs/guide/ags/utilities.md | 174 | ||||
-rw-r--r-- | docs/guide/ags/variable.md | 153 | ||||
-rw-r--r-- | docs/guide/ags/widget.md | 208 |
10 files changed, 0 insertions, 1996 deletions
diff --git a/docs/guide/ags/binding.md b/docs/guide/ags/binding.md deleted file mode 100644 index f1592a0..0000000 --- a/docs/guide/ags/binding.md +++ /dev/null @@ -1,234 +0,0 @@ -# Binding - -As mentioned before binding an object's state to another - -so in most cases a `Variable` or a `GObject.Object` property to a widget's property - -is done through the `bind` function which returns a `Binding` object. - -`Binding` objects simply hold information about the source and how it should be transformed -which Widget constructors can use to setup a connection between themselves and the source. - -```ts -class Binding<Value> { - private transformFn: (v: any) => unknown - private emitter: Subscribable<Value> | Connectable - private prop?: string - - as<T>(fn: (v: Value) => T): Binding<T> - get(): Value - subscribe(callback: (value: Value) => void): () => void -} -``` - -A `Binding` can be constructed from an object implementing -the `Subscribable` interface (usually a `Variable`) -or an object implementing the `Connectable` interface and one of its properties -(usually a `GObject.Object` instance). - -```ts -function bind<T>(obj: Subscribable): Binding<T> - -function bind< - Obj extends Connectable, - Prop extends keyof Obj, ->(obj: Obj, prop: Prop): Binding<Obj[Prop]> -``` - -## Subscribable and Connectable interface - -Any object implementing one of these interfaces can be used with `bind`. - -```ts -interface Subscribable<T> { - subscribe(callback: (value: T) => void): () => void - get(): T -} - -interface Connectable { - connect(signal: string, callback: (...args: any[]) => unknown): number - disconnect(id: number): void -} -``` - -## Example Custom Subscribable - -When binding the children of a box from an array, usually not all elements -of the array changes each time, so it would make sense to not destroy -the widget which represents the element. - -::: code-group - -```ts :line-numbers [varmap.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) - } - - set(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> -} -``` - -## Example Custom Connectable - -This was formerly known as a "Service" in AGS. -Astal provides [decorator functions](./gobject#example-usage) that make it easy to subclass gobjects, however -you can read more about GObjects and subclassing on [gjs.guide](https://gjs.guide/guides/gobject/subclassing.html#gobject-subclassing). - -Objects coming from [libraries](../libraries/references#astal-libraries) -usually have a singleton gobject you can access with `.get_default()`. - -Here is an example of a Brightness library by wrapping the `brightnessctl` cli utility -and by monitoring `/sys/class/backlight` - -::: code-group - -```ts :line-numbers [brightness.ts] -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"`) -const kbd = exec(`bash -c "ls -w1 /sys/class/leds | 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 - } - - #kbdMax = get(`--device ${kbd} max`) - #kbd = get(`--device ${kbd} get`) - #screenMax = get("max") - #screen = get("get") / (get("max") || 1) - - @property(Number) - get kbd() { return this.#kbd } - - set kbd(value) { - if (value < 0 || value > this.#kbdMax) - return - - execAsync(`brightnessctl -d ${kbd} s ${value} -q`).then(() => { - this.#kbd = value - this.notify("kbd") - }) - } - - @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() - - const screenPath = `/sys/class/backlight/${screen}/brightness` - const kbdPath = `/sys/class/leds/${kbd}/brightness` - - monitorFile(screenPath, async f => { - const v = await readFileAsync(f) - this.#screen = Number(v) / this.#screenMax - this.notify("screen") - }) - - monitorFile(kbdPath, async f => { - const v = await readFileAsync(f) - this.#kbd = Number(v) / this.#kbdMax - this.notify("kbd") - }) - } -} -``` - -::: - -And it can be used like any other library object. - -```tsx -function BrightnessSlider() { - const brightness = Brightness.get_default() - - return <slider - value={bind(brightness, "screen")} - onDragged={({ value }) => brightness.screen = value} - /> -} -``` diff --git a/docs/guide/ags/cli-app.md b/docs/guide/ags/cli-app.md deleted file mode 100644 index ec6d174..0000000 --- a/docs/guide/ags/cli-app.md +++ /dev/null @@ -1,167 +0,0 @@ -# CLI and App - -`App` is a singleton **instance** of [Astal.Application](https://aylur.github.io/libastal/class.Application.html). - -```ts -import { App } from "astal" -``` - -## Entry point - -:::code-group - -```ts [app.ts] -App.start({ - main() { - // setup anything - // instantiate widgets - }, -}) -``` - -::: - -:::warning -You can not instantiate widgets outside of the main function. -::: - -## Instance identifier - -You can run multiple instance by defining a unique instance name. - -```ts -App.start({ - instanceName: "my-instance", // defaults to "astal" - main() {}, -}) -``` - -## Messaging from CLI - -If you want to interact with an instance from the cli, you can do so by sending a message. - -```ts -App.start({ - main() {}, - requestHandler(request: string, res: (response: any) => void) { - if (request == "say hi") { - res("hi cli") - } - res("unknown command") - }, -}) -``` - -:::code-group - -```sh [ags] -ags -m "say hi" -# hi cli -``` - -```sh [astal] -astal say hi -# hi cli -``` - -::: - -If you want to run arbitrary JavaScript from cli, you can use `App.eval`. -It will evaluate the passed string as the body of an `async` function. - -```ts -App.start({ - main() {}, - requestHandler(js: string, res) { - App.eval(js).then(res).catch(res) - }, -}) -``` - -If the string does not contain a semicolon, a single expression is assumed and returned implicity. - -```sh -ags -m "'hello'" -# hello -``` - -If the string contains a semicolon, you have to return explicitly - -```sh -ags -m "'hello';" -# undefined - -ags -m "return 'hello';" -# hello -``` - -## Toggling Windows by their name - -In order for AGS to know about your windows, you have to register them. -You can do this by specifying a **unique** `name` and calling `App.add_window` - -```tsx {4} -import { App } from "astal" - -function Bar() { - return <window name="Bar" setup={self => App.add_window(self)}> - <box /> - </window> -} -``` - -You can also invoke `App.add_window` by simply passing the `App` to the `application` prop. - -```tsx {4} -import { App } from "astal" - -function Bar() { - return <window name="Bar" application={App}> - <box /> - </window> -} -``` - -:::warning -When assigning the `application` prop make sure `name` comes before. -Props are set sequentially and if name is applied after application it won't work. -::: - -```sh -ags -t Bar -``` - -## App without AGS - -As mentioned before AGS is only a scaffolding tool. You can setup -a dev environment and a bundler yourself. In which case you won't be using -the ags cli to run the bundled scripts. The produced script can run as the main instance -and a "client" instance. - -The first time you run your bundled script the `main` function gets executed. -While that instance is running any subsequent execution of the script will call -the `client` function. - -:::code-group - -```ts [main.ts] -App.start({ - // main instance - main(...args: Array<string>) { - print(...args) - }, - - // every subsequent calls - client(message: (msg: string) => string, ...args: Array<string>) { - const res = message("you can message the main instance") - console.log(res) - }, - - // this runs in the main instance - requestHandler(request: string, res: (response: any) => void) { - res("response from main") - }, -}) -``` - -::: diff --git a/docs/guide/ags/faq.md b/docs/guide/ags/faq.md deleted file mode 100644 index 76d8e72..0000000 --- a/docs/guide/ags/faq.md +++ /dev/null @@ -1,266 +0,0 @@ -# Frequently asked question, common issues, tips and tricks - -## Monitor id does not match compositor - -The monitor id 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. - -```tsx -import { App } from "astal" - -function Bar(gdkmonitor) { - return <window gdkmonitor={gdkmonitor} /> -} - -function main() { - for (const monitor of App.get_monitors()) { - if (monitor.model == "your-desired-model") { - Bar(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") -``` - -## 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> -``` - -## Is there a way to limit the width/height of a widget? - -Unfortunately not. You can set a minimum size with `min-width` and `min-heigth` css attributes, -but you can not set max size. - -## Custom widgets with bindable properties - -In function components you can wrap any primitive to handle both -binding and value cases as one. - -```tsx -function MyWidget(props: { prop: string | Binding<string> }) { - const prop = props.prop instanceof Binding - ? props.prop - : bind({ get: () => props.prop, subscribe: () => () => {} }) - - function setup(self: Widget.Box) { - self.hook(prop, () => { - const value = prop.get() - // handler - }) - } - - return <box setup={setup}> - </box> -} -``` - -You can pass the prop the super constructor in subclasses - -```tsx -@register() -class MyWidget extends Widget.Box { - @property(String) - set prop(v: string) { - // handler - } - - constructor(props: { prop: string | Binding<string> }) { - super(props) - } -} -``` diff --git a/docs/guide/ags/first-widgets.md b/docs/guide/ags/first-widgets.md deleted file mode 100644 index 6dba7b3..0000000 --- a/docs/guide/ags/first-widgets.md +++ /dev/null @@ -1,402 +0,0 @@ -# First Widgets - -AGS is the predecessor of Astal, which was written purely in TypeScript and so only supported -JavaScript/TypeScript. Now it serves as a scaffolding tool for Astal projects in TypeScript. -While what made AGS what it is, is now part of the Astal project, for simplicity we will -refer to the Astal TypeScript lib as AGS. - -:::tip -If you are not familiar with the JavaScript syntax [MDN](https://developer.mozilla.org/en-US/) -and [javascript.info](https://javascript.info/) have great references. -::: - -## Getting Started - -Start by initializing a project - -```sh -ags --init -``` - -then run `ags` in the terminal - -```sh -ags -``` - -Done! You have now a custom written bar using Gtk. - -:::tip -AGS will transpile every `.ts`, `.jsx` and `.tsx` files into regular javascript then -it will bundle everything into a single javascript file which then GJS can execute. -The bundler used is [esbuild](https://esbuild.github.io/). -::: - -## Root of every shell component: Window - -Astal apps are composed of widgets. A widget is a piece of UI that has its own logic and style. -A widget can be as small as a button or an entire bar. -The top level widget is always a [Window](https://aylur.github.io/libastal/class.Window.html) which will hold all widgets. - -::: code-group - -```tsx [widget/Bar.tsx] -function Bar(monitor = 0) { - return <window className="Bar" monitor={monitor}> - <box>Content of the widget</box> - </window> -} -``` - -::: - -::: code-group - -```ts [app.ts] -import Bar from "./widget/Bar" - -App.start({ - main() { - Bar(0) - Bar(1) // instantiate for each monitor - }, -}) -``` - -::: - -## Creating and nesting widgets - -Widgets are JavaScript functions which return Gtk widgets, -either by using JSX or using a widget constructor. - -:::code-group - -```tsx [MyButton.tsx] -function MyButton(): JSX.Element { - return <button onClicked="echo hello"> - <label label="Click me!" /> - </button> -} -``` - -```ts [MyButton.ts] -import { Widget } from "astal" - -function MyButton(): Widget.Button { - return new Widget.Button( - { onClicked: "echo hello" }, - new Widget.Label({ label: "Click me!" }), - ) -} -``` - -::: - -:::info -The only difference between the two is the return type. -Using markup the return type is always `Gtk.Widget` (globally available as `JSX.Element`), -while using constructors the return type is the actual type of the widget. -It is rare to need the actual return type, so most if not all of the time, you can use markup. -::: - -Now that you have declared `MyButton`, you can nest it into another component. - -```tsx -function MyBar() { - return <window> - <box> - Click The button - <MyButton /> - </box> - </window> -} -``` - -Notice that widgets you defined start with a capital letter `<MyButton />`. -Lowercase tags are builtin widgets, while capital letter is for custom widgets. - -## Displaying Data - -JSX lets you put markup into JavaScript. -Curly braces let you “escape back” into JavaScript so that you can embed some variable -from your code and display it. - -```tsx -function MyWidget() { - const label = "hello" - - return <button>{label}</button> -} -``` - -You can also pass JavaScript to markup attributes - -```tsx -function MyWidget() { - const label = "hello" - - return <button label={label} /> -} -``` - -## Conditional Rendering - -You can use the same techniques as you use when writing regular JavaScript code. -For example, you can use an if statement to conditionally include JSX: - -```tsx -function MyWidget() { - let content - - if (condition) { - content = <True /> - } else { - content = <False /> - } - - return <box>{content}</box> -} -``` - -You can also inline a [conditional `?`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Conditional_operator) (ternary) expression. - -```tsx -function MyWidget() { - return <box>{condition ? <True /> : <False />}</box> -} -``` - -When you don’t need the `else` branch, you can also use a shorter [logical && syntax](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Logical_AND#short-circuit_evaluation): - -```tsx -function MyWidget() { - return <box>{condition && <True />}</box> -} -``` - -:::info -As you can guess from the above snippet, [falsy](https://developer.mozilla.org/en-US/docs/Glossary/Falsy) values are not rendered. -::: - -## Rendering lists - -You can use [`for` loops](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for) or [array `map()` function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map). - -```tsx -function MyWidget() { - const labels = [ - "label1" - "label2" - "label3" - ] - - return <box> - {labels.map(label => ( - <label label={label} /> - ))} - </box> -} -``` - -## Widget signal handlers - -You can respond to events by declaring event handler functions inside your widget: - -```tsx -function MyButton() { - function onClicked(self: Widget.Button, ...args) { - console.log(self, "was clicked") - } - - return <button onClicked={onClicked} /> -} -``` - -The handler can also be a string, which will get executed in a subprocess asynchronously. - -```tsx -function MyButton() { - return <button onClicked="echo hello" /> -} -``` - -:::info -Attributes prefixed with `on` will connect to a `signal` of the widget. -Their types are not generated, but written by hand, which means not all of them are typed. -Refer to the Gtk and Astal docs to have a full list of them. -::: - -## How properties are passed - -Using JSX, a custom widget will always have a single object as its parameter. - -```ts -type Props = { - myprop: string - child?: JSX.Element // when only one child is passed - children?: Array<JSX.Element> // when multiple children are passed -} - -function MyWidget({ myprop, child, children }: Props) { - // -} -``` - -```tsx -// child prop of MyWidget is the box -return <MyWidget myprop="hello"> - <box /> -</MyWidget> -``` - -```tsx -// children prop of MyWidget is [box, box, box] -return <MyWidget myprop="hello"> - <box /> - <box /> - <box /> -</MyWidget> -``` - -## State management - -The state of widgets are handled with Bindings. A [Binding](./binding) lets you -connect the state of an [object](./binding#subscribable-and-connectable-interface) -to a widget so it re-renders when that state changes. - -Use the `bind` function to create a `Binding` object from a `Variable` or -a regular `GObject` and one of its properties. - -Here is an example of a Counter widget that uses a `Variable` as its state: - -```tsx -import { Variable, bind } from "astal" - -function Counter() { - const count = Variable(0) - - function increment() { - count.set(count.get() + 1) - } - - return <box> - <label label={bind(count).as(num => num.toString())} /> - <button onClicked={increment}> - Click to increment - <button> - </box> -} -``` - -:::info -Bindings have an `.as()` method which lets you transform the assigned value. -In the case of a Label, its label property expects a string, so it needs to be -turned to a string first. -::: - -:::tip -`Variables` have a shorthand for `bind(variable).as(transform)` - -```tsx -const v = Variable(0) -const transform = (v) => v.toString() - -return <box> - {/* these two are equivalent */} - <label label={bind(v).as(transform)} /> - <label label={v(transform)} /> -</box> -``` - -::: - -Here is an example of a battery percent label that binds the `percentage` -property of the Battery object from the [Battery Library](/guide/libraries/battery): - -```tsx -import Battery from "gi://AstalBattery" -import { bind } from "astal" - -function BatteryPercentage() { - const bat = Battery.get_default() - - return <label label={bind(bat, "percentage").as((p) => p * 100 + " %")} /> -} -``` - -## Dynamic children - -You can also use a `Binding` for `child` and `children` properties. - -```tsx -const child = Variable(<box />) - -return <box>{child}</box> -``` - -```tsx -const num = Variable(3) -const range = (n) => [...Array(n).keys()] - -return <box> - {num(n => range(n).map(i => ( - <button> - {i.toString()} - <button/> - )))} -<box> -``` - -:::tip -Binding children of widgets will implicitly call `.destroy()` on widgets -that would be left without a parent. You can opt out of this behavior -by setting `noImplicityDestroy` property on the container widget. -::: - -:::info -The above example destroys and recreates every widget in the list **every time** -the value of the `Variable` changes. There might be cases where you would -want to [handle child creation and deletion](/guide/ags/faq#avoiding-unnecessary-re-rendering) -yourself, because you don't want to lose the -inner state of widgets that does not need to be recreated. In this case -you can create a [custom reactive structure](./binding#example-custom-subscribable) -::: - -When there is at least one `Binding` passed as a child, the `children` -parameter will always be a flattened `Binding<Array<JSX.Element>>`. -When there is a single `Binding` passed as a child, the `child` parameter will -be a `Binding<JSX.Element>` or a flattened `Binding<Array<JSX.Element>>`. - -```tsx -function MyContainer({ children }) { - // children is a Binding over an Array of widgets -} - -return <MyContainer> - <box /> - {num(n => range(n).map(i => ( - <button> - {i.toString()} - <button/> - )))} - [ - [ - <button /> - ] - <button /> - ] -</MyContainer> -``` - -:::info -You can pass the followings as children: - -- widgets -- deeply nested arrays of widgets -- bindings of widgets, -- bindings of deeply nested arrays of widgets - -[falsy](https://developer.mozilla.org/en-US/docs/Glossary/Falsy) values are not rendered and anything not from this list -will be coerced into a string and rendered as a label -::: diff --git a/docs/guide/ags/gobject.md b/docs/guide/ags/gobject.md deleted file mode 100644 index 83b7c45..0000000 --- a/docs/guide/ags/gobject.md +++ /dev/null @@ -1,166 +0,0 @@ -# Subclassing GObject.Object - -These were formerly known as custom services in AGS. -Astal provides decorator functions that make it easy to subclass gobjects. - -## Example Usage - -```ts -import GObject, { register, property } from "astal/gobject" - -@register() -class MyObj extends GObject.Object { - @property(String) - declare myProp: string - - @signal(String, Number) - declare mySignal: (a: string, b: number) => void -} -``` - -## Property decorator - -```ts -type PropertyDeclaration = - | GObject.ParamSpec - | { $gtype: GObject.GType } - -function property(declaration: PropertyDeclaration) -``` - -The `property` decorator can take any class that has a registered GType. -This includes the globally available `String`, `Number`, `Boolean` and `Object` -javascript constructors. They are mapped to their relative `GObject.ParamSpec`. - -The property decorator can be applied in the following ways: - -1. On a property declaration - -```ts {3,4} -@register() -class MyObj extends GObject.Object { - @property(String) - declare myProp: string -} -``` - -This will create a getter and setter for the property and will also -emit the notify signal when the value is set to a new value. - -:::info -The `declare` keyword is required so that the property declaration -is not transpiled into JavaScript, otherwise the initial value of the -property would be `undefined`. -::: - -:::warning -The value is checked by reference, this is important if your -property is an object type. - -```ts -const dict = obj.prop -dict["key"] = 0 -obj.prop = dict // This will not emit notify::prop // [!code error] -obj.prop = { ...dict } // This will emit notify::prop -``` - -::: - -If you want to set a custom default value, do so in the constructor of your class. - -```ts {7} -@register() -class MyObj extends GObject.Object { - @property(String) - declare myProp: string - - constructor() { - super({ myProp: "default-value" }) - } -} -``` - -2. On a getter - -```ts {3,4} -@register() -class MyObj extends GObject.Object { - @property(String) - get myProp () { - return "value" - } -} -``` - -This will create a read-only property. - -3. On a getter and setter - -```ts {5,6,10} -@register() -class MyObj extends GObject.Object { - declare private _prop: string - - @property(String) - get myProp () { - return "value" - } - - set myProp (v: string) { - if (v !== this._prop) { - this._prop = v - this.notify("my-prop") - } - } -} -``` - -This will create a read-write property. - -:::info -When defining getter/setters for the property, notify signal emission has to be done explicitly. -::: - -## Signal decorator - -```ts -function signal(...params: Array<{ $gtype: GObject.GType }) - -function signal(declaration?: SignalDeclaration) // Object you would pass to GObject.registerClass -``` - -You can apply the signal decorator to either a property declaration or a method. - -```ts {3,4,6,7} -@register() -class MyObj extends GObject.Object { - @signal(String, String) - declare mySig: (a: String, b: String) => void - - @signal(String, String) - mySig(a: string, b: string) { - // default signal handler - } -} -``` - -You can emit the signal by calling the signal method or using `emit`. - -```ts -const obj = new MyObj() -obj.connect("my-sig", (obj, a: string, b: string) => {}) - -obj.mySig("a", "b") -obj.emit("my-sig", "a", "b") -``` - -## Register decorator - -Every GObject subclass has to be registered. You can pass the same options -to this decorator as you would to `GObject.registerClass` - -```ts -@register({ GTypeName: "MyObj" }) -class MyObj extends GObject.Object { -} -``` diff --git a/docs/guide/ags/installation.md b/docs/guide/ags/installation.md deleted file mode 100644 index 0adcf68..0000000 --- a/docs/guide/ags/installation.md +++ /dev/null @@ -1,65 +0,0 @@ -# Installation - -## Nix - -maintainer: [@Aylur](https://github.com/Aylur) - -Read more about it on the [nix page](../getting-started/nix#ags) - -You can try without installing. - -<!--TODO: remove v2 after merge--> -```sh -nix run github:aylur/ags/v2 -- --help -``` - -## Bulding AGS from source - -1. [Install Astal](../getting-started/installation.md) if you have not already - -2. Install the following dependencies - -:::code-group - -```sh [<i class="devicon-archlinux-plain"></i> Arch] -sudo pacman -Syu go npm gjs -``` - -```sh [<i class="devicon-fedora-plain"></i> Fedora] -sudo dnf install golang npm gjs -``` - -```sh [<i class="devicon-ubuntu-plain"></i> Ubuntu] -sudo apt install golang-go npm gjs -``` - -::: - -3. Clone the repo and Install - -<!--TODO: remove v2 after merge--> -```sh -git clone https://github.com/aylur/ags.git -cd ags/src -git checkout v2 # https://github.com/Aylur/ags/pull/504 -go install -``` - -:::info -If you have installed Astal **not** in `/usr` make sure to set its path. - -```sh -go install -ldflags "-X main.astalGjs=$(pkg-config --variable prefix astal-0.1)/share/astal/gjs" -``` - -::: - -:::tip -`go install` installs the `ags` binary to `$GOPATH/bin` so make sure its in your `$PATH`. -You can move it to another directory if you like. For example - -```sh -mv $GOPATH/bin/ags ~/.local/bin/ags -``` - -::: diff --git a/docs/guide/ags/theming.md b/docs/guide/ags/theming.md deleted file mode 100644 index cb0ebff..0000000 --- a/docs/guide/ags/theming.md +++ /dev/null @@ -1,161 +0,0 @@ -# Theming - -Since the widget toolkit is **GTK3** theming is done with **CSS**. - -- [CSS tutorial](https://www.w3schools.com/css/) -- [GTK CSS Overview wiki](https://docs.gtk.org/gtk3/css-overview.html) -- [GTK CSS Properties Overview wiki](https://docs.gtk.org/gtk3/css-properties.html) - -:::warning GTK is not the web -While most features are implemented in GTK, -you can't assume anything that works on the web will work with GTK. -Refer to the [GTK docs](https://docs.gtk.org/gtk3/css-overview.html) -to see what is available. -::: - -So far every widget you made used your default GTK3 theme. -To make them more custom, you can apply stylesheets to them. - -## From file at startup - -You can pass a path to a file or css as a string in `App.start` - -:::code-group - -```ts [app.ts] -const inlineCss = ` -window { - background-color: transparent; -} -` - -App.start({ - css: "./style.css", - css: `${SRC}/style.css'`, - css: inlineCss, -}) -``` - -::: - -:::info -The global `SRC` will point to the directory `app.ts` is in. -AGS will set the current working directory to `--config`, so `./style.css` also works. -::: - -## Css Property on Widgets - -```ts -Widget.Label({ - css: "color: blue; padding: 1em;", - label: "hello", -}) -``` - -:::info -The `css` property of a widget will not cascade to its children. -::: - -## Apply Stylesheets at Runtime - -You can apply additional styles at runtime. - -```ts -App.apply_css("/path/to/file.css") -``` - -```ts -App.apply_css(` -window { - background-color: transparent; -} -`) -``` - -```ts -App.reset_css() // reset if need -``` - -:::warning -`App.apply_css` will apply on top of other stylesheets applied before. -You can reset stylesheets with `App.resetCss` -::: - -## Inspector - -If you are not sure about the widget hierarchy or any CSS selector, -you can use the [GTK inspector](https://wiki.gnome.org/Projects/GTK/Inspector) - -```sh -# to bring up the inspector run -ags --inspector -``` - -## Using SCSS - -Gtk's CSS only supports a subset of what the web offers. -Most notably nested selectors are unsupported by Gtk, but this can be -workaround by using preprocessors like [SCSS](https://sass-lang.com/). - -:::code-group - -```sh [<i class="devicon-archlinux-plain"></i> Arch] -sudo pacman -Syu dart-sass -``` - -```sh [<i class="devicon-fedora-plain"></i> Fedora] -npm install -g sass # not packaged on Fedora -``` - -```sh [<i class="devicon-ubuntu-plain"></i> Ubuntu] -npm install -g sass # not packaged on Ubuntu -``` - -::: - -Importing `scss` files will simply return transpiled css. - -:::code-group - -```ts [app.ts] -import style from "./style.scss" - -App.start({ - css: style, - main() {}, -}) -``` - -::: - -:::tip -If you for example want to set scss varibles from JS, -You can inline import, compose, and transpile yourself. - -```ts [app.ts] -import style1 from "inline:./style1.scss" -import style2 from "inline:./style2.scss" - -const tmpscss = "/tmp/style.scss" -const target = "/tmp/style.css" - -writeFile(tmpscss, ` - $var1: red; - $var1: blue; - ${style1} - ${style1} -`) - -exec(`sass ${tmpscss} ${target}`) - -App.start({ - css: target, -}) - -``` - -::: - -:::info -If you want other preprocessors support builtin open an Issue. -::: diff --git a/docs/guide/ags/utilities.md b/docs/guide/ags/utilities.md deleted file mode 100644 index aae255c..0000000 --- a/docs/guide/ags/utilities.md +++ /dev/null @@ -1,174 +0,0 @@ -# Utilities - -## File functions - -Import them from `astal` or `astal/file` - -```ts -import { - readFile, - readFileAsync, - writeFile, - writeFileAsync, - monitorFile, -} from "astal" -``` - -### Reading files - -```ts -function readFile(path: string): string -function readFileAsync(path: string): Promise<string> -``` - -### Writing files - -```ts -function writeFile(path: string, content: string): void -function writeFileAsync(path: string, content: string): Promise<void> -``` - -### Monitoring files - -```ts -function monitorFile( - path: string, - callback: (file: string, event: Gio.FileMonitorEvent) => void, -): Gio.FileMonitor -``` - -## Timeouts and Intervals - -Import them from `astal` or `astal/time` - -```ts -import { interval, timeout, idle } from "astal" -``` - -You can use javascript native `setTimeout` or `setInterval` -they return a [GLib.Source](https://docs.gtk.org/glib/struct.Source.html) instance. -Alternatively you can use these functions provided by Astal, -which return an [Astal.Time](https://aylur.github.io/libastal/class.Time.html) instance. - -`Astal.Time` has a `now` signal and a `cancelled` signal. - -### Interval - -Will immediately execute the function and every `interval` millisecond. - -```ts -function interval(interval: number, callback?: () => void): Astal.Time -``` - -### Timeout - -Will execute the `callback` after `timeout` millisecond. - -```ts -function timeout(timeout: number, callback?: () => void): Astal.Time -``` - -### Idle - -Executes `callback` whenever there are no higher priority events pending. - -```ts -function idle(callback?: () => void): Astal.Time -``` - -Example: - -```ts -const timer = interval(1000, () => { - console.log("optional callback") -}) - -timer.connect("now", () => { - console.log("tick") -}) - -timer.connect("cancelled", () => { - console.log("cancelled") -}) - -timer.cancel() -``` - -## Process functions - -Import them from `astal` or `astal/proc` - -```ts -import { subprocess, exec, execAsync } from "astal" -``` - -### Subprocess - -You can start a subprocess and run callback functions whenever it outputs to -stdout or stderr. [Astal.Process](https://aylur.github.io/libastal/class.Process.html) has a `stdout` and `stderr` signal. - -```ts -function subprocess(args: { - cmd: string | string[] - out?: (stdout: string) => void - err?: (stderr: string) => void -}): Astal.Process - -function subprocess( - cmd: string | string[], - onOut?: (stdout: string) => void, - onErr?: (stderr: string) => void, -): Astal.Process -``` - -Example: - -```ts -const proc = subprocess( - "some-command", - (out) => console.log(out), // optional - (err) => console.error(out), // optional -) - -// or with signals -const proc = subprocess("some-command") -proc.connect("stdout", (out) => console.log(out)) -proc.connect("stderr", (err) => console.error(err)) -``` - -### Executing external commands and scripts - -```ts -function exec(cmd: string | string[]): string -function execAsync(cmd: string | string[]): Promise<string> -``` - -Example: - -```ts -try { - const out = exec("/path/to/script") - console.log(out) -} catch (err) { - console.error(err) -} - -execAsync(["bash", "-c", "/path/to/script.sh"]) - .then((out) => console.log(out)) - .catch((err) => console.error(err)) -``` - -:::warning -`subprocess`, `exec`, and `execAsync` executes the passed executable as is. -They are **not** executed in a shell environment, -they do **not** expand ENV variables like `$HOME`, -and they do **not** handle logical operators like `&&` and `||`. - -If you want bash, run them with bash. - -```ts -exec(["bash", "-c", "command $VAR && command"]) -exec("bash -c 'command $VAR' && command") -``` - -::: diff --git a/docs/guide/ags/variable.md b/docs/guide/ags/variable.md deleted file mode 100644 index 4207f61..0000000 --- a/docs/guide/ags/variable.md +++ /dev/null @@ -1,153 +0,0 @@ -# Variable - -```js -import { Variable } from "astal" -``` - -Variable is just a simple object which holds a single value. -It also has some shortcuts for hooking up subprocesses, intervals and other gobjects. - -:::info -The `Variable` object imported from the `"astal"` package is **not** [Astal.Variable](https://aylur.github.io/libastal/class.Variable.html). -::: - -## Example Usage - -```typescript -const myvar = Variable("initial-value") - -// whenever its value changes, callback will be executed -myvar.subscribe((value: string) => { - console.log(value) -}) - -// settings its value -myvar.set("new value") - -// getting its value -const value = myvar.get() - -// binding them to widgets -Widget.Label({ - label: bind(myvar).as((value) => `transformed ${value}`), - label: myvar((value) => `transformed ${value}`), // shorthand for the above -}) -``` - -:::warning -Make sure to make the transform functions passed to `.as()` are pure. -The `.get()` function can be called anytime by `astal` especially when `deriving`, -so make sure there are no sideeffects. -::: - -## Variable Composition - -Using `Variable.derive` any `Subscribable` object can be composed. - -```typescript -const v1: Variable<number> = Variable(1) -const v2: Binding<number> = bind(obj, "prop") -const v3: Subscribable<number> = { - get: () => 3, - subscribe: () => () => {}, -} - -// first argument is a list of dependencies -// second argument is a transform function, -// where the parameters are the values of the dependencies in the order they were passed -const v4: Variable<number> = Variable.derive( - [v1, v2, v3], - (v1: number, v2: number, v3: number) => { - return v1 * v2 * v3 - } -) -``` - -:::info -The types are only for demonstration purposes, you do not have to declare -the type of a Variable, they will be inferred from their initial value. -::: - -## Subprocess shorthands - -Using `.poll` and `.watch` we can start subprocesses and capture their -output. They can poll and watch at the same time, but they -can only poll/watch once. - -:::warning -The command parameter is passed to [execAsync](/guide/ags/utilities#executing-external-commands-and-scripts) -which means they are **not** executed in a shell environment, -they do **not** expand ENV variables like `$HOME`, -and they do **not** handle logical operators like `&&` and `||`. - -If you want bash, run them with bash. - -```js -Variable("").poll(1000, ["bash", "-c", "command $VAR && command"]) -``` - -::: - -```typescript -const myVar = Variable(0) - .poll(1000, "command", (out: string, prev: number) => parseInt(out)) - .poll(1000, ["bash", "-c", "command"], (out, prev) => parseInt(out)) - .poll(1000, (prev) => prev + 1) -``` - -```typescript -const myVar = Variable(0) - .watch("command", (out: string, prev: number) => parseInt(out)) - .watch(["bash", "-c", "command"], (out, prev) => parseInt(out)) -``` - -You can temporarily stop them and restart them whenever. - -```js -myvar.stopWatch() // this kills the subprocess -myvar.stopPoll() - -myvar.startListen() // launches the subprocess again -myvar.startPoll() - -console.log(myvar.isListening()) -console.log(myvar.isPolling()) -``` - -## Gobject connection shorthands - -Using `.observe` you can connect gobject signals and capture their value. - -```typescript -const myvar = Variable("") - .observe(obj1, "signal", () => "") - .observe(obj2, "signal", () => "") -``` - -## Dispose if no longer needed - -This will stop the interval and force exit the subprocess and disconnect gobjects. - -```js -myVar.drop() -``` - -:::warning -Don't forget to drop derived variables or variables with -either `.poll`, `.watch` or `.observe` when they are defined inside closures. - -```tsx -function MyWidget() { - const myvar = Variable().poll() - const derived = Variable.derive() - - return <box - onDestroy={() => { - myvar.drop() - derived.drop() - }} - /> -} -``` - -::: diff --git a/docs/guide/ags/widget.md b/docs/guide/ags/widget.md deleted file mode 100644 index 01cff81..0000000 --- a/docs/guide/ags/widget.md +++ /dev/null @@ -1,208 +0,0 @@ -# Widget - -## Additional widget properties - -These are properties that Astal additionally adds to Gtk.Widgets - -- className: `string` - List of class CSS selectors separated by white space. -- css: `string` - Inline CSS. e.g `label { color: white; }`. If no selector is specified `*` will be assumed. e.g `color: white;` will be inferred as `* { color: white; }`. -- cursor: `string` - Cursor style when hovering over widgets that have hover states, e.g it won't work on labels. [list of valid values](https://docs.gtk.org/gdk3/ctor.Cursor.new_from_name.html). -- clickThrough: `boolean` - Lets click events through. - -To have a full list of available properties, reference the documentation of the widget. - -- [Astal widgets](https://aylur.github.io/libastal/index.html#classes) -- [Gtk widgets](https://docs.gtk.org/gtk3/#classes) - -## Additional widget methods - -### setup - -`setup` is a convenience prop to remove the need to predefine widgets -before returning them in cases where a reference is needed. - -without `setup` - -```tsx -function MyWidget() { - const button = Widget.Button() - // setup button - return button -} -``` - -using `setup` - -```tsx -function MyWidget() { - function setup(button: Widget.Button) { - // setup button - } - - return <buttons setup={setup} /> -} -``` - -### hook - -Shorthand for connection and disconnecting to [Subscribable and Connectable](./binding#subscribable-and-connectable-interface) objects. - -without `hook` - -```tsx -function MyWidget() { - const id = gobject.connect("signal", callback) - const unsub = variable.subscribe(callback) - - return <box - onDestroy={() => { - gobject.disconnect(id) - unsub() - }} - /> -} -``` - -with `hook` - -```tsx -function MyWidget() { - return <box - setup={(self) => { - self.hook(gobject, "signal", callback) - self.hook(variable, callback) - }} - /> -} -``` - -### toggleClassName - -Toggle classNames based on a condition - -```tsx -function MyWidget() { - return <box - setup={(self) => { - self.toggleClassName("classname", someCondition) - }} - /> -} -``` - -## How to use non builtin Gtk widgets - -Using the `Widget.astalify` mixin you can subclass widgets -to behave like builtin widgets. -The `astalify` mixin will apply the following: - -- set `visible` to true by default (Gtk3 widgets are invisible by default) -- make gobject properties accept and consume `Binding` objects -- add properties and methods listed above -- sets up signal handlers that are passed as props prefixed with `on` - -```tsx -import { Widget, Gtk, GObject, Gdk } from "astal" - -// subclass, register, define constructor props -class ColorButton extends Widget.astalify(Gtk.ColorButton) { - static { GObject.registerClass(this) } - - constructor(props: Widget.ConstructProps< - ColorButton, - Gtk.ColorButton.ConstructorProps, - { onColorSet: [] } // signals of Gtk.ColorButton have to be manually typed - >) { - super(props as any) - } -} - -function MyWidget() { - function setup(button: ColorButton) { - - } - - return <ColorButton - setup={setup} - useAlpha - rgba={new Gdk.RGBA({ - red: 1, - green: 0, - blue: 0, - alpha: 0.5, - })} - onColorSet={(self) => { - console.log(self.rgba) - }} - /> -} -``` - -:::info -Signal properties have to be annotated manually for TypeScript. -You can reference [Gtk3](https://gjs-docs.gnome.org/gtk30~3.0/) -and [Astal](https://aylur.github.io/libastal/index.html#classes) for available signals. -::: - -## TypeScript - -Type of widgets are available through `Widget`. -Here is an example Widget that takes in and handles a possibly `Binding` prop. - -```tsx -import { Binding, Variable, Widget } from "astal" - -export interface ToggleButtonProps extends Widget.ButtonProps { - onToggled?: (self: Widget.Button, on: boolean) => void - state?: Binding<boolean> | boolean - child?: JSX.Element -} - -export default function ToggleButton(btnprops: ToggleButtonProps) { - const { state = false, onToggled, setup, child, ...props } = btnprops - const innerState = Variable(state instanceof Binding ? state.get() : state) - - return <button - {...props} - setup={self => { - setup?.(self) - - self.toggleClassName("active", innerState.get()) - self.hook(innerState, () => self.toggleClassName("active", innerState.get())) - - if (state instanceof Binding) { - self.hook(state, () => innerState.set(state.get())) - } - }} - onClicked={self => { - onToggled?.(self, !innerState.get()) - }} - > - {child} - </button> -} -``` - -## Builtin Widgets - -You can check the [source code](https://github.com/aylur/astal/blob/main/core/gjs/src/widgets.ts) to have a full list of builtin widgets. - -These widgets are available by default in JSX. - -- box: [Astal.Box](https://aylur.github.io/libastal/class.Box.html) -- button: [Astal.Button](https://aylur.github.io/libastal/class.Button.html) -- centerbox: [Astal.CenterBox](https://aylur.github.io/libastal/class.CenterBox.html) -- circularprogress: [Astal.CircularProgress](https://aylur.github.io/libastal/class.CircularProgress.html) -- drawingarea: [Gtk.DrawingArea](https://docs.gtk.org/gtk3/class.DrawingArea.html) -- entry: [Gtk.Entry](https://docs.gtk.org/gtk3/class.Entry.html) -- eventbox: [Astal.EventBox](https://aylur.github.io/libastal/class.EventBox.html) -- icon: [Astal.Icon](https://aylur.github.io/libastal/class.Icon.html) -- label: [Astal.Label](https://aylur.github.io/libastal/class.Label.html) -- levelbar: [Astal.LevelBar](https://aylur.github.io/libastal/class.LevelBar.html) -- overlay: [Astal.Overlay](https://aylur.github.io/libastal/class.Overlay.html) -- revealer: [Gtk.Revealer](https://docs.gtk.org/gtk3/class.Revealer.html) -- scrollable: [Astal.Scrollable](https://aylur.github.io/libastal/class.Scrollable.html) -- slider: [Astal.Slider](https://aylur.github.io/libastal/class.Slider.html) -- stack: [Astal.Stack](https://aylur.github.io/libastal/class.Stack.html) -- switch: [Gtk.Switch](https://docs.gtk.org/gtk3/class.Switch.html) -- window: [Astal.Window](https://aylur.github.io/libastal/class.Window.html) |