summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/guide/lua/binding.md139
-rw-r--r--docs/guide/lua/cli-app.md131
-rw-r--r--docs/guide/lua/first-widgets.md264
-rw-r--r--docs/guide/lua/installation.md24
-rw-r--r--docs/guide/lua/theming.md130
-rw-r--r--docs/guide/lua/utilities.md191
-rw-r--r--docs/guide/lua/variable.md162
-rw-r--r--docs/guide/lua/widget.md158
-rw-r--r--docs/guide/typescript/binding.md6
-rw-r--r--docs/guide/typescript/utilities.md4
-rw-r--r--docs/guide/typescript/variable.md4
-rw-r--r--docs/vitepress.config.ts12
-rw-r--r--lang/lua/astal/binding.lua10
-rw-r--r--lang/lua/astal/gtk3/init.lua6
14 files changed, 1225 insertions, 16 deletions
diff --git a/docs/guide/lua/binding.md b/docs/guide/lua/binding.md
new file mode 100644
index 0000000..f4d5f0b
--- /dev/null
+++ b/docs/guide/lua/binding.md
@@ -0,0 +1,139 @@
+# 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.
+
+```lua
+---@class Binding<T>
+---@field private transform_fn fun(value: T): any
+---@field private emitter Connectable | Subscribable<T>
+---@field private property? string
+---@field as fun(transform: fun(value: T): any): Binding
+---@field get fun(): T
+---@field subscribe fun(self, callback: fun(value: T)): function
+```
+
+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).
+
+Lua type annotations are not expressive enough to explain this,
+so I'll use TypeScript to demonstrate it.
+
+<!--TODO: use Teal maybe?-->
+
+```ts
+function bind<T>(obj: Subscribable<T>): 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
+}
+```
+
+`Connectable` is usually used for GObjects coming from [libraries](../libraries/references)
+You won't be implementing it in Lua code.
+
+## 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
+
+```lua :line-numbers [varmap.lua]
+local Gtk = require("astal.gtk3").Gtk
+local Variable = require("astal.variable")
+
+---@param initial table
+return function(initial)
+ local map = initial
+ local var = Variable()
+
+ local function notify()
+ local arr
+ for _, value in pairs(map) do
+ table.insert(arr, value)
+ end
+ var:set(arr)
+ end
+
+ local function delete(key)
+ if Gtk.Widget:is_type_of(map[key]) then
+ map[key]:destroy()
+ end
+
+ map[key] = nil
+ end
+
+ notify() -- init
+
+ return {
+ set = function(key, value)
+ delete(key)
+ map[key] = value
+ notify()
+ end,
+ delete = function(key)
+ delete(key)
+ notify()
+ end,
+ get = function()
+ return var:get()
+ end,
+ subscribe = function(callback)
+ return var:subscribe(callback)
+ end,
+ }
+end
+```
+
+:::
+
+And this `VarMap<key, Widget>` can be used as an alternative to `Variable<Array<Widget>>`.
+
+```lua
+function MappedBox()
+ local map = varmap({
+ ["1"] = Widget.Label({ label = "1" }),
+ ["2"] = Widget.Label({ label = "2" }),
+ })
+
+ return Widget.Box({
+ setup = function (self)
+ self:hook(gobject, "added", function (_, id)
+ map.set(id, Widget.Label({ label = id }))
+ end)
+ self:hook(gobject, "removed", function (_, id)
+ map.delete(id)
+ end)
+ end,
+ bind(map):as(function (arr)
+ -- can be sorted here
+ return arr
+ end),
+ })
+end
+```
diff --git a/docs/guide/lua/cli-app.md b/docs/guide/lua/cli-app.md
new file mode 100644
index 0000000..6bfaa9e
--- /dev/null
+++ b/docs/guide/lua/cli-app.md
@@ -0,0 +1,131 @@
+# CLI and App
+
+`App` is a singleton **instance** of an [Astal.Application](https://aylur.github.io/libastal/astal3/class.Application.html).
+
+Depending on gtk version require paths will differ
+
+<!--TODO: remove gtk4 notice when its available-->
+
+```lua
+local App = require("astal.gtk3.app")
+
+local App = require("astal.gtk4.app") -- not yet available
+```
+
+## Entry point
+
+`App:start` is a wrapper function for `App:run`
+that will only run the application if there is no
+instance already running with the specified name.
+
+:::code-group
+
+```lua [init.lua]
+App:start({
+ instance_name = "my-instance", -- defaults to "astal"
+ main = function()
+ -- setup anything
+ -- instantiate widgets
+ end,
+})
+```
+
+:::
+
+## Messaging from CLI
+
+If you want to interact with an instance from the CLI,
+you can do so by sending a request.
+
+```lua
+App:start({
+ main = function() end,
+
+ ---@param request string
+ ---@param res fun(response: any): nil
+ request_handler = function(request, res)
+ if request == "say hi" then
+ return res("hi cli")
+ end
+ res("unknown command")
+ end
+})
+```
+
+```sh
+astal say hi
+# hi cli
+```
+
+## Toggling Windows by their name
+
+In order for Astal to know about your windows, you have to register them.
+You can do this by specifying a **unique** `name` and calling `App:add_window`.
+
+```lua
+local App = require("astal.gtk3.app")
+
+local function Bar()
+ return Widget.Window({
+ name = "Bar",
+ setup = function(self)
+ App:add_window(self)
+ end,
+ Widget.Box()
+ })
+end
+```
+
+You can also invoke `App:add_window` by simply passing the `App` to the `application` prop.
+
+```lua
+local App = require("astal.gtk3.app")
+
+local function Bar()
+ return Widget.Window({
+ name = "Bar",
+ application = App,
+ Widget.Box()
+ })
+end
+```
+
+```sh
+astal -t Bar
+```
+
+:::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.
+:::
+
+## Client instances
+
+The first time `App:start` is invoked the `main` function gets called.
+While that instance is running any subsequent execution of the script will call
+the `client` function.
+
+:::code-group
+
+```lua [init.lua]
+App:start({
+ -- main instance
+ main = function(...)
+ local args = { ... }
+ print(string.format("{%s}", table.concat(args, ", ")))
+ end,
+
+ -- client instance
+ client = function(request, ...)
+ local res = request("you can send a request to the main instance")
+ print(res)
+ end,
+
+ -- this runs in the main instance
+ request_handler = function(request, res)
+ res("response from main instance")
+ end
+})
+```
+
+:::
diff --git a/docs/guide/lua/first-widgets.md b/docs/guide/lua/first-widgets.md
index 2abe7c5..efc1c4f 100644
--- a/docs/guide/lua/first-widgets.md
+++ b/docs/guide/lua/first-widgets.md
@@ -1,3 +1,265 @@
# First Widgets
-🚧 Lua documentation is in Progress 🚧
+## Getting Started
+
+Start by importing the singleton
+[Astal.Application](https://aylur.github.io/libastal/astal3/class.Application.html) instance.
+
+:::code-group
+
+```lua [init.lua]
+local App = require("astal.gtk3.app")
+
+App:start({
+ main = function()
+ -- you will instantiate Widgets here
+ -- or setup anything else if you need
+ print("hi")
+ end
+})
+```
+
+:::
+
+Then run `lua init.lua` in the terminal, and that's it!
+Now you have an instance running with Lua.
+
+## 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/astal3/class.Window.html)
+which will hold all widgets.
+
+::: code-group
+
+```lua [widget/Bar.lua]
+local Widget = require("astal.gtk3.widget")
+local Anchor = require("astal.gtk3").Astal.WindowAnchor
+
+return function(monitor)
+ return Widget.Window({
+ monitor = monitor,
+ anchor = Anchor.TOP + Anchor.LEFT + Anchor.RIGHT,
+ exclusivity = "EXCLUSIVE",
+ Widget.Label({
+ label = "Example label content",
+ }),
+ })
+end
+```
+
+:::
+
+::: code-group
+
+```lua [init.lua]
+local App = require("astal.gtk3.app")
+local Bar = require("widget.Bar")
+
+App:start {
+ main = function()
+ Bar(0)
+ Bar(1) -- instantiate for each monitor
+ end,
+}
+```
+
+:::
+
+## Creating and nesting widgets
+
+Widgets are simply Lua functions that return Gtk widgets,
+you can nest widgets by passing them as arguments to the table in the function.
+
+:::code-group
+
+```lua [widget/MyButton.lua]
+local Widget = require("astal.gtk3.widget")
+
+return function(text)
+ return Widget.Button({
+ on_click_release = function(_, event)
+ if event.button == "PRIMARY" then
+ print("Left click")
+ elseif event.button == "SECONDARY" then
+ print("Right click")
+ end
+ end,
+ Widget.Label({
+ label = text,
+ }),
+ })
+end
+```
+
+:::
+
+Now, you should be able to nest it into another widgets.
+
+::: code-group
+
+```lua [widget/Bar.lua] {13}
+local MyButton = require("widget.MyButton")
+local Anchor = require("astal.gtk3").Astal.WindowAnchor
+
+return function(monitor)
+ return Widget.Window({
+ monitor = monitor,
+ anchor = Anchor.TOP + Anchor.LEFT + Anchor.RIGHT,
+ exclusivity = "EXCLUSIVE",
+ Widget.Box({
+ Widget.Label({
+ label = "Click the button",
+ }),
+ MyButton("hi, im a button"),
+ }),
+ })
+end
+```
+
+:::
+
+## Widget signal handlers
+
+You can respond to events by declaring event handler functions inside your widget:
+
+```lua
+local function MyButton()
+ return Widget.Button({
+ on_click_release = function(_, event)
+ print(event.button)
+ end,
+ })
+end
+```
+
+:::info
+Keys prefixed with `on_` will connect to a `signal` of the widget.
+Refer to the Gtk and Astal docs to have a full list of them.
+:::
+
+## 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:
+
+```lua
+local astal = require("astal")
+local bind = astal.bind
+local Variable = astal.Variable
+local Widget = require("astal.gtk3.widget")
+
+local function Counter()
+ local count = Variable(0)
+ return Widget.Box({
+ Widget.Label({
+ label = bind(count):as(tostring),
+ }),
+ Widget.Button({
+ label = "Click to increment",
+ on_click_release = function()
+ count:set(count:get() + 1)
+ end,
+ }),
+ })
+end
+```
+
+:::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
+converted into a string first.
+:::
+
+:::tip
+`Variables` have a shorthand for `bind(variable):as(transform)`
+
+```lua
+local v = Variable(0)
+
+return Widget.Box {
+ -- these three are equivalent
+ Widget.Label({ label = bind(v):as(tostring) }),
+ Widget.Label({ label = v():as(tostring) }),
+ Widget.Label({ label = v(tostring) }),
+}
+```
+
+:::
+
+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):
+
+```lua
+local astal = require("astal")
+local bind = astal.bind
+local Battery = astal.require("AstalBattery")
+local Widget = require("astal.gtk3.widget")
+
+local function BatteryPercentage()
+ local bat = Battery.get_default()
+
+ return Widget.Label({
+ label = bind(bat, "percentage"):as(function(p)
+ return string.format("%.0f%%", p * 100)
+ end),
+ })
+end
+```
+
+## Dynamic children
+
+You can also use a `Binding` for `child` and `children` properties.
+
+```lua
+local astal = require("astal")
+local Variable = astal.Variable
+local Widget = require("astal.gtk3.widget")
+
+local child = Variable(Widget.Box())
+
+return Widget.Box({
+ child(),
+})
+```
+
+```lua
+local num = Variable(3)
+
+return Widget.Box {
+ num():as(function(n)
+ local tbl = {}
+ for i = 1, n do
+ table.insert(tbl, Widget.Button({
+ label = tostring(i)
+ }))
+ end
+ return tbl
+ end)
+}
+```
+
+:::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 `no_implicity_destroy` property on the container widget.
+:::
+
+:::info
+You can pass the followings as children:
+
+- widgets
+- deeply nested arrays of widgets
+- bindings of widgets,
+- bindings of deeply nested arrays of widgets
+
+`nil` is the only value that is not rendered and anything not from this list
+will be coerced into a string and rendered as a label.
+:::
diff --git a/docs/guide/lua/installation.md b/docs/guide/lua/installation.md
index 48241f9..b99d8df 100644
--- a/docs/guide/lua/installation.md
+++ b/docs/guide/lua/installation.md
@@ -1,3 +1,25 @@
# Installation
-🚧 Lua documentation is in Progress 🚧
+## Nix
+
+maintainer: [@Aylur](https://github.com/Aylur)
+
+Read more about it on the [nix page](../getting-started/nix)
+
+## Arch
+
+```sh
+yay -S lua-libastal-git
+```
+
+## From Source
+
+1. [Install Astal](../getting-started/installation.md) if you have not already
+
+2. Install the Astal Lua package
+
+```sh
+git clone https://github.com/aylur/astal.git
+cd lang/lua
+sudo luarocks make
+```
diff --git a/docs/guide/lua/theming.md b/docs/guide/lua/theming.md
new file mode 100644
index 0000000..502e8e9
--- /dev/null
+++ b/docs/guide/lua/theming.md
@@ -0,0 +1,130 @@
+# 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
+
+```lua [init.lua]
+local inline_css = [[
+ window {
+ background-color: transparent;
+ }
+]]
+
+App:start({
+ css = inline_css,
+ css = "/path/to/style.css",
+ css = "./style.css",
+})
+```
+
+:::
+
+:::warning
+When using relative paths, so for example `./style.css` keep in mind that they
+will be relative to the current working directory.
+:::
+
+## Css Property on Widgets
+
+```lua
+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.
+
+```lua
+App:apply_css("/path/to/file.css")
+```
+
+```lua
+App:apply_css([[
+ window {
+ background-color: transparent;
+ }
+]])
+```
+
+```lua
+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:reset_css`
+or by passing `true` as a second parameter to `App:apply_css`.
+:::
+
+## 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
+astal --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
+```
+
+:::
+
+:::code-group
+
+```lua [init.lua]
+local scss = "./style.scss"
+local css = "/tmp/style.css"
+
+astal.exec(string.format("sass %s %s", scss, css))
+
+App:start({
+ css = css,
+})
+```
+
+:::
diff --git a/docs/guide/lua/utilities.md b/docs/guide/lua/utilities.md
new file mode 100644
index 0000000..1ef70c7
--- /dev/null
+++ b/docs/guide/lua/utilities.md
@@ -0,0 +1,191 @@
+# Utilities
+
+## File functions
+
+```lua
+local read_file = astal.read_file
+local read_file_async = astal.read_file_async
+local write_file = astal.write_file
+local write_file_async = astal.write_file_async
+local monitor_file = astal.monitor_file
+```
+
+### Reading files
+
+```lua
+---@param path string
+---@return string
+local function read_file(path) end
+
+---@param path string
+---@param callback fun(content: string, err: string): nil
+local function read_file_async(path, callback) end
+```
+
+### Writing files
+
+```lua
+---@param path string
+---@param content string
+local function write_file(path, content) end
+
+---@param path string
+---@param content string
+---@param callback? fun(err: string): nil
+local function write_file_async(path, content, callback) end
+```
+
+### Monitoring files
+
+```lua
+---@param path string
+---@param callback fun(file: string, event: Gio.FileMonitorEvent): nil
+local function monitor_file(path, callback) end
+```
+
+## Timeouts and Intervals
+
+```lua
+local interval = astal.interval
+local timeout = astal.timeout
+local idle = astal.idle
+```
+
+### Interval
+
+Will immediately execute the function and every `interval` millisecond.
+
+```lua
+---@param interval number
+---@param callback? function
+---@return Astal.Time
+local function interval(interval, callback) end
+```
+
+### Timeout
+
+Will execute the `callback` after `timeout` millisecond.
+
+```lua
+---@param timeout number
+---@param callback? function
+---@return Astal.Time
+local function timeout(timeout, callback) end
+```
+
+### Idle
+
+Executes `callback` whenever there are no higher priority events pending.
+
+```lua
+---@param callback? function
+---@return Astal.Time
+local function idle(callback) end
+```
+
+Example:
+
+```lua
+local timer = interval(1000, function()
+ print("optional callback")
+end)
+
+timer.on_now = function()
+ print("now")
+end
+
+timer.on_cancelled = function()
+ print("cancelled")
+end
+
+timer:cancel()
+```
+
+## Process functions
+
+```lua
+local subprocess = astal.subprocess
+local exec = astal.exec
+local exec_async = astal.exec_async
+```
+
+### Subprocess
+
+You can start a subprocess and run callback functions whenever it outputs to
+stdout or stderr. [Astal.Process](https://aylur.github.io/libastal/io/class.Process.html) has a `stdout` and `stderr` signal.
+
+```lua
+---@param commandline string | string[]
+---@param on_stdout? fun(out: string): nil
+---@param on_stderr? fun(err: string): nil
+---@return Astal.Process | nil
+local function subprocess(commandline, on_stdout, on_stderr) end
+```
+
+Example:
+
+```lua
+local proc = subprocess(
+ "some-command",
+ function(out) print(out) end,
+ function(err) print(err) end,
+)
+
+-- with signals
+local proc = subprocess("some-command")
+
+proc.on_stdout = function(_, stdout)
+ print(stdout)
+end
+
+proc.on_stderr = function(_, stderr)
+ print(stderr)
+end
+```
+
+### Executing external commands and scripts
+
+```lua
+---@param commandline string | string[]
+---@return string, string
+local function exec(commandline) end
+
+---@param commandline string | string[]
+---@param callback? fun(out: string, err: string): nil
+local function exec_async(commandline, callback) end
+```
+
+Example:
+
+```lua
+local out, err = exec("/path/to/script")
+
+if err ~= nil then
+ print(err)
+else
+ print(out)
+end
+
+exec_async({ "bash", "-c", "/path/to/script.sh" }, function(out, err)
+ if err ~= nil then
+ print(err)
+ else
+ print(out)
+ end
+end)
+```
+
+:::warning
+`subprocess`, `exec`, and `exec_async` 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.
+
+```lua
+exec({ "bash", "-c", "command $VAR && command" })
+exec("bash -c 'command $VAR' && command")
+```
+
+:::
diff --git a/docs/guide/lua/variable.md b/docs/guide/lua/variable.md
new file mode 100644
index 0000000..915632c
--- /dev/null
+++ b/docs/guide/lua/variable.md
@@ -0,0 +1,162 @@
+# Variable
+
+```lua
+local Variable = require("astal").Variable
+local Variable = require("astal.variable")
+```
+
+Variable is just a simple object which holds a single value.
+It also has some shortcuts for hooking up subprocesses, intervals and other gobjects.
+
+## Example Usage
+
+```lua
+local my_var = Variable("initial-value")
+
+-- whenever its value changes, callback will be executed
+my_var:subscribe(function(value)
+ print(value)
+end)
+
+-- settings its value
+my_var:set("new value")
+
+-- getting its value
+local value = my_var:get()
+
+-- binding them to widgets
+Widget.Label({
+ label = bind(my_var):as(function(value)
+ return string.format("transformed %s", value)
+ end),
+ -- shorthand for the above
+ label = my_var(function(value)
+ return string.format("transformed %s", value)
+ end)
+})
+```
+
+:::warning
+Make sure to the transform functions you pass 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.
+
+```lua
+local v1 = Variable(1) -- Variable
+local v2 = bind(obj, "prop") -- Binding
+local v3 = { -- Subscribable
+ get = function()
+ return 3
+ end,
+ subscribe = function()
+ return function() end
+ end,
+}
+
+-- 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
+local v4 = Variable.derive({ v1, v2, v3 }, function(v1, v2, v3)
+ return v1 * v2 * v3
+end)
+```
+
+## Subprocess shorthands
+
+Using `:poll` and `:watch` you 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 [exec_async](/guide/lua/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.
+
+```lua
+Variable(""):poll(1000, { "bash", "-c", "command $VAR && command" })
+```
+
+:::
+
+```lua
+local my_var = Variable(0)
+ :poll(1000, "command", function(out, prev)
+ return tonumber(out)
+ end)
+ :poll(1000, { "bash", "-c", "command" }, function(out, prev)
+ return tonumber(out)
+ end)
+ :poll(1000, function(prev)
+ return prev + 1
+ end)
+```
+
+```lua
+local my_var = Variable(0)
+ :watch("command", function(out, prev)
+ return tonumber(out)
+ end)
+ :watch({ "bash", "-c", "command" }, function(out, prev)
+ return tonumber(out)
+ end)
+```
+
+You can temporarily stop them and restart them whenever.
+
+```lua
+my_var:stop_watch() -- this kills the subprocess
+my_var:stop_poll()
+
+my_var:start_listen() -- launches the subprocess again
+my_var:start_poll()
+
+print(my_var:is_listening())
+print(my_var:is_polling())
+```
+
+## Gobject connection shorthands
+
+Using `:observe` you can connect gobject signals and capture their value.
+
+```lua
+local my_var = Variable("")
+ :observe(obj1, "signal", function()
+ return ""
+ end):observe(obj2, "signal", function()
+ return ""
+ end)
+```
+
+## Dispose if no longer needed
+
+This will stop the interval, force exit the subprocess and disconnect gobjects.
+
+```lua
+my_var:drop()
+```
+
+:::warning
+Don't forget to drop derived variables or variables with
+either `:poll`, `:watch` or `:observe` when they are defined inside closures.
+
+```lua
+local function MyWidget()
+ local my_var = Variable():poll()
+
+ return Widget.Box({
+ on_destroy = function()
+ my_var:drop()
+ end
+ })
+end
+```
+
+:::
diff --git a/docs/guide/lua/widget.md b/docs/guide/lua/widget.md
new file mode 100644
index 0000000..d9f99fa
--- /dev/null
+++ b/docs/guide/lua/widget.md
@@ -0,0 +1,158 @@
+# Widget
+
+## Gtk3
+
+### Additional widget properties
+
+These are properties that Astal additionally adds to Gtk.Widgets
+
+- class_name: `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).
+- click_through: `boolean` - Lets click events through.
+
+To have a full list of available properties, reference the documentation of the widget.
+
+- [Astal3 widgets](https://aylur.github.io/libastal/astal3/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`
+
+```lua
+local function MyWidget()
+ local button = Widget.Button()
+ -- setup button
+ return button
+end
+```
+
+using `setup`
+
+```lua
+local function MyWidget()
+ return Widget.Button({
+ setup = function(self)
+ -- setup button
+ end,
+ })
+end
+```
+
+#### hook
+
+Shorthand for connecting and disconnecting to [Subscribable and Connectable](./binding#subscribable-and-connectable-interface) objects.
+
+without `hook`
+
+```lua
+local function MyWidget()
+ local id = gobject.on_signal:connect(callback)
+ local unsub = variable:subscribe(callback)
+
+ return Widget.Box({
+ on_destroy = function()
+ GObject.signal_handler_disconnect(gobject, id)
+ unsub()
+ end,
+ })
+end
+```
+
+with `hook`
+
+```lua
+local function MyWidget()
+ return Widget.Box({
+ setup = function(self)
+ self:hook(gobject, "signal", callback)
+ self:hook(variable, callback)
+ end,
+ })
+end
+```
+
+#### toggle_class_name
+
+Toggle class names based on a condition.
+
+```lua
+local function MyWidget()
+ return Widget.Box({
+ setup = function(self)
+ self:toggle_class_name("classname", some_condition)
+ end,
+ })
+end
+```
+
+### How to use non builtin Gtk widgets
+
+Using the `astalify` function you can wrap widgets
+to behave like builtin widgets.
+It 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
+
+```lua
+local astal = require("astal.gtk3")
+local astalify = astal.astalify
+local Gtk = astal.Gtk
+local Gdk = astal.Gdk
+
+local ColorButton = astalify(Gtk.ColorButton)
+
+local function MyWidget()
+ return ColorButton({
+ setup = function(self)
+ -- setup ColorButton instance
+ end,
+ use_alpha = true,
+ rgba = Gdk.RGBA({
+ red = 1,
+ green = 0,
+ blue = 0,
+ alpha = 0.5,
+ }),
+ on_color_set = function(self)
+ print(self.rgba:to_string())
+ end
+ })
+end
+```
+
+### Builtin Widgets
+
+You can check the [source code](https://github.com/Aylur/astal/blob/main/lang/lua/astal/gtk3/widget.lua) to have a full list of builtin widgets.
+
+These widgets are available by default in Lua.
+
+- Box: [Astal.Box](https://aylur.github.io/libastal/astal3/class.Box.html)
+- Button: [Astal.Button](https://aylur.github.io/libastal/astal3/class.Button.html)
+- CenterBox: [Astal.CenterBox](https://aylur.github.io/libastal/astal3/class.CenterBox.html)
+- CircularProgress: [Astal.CircularProgress](https://aylur.github.io/libastal/astal3/class.CircularProgress.html)
+- DrawingArea: [Gtk.DrawingArea](https://docs.gtk.org/gtk3/astal3/class.DrawingArea.html)
+- Entry: [Gtk.Entry](https://docs.gtk.org/gtk3/astal3/class.Entry.html)
+- Eventbox: [Astal.EventBox](https://aylur.github.io/libastal/astal3/class.EventBox.html)
+- Icon: [Astal.Icon](https://aylur.github.io/libastal/astal3/class.Icon.html)
+- Label: [Astal.Label](https://aylur.github.io/libastal/astal3/class.Label.html)
+- Levelbar: [Astal.LevelBar](https://aylur.github.io/libastal/astal3/class.LevelBar.html)
+- Overlay: [Astal.Overlay](https://aylur.github.io/libastal/astal3/class.Overlay.html)
+- Revealer: [Gtk.Revealer](https://docs.gtk.org/gtk3/astal3/class.Revealer.html)
+- Scrollable: [Astal.Scrollable](https://aylur.github.io/libastal/astal3/class.Scrollable.html)
+- Slider: [Astal.Slider](https://aylur.github.io/libastal/astal3/class.Slider.html)
+- Stack: [Astal.Stack](https://aylur.github.io/libastal/astal3/class.Stack.html)
+- Switch: [Gtk.Switch](https://docs.gtk.org/gtk3/astal3/class.Switch.html)
+- Window: [Astal.Window](https://aylur.github.io/libastal/astal3/class.Window.html)
+
+## Gtk4
+
+🚧 Work in Progress 🚧
diff --git a/docs/guide/typescript/binding.md b/docs/guide/typescript/binding.md
index 05645ab..15fe3cc 100644
--- a/docs/guide/typescript/binding.md
+++ b/docs/guide/typescript/binding.md
@@ -25,11 +25,11 @@ 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<T>(obj: Subscribable<T>): Binding<T>
function bind<
- Obj extends Connectable,
- Prop extends keyof Obj,
+ Obj extends Connectable,
+ Prop extends keyof Obj,
>(obj: Obj, prop: Prop): Binding<Obj[Prop]>
```
diff --git a/docs/guide/typescript/utilities.md b/docs/guide/typescript/utilities.md
index 02dfdaf..361c33b 100644
--- a/docs/guide/typescript/utilities.md
+++ b/docs/guide/typescript/utilities.md
@@ -128,8 +128,8 @@ const proc = subprocess(
// or with signals
const proc = subprocess("some-command")
-proc.connect("stdout", (out) => console.log(out))
-proc.connect("stderr", (err) => console.error(err))
+proc.connect("stdout", (_, out) => console.log(out))
+proc.connect("stderr", (_, err) => console.error(err))
```
### Executing external commands and scripts
diff --git a/docs/guide/typescript/variable.md b/docs/guide/typescript/variable.md
index 2abacbd..e6f3434 100644
--- a/docs/guide/typescript/variable.md
+++ b/docs/guide/typescript/variable.md
@@ -35,7 +35,7 @@ Widget.Label({
```
:::warning
-Make sure to make the transform functions passed to `.as()` are pure.
+Make sure to the transform functions you pass to `:as()` are pure.
The `.get()` function can be called anytime by `astal` especially when `deriving`,
so make sure there are no sideeffects.
:::
@@ -126,7 +126,7 @@ const myvar = Variable("")
## Dispose if no longer needed
-This will stop the interval and force exit the subprocess and disconnect gobjects.
+This will stop the interval, force exit the subprocess and disconnect gobjects.
```js
myVar.drop()
diff --git a/docs/vitepress.config.ts b/docs/vitepress.config.ts
index 2880477..2df3eea 100644
--- a/docs/vitepress.config.ts
+++ b/docs/vitepress.config.ts
@@ -61,7 +61,7 @@ export default defineConfig({
{
text: "TypeScript",
base: "/guide/typescript",
- collapsed: false,
+ collapsed: true,
items: [
{ text: "Installation", link: "/installation" },
{ text: "First Widgets", link: "/first-widgets" },
@@ -78,10 +78,18 @@ export default defineConfig({
{
text: "Lua",
base: "/guide/lua",
- collapsed: false,
+ collapsed: true,
items: [
{ text: "Installation", link: "/installation" },
{ text: "First Widgets", link: "/first-widgets" },
+ { text: "Theming", link: "/theming" },
+ { text: "CLI and App", link: "/cli-app" },
+ { text: "Widget", link: "/widget" },
+ { text: "Variable", link: "/variable" },
+ { text: "Binding", link: "/binding" },
+ // { text: "GObject", link: "/gobject" },
+ { text: "Utilities", link: "/utilities" },
+ // { text: "FAQ", link: "/faq" },
],
},
{
diff --git a/lang/lua/astal/binding.lua b/lang/lua/astal/binding.lua
index ba1e6e4..a21fc26 100644
--- a/lang/lua/astal/binding.lua
+++ b/lang/lua/astal/binding.lua
@@ -4,7 +4,7 @@ local GObject = lgi.require("GObject", "2.0")
---@class Binding
---@field emitter table|Variable
---@field property? string
----@field transformFn function
+---@field transform_fn function
local Binding = {}
---@param emitter table
@@ -30,10 +30,10 @@ end
function Binding:get()
if self.property ~= nil and GObject.Object:is_type_of(self.emitter) then
- return self.transformFn(self.emitter[self.property])
+ return self.transform_fn(self.emitter[self.property])
end
if type(self.emitter.get) == "function" then
- return self.transformFn(self.emitter:get())
+ return self.transform_fn(self.emitter:get())
end
error("can not get: Not a GObject or a Variable " + self)
end
@@ -42,8 +42,8 @@ end
---@return Binding
function Binding:as(transform)
local b = Binding.new(self.emitter, self.property)
- b.transformFn = function(v)
- return transform(self.transformFn(v))
+ b.transform_fn = function(v)
+ return transform(self.transform_fn(v))
end
return b
end
diff --git a/lang/lua/astal/gtk3/init.lua b/lang/lua/astal/gtk3/init.lua
index 6fb5455..e5cc0e6 100644
--- a/lang/lua/astal/gtk3/init.lua
+++ b/lang/lua/astal/gtk3/init.lua
@@ -1,5 +1,11 @@
+local lgi = require("lgi")
+
return {
App = require("astal.gtk3.app"),
astalify = require("astal.gtk3.astalify"),
Widget = require("astal.gtk3.widget"),
+
+ Gtk = lgi.require("Gtk", "3.0"),
+ Gdk = lgi.require("Gdk", "3.0"),
+ Astal = lgi.require("Astal", "3.0"),
}