summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/FUNDING.yml2
-rw-r--r--CONTRIBUTING.md7
-rw-r--r--docs/default.nix7
-rw-r--r--docs/guide/getting-started/installation.md2
-rw-r--r--docs/guide/libraries/apps.md27
-rw-r--r--docs/guide/libraries/battery.md2
-rw-r--r--docs/guide/libraries/bluetooth.md2
-rw-r--r--docs/guide/libraries/cava.md91
-rw-r--r--docs/guide/libraries/hyprland.md2
-rw-r--r--docs/guide/libraries/mpris.md2
-rw-r--r--docs/guide/libraries/network.md2
-rw-r--r--docs/guide/libraries/notifd.md2
-rw-r--r--docs/guide/libraries/powerprofiles.md2
-rw-r--r--docs/guide/libraries/wireplumber.md2
-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/guide/typescript/widget.md8
-rw-r--r--docs/vitepress.config.ts13
-rw-r--r--flake.nix4
-rw-r--r--lang/lua/astal/binding.lua12
-rw-r--r--lang/lua/astal/gtk3/init.lua6
-rw-r--r--lib/apps/application.vala52
-rw-r--r--lib/apps/apps.vala58
-rw-r--r--lib/astal/gtk3/src/widget/window.vala13
-rw-r--r--lib/astal/io/application.vala69
-rw-r--r--lib/astal/io/cli.vala53
-rw-r--r--lib/auth/src/pam.c1
-rw-r--r--lib/bluetooth/adapter.vala117
-rw-r--r--lib/bluetooth/bluetooth.vala50
-rw-r--r--lib/bluetooth/device.vala169
l---------lib/bluetooth/gir.py1
-rw-r--r--lib/bluetooth/interfaces.vala46
-rw-r--r--lib/bluetooth/meson.build49
-rw-r--r--lib/cava/.gitignore1
-rw-r--r--lib/cava/astal-cava.h70
-rw-r--r--lib/cava/cava.c659
-rw-r--r--lib/cava/meson.build80
-rw-r--r--lib/cava/meson_options.txt13
-rw-r--r--lib/cava/subprojects/cava.wrap7
-rw-r--r--lib/cava/version1
-rw-r--r--lib/hyprland/client.vala5
l---------lib/mpris/gir.py1
-rw-r--r--lib/mpris/ifaces.vala40
-rw-r--r--lib/mpris/meson.build46
-rw-r--r--lib/mpris/mpris.vala35
-rw-r--r--lib/mpris/player.vala365
-rw-r--r--lib/notifd/notification.vala12
-rw-r--r--lib/notifd/proxy.vala4
-rw-r--r--lib/notifd/signals.md2
-rw-r--r--nix/libcava.nix60
-rw-r--r--nix/lua.nix2
60 files changed, 3102 insertions, 387 deletions
diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
index 6446526..46f45a4 100644
--- a/.github/FUNDING.yml
+++ b/.github/FUNDING.yml
@@ -1,6 +1,6 @@
# These are supported funding model platforms
-github: [aylur]
+github: [aylur, kotontrion]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: aylur
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index e6c097e..e09c3dc 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,6 +1,7 @@
# Contributing
You can contribute by:
+
- [Suggesting new features](https://github.com/Aylur/astal/issues/new?assignees=&labels=enhancement&projects=&template=feature_request.md&title=)
- [Reporting bugs](https://github.com/Aylur/astal/issues/new?assignees=&labels=bug&projects=&template=bug_report.md&title=)
- Improving docs with additional contexts and examples
@@ -18,9 +19,11 @@ Write libraries preferably in Vala. Only choose C if some dependency is only ava
## Todo
Planned features, you could help with:
+
- [niri ipc library](https://github.com/Aylur/astal/issues/8)
- sway ipc library
- greetd ipc library
-- core: http request library, abstraction over libsoup included (mostly to be used in gjs and lua)
-- core: notification sending, libnotify clone [#26](https://github.com/Aylur/astal/issues/26)
+- http request library abstraction over libsoup (mostly to be used in gjs and lua)
+- notification sending libnotify clone [#26](https://github.com/Aylur/astal/issues/26)
- setting up [uncrustify](https://github.com/uncrustify/uncrustify) for Vala
+- bluetooth custom errordomains, currently every error is simply Error
diff --git a/docs/default.nix b/docs/default.nix
index 0d79ce1..1370fc6 100644
--- a/docs/default.nix
+++ b/docs/default.nix
@@ -154,6 +154,13 @@ in
version = ../lib/bluetooth/version;
}}
${genLib {
+ flakepkg = "cava";
+ gir = "Cava";
+ description = "Audio visualization library using cava";
+ version = ../lib/cava/version;
+ authors = "kotontrion";
+ }}
+ ${genLib {
flakepkg = "hyprland";
gir = "Hyprland";
description = "IPC client for Hyprland";
diff --git a/docs/guide/getting-started/installation.md b/docs/guide/getting-started/installation.md
index 844de25..fa7863e 100644
--- a/docs/guide/getting-started/installation.md
+++ b/docs/guide/getting-started/installation.md
@@ -33,7 +33,7 @@ sudo pacman -Syu meson vala gtk3 gtk-layer-shell gobject-introspection
```
```sh [<i class="devicon-fedora-plain"></i> Fedora]
-sudo dnf install meson gcc valac gtk3-devel gtk-layer-shell-devel gobject-introspection-devel
+sudo dnf install meson vala valadoc gtk3-devel gtk-layer-shell-devel gobject-introspection-devel
```
```sh [<i class="devicon-ubuntu-plain"></i> Ubuntu]
diff --git a/docs/guide/libraries/apps.md b/docs/guide/libraries/apps.md
index 7f9ee6e..1871d18 100644
--- a/docs/guide/libraries/apps.md
+++ b/docs/guide/libraries/apps.md
@@ -14,7 +14,7 @@ sudo pacman -Syu meson vala json-glib gobject-introspection
```
```sh [<i class="devicon-fedora-plain"></i> Fedora]
-sudo dnf install meson gcc valac json-glib-devel gobject-introspection-devel
+sudo dnf install meson vala valadoc json-glib-devel gobject-introspection-devel
```
```sh [<i class="devicon-ubuntu-plain"></i> Ubuntu]
@@ -55,8 +55,9 @@ astal-apps --help
import Apps from "gi://AstalApps"
const apps = new Apps.Apps({
- includeEntry: true,
- includeExecutable: true,
+ nameMultiplier: 2,
+ entryMultiplier: 0,
+ executableMultiplier: 2,
})
for (const app of apps.fuzzy_query("spotify")) {
@@ -68,8 +69,9 @@ for (const app of apps.fuzzy_query("spotify")) {
from gi.repository import AstalApps as Apps
apps = Apps.Apps(
- include_entry=True,
- include_executable=True,
+ name_multiplier=2,
+ entry_multiplier=0,
+ executable_multiplier=2,
)
for app in apps.fuzzy_query("obsidian"):
@@ -81,8 +83,9 @@ for app in apps.fuzzy_query("obsidian"):
local Apps = require("lgi").require("AstalApps")
local apps = Apps.Apps({
- include_entry = true,
- include_executable = true,
+ name_multiplier = 2,
+ entry_multiplier = 0,
+ executable_multiplier = 2,
})
for _, app in ipairs(apps:fuzzy_query("lutris")) do
@@ -91,7 +94,15 @@ end
```
```vala [<i class="devicon-vala-plain"></i> Vala]
-// Not yet documented, contributions are appreciated
+var apps = new AstalApps.Apps() {
+ name_multiplier = 2,
+ entry_multiplier = 0,
+ executable_multiplier = 2,
+};
+
+foreach (var app in apps.fuzzy_query("firefox")) {
+ print(app.name);
+}
```
:::
diff --git a/docs/guide/libraries/battery.md b/docs/guide/libraries/battery.md
index 7f94297..56f955c 100644
--- a/docs/guide/libraries/battery.md
+++ b/docs/guide/libraries/battery.md
@@ -13,7 +13,7 @@ sudo pacman -Syu meson vala json-glib gobject-introspection
```
```sh [<i class="devicon-fedora-plain"></i> Fedora]
-sudo dnf install meson gcc valac json-glib-devel gobject-introspection-devel
+sudo dnf install meson vala valadoc json-glib-devel gobject-introspection-devel
```
```sh [<i class="devicon-ubuntu-plain"></i> Ubuntu]
diff --git a/docs/guide/libraries/bluetooth.md b/docs/guide/libraries/bluetooth.md
index 672f66d..03ac9c9 100644
--- a/docs/guide/libraries/bluetooth.md
+++ b/docs/guide/libraries/bluetooth.md
@@ -13,7 +13,7 @@ sudo pacman -Syu meson vala gobject-introspection
```
```sh [<i class="devicon-fedora-plain"></i> Fedora]
-sudo dnf install meson gcc valac gobject-introspection-devel
+sudo dnf install meson vala valadoc gobject-introspection-devel
```
```sh [<i class="devicon-ubuntu-plain"></i> Ubuntu]
diff --git a/docs/guide/libraries/cava.md b/docs/guide/libraries/cava.md
new file mode 100644
index 0000000..e695e16
--- /dev/null
+++ b/docs/guide/libraries/cava.md
@@ -0,0 +1,91 @@
+# Cava
+
+Audio visualizer using [cava](https://github.com/karlstav/cava).
+
+## Installation
+
+1. install dependencies
+
+Note that it requires [libcava](https://github.com/LukashonakV/cava), a fork of cava, which provides cava as a shared library.
+
+:::code-group
+
+```sh [<i class="devicon-archlinux-plain"></i> Arch]
+sudo pacman -Syu meson vala gobject-introspection
+paru -S libcava
+```
+
+```sh [<i class="devicon-fedora-plain"></i> Fedora]
+# Not yet documented
+```
+
+```sh [<i class="devicon-ubuntu-plain"></i> Ubuntu]
+# Not yet documented
+```
+
+:::
+
+2. clone repo
+
+```sh
+git clone https://github.com/aylur/astal.git
+cd astal/lib/cava
+```
+
+3. install
+
+```sh
+meson setup build
+meson install -C build
+```
+
+:::tip
+Most distros recommend manual installs in `/usr/local`,
+which is what `meson` defaults to. If you want to install to `/usr`
+instead which most package managers do, set the `prefix` option:
+
+```sh
+meson setup --prefix /usr build
+```
+
+:::
+
+## Usage
+
+You can browse the [Cava reference](https://aylur.github.io/libastal/cava).
+
+### CLI
+
+There is no CLI for this library, use the one provided by cava.
+
+```sh
+cava
+```
+
+### Library
+
+:::code-group
+
+```js [<i class="devicon-javascript-plain"></i> JavaScript]
+import Cava from "gi://AstalCava"
+
+const cava = Cava.get_default()
+
+cava.connect("notify::values", () => {
+ print(cava.get_values())
+})
+```
+
+```py [<i class="devicon-python-plain"></i> Python]
+# Not yet documented
+```
+
+```lua [<i class="devicon-lua-plain"></i> Lua]
+-- Not yet documented
+```
+
+```vala [<i class="devicon-vala-plain"></i> Vala]
+// Not yet documented
+```
+
+:::
diff --git a/docs/guide/libraries/hyprland.md b/docs/guide/libraries/hyprland.md
index 672faad..94a398f 100644
--- a/docs/guide/libraries/hyprland.md
+++ b/docs/guide/libraries/hyprland.md
@@ -13,7 +13,7 @@ sudo pacman -Syu meson vala json-glib gobject-introspection
```
```sh [<i class="devicon-fedora-plain"></i> Fedora]
-sudo dnf install meson gcc valac json-glib-devel gobject-introspection-devel
+sudo dnf install meson vala valadoc json-glib-devel gobject-introspection-devel
```
```sh [<i class="devicon-ubuntu-plain"></i> Ubuntu]
diff --git a/docs/guide/libraries/mpris.md b/docs/guide/libraries/mpris.md
index 8f28924..30f3d13 100644
--- a/docs/guide/libraries/mpris.md
+++ b/docs/guide/libraries/mpris.md
@@ -29,7 +29,7 @@ sudo pacman -Syu meson vala gvfs json-glib gobject-introspection
```
```sh [<i class="devicon-fedora-plain"></i> Fedora]
-sudo dnf install meson gcc valac gvfs json-glib-devel gobject-introspection-devel
+sudo dnf install meson vala valadoc gvfs json-glib-devel gobject-introspection-devel
```
```sh [<i class="devicon-ubuntu-plain"></i> Ubuntu]
diff --git a/docs/guide/libraries/network.md b/docs/guide/libraries/network.md
index e076950..21c7b10 100644
--- a/docs/guide/libraries/network.md
+++ b/docs/guide/libraries/network.md
@@ -13,7 +13,7 @@ sudo pacman -Syu meson vala libnm gobject-introspection
```
```sh [<i class="devicon-fedora-plain"></i> Fedora]
-sudo dnf install meson gcc valac NetworkManager-libnm-devel gobject-introspection-devel
+sudo dnf install meson vala valadoc NetworkManager-libnm-devel gobject-introspection-devel
```
```sh [<i class="devicon-ubuntu-plain"></i> Ubuntu]
diff --git a/docs/guide/libraries/notifd.md b/docs/guide/libraries/notifd.md
index 094b770..4208700 100644
--- a/docs/guide/libraries/notifd.md
+++ b/docs/guide/libraries/notifd.md
@@ -17,7 +17,7 @@ sudo pacman -Syu meson vala gdk-pixbuf2 json-glib gobject-introspection
```
```sh [<i class="devicon-fedora-plain"></i> Fedora]
-sudo dnf install meson gcc valac gdk-pixbuf2-devel json-glib-devel gobject-introspection-devel
+sudo dnf install meson vala valadoc gdk-pixbuf2-devel json-glib-devel gobject-introspection-devel
```
```sh [<i class="devicon-ubuntu-plain"></i> Ubuntu]
diff --git a/docs/guide/libraries/powerprofiles.md b/docs/guide/libraries/powerprofiles.md
index 159f3ff..bdafcde 100644
--- a/docs/guide/libraries/powerprofiles.md
+++ b/docs/guide/libraries/powerprofiles.md
@@ -13,7 +13,7 @@ sudo pacman -Syu meson vala json-glib gobject-introspection
```
```sh [<i class="devicon-fedora-plain"></i> Fedora]
-sudo dnf install meson gcc valac json-glib-devel gobject-introspection-devel
+sudo dnf install meson vala valadoc json-glib-devel gobject-introspection-devel
```
```sh [<i class="devicon-ubuntu-plain"></i> Ubuntu]
diff --git a/docs/guide/libraries/wireplumber.md b/docs/guide/libraries/wireplumber.md
index 0592628..c06161e 100644
--- a/docs/guide/libraries/wireplumber.md
+++ b/docs/guide/libraries/wireplumber.md
@@ -13,7 +13,7 @@ sudo pacman -Syu meson vala wireplumber gobject-introspection
```
```sh [<i class="devicon-fedora-plain"></i> Fedora]
-sudo dnf install meson gcc valac wireplumber-devel gobject-introspection-devel
+sudo dnf install meson vala valadoc wireplumber-devel gobject-introspection-devel
```
```sh [<i class="devicon-ubuntu-plain"></i> Ubuntu]
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/guide/typescript/widget.md b/docs/guide/typescript/widget.md
index 3bdf394..7ed69e3 100644
--- a/docs/guide/typescript/widget.md
+++ b/docs/guide/typescript/widget.md
@@ -197,18 +197,18 @@ These widgets are available by default in JSX.
- 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)
+- 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/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)
+- revealer: [Gtk.Revealer](https://docs.gtk.org/gtk3/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)
+- switch: [Gtk.Switch](https://docs.gtk.org/gtk3/class.Switch.html)
- window: [Astal.Window](https://aylur.github.io/libastal/astal3/class.Window.html)
## Gtk4
diff --git a/docs/vitepress.config.ts b/docs/vitepress.config.ts
index 2880477..f542a68 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" },
],
},
{
@@ -96,6 +104,7 @@ export default defineConfig({
{ text: "Auth", link: "/guide/libraries/auth" },
{ text: "Battery", link: "/guide/libraries/battery" },
{ text: "Bluetooth", link: "/guide/libraries/bluetooth" },
+ { text: "Cava", link: "/guide/libraries/cava" },
{ text: "Hyprland", link: "/guide/libraries/hyprland" },
{ text: "Mpris", link: "/guide/libraries/mpris" },
{ text: "Network", link: "/guide/libraries/network" },
diff --git a/flake.nix b/flake.nix
index 4dc3cf3..734a110 100644
--- a/flake.nix
+++ b/flake.nix
@@ -54,6 +54,7 @@
auth = mkPkg "astal-auth" ./lib/auth [pam];
battery = mkPkg "astal-battery" ./lib/battery [json-glib];
bluetooth = mkPkg "astal-bluetooth" ./lib/bluetooth [];
+ cava = mkPkg "astal-cava" ./lib/cava [(pkgs.callPackage ./nix/libcava.nix {})];
hyprland = mkPkg "astal-hyprland" ./lib/hyprland [json-glib];
mpris = mkPkg "astal-mpris" ./lib/mpris [gvfs json-glib];
network = mkPkg "astal-network" ./lib/network [networkmanager];
@@ -62,12 +63,11 @@
river = mkPkg "astal-river" ./lib/river [json-glib];
tray = mkPkg "astal-tray" ./lib/tray [gtk3 gdk-pixbuf libdbusmenu-gtk3 json-glib];
wireplumber = mkPkg "astal-wireplumber" ./lib/wireplumber [wireplumber];
- # polkit = mkPkg "astal-polkit" ./lib/polkit [polkit];
gjs = pkgs.stdenvNoCC.mkDerivation {
src = ./lang/gjs;
name = "astal-gjs";
- buildInputs = [
+ nativeBuildInputs = [
meson
ninja
pkg-config
diff --git a/lang/lua/astal/binding.lua b/lang/lua/astal/binding.lua
index ba1e6e4..2944ec4 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
@@ -14,7 +14,7 @@ function Binding.new(emitter, property)
return setmetatable({
emitter = emitter,
property = property,
- transformFn = function(v)
+ transform_fn = function(v)
return v
end,
}, Binding)
@@ -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"),
}
diff --git a/lib/apps/application.vala b/lib/apps/application.vala
index 0a2f73c..3a9900a 100644
--- a/lib/apps/application.vala
+++ b/lib/apps/application.vala
@@ -1,3 +1,6 @@
+/**
+ * Object representing an applications .desktop file.
+ */
public class AstalApps.Application : Object {
/**
* The underlying DesktopAppInfo.
@@ -47,6 +50,20 @@ public class AstalApps.Application : Object {
*/
public string[] keywords { owned get { return app.get_keywords(); } }
+ /**
+ * `Categories` field from the desktop file.
+ */
+ public string[] categories {
+ owned get {
+ if (app.get_categories() == null)
+ return {};
+
+ var categories = app.get_categories();
+ var arr = categories.split(";");
+ return categories.has_suffix(";") ? arr[0:arr.length-1] : arr;
+ }
+ }
+
internal Application(string id, int? frequency = 0) {
Object(app: new DesktopAppInfo(id));
this.frequency = frequency;
@@ -94,6 +111,12 @@ public class AstalApps.Application : Object {
score.keywords = s;
}
}
+ foreach (var category in categories) {
+ var s = fuzzy_match_string(term, category);
+ if (s > score.categories) {
+ score.categories = s;
+ }
+ }
return score;
}
@@ -117,12 +140,27 @@ public class AstalApps.Application : Object {
score.keywords = keyword.down().contains(term.down()) ? 1 : 0;
}
}
+ foreach (var category in categories) {
+ if (score.categories == 0) {
+ score.categories = category.down().contains(term.down()) ? 1 : 0;
+ }
+ }
return score;
}
internal Json.Node to_json() {
- var builder = new Json.Builder()
+ var keyword_arr = new Json.Builder().begin_array();
+ foreach (string keyword in keywords) {
+ keyword_arr.add_string_value(keyword);
+ }
+
+ var category_arr = new Json.Builder().begin_array();
+ foreach (string category in categories) {
+ category_arr.add_string_value(category);
+ }
+
+ return new Json.Builder()
.begin_object()
.set_member_name("name").add_string_value(name)
.set_member_name("entry").add_string_value(entry)
@@ -130,15 +168,8 @@ public class AstalApps.Application : Object {
.set_member_name("description").add_string_value(description)
.set_member_name("icon_name").add_string_value(icon_name)
.set_member_name("frequency").add_int_value(frequency)
- .set_member_name("keywords")
- .begin_array();
-
- foreach (string keyword in keywords) {
- builder.add_string_value(keyword);
- }
-
- return builder
- .end_array()
+ .set_member_name("keywords").add_value(keyword_arr.end_array().get_root())
+ .set_member_name("categories").add_value(category_arr.end_array().get_root())
.end_object()
.get_root();
}
@@ -150,4 +181,5 @@ public struct AstalApps.Score {
int executable;
int description;
int keywords;
+ int categories;
}
diff --git a/lib/apps/apps.vala b/lib/apps/apps.vala
index dde7d44..999643c 100644
--- a/lib/apps/apps.vala
+++ b/lib/apps/apps.vala
@@ -1,3 +1,8 @@
+/**
+ * This object can be used to query applications.
+ * Multipliers can be set to customize [[email protected]] results
+ * from queries which then are summed and sorted accordingly.
+ */
public class AstalApps.Apps : Object {
private string cache_directory;
private string cache_file;
@@ -27,21 +32,21 @@ public class AstalApps.Apps : Object {
/**
* Extra multiplier to apply when matching the entry of an application.
- * Defaults to `1`
+ * Defaults to `0`
*/
- public double entry_multiplier { get; set; default = 1; }
+ public double entry_multiplier { get; set; default = 0; }
/**
* Extra multiplier to apply when matching the executable of an application.
- * Defaults to `1`
+ * Defaults to `0.5`
*/
- public double executable_multiplier { get; set; default = 1; }
+ public double executable_multiplier { get; set; default = 0.5; }
/**
* Extra multiplier to apply when matching the description of an application.
- * Defaults to `0.5`
+ * Defaults to `0`
*/
- public double description_multiplier { get; set; default = 0.5; }
+ public double description_multiplier { get; set; default = 0; }
/**
* Extra multiplier to apply when matching the keywords of an application.
@@ -50,34 +55,10 @@ public class AstalApps.Apps : Object {
public double keywords_multiplier { get; set; default = 0.5; }
/**
- * Consider the name of an application during queries.
- * Defaults to `true`
- */
- public bool include_name { get; set; default = true; }
-
- /**
- * Consider the entry of an application during queries.
- * Defaults to `false`
- */
- public bool include_entry { get; set; default = false; }
-
- /**
- * Consider the executable of an application during queries.
- * Defaults to `false`
- */
- public bool include_executable { get; set; default = false; }
-
- /**
- * Consider the description of an application during queries.
- * Defaults to `false`
- */
- public bool include_description { get; set; default = false; }
-
- /**
- * Consider the keywords of an application during queries.
- * Defaults to `false`
+ * Extra multiplier to apply when matching the categories of an application.
+ * Defaults to `0`
*/
- public bool include_keywords { get; set; default = false; }
+ public double categories_multiplier { get; set; default = 0; }
construct {
cache_directory = Environment.get_user_cache_dir() + "/astal";
@@ -115,11 +96,12 @@ public class AstalApps.Apps : Object {
if (alg == FUZZY) s = a.fuzzy_match(search);
if (alg == EXACT) s = a.exact_match(search);
- if (include_name) r += s.name * name_multiplier;
- if (include_entry) r += s.entry * entry_multiplier;
- if (include_executable) r += s.executable * executable_multiplier;
- if (include_description) r += s.description * description_multiplier;
- if (include_keywords) r += s.keywords * keywords_multiplier;
+ r += s.name * name_multiplier;
+ r += s.entry * entry_multiplier;
+ r += s.executable * executable_multiplier;
+ r += s.description * description_multiplier;
+ r += s.keywords * keywords_multiplier;
+ r += s.categories * categories_multiplier;
return r;
}
diff --git a/lib/astal/gtk3/src/widget/window.vala b/lib/astal/gtk3/src/widget/window.vala
index e513242..9287200 100644
--- a/lib/astal/gtk3/src/widget/window.vala
+++ b/lib/astal/gtk3/src/widget/window.vala
@@ -1,11 +1,12 @@
using GtkLayerShell;
+[Flags]
public enum Astal.WindowAnchor {
- NONE = 0,
- TOP = 1,
- RIGHT = 2,
- LEFT = 4,
- BOTTOM = 8,
+ NONE,
+ TOP,
+ RIGHT,
+ LEFT,
+ BOTTOM,
}
public enum Astal.Exclusivity {
@@ -112,7 +113,7 @@ public class Astal.Window : Gtk.Window {
* If two perpendicular edges are anchored, the surface will be anchored to that corner.
* If two opposite edges are anchored, the window will be stretched across the screen in that direction.
*/
- public int anchor {
+ public WindowAnchor anchor {
set {
if (check("set anchor"))
return;
diff --git a/lib/astal/io/application.vala b/lib/astal/io/application.vala
index c7bd311..09b61b5 100644
--- a/lib/astal/io/application.vala
+++ b/lib/astal/io/application.vala
@@ -103,75 +103,58 @@ public static List<string> get_instances() {
* Quit an an Astal instances.
* It is the equivalent of `astal --quit -i instance`.
*/
-public static void quit_instance(string instance) {
- try {
- IApplication proxy = Bus.get_proxy_sync(
- BusType.SESSION,
- "io.Astal." + instance,
- "/io/Astal/Application"
- );
+public static void quit_instance(string instance) throws Error {
+ IApplication proxy = Bus.get_proxy_sync(
+ BusType.SESSION,
+ "io.Astal." + instance,
+ "/io/Astal/Application"
+ );
- proxy.quit();
- } catch (Error err) {
- critical(err.message);
- }
+ proxy.quit();
}
/**
* Open the Gtk debug tool of an an Astal instances.
* It is the equivalent of `astal --inspector -i instance`.
*/
-public static void open_inspector(string instance) {
- try {
- IApplication proxy = Bus.get_proxy_sync(
- BusType.SESSION,
- "io.Astal." + instance,
- "/io/Astal/Application"
- );
+public static void open_inspector(string instance) throws Error {
+ IApplication proxy = Bus.get_proxy_sync(
+ BusType.SESSION,
+ "io.Astal." + instance,
+ "/io/Astal/Application"
+ );
- proxy.inspector();
- } catch (Error err) {
- critical(err.message);
- }
+ proxy.inspector();
}
/**
* Toggle a Window of an Astal instances.
* It is the equivalent of `astal -i instance --toggle window`.
*/
-public static void toggle_window_by_name(string instance, string window) {
- try {
- IApplication proxy = Bus.get_proxy_sync(
- BusType.SESSION,
- "io.Astal." + instance,
- "/io/Astal/Application"
- );
+public static void toggle_window_by_name(string instance, string window) throws Error {
+ IApplication proxy = Bus.get_proxy_sync(
+ BusType.SESSION,
+ "io.Astal." + instance,
+ "/io/Astal/Application"
+ );
- proxy.toggle_window(window);
- } catch (Error err) {
- critical(err.message);
- }
+ proxy.toggle_window(window);
}
/**
* Send a message to an Astal instances.
* It is the equivalent of `astal -i instance content of the message`.
*/
-public static string send_message(string instance, string msg) {
+public static string send_message(string instance, string msg) throws Error {
var rundir = Environment.get_user_runtime_dir();
var socket_path = @"$rundir/astal/$instance.sock";
var client = new SocketClient();
- try {
- var conn = client.connect(new UnixSocketAddress(socket_path), null);
- conn.output_stream.write(msg.concat("\x04").data);
+ var conn = client.connect(new UnixSocketAddress(socket_path), null);
+ conn.output_stream.write(msg.concat("\x04").data);
- var stream = new DataInputStream(conn.input_stream);
- return stream.read_upto("\x04", -1, null, null);
- } catch (Error err) {
- printerr(err.message);
- return "";
- }
+ var stream = new DataInputStream(conn.input_stream);
+ return stream.read_upto("\x04", -1, null, null);
}
/**
diff --git a/lib/astal/io/cli.vala b/lib/astal/io/cli.vala
index 8fc0523..9e47b53 100644
--- a/lib/astal/io/cli.vala
+++ b/lib/astal/io/cli.vala
@@ -11,13 +11,19 @@ const OptionEntry[] options = {
{ "help", 'h', OptionFlags.NONE, OptionArg.NONE, ref help, null, null },
{ "list", 'l', OptionFlags.NONE, OptionArg.NONE, ref list, null, null },
{ "quit", 'q', OptionFlags.NONE, OptionArg.NONE, ref quit, null, null },
- { "quit", 'q', OptionFlags.NONE, OptionArg.NONE, ref quit, null, null },
{ "inspector", 'I', OptionFlags.NONE, OptionArg.NONE, ref inspector, null, null },
{ "toggle-window", 't', OptionFlags.NONE, OptionArg.STRING, ref toggle_window, null, null },
{ "instance", 'i', OptionFlags.NONE, OptionArg.STRING, ref instance_name, null, null },
{ null },
};
+int err(string msg) {
+ var red = "\x1b[31m";
+ var r = "\x1b[0m";
+ printerr(@"$(red)error: $(r)$msg");
+ return 1;
+}
+
int main(string[] argv) {
try {
var opts = new OptionContext();
@@ -25,9 +31,8 @@ int main(string[] argv) {
opts.set_help_enabled(false);
opts.set_ignore_unknown_options(false);
opts.parse(ref argv);
- } catch (OptionError err) {
- printerr (err.message);
- return 1;
+ } catch (OptionError e) {
+ return err(e.message);
}
if (help) {
@@ -55,24 +60,30 @@ int main(string[] argv) {
if (list) {
foreach (var name in AstalIO.get_instances())
- stdout.printf("%s\n", name);
+ print(@"$name\n");
return 0;
}
- if (quit) {
- AstalIO.quit_instance(instance_name);
- return 0;
- }
+ try {
+ if (quit) {
+ AstalIO.quit_instance(instance_name);
+ return 0;
+ }
- if (inspector) {
- AstalIO.open_inspector(instance_name);
- return 0;
- }
+ if (inspector) {
+ AstalIO.open_inspector(instance_name);
+ return 0;
+ }
- if (toggle_window != null) {
- AstalIO.toggle_window_by_name(instance_name, toggle_window);
- return 0;
+ if (toggle_window != null) {
+ AstalIO.toggle_window_by_name(instance_name, toggle_window);
+ return 0;
+ }
+ } catch (DBusError.SERVICE_UNKNOWN e) {
+ return err(@"there is no \"$instance_name\" instance runnning\n");
+ } catch (Error e) {
+ return err(e.message);
}
var request = "";
@@ -80,8 +91,14 @@ int main(string[] argv) {
request = request.concat(" ", argv[i]);
}
- var reply = AstalIO.send_message(instance_name, request);
- print("%s\n", reply);
+ try {
+ var reply = AstalIO.send_message(instance_name, request);
+ print("%s\n", reply);
+ } catch (IOError.NOT_FOUND e) {
+ return err(@"there is no \"$instance_name\" instance runnning\n");
+ } catch (Error e) {
+ return err(e.message);
+ }
return 0;
}
diff --git a/lib/auth/src/pam.c b/lib/auth/src/pam.c
index d0afec4..f50107e 100644
--- a/lib/auth/src/pam.c
+++ b/lib/auth/src/pam.c
@@ -58,7 +58,6 @@ static GParamSpec *astal_auth_pam_properties[ASTAL_AUTH_PAM_N_PROPERTIES] = {
G_DEFINE_TYPE_WITH_PRIVATE(AstalAuthPam, astal_auth_pam, G_TYPE_OBJECT);
/**
- *
* AstalAuthPam
*
* For simple authentication using only a password, using the [[email protected]]
diff --git a/lib/bluetooth/adapter.vala b/lib/bluetooth/adapter.vala
index 0c9d00e..99a59fb 100644
--- a/lib/bluetooth/adapter.vala
+++ b/lib/bluetooth/adapter.vala
@@ -1,27 +1,10 @@
-namespace AstalBluetooth {
-[DBus (name = "org.bluez.Adapter1")]
-internal interface IAdapter : DBusProxy {
- public abstract void remove_device(ObjectPath device) throws Error;
- public abstract void start_discovery() throws Error;
- public abstract void stop_discovery() throws Error;
-
- public abstract string[] uuids { owned get; }
- public abstract bool discoverable { get; set; }
- public abstract bool discovering { get; }
- public abstract bool pairable { get; set; }
- public abstract bool powered { get; set; }
- public abstract string address { owned get; }
- public abstract string alias { owned get; set; }
- public abstract string modalias { owned get; }
- public abstract string name { owned get; }
- public abstract uint class { get; }
- public abstract uint discoverable_timeout { get; set; }
- public abstract uint pairable_timeout { get; set; }
-}
-
-public class Adapter : Object {
+/**
+ * Object representing an [[https://github.com/RadiusNetworks/bluez/blob/master/doc/adapter-api.txt|adapter]].
+ */
+public class AstalBluetooth.Adapter : Object {
private IAdapter proxy;
- public string object_path { owned get; construct set; }
+
+ internal string object_path { owned get; private set; }
internal Adapter(IAdapter proxy) {
this.proxy = proxy;
@@ -37,53 +20,127 @@ public class Adapter : Object {
});
}
+ /**
+ * List of 128-bit UUIDs that represents the available local services.
+ */
public string[] uuids { owned get { return proxy.uuids; } }
+
+ /**
+ * Indicates that a device discovery procedure is active.
+ */
public bool discovering { get { return proxy.discovering; } }
+
+ /**
+ * Local Device ID information in modalias format used by the kernel and udev.
+ */
public string modalias { owned get { return proxy.modalias; } }
+
+ /**
+ * The Bluetooth system name (pretty hostname).
+ */
public string name { owned get { return proxy.name; } }
+
+ /**
+ * The Bluetooth class of device.
+ */
public uint class { get { return proxy.class; } }
+
+ /**
+ * The Bluetooth device address.
+ */
public string address { owned get { return proxy.address; } }
+
+ /**
+ * Switch an adapter to discoverable or non-discoverable
+ * to either make it visible or hide it.
+ */
public bool discoverable {
get { return proxy.discoverable; }
set { proxy.discoverable = value; }
}
+
+ /**
+ * Switch an adapter to pairable or non-pairable.
+ */
public bool pairable {
get { return proxy.pairable; }
set { proxy.pairable = value; }
}
+
+ /**
+ * Switch an adapter on or off.
+ */
public bool powered {
get { return proxy.powered; }
set { proxy.powered = value; }
}
+
+ /**
+ * The Bluetooth friendly name.
+ *
+ * In case no alias is set, it will return [[email protected]:name].
+ */
public string alias {
owned get { return proxy.alias; }
set { proxy.alias = value; }
}
+
+ /**
+ * The discoverable timeout in seconds.
+ * A value of zero means that the timeout is disabled
+ * and it will stay in discoverable/limited mode forever
+ * until [[email protected]_discovery] is invoked.
+ * The default value for the discoverable timeout should be `180`.
+ */
public uint discoverable_timeout {
get { return proxy.discoverable_timeout; }
set { proxy.discoverable_timeout = value; }
}
+
+ /**
+ * The pairable timeout in seconds.
+ *
+ * A value of zero means that the timeout is disabled and it will stay in pairable mode forever.
+ * The default value for pairable timeout should be disabled `0`.
+ */
public uint pairable_timeout {
get { return proxy.pairable_timeout; }
set { proxy.pairable_timeout = value; }
}
- public void remove_device(Device device) {
- try { proxy.remove_device((ObjectPath)device.object_path); } catch (Error err) { critical(err.message); }
+
+ /**
+ * This removes the remote device and the pairing information.
+ *
+ * Possible errors: `InvalidArguments`, `Failed`.
+ */
+ public void remove_device(Device device) throws Error {
+ proxy.remove_device(device.object_path);
}
- public void start_discovery() {
- try { proxy.start_discovery(); } catch (Error err) { critical(err.message); }
+
+ /**
+ * This method starts the device discovery procedure.
+ *
+ * Possible errors: `NotReady`, `Failed`.
+ */
+ public void start_discovery() throws Error {
+ proxy.start_discovery();
}
- public void stop_discovery() {
- try { proxy.stop_discovery(); } catch (Error err) { critical(err.message); }
+
+ /**
+ * This method will cancel any previous [[email protected]_discovery] procedure.
+ *
+ * Possible errors: `NotReady`, `Failed`, `NotAuthorized`.
+ */
+ public void stop_discovery() throws Error {
+ proxy.stop_discovery();
}
}
-}
diff --git a/lib/bluetooth/bluetooth.vala b/lib/bluetooth/bluetooth.vala
index ce086ba..6eb6b76 100644
--- a/lib/bluetooth/bluetooth.vala
+++ b/lib/bluetooth/bluetooth.vala
@@ -1,11 +1,21 @@
namespace AstalBluetooth {
-public Bluetooth get_default() {
- return Bluetooth.get_default();
+ /**
+ * Gets the default singleton Bluetooth object.
+ */
+ public Bluetooth get_default() {
+ return Bluetooth.get_default();
+ }
}
-public class Bluetooth : Object {
+/**
+ * Manager object for `org.bluez`.
+ */
+public class AstalBluetooth.Bluetooth : Object {
private static Bluetooth _instance;
+ /**
+ * Gets the default singleton Bluetooth object.
+ */
public static Bluetooth get_default() {
if (_instance == null)
_instance = new Bluetooth();
@@ -21,30 +31,59 @@ public class Bluetooth : Object {
private HashTable<string, Device> _devices =
new HashTable<string, Device>(str_hash, str_equal);
+ /**
+ * Emitted when a new device is registered on the `org.bluez` bus.
+ */
public signal void device_added (Device device) {
notify_property("devices");
}
+ /**
+ * Emitted when a device is unregistered on the `org.bluez` bus.
+ */
public signal void device_removed (Device device) {
notify_property("devices");
}
+ /**
+ * Emitted when an adapter is registered on the `org.bluez` bus.
+ */
public signal void adapter_added (Adapter adapter) {
notify_property("adapters");
}
+ /**
+ * Emitted when an adapter is unregistered on the `org.bluez` bus.
+ */
public signal void adapter_removed (Adapter adapter) {
notify_property("adapters");
}
+ /**
+ * `true` if any of the [[email protected]:adapters] are powered.
+ */
public bool is_powered { get; private set; default = false; }
+
+ /**
+ * `true` if any of the [[email protected]:devices] is connected.
+ */
public bool is_connected { get; private set; default = false; }
+
+ /**
+ * The first registered adapter which is usually the only adapter.
+ */
public Adapter? adapter { get { return adapters.nth_data(0); } }
+ /**
+ * List of adapters available on the host device.
+ */
public List<weak Adapter> adapters {
owned get { return _adapters.get_values(); }
}
+ /**
+ * List of registered devices on the `org.bluez` bus.
+ */
public List<weak Device> devices {
owned get { return _devices.get_values(); }
}
@@ -85,6 +124,10 @@ public class Bluetooth : Object {
}
}
+ /**
+ * Toggle the [[email protected]:powered]
+ * property of the [[email protected]:adapter].
+ */
public void toggle() {
adapter.powered = !adapter.powered;
}
@@ -178,4 +221,3 @@ public class Bluetooth : Object {
return false;
}
}
-}
diff --git a/lib/bluetooth/device.vala b/lib/bluetooth/device.vala
index 8fe086f..3f00cd9 100644
--- a/lib/bluetooth/device.vala
+++ b/lib/bluetooth/device.vala
@@ -1,37 +1,14 @@
-namespace AstalBluetooth {
-[DBus (name = "org.bluez.Device1")]
-internal interface IDevice : DBusProxy {
- public abstract void cancel_pairing() throws Error;
- public abstract async void connect() throws Error;
- public abstract void connect_profile(string uuid) throws Error;
- public abstract async void disconnect() throws Error;
- public abstract void disconnect_profile(string uuid) throws Error;
- public abstract void pair() throws Error;
-
- public abstract string[] uuids { owned get; }
- public abstract bool blocked { get; set; }
- public abstract bool connected { get; }
- public abstract bool legacy_pairing { get; }
- public abstract bool paired { get; }
- public abstract bool trusted { get; set; }
- public abstract int16 rssi { get; }
- public abstract ObjectPath adapter { owned get; }
- public abstract string address { owned get; }
- public abstract string alias { owned get; set; }
- public abstract string icon { owned get; }
- public abstract string modalias { owned get; }
- public abstract string name { owned get; }
- public abstract uint16 appearance { get; }
- public abstract uint32 class { get; }
-}
-
-public class Device : Object {
+/**
+ * Object representing a [[https://github.com/luetzel/bluez/blob/master/doc/device-api.txt|device]].
+ */
+public class AstalBluetooth.Device : Object {
private IDevice proxy;
- public string object_path { owned get; construct set; }
+
+ internal ObjectPath object_path { owned get; private set; }
internal Device(IDevice proxy) {
this.proxy = proxy;
- this.object_path = proxy.g_object_path;
+ this.object_path = (ObjectPath)proxy.g_object_path;
proxy.g_properties_changed.connect((props) => {
var map = (HashTable<string, Variant>)props;
foreach (var key in map.get_keys()) {
@@ -43,64 +20,164 @@ public class Device : Object {
});
}
+ /**
+ * List of 128-bit UUIDs that represents the available remote services.
+ */
public string[] uuids { owned get { return proxy.uuids; } }
+
+ /**
+ * Indicates if the remote device is currently connected.
+ */
public bool connected { get { return proxy.connected; } }
+
+ /**
+ * `true` if the device only supports the pre-2.1 pairing mechanism.
+ */
public bool legacy_pairing { get { return proxy.legacy_pairing; } }
+
+ /**
+ * Indicates if the remote device is paired.
+ */
public bool paired { get { return proxy.paired; } }
+
+ /**
+ * Received Signal Strength Indicator of the remote device (inquiry or advertising).
+ */
public int16 rssi { get { return proxy.rssi; } }
+
+ /**
+ * The object path of the adapter the device belongs to.
+ */
public ObjectPath adapter { owned get { return proxy.adapter; } }
+
+ /**
+ * The Bluetooth device address of the remote device.
+ */
public string address { owned get { return proxy.address; } }
+
+ /**
+ * Proposed icon name.
+ */
public string icon { owned get { return proxy.icon; } }
+
+ /**
+ * Remote Device ID information in modalias format used by the kernel and udev.
+ */
public string modalias { owned get { return proxy.modalias; } }
+
+ /**
+ * The Bluetooth remote name.
+ *
+ * It is always better to use [[email protected]:alias].
+ */
public string name { owned get { return proxy.name; } }
+
+ /**
+ * External appearance of device, as found on GAP service.
+ */
public uint16 appearance { get { return proxy.appearance; } }
+
+ /**
+ * The Bluetooth class of device of the remote device.
+ */
public uint32 class { get { return proxy.class; } }
+
+ /**
+ * Indicates if this device is currently trying to be connected.
+ */
public bool connecting { get; private set; }
+ /**
+ * If set to `true` any incoming connections from the device will be immediately rejected.
+ */
public bool blocked {
get { return proxy.blocked; }
set { proxy.blocked = value; }
}
+ /**
+ * Indicates if the remote is seen as trusted.
+ */
public bool trusted {
get { return proxy.trusted; }
set { proxy.trusted = value; }
}
+ /**
+ * The name alias for the remote device.
+ *
+ * In case no alias is set, it will return the remote device [[email protected]:name].
+ */
public string alias {
owned get { return proxy.alias; }
set { proxy.alias = value; }
}
- public void cancel_pairing() {
- try { proxy.cancel_pairing(); } catch (Error err) { critical(err.message); }
- }
-
- public async void connect_device() {
+ /**
+ * This is a generic method to connect any profiles
+ * the remote device supports that can be connected to.
+ *
+ * Possible errors: `NotReady`, `Failed`, `InProgress`, `AlreadyConnected`.
+ */
+ public async void connect_device() throws Error {
try {
connecting = true;
yield proxy.connect();
- } catch (Error err) {
- critical(err.message);
} finally {
connecting = false;
}
}
- public async void disconnect_device() {
- try { yield proxy.disconnect(); } catch (Error err) { critical(err.message); }
+ /**
+ * This method gracefully disconnects all connected profiles.
+ *
+ * Possible errors: `NotConnected`.
+ */
+ public async void disconnect_device() throws Error {
+ yield proxy.disconnect();
}
- public void connect_profile(string uuid) {
- try { proxy.connect_profile(uuid); } catch (Error err) { critical(err.message); }
+ /**
+ * This method connects a specific profile of this device.
+ * The UUID provided is the remote service UUID for the profile.
+ *
+ * Possible errors: `Failed`, `InProgress`, `InvalidArguments`, `NotAvailable`, `NotReady`.
+ *
+ * @param uuid the remote service UUID.
+ */
+ public void connect_profile(string uuid) throws Error {
+ proxy.connect_profile(uuid);
}
- public void disconnect_profile(string uuid) {
- try { proxy.disconnect_profile(uuid); } catch (Error err) { critical(err.message); }
+ /**
+ * This method disconnects a specific profile of this device.
+ *
+ * Possible errors: `Failed`, `InProgress`, `InvalidArguments`, `NotSupported`.
+ *
+ * @param uuid the remote service UUID.
+ */
+ public void disconnect_profile(string uuid) throws Error {
+ proxy.disconnect_profile(uuid);
}
- public void pair() {
- try { proxy.pair(); } catch (Error err) { critical(err.message); }
+ /**
+ * This method will connect to the remote device and initiate pairing.
+ *
+ * Possible errors: `InvalidArguments`, `Failed`, `AlreadyExists`,
+ * `AuthenticationCanceled`, `AuthenticationFailed`, `AuthenticationRejected`,
+ * `AuthenticationTimeout`, `ConnectionAttemptFailed`.
+ */
+ public void pair() throws Error {
+ proxy.pair();
+ }
+
+ /**
+ * This method can be used to cancel a pairing operation
+ * initiated by [[email protected]].
+ *
+ * Possible errors: `DoesNotExist`, `Failed`.
+ */
+ public void cancel_pairing() throws Error {
+ proxy.cancel_pairing();
}
-}
}
diff --git a/lib/bluetooth/gir.py b/lib/bluetooth/gir.py
new file mode 120000
index 0000000..b5b4f1d
--- /dev/null
+++ b/lib/bluetooth/gir.py
@@ -0,0 +1 @@
+../gir.py \ No newline at end of file
diff --git a/lib/bluetooth/interfaces.vala b/lib/bluetooth/interfaces.vala
new file mode 100644
index 0000000..dcb1c4b
--- /dev/null
+++ b/lib/bluetooth/interfaces.vala
@@ -0,0 +1,46 @@
+[DBus (name = "org.bluez.Adapter1")]
+private interface AstalBluetooth.IAdapter : DBusProxy {
+ public abstract void remove_device(ObjectPath device) throws Error;
+ public abstract void start_discovery() throws Error;
+ public abstract void stop_discovery() throws Error;
+
+ public abstract string[] uuids { owned get; }
+ public abstract bool discoverable { get; set; }
+ public abstract bool discovering { get; }
+ public abstract bool pairable { get; set; }
+ public abstract bool powered { get; set; }
+ public abstract string address { owned get; }
+ public abstract string alias { owned get; set; }
+ public abstract string modalias { owned get; }
+ public abstract string name { owned get; }
+ public abstract uint class { get; }
+ public abstract uint discoverable_timeout { get; set; }
+ public abstract uint pairable_timeout { get; set; }
+}
+
+[DBus (name = "org.bluez.Device1")]
+private interface AstalBluetooth.IDevice : DBusProxy {
+ public abstract void cancel_pairing() throws Error;
+ public abstract async void connect() throws Error;
+ public abstract void connect_profile(string uuid) throws Error;
+ public abstract async void disconnect() throws Error;
+ public abstract void disconnect_profile(string uuid) throws Error;
+ public abstract void pair() throws Error;
+
+ public abstract string[] uuids { owned get; }
+ public abstract bool blocked { get; set; }
+ public abstract bool connected { get; }
+ public abstract bool legacy_pairing { get; }
+ public abstract bool paired { get; }
+ public abstract bool trusted { get; set; }
+ public abstract int16 rssi { get; }
+ public abstract ObjectPath adapter { owned get; }
+ public abstract string address { owned get; }
+ public abstract string alias { owned get; set; }
+ public abstract string icon { owned get; }
+ public abstract string modalias { owned get; }
+ public abstract string name { owned get; }
+ public abstract uint16 appearance { get; }
+ public abstract uint32 class { get; }
+}
+
diff --git a/lib/bluetooth/meson.build b/lib/bluetooth/meson.build
index 934d380..347b463 100644
--- a/lib/bluetooth/meson.build
+++ b/lib/bluetooth/meson.build
@@ -33,34 +33,41 @@ deps = [
dependency('gio-2.0'),
]
-sources = [
- config,
- 'utils.vala',
- 'device.vala',
+sources = [config] + files(
'adapter.vala',
'bluetooth.vala',
-]
+ 'device.vala',
+ 'interfaces.vala',
+ 'utils.vala',
+)
lib = library(
meson.project_name(),
sources,
dependencies: deps,
+ vala_args: ['--vapi-comments'],
vala_header: meson.project_name() + '.h',
vala_vapi: meson.project_name() + '-' + api_version + '.vapi',
- vala_gir: gir,
version: meson.project_version(),
install: true,
- install_dir: [true, true, true, true],
+ install_dir: [true, true, true],
)
-import('pkgconfig').generate(
- lib,
- name: meson.project_name(),
- filebase: meson.project_name() + '-' + api_version,
- version: meson.project_version(),
- subdirs: meson.project_name(),
- requires: deps,
- install_dir: get_option('libdir') / 'pkgconfig',
+pkgs = []
+foreach dep : deps
+ pkgs += ['--pkg=' + dep.name()]
+endforeach
+
+gir_tgt = custom_target(
+ gir,
+ command: [find_program('python3'), files('gir.py'), meson.project_name(), gir]
+ + pkgs
+ + sources,
+ input: sources,
+ depends: lib,
+ output: gir,
+ install: true,
+ install_dir: get_option('datadir') / 'gir-1.0',
)
custom_target(
@@ -73,7 +80,17 @@ custom_target(
],
input: lib,
output: typelib,
- depends: lib,
+ depends: [lib, gir_tgt],
install: true,
install_dir: get_option('libdir') / 'girepository-1.0',
)
+
+import('pkgconfig').generate(
+ lib,
+ name: meson.project_name(),
+ filebase: meson.project_name() + '-' + api_version,
+ version: meson.project_version(),
+ subdirs: meson.project_name(),
+ requires: deps,
+ install_dir: get_option('libdir') / 'pkgconfig',
+)
diff --git a/lib/cava/.gitignore b/lib/cava/.gitignore
new file mode 100644
index 0000000..2c7a6aa
--- /dev/null
+++ b/lib/cava/.gitignore
@@ -0,0 +1 @@
+/subprojects/**/
diff --git a/lib/cava/astal-cava.h b/lib/cava/astal-cava.h
new file mode 100644
index 0000000..343234a
--- /dev/null
+++ b/lib/cava/astal-cava.h
@@ -0,0 +1,70 @@
+#ifndef ASTAL_CAVA_H
+#define ASTAL_CAVA_H
+
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+#define ASTAL_CAVA_TYPE_INPUT (astal_cava_input_get_type())
+
+typedef enum {
+ ASTAL_CAVA_INPUT_FIFO,
+ ASTAL_CAVA_INPUT_PORTAUDIO,
+ ASTAL_CAVA_INPUT_PIPEWIRE,
+ ASTAL_CAVA_INPUT_ALSA,
+ ASTAL_CAVA_INPUT_PULSE,
+ ASTAL_CAVA_INPUT_SNDIO,
+ ASTAL_CAVA_INPUT_OSS,
+ ASTAL_CAVA_INPUT_JACK,
+ ASTAL_CAVA_INPUT_SHMEM,
+ ASTAL_CAVA_INPUT_WINSCAP,
+} AstalCavaInput;
+
+#define ASTAL_CAVA_TYPE_CAVA (astal_cava_cava_get_type())
+
+G_DECLARE_FINAL_TYPE(AstalCavaCava, astal_cava_cava, ASTAL_CAVA, CAVA, GObject)
+
+AstalCavaCava* astal_cava_cava_get_default();
+AstalCavaCava* astal_cava_get_default();
+
+gboolean astal_cava_cava_get_active(AstalCavaCava* self);
+void astal_cava_cava_set_active(AstalCavaCava* self, gboolean active);
+
+GArray* astal_cava_cava_get_values(AstalCavaCava* self);
+
+gint astal_cava_cava_get_bars(AstalCavaCava* self);
+void astal_cava_cava_set_bars(AstalCavaCava* self, gint bars);
+
+gboolean astal_cava_cava_get_autosens(AstalCavaCava* self);
+void astal_cava_cava_set_autosens(AstalCavaCava* self, gboolean autosens);
+
+gboolean astal_cava_cava_get_stereo(AstalCavaCava* self);
+void astal_cava_cava_set_stereo(AstalCavaCava* self, gboolean stereo);
+
+gdouble astal_cava_cava_get_noise_reduction(AstalCavaCava* self);
+void astal_cava_cava_set_noise_reduction(AstalCavaCava* self, gdouble noise);
+
+gint astal_cava_cava_get_framerate(AstalCavaCava* self);
+void astal_cava_cava_set_framerate(AstalCavaCava* self, gint framerate);
+
+AstalCavaInput astal_cava_cava_get_input(AstalCavaCava* self);
+void astal_cava_cava_set_input(AstalCavaCava* self, AstalCavaInput input);
+
+gchar* astal_cava_cava_get_source(AstalCavaCava* self);
+void astal_cava_cava_set_source(AstalCavaCava* self, const gchar* source);
+
+gint astal_cava_cava_get_channels(AstalCavaCava* self);
+void astal_cava_cava_set_channels(AstalCavaCava* self, gint channels);
+
+gint astal_cava_cava_get_low_cutoff(AstalCavaCava* self);
+void astal_cava_cava_set_low_cutoff(AstalCavaCava* self, gint low_cutoff);
+
+gint astal_cava_cava_get_high_cutoff(AstalCavaCava* self);
+void astal_cava_cava_set_high_cutoff(AstalCavaCava* self, gint high_cutoff);
+
+gint astal_cava_cava_get_samplerate(AstalCavaCava* self);
+void astal_cava_cava_set_samplerate(AstalCavaCava* self, gint samplerate);
+
+G_END_DECLS
+
+#endif // !ASTAL_CAVA_H
diff --git a/lib/cava/cava.c b/lib/cava/cava.c
new file mode 100644
index 0000000..1c5ef66
--- /dev/null
+++ b/lib/cava/cava.c
@@ -0,0 +1,659 @@
+#include <cava/common.h>
+#include <gio/gio.h>
+
+#include "astal-cava.h"
+#include "cava/config.h"
+#include "glib-object.h"
+#include "glib.h"
+#include "glibconfig.h"
+
+struct _AstalCavaCava {
+ GObject parent_instance;
+
+ gint bars;
+ gboolean autosens;
+ gboolean stereo;
+ gdouble noise_reduction;
+ gint framerate;
+ AstalCavaInput input;
+ gchar* audio_source;
+ gboolean active;
+ gint channels;
+ gint low_cutoff;
+ gint high_cutoff;
+ gint samplerate;
+
+ GArray* values;
+};
+
+typedef struct {
+ struct cava_plan plan;
+ struct config_params cfg;
+ struct audio_data audio_data;
+ struct audio_raw audio_raw;
+ ptr input_src;
+
+ gboolean constructed;
+ GThread* input_thread;
+ guint timer_id;
+
+} AstalCavaCavaPrivate;
+
+G_DEFINE_ENUM_TYPE(AstalCavaInput, astal_cava_input,
+ G_DEFINE_ENUM_VALUE(ASTAL_CAVA_INPUT_FIFO, "fifo"),
+ G_DEFINE_ENUM_VALUE(ASTAL_CAVA_INPUT_PORTAUDIO, "portaudio"),
+ G_DEFINE_ENUM_VALUE(ASTAL_CAVA_INPUT_PIPEWIRE, "pipewire"),
+ G_DEFINE_ENUM_VALUE(ASTAL_CAVA_INPUT_ALSA, "alsa"),
+ G_DEFINE_ENUM_VALUE(ASTAL_CAVA_INPUT_PULSE, "pulse"),
+ G_DEFINE_ENUM_VALUE(ASTAL_CAVA_INPUT_SNDIO, "sndio"),
+ G_DEFINE_ENUM_VALUE(ASTAL_CAVA_INPUT_SHMEM, "shmem"),
+ G_DEFINE_ENUM_VALUE(ASTAL_CAVA_INPUT_WINSCAP, "winscap"));
+
+G_DEFINE_TYPE_WITH_PRIVATE(AstalCavaCava, astal_cava_cava, G_TYPE_OBJECT)
+
+typedef enum {
+ ASTAL_CAVA_CAVA_PROP_VALUES = 1,
+ ASTAL_CAVA_CAVA_PROP_ACTIVE,
+ ASTAL_CAVA_CAVA_PROP_BARS,
+ ASTAL_CAVA_CAVA_PROP_AUTOSENS,
+ ASTAL_CAVA_CAVA_PROP_STEREO,
+ ASTAL_CAVA_CAVA_PROP_NOISE,
+ ASTAL_CAVA_CAVA_PROP_FRAMERATE,
+ ASTAL_CAVA_CAVA_PROP_INPUT,
+ ASTAL_CAVA_CAVA_PROP_SOURCE,
+ ASTAL_CAVA_CAVA_PROP_CHANNELS,
+ ASTAL_CAVA_CAVA_PROP_LOW_CUTOFF,
+ ASTAL_CAVA_CAVA_PROP_HIGH_CUTOFF,
+ ASTAL_CAVA_CAVA_PROP_SAMPLERATE,
+ ASTAL_CAVA_CAVA_N_PROPERTIES
+} AstalCavaProperties;
+
+static GParamSpec* astal_cava_cava_properties[ASTAL_CAVA_CAVA_N_PROPERTIES] = {
+ NULL,
+};
+
+static gboolean exec_cava(AstalCavaCava* self) {
+ AstalCavaCavaPrivate* priv = astal_cava_cava_get_instance_private(self);
+
+ pthread_mutex_lock(&priv->audio_data.lock);
+ cava_execute(priv->audio_data.cava_in, priv->audio_data.samples_counter,
+ priv->audio_raw.cava_out, &priv->plan);
+ if (priv->audio_data.samples_counter > 0) priv->audio_data.samples_counter = 0;
+ pthread_mutex_unlock(&priv->audio_data.lock);
+
+ g_array_remove_range(self->values, 0, priv->audio_raw.number_of_bars);
+ g_array_insert_vals(self->values, 0, priv->audio_raw.cava_out, priv->audio_raw.number_of_bars);
+
+ g_object_notify(G_OBJECT(self), "values");
+
+ return G_SOURCE_CONTINUE;
+}
+
+static void astal_cava_cava_cleanup(AstalCavaCava* self) {
+ AstalCavaCavaPrivate* priv = astal_cava_cava_get_instance_private(self);
+
+ g_source_remove(priv->timer_id);
+ pthread_mutex_lock(&priv->audio_data.lock);
+ priv->audio_data.terminate = 1;
+ pthread_mutex_unlock(&priv->audio_data.lock);
+ g_thread_join(priv->input_thread);
+
+ cava_destroy(&priv->plan);
+
+ g_free(priv->audio_data.cava_in);
+ g_free(priv->audio_data.source);
+
+ g_free(priv->cfg.audio_source);
+ g_free(priv->cfg.raw_target);
+ g_free(priv->cfg.data_format);
+}
+
+static void astal_cava_cava_start(AstalCavaCava* self) {
+ AstalCavaCavaPrivate* priv = astal_cava_cava_get_instance_private(self);
+
+ if (self->framerate < 1) {
+ self->framerate = 1;
+ }
+
+ if (self->low_cutoff < 1) {
+ self->low_cutoff = 1;
+ }
+
+ if (self->high_cutoff < self->low_cutoff) {
+ self->high_cutoff = self->low_cutoff + 1;
+ }
+
+ if (self->samplerate / 2 <= self->high_cutoff) {
+ self->samplerate = self->high_cutoff * 2 + 1;
+ }
+
+ if (self->bars < 1) {
+ self->bars = 1;
+ }
+
+ if (self->channels < 1 || self->channels > 2) {
+ self->channels = 2;
+ }
+
+ priv->cfg = (struct config_params){
+ .inAtty = 0,
+ .output = OUTPUT_RAW,
+ .raw_target = strdup("/dev/stdout"),
+ .data_format = strdup("binary"),
+
+ .fixedbars = self->bars,
+ .autosens = self->autosens,
+ .stereo = self->stereo,
+ .noise_reduction = self->noise_reduction,
+ .framerate = self->framerate,
+ .input = (enum input_method)self->input,
+ .channels = self->channels,
+ .lower_cut_off = self->low_cutoff,
+ .upper_cut_off = self->high_cutoff,
+ .samplerate = self->samplerate,
+
+ // not needed in this lib
+ .mono_opt = AVERAGE,
+ .waves = 0,
+ .userEQ = NULL,
+ .userEQ_keys = 0,
+ .userEQ_enabled = 0,
+ .samplebits = 16,
+ .waveform = 0,
+ .monstercat = 0,
+ .sens = 1,
+ .autoconnect = 2,
+ .reverse = 0,
+ .sleep_timer = 0,
+ .show_idle_bar_heads = 1,
+ .continuous_rendering = 0,
+ .sdl_width = 1000,
+ .sdl_height = 500,
+ .sdl_x = -1,
+ .sdl_y = -1,
+ .sdl_full_screen = 0,
+ .draw_and_quit = 0,
+ .zero_test = 0,
+ .non_zero_test = 0,
+ .sync_updates = 0,
+ .disable_blanking = 1,
+ .bar_height = 32,
+ .col = 0,
+ .bgcol = 0,
+ .autobars = 0,
+ .raw_format = FORMAT_BINARY,
+ .ascii_range = 1000,
+ .bit_format = 16,
+ .gradient = 0,
+ .gradient_count = 0,
+ .bar_width = 2,
+ .bar_spacing = 1,
+ .xaxis = NONE,
+ .orientation = ORIENT_BOTTOM,
+ .color = NULL,
+ .bcolor = NULL,
+ .gradient_colors = NULL,
+ .vertex_shader = NULL,
+ .fragment_shader = NULL,
+ .bar_delim = ';',
+ .frame_delim = '\n',
+ };
+
+ if (g_strcmp0(self->audio_source, "auto") == 0) {
+ switch (priv->cfg.input) {
+ case INPUT_ALSA:
+ priv->cfg.audio_source = g_strdup("hw:Loopback,1");
+ break;
+ case INPUT_FIFO:
+ priv->cfg.audio_source = g_strdup("/tmp/mpd.fifo");
+ break;
+ case INPUT_PULSE:
+ priv->cfg.audio_source = g_strdup("auto");
+ break;
+ case INPUT_PIPEWIRE:
+ priv->cfg.audio_source = g_strdup("auto");
+ break;
+ case INPUT_SNDIO:
+ priv->cfg.audio_source = g_strdup("default");
+ break;
+ case INPUT_OSS:
+ priv->cfg.audio_source = g_strdup("/dev/dsp");
+ break;
+ case INPUT_JACK:
+ priv->cfg.audio_source = g_strdup("default");
+ break;
+ case INPUT_SHMEM:
+ priv->cfg.audio_source = g_strdup("/squeezelite-00:00:00:00:00:00");
+ break;
+ case INPUT_PORTAUDIO:
+ priv->cfg.audio_source = g_strdup("auto");
+ break;
+ default:
+ g_critical("unsupported audio source");
+ }
+ } else {
+ priv->cfg.audio_source = g_strdup(self->audio_source);
+ }
+
+ priv->audio_data = (struct audio_data){
+ .cava_in = calloc(BUFFER_SIZE * priv->cfg.channels * 8, sizeof(gdouble)),
+ .input_buffer_size = BUFFER_SIZE * priv->cfg.channels,
+ .cava_buffer_size = BUFFER_SIZE * priv->cfg.channels * 8,
+ .format = -1,
+ .rate = 0,
+ .channels = priv->cfg.channels,
+ .source = g_strdup(priv->cfg.audio_source),
+ .terminate = 0,
+ .samples_counter = 0,
+ .IEEE_FLOAT = 0,
+ .suspendFlag = false,
+ };
+
+ priv->input_src = get_input(&priv->audio_data, &priv->cfg);
+
+ audio_raw_init(&priv->audio_data, &priv->audio_raw, &priv->cfg, &priv->plan);
+
+ priv->input_thread = g_thread_new("cava_input", priv->input_src, &priv->audio_data);
+
+ priv->timer_id = g_timeout_add(1000 / priv->cfg.framerate, G_SOURCE_FUNC(exec_cava), self);
+}
+
+static void astal_cava_cava_restart(AstalCavaCava* self) {
+ if (!self->active) return;
+ astal_cava_cava_cleanup(self);
+ astal_cava_cava_start(self);
+}
+
+gboolean astal_cava_cava_get_active(AstalCavaCava* self) { return self->active; }
+
+void astal_cava_cava_set_active(AstalCavaCava* self, gboolean active) {
+ if (self->active == active) return;
+ AstalCavaCavaPrivate* priv = astal_cava_cava_get_instance_private(self);
+
+ self->active = active;
+
+ if (!priv->constructed) return;
+ if (!active)
+ astal_cava_cava_cleanup(self);
+ else
+ astal_cava_cava_start(self);
+}
+
+/**
+ * astal_cava_cava_get_values
+ * @self: the AstalCavaCava object
+ *
+ * Returns: (transfer none) (element-type gdouble): a list of values
+ *
+ */
+GArray* astal_cava_cava_get_values(AstalCavaCava* self) { return self->values; }
+
+gint astal_cava_cava_get_bars(AstalCavaCava* self) { return self->bars; }
+
+void astal_cava_cava_set_bars(AstalCavaCava* self, gint bars) {
+ AstalCavaCavaPrivate* priv = astal_cava_cava_get_instance_private(self);
+ self->bars = bars;
+ if (priv->constructed) {
+ g_array_set_size(self->values, self->bars);
+ astal_cava_cava_restart(self);
+ }
+}
+
+gboolean astal_cava_cava_get_autosens(AstalCavaCava* self) { return self->autosens; }
+
+void astal_cava_cava_set_autosens(AstalCavaCava* self, gboolean autosens) {
+ AstalCavaCavaPrivate* priv = astal_cava_cava_get_instance_private(self);
+ self->autosens = autosens;
+ if (priv->constructed) astal_cava_cava_restart(self);
+}
+
+gboolean astal_cava_cava_get_stereo(AstalCavaCava* self) { return self->stereo; }
+
+void astal_cava_cava_set_stereo(AstalCavaCava* self, gboolean stereo) {
+ AstalCavaCavaPrivate* priv = astal_cava_cava_get_instance_private(self);
+ self->stereo = stereo;
+ if (priv->constructed) astal_cava_cava_restart(self);
+}
+
+gdouble astal_cava_cava_get_noise_reduction(AstalCavaCava* self) { return self->noise_reduction; }
+
+void astal_cava_cava_set_noise_reduction(AstalCavaCava* self, gdouble noise) {
+ AstalCavaCavaPrivate* priv = astal_cava_cava_get_instance_private(self);
+ self->noise_reduction = noise;
+ if (priv->constructed) astal_cava_cava_restart(self);
+}
+
+gint astal_cava_cava_get_framerate(AstalCavaCava* self) { return self->framerate; }
+
+void astal_cava_cava_set_framerate(AstalCavaCava* self, gint framerate) {
+ AstalCavaCavaPrivate* priv = astal_cava_cava_get_instance_private(self);
+ self->framerate = framerate;
+ if (priv->constructed) astal_cava_cava_restart(self);
+}
+
+AstalCavaInput astal_cava_cava_get_input(AstalCavaCava* self) { return self->input; }
+
+void astal_cava_cava_set_input(AstalCavaCava* self, AstalCavaInput input) {
+ AstalCavaCavaPrivate* priv = astal_cava_cava_get_instance_private(self);
+ self->input = input;
+ if (priv->constructed) astal_cava_cava_restart(self);
+}
+
+gchar* astal_cava_cava_get_source(AstalCavaCava* self) { return self->audio_source; }
+
+void astal_cava_cava_set_source(AstalCavaCava* self, const gchar* source) {
+ AstalCavaCavaPrivate* priv = astal_cava_cava_get_instance_private(self);
+ g_free(self->audio_source);
+ self->audio_source = g_strdup(source);
+ if (priv->constructed) astal_cava_cava_restart(self);
+}
+
+gint astal_cava_cava_get_channels(AstalCavaCava* self) { return self->channels; }
+
+void astal_cava_cava_set_channels(AstalCavaCava* self, gint channels) {
+ AstalCavaCavaPrivate* priv = astal_cava_cava_get_instance_private(self);
+ self->channels = channels;
+ if (priv->constructed) astal_cava_cava_restart(self);
+}
+
+gint astal_cava_cava_get_low_cutoff(AstalCavaCava* self) { return self->low_cutoff; }
+
+void astal_cava_cava_set_low_cutoff(AstalCavaCava* self, gint low_cutoff) {
+ AstalCavaCavaPrivate* priv = astal_cava_cava_get_instance_private(self);
+ self->low_cutoff = low_cutoff;
+ if (priv->constructed) astal_cava_cava_restart(self);
+}
+
+gint astal_cava_cava_get_high_cutoff(AstalCavaCava* self) { return self->high_cutoff; }
+
+void astal_cava_cava_set_high_cutoff(AstalCavaCava* self, gint high_cutoff) {
+ AstalCavaCavaPrivate* priv = astal_cava_cava_get_instance_private(self);
+ self->high_cutoff = high_cutoff;
+ if (priv->constructed) astal_cava_cava_restart(self);
+}
+
+gint astal_cava_cava_get_samplerate(AstalCavaCava* self) { return self->samplerate; }
+
+void astal_cava_cava_set_samplerate(AstalCavaCava* self, gint samplerate) {
+ AstalCavaCavaPrivate* priv = astal_cava_cava_get_instance_private(self);
+ self->samplerate = samplerate;
+ if (priv->constructed) astal_cava_cava_restart(self);
+}
+
+static void astal_cava_cava_set_property(GObject* object, guint property_id, const GValue* value,
+ GParamSpec* pspec) {
+ AstalCavaCava* self = ASTAL_CAVA_CAVA(object);
+
+ switch (property_id) {
+ case ASTAL_CAVA_CAVA_PROP_BARS:
+ astal_cava_cava_set_bars(self, g_value_get_int(value));
+ break;
+ case ASTAL_CAVA_CAVA_PROP_ACTIVE:
+ astal_cava_cava_set_active(self, g_value_get_boolean(value));
+ break;
+ case ASTAL_CAVA_CAVA_PROP_AUTOSENS:
+ astal_cava_cava_set_autosens(self, g_value_get_boolean(value));
+ break;
+ case ASTAL_CAVA_CAVA_PROP_NOISE:
+ astal_cava_cava_set_noise_reduction(self, g_value_get_double(value));
+ break;
+ case ASTAL_CAVA_CAVA_PROP_STEREO:
+ astal_cava_cava_set_stereo(self, g_value_get_boolean(value));
+ break;
+ case ASTAL_CAVA_CAVA_PROP_FRAMERATE:
+ astal_cava_cava_set_framerate(self, g_value_get_int(value));
+ break;
+ case ASTAL_CAVA_CAVA_PROP_INPUT:
+ astal_cava_cava_set_input(self, g_value_get_enum(value));
+ break;
+ case ASTAL_CAVA_CAVA_PROP_SOURCE:
+ g_free(self->audio_source);
+ astal_cava_cava_set_source(self, g_value_get_string(value));
+ break;
+ case ASTAL_CAVA_CAVA_PROP_CHANNELS:
+ astal_cava_cava_set_channels(self, g_value_get_int(value));
+ break;
+ case ASTAL_CAVA_CAVA_PROP_LOW_CUTOFF:
+ astal_cava_cava_set_low_cutoff(self, g_value_get_int(value));
+ break;
+ case ASTAL_CAVA_CAVA_PROP_HIGH_CUTOFF:
+ astal_cava_cava_set_high_cutoff(self, g_value_get_int(value));
+ break;
+ case ASTAL_CAVA_CAVA_PROP_SAMPLERATE:
+ astal_cava_cava_set_samplerate(self, g_value_get_int(value));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec);
+ break;
+ }
+}
+
+static void astal_cava_cava_get_property(GObject* object, guint property_id, GValue* value,
+ GParamSpec* pspec) {
+ AstalCavaCava* self = ASTAL_CAVA_CAVA(object);
+
+ switch (property_id) {
+ case ASTAL_CAVA_CAVA_PROP_ACTIVE:
+ g_value_set_boolean(value, self->active);
+ break;
+ case ASTAL_CAVA_CAVA_PROP_BARS:
+ g_value_set_int(value, self->bars);
+ break;
+ case ASTAL_CAVA_CAVA_PROP_VALUES:
+ g_value_set_pointer(value, self->values);
+ break;
+ case ASTAL_CAVA_CAVA_PROP_AUTOSENS:
+ g_value_set_boolean(value, self->autosens);
+ break;
+ case ASTAL_CAVA_CAVA_PROP_NOISE:
+ g_value_set_double(value, self->noise_reduction);
+ break;
+ case ASTAL_CAVA_CAVA_PROP_STEREO:
+ g_value_set_boolean(value, self->stereo);
+ break;
+ case ASTAL_CAVA_CAVA_PROP_FRAMERATE:
+ g_value_set_int(value, self->framerate);
+ break;
+ case ASTAL_CAVA_CAVA_PROP_INPUT:
+ g_value_set_enum(value, self->input);
+ break;
+ case ASTAL_CAVA_CAVA_PROP_SOURCE:
+ g_value_set_string(value, self->audio_source);
+ break;
+ case ASTAL_CAVA_CAVA_PROP_CHANNELS:
+ g_value_set_int(value, self->channels);
+ break;
+ case ASTAL_CAVA_CAVA_PROP_LOW_CUTOFF:
+ g_value_set_int(value, self->low_cutoff);
+ break;
+ case ASTAL_CAVA_CAVA_PROP_HIGH_CUTOFF:
+ g_value_set_int(value, self->high_cutoff);
+ break;
+ case ASTAL_CAVA_CAVA_PROP_SAMPLERATE:
+ g_value_set_int(value, self->samplerate);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec);
+ break;
+ }
+}
+
+static void astal_cava_cava_constructed(GObject* object) {
+ AstalCavaCava* self = ASTAL_CAVA_CAVA(object);
+ AstalCavaCavaPrivate* priv = astal_cava_cava_get_instance_private(self);
+
+ gdouble* data = calloc(self->bars, sizeof(gdouble));
+ memset(data, 0, self->bars * sizeof(gdouble));
+ self->values = g_array_new_take(data, self->bars, TRUE, sizeof(gdouble));
+
+ priv->constructed = true;
+
+ if (self->active) astal_cava_cava_start(self);
+}
+
+static void astal_cava_cava_init(AstalCavaCava* self) {
+ AstalCavaCavaPrivate* priv = astal_cava_cava_get_instance_private(self);
+ priv->constructed = false;
+ self->low_cutoff = 50;
+ self->high_cutoff = 10000;
+ self->samplerate = 44100;
+}
+
+/**
+ * astal_cava_get_default
+ *
+ * gets the default Cava object.
+ *
+ * Returns: (nullable) (transfer none):
+ */
+AstalCavaCava* astal_cava_get_default() { return astal_cava_cava_get_default(); }
+
+/**
+ * astal_cava_cava_get_default
+ *
+ * gets the default Cava object.
+ *
+ * Returns: (nullable) (transfer none):
+ */
+AstalCavaCava* astal_cava_cava_get_default() {
+ static AstalCavaCava* self = NULL;
+
+ if (self == NULL) self = g_object_new(ASTAL_CAVA_TYPE_CAVA, NULL);
+
+ return self;
+}
+
+static void astal_cava_cava_dispose(GObject* object) {
+ AstalCavaCava* self = ASTAL_CAVA_CAVA(object);
+
+ if (self->active) astal_cava_cava_cleanup(self);
+ G_OBJECT_CLASS(astal_cava_cava_parent_class)->dispose(object);
+}
+
+static void astal_cava_cava_finalize(GObject* object) {
+ AstalCavaCava* self = ASTAL_CAVA_CAVA(object);
+
+ g_array_free(self->values, TRUE);
+
+ G_OBJECT_CLASS(astal_cava_cava_parent_class)->finalize(object);
+}
+
+static void astal_cava_cava_class_init(AstalCavaCavaClass* class) {
+ GObjectClass* object_class = G_OBJECT_CLASS(class);
+ object_class->get_property = astal_cava_cava_get_property;
+ object_class->set_property = astal_cava_cava_set_property;
+ object_class->constructed = astal_cava_cava_constructed;
+ object_class->dispose = astal_cava_cava_dispose;
+ object_class->finalize = astal_cava_cava_finalize;
+
+ /**
+ * AstalCavaCava:values: (type GArray(gdouble))
+ *
+ * A list of values, each represent the height of one bar. The values are generally between 0
+ * and 1 but can overshoot occasionally, in which case the sensitivity will be decreased
+ * automatically if [[email protected]:autosens] is set. The array will have
+ * [[email protected]:bars] entries. If [[email protected]:stereo] is set, the first
+ * half of the array will represent the left channel and the second half the right channel, so
+ * there will be only bars/2 bars per channel. If the number of bars is odd, the last value will
+ * be 0.
+ */
+ astal_cava_cava_properties[ASTAL_CAVA_CAVA_PROP_VALUES] =
+ g_param_spec_pointer("values", "values", "a list of values", G_PARAM_READABLE);
+ /**
+ * AstalCavaCava:active:
+ *
+ * whether or not the audio capture and visualization is running. if false the values array will
+ * not be updated.
+ */
+ astal_cava_cava_properties[ASTAL_CAVA_CAVA_PROP_ACTIVE] = g_param_spec_boolean(
+ "active", "active", "active", TRUE, G_PARAM_READWRITE | G_PARAM_CONSTRUCT);
+ /**
+ * AstalCavaCava:bars:
+ *
+ * the number of bars the visualizer should create.
+ */
+ astal_cava_cava_properties[ASTAL_CAVA_CAVA_PROP_BARS] =
+ g_param_spec_int("bars", "bars", "number of bars per channel", 1, G_MAXINT, 20,
+ G_PARAM_READWRITE | G_PARAM_CONSTRUCT);
+ /**
+ * AstalCavaCava:autosens:
+ *
+ * When set, the sensitivity will automatically be adjusted.
+ */
+ astal_cava_cava_properties[ASTAL_CAVA_CAVA_PROP_AUTOSENS] =
+ g_param_spec_boolean("autosens", "autosens", "dynamically adjust sensitivity", TRUE,
+ G_PARAM_READWRITE | G_PARAM_CONSTRUCT);
+ /**
+ * AstalCavaCava:cava:
+ *
+ * When set the output will contain visualization data for both channels.
+ */
+ astal_cava_cava_properties[ASTAL_CAVA_CAVA_PROP_STEREO] = g_param_spec_boolean(
+ "stereo", "stereo", "stereo", FALSE, G_PARAM_READWRITE | G_PARAM_CONSTRUCT);
+ /**
+ * AstalCavaCava:noise-reduction:
+ *
+ * adjusts the noise-reduction filter. low values are fast and noisy, large values are slow and
+ * smooth.
+ */
+ astal_cava_cava_properties[ASTAL_CAVA_CAVA_PROP_NOISE] =
+ g_param_spec_double("noise_reduction", "noise_reduction", "noise reduction", 0, 1, 0.77,
+ G_PARAM_READWRITE | G_PARAM_CONSTRUCT);
+ /**
+ * AstalCavaCava:framerate:
+ *
+ * how often the values should be updated
+ */
+ astal_cava_cava_properties[ASTAL_CAVA_CAVA_PROP_FRAMERATE] =
+ g_param_spec_int("framerate", "framerate", "framerate", 1, G_MAXINT, 60,
+ G_PARAM_READWRITE | G_PARAM_CONSTRUCT);
+ /**
+ * AstalCavaCava:channels:
+ *
+ * how many input channels to consider
+ */
+ astal_cava_cava_properties[ASTAL_CAVA_CAVA_PROP_CHANNELS] = g_param_spec_int(
+ "channels", "channels", "channels", 1, 2, 2, G_PARAM_READWRITE | G_PARAM_CONSTRUCT);
+ /**
+ * AstalCavaCava:low-cutoff:
+ *
+ * cut off frequencies below this value
+ */
+ astal_cava_cava_properties[ASTAL_CAVA_CAVA_PROP_LOW_CUTOFF] =
+ g_param_spec_int("low-cutoff", "low-cutoff", "lower frequency cutoff", 1, G_MAXINT, 50,
+ G_PARAM_READWRITE | G_PARAM_CONSTRUCT);
+ /**
+ * AstalCavaCava:high-cutoff:
+ *
+ * cut off frequencies above this value
+ */
+ astal_cava_cava_properties[ASTAL_CAVA_CAVA_PROP_HIGH_CUTOFF] =
+ g_param_spec_int("high-cutoff", "high-cutoff", "higher frequency cutoff", 1, G_MAXINT,
+ 10000, G_PARAM_READWRITE | G_PARAM_CONSTRUCT);
+ /**
+ * AstalCavaCava:samplerate:
+ *
+ * the samplerate of the input
+ */
+ astal_cava_cava_properties[ASTAL_CAVA_CAVA_PROP_SAMPLERATE] =
+ g_param_spec_int("samplerate", "samplerate", "samplerate", 1, G_MAXINT, 44100,
+ G_PARAM_READWRITE | G_PARAM_CONSTRUCT);
+ /**
+ * AstalCavaCava:input: (type AstalCavaInput)
+ *
+ * specifies which audio server should be used.
+ */
+ astal_cava_cava_properties[ASTAL_CAVA_CAVA_PROP_INPUT] =
+ g_param_spec_enum("input", "input", "input", ASTAL_CAVA_TYPE_INPUT,
+ ASTAL_CAVA_INPUT_PIPEWIRE, G_PARAM_READWRITE | G_PARAM_CONSTRUCT);
+ /**
+ * AstalCavaCava:source:
+ *
+ * specifies which audio source should be used. Refer to the cava docs on how to use this
+ * property.
+ */
+ astal_cava_cava_properties[ASTAL_CAVA_CAVA_PROP_SOURCE] = g_param_spec_string(
+ "source", "source", "source", "auto", G_PARAM_READWRITE | G_PARAM_CONSTRUCT);
+ g_object_class_install_properties(object_class, ASTAL_CAVA_CAVA_N_PROPERTIES,
+ astal_cava_cava_properties);
+}
diff --git a/lib/cava/meson.build b/lib/cava/meson.build
new file mode 100644
index 0000000..874eaf7
--- /dev/null
+++ b/lib/cava/meson.build
@@ -0,0 +1,80 @@
+project(
+ 'astal-cava',
+ 'c',
+ version: run_command('cat', join_paths(meson.project_source_root(), 'version')).stdout().strip(),
+ default_options: ['c_std=gnu11', 'warning_level=3', 'prefix=/usr'],
+)
+
+add_project_arguments(['-Wno-pedantic', '-Wno-unused-parameter'], language: 'c')
+
+version_split = meson.project_version().split('.')
+lib_so_version = version_split[0] + '.' + version_split[1]
+
+pkg_config = import('pkgconfig')
+gnome = import('gnome')
+
+srcs = files(
+ 'astal-cava.h',
+ 'cava.c',
+)
+
+install_headers('astal-cava.h')
+
+cava = dependency(
+ 'cava',
+ version: '>=0.10.3',
+ required: true,
+ fallback: ['cava', 'cava_dep'],
+)
+
+deps = [
+ dependency('gobject-2.0'),
+ dependency('gio-2.0'),
+ cava,
+]
+
+astal_cava_lib = library(
+ 'astal-cava',
+ sources: srcs,
+ dependencies: deps,
+ version: meson.project_version(),
+ install: true,
+)
+
+libastal_cava = declare_dependency(link_with: astal_cava_lib)
+
+pkg_config_name = 'astal-cava-' + lib_so_version
+
+if get_option('introspection')
+ gir = gnome.generate_gir(
+ astal_cava_lib,
+ sources: srcs,
+ nsversion: '0.1',
+ namespace: 'AstalCava',
+ symbol_prefix: 'astal_cava',
+ identifier_prefix: 'AstalCava',
+ includes: ['GObject-2.0', 'Gio-2.0'],
+ header: 'astal-cava.h',
+ export_packages: pkg_config_name,
+ install: true,
+ )
+
+ if get_option('vapi')
+ gnome.generate_vapi(
+ pkg_config_name,
+ sources: [gir[0]],
+ packages: ['gobject-2.0', 'gio-2.0'],
+ install: true,
+ )
+ endif
+endif
+
+pkg_config.generate(
+ name: 'astal-cava',
+ version: meson.project_version(),
+ libraries: [astal_cava_lib],
+ filebase: pkg_config_name,
+ subdirs: 'astal',
+ description: 'audio analyzing service using cava',
+ url: 'https://github.com/Aylur/astal',
+)
diff --git a/lib/cava/meson_options.txt b/lib/cava/meson_options.txt
new file mode 100644
index 0000000..97aa4e7
--- /dev/null
+++ b/lib/cava/meson_options.txt
@@ -0,0 +1,13 @@
+option(
+ 'introspection',
+ type: 'boolean',
+ value: true,
+ description: 'Build gobject-introspection data',
+)
+
+option(
+ 'vapi',
+ type: 'boolean',
+ value: true,
+ description: 'Generate vapi data (needs vapigen & introspection option)',
+)
diff --git a/lib/cava/subprojects/cava.wrap b/lib/cava/subprojects/cava.wrap
new file mode 100644
index 0000000..f0309bf
--- /dev/null
+++ b/lib/cava/subprojects/cava.wrap
@@ -0,0 +1,7 @@
+[wrap-file]
+directory = cava-0.10.3
+source_url = https://github.com/LukashonakV/cava/archive/0.10.3.tar.gz
+source_filename = cava-0.10.3.tar.gz
+source_hash = aab0a4ed3f999e8461ad9de63ef8a77f28b6b2011f7dd0c69ba81819d442f6f9
+[provide]
+cava = cava_dep
diff --git a/lib/cava/version b/lib/cava/version
new file mode 100644
index 0000000..6e8bf73
--- /dev/null
+++ b/lib/cava/version
@@ -0,0 +1 @@
+0.1.0
diff --git a/lib/hyprland/client.vala b/lib/hyprland/client.vala
index 3df644b..3f2d0fb 100644
--- a/lib/hyprland/client.vala
+++ b/lib/hyprland/client.vala
@@ -73,10 +73,11 @@ public class Client : Object {
}
}
+[Flags]
public enum Fullscreen {
CURRENT = -1,
NONE = 0,
- FULLSCREEN = 1,
- MAXIMIZED = 2,
+ MAXIMIZED = 1,
+ FULLSCREEN = 2,
}
}
diff --git a/lib/mpris/gir.py b/lib/mpris/gir.py
new file mode 120000
index 0000000..b5b4f1d
--- /dev/null
+++ b/lib/mpris/gir.py
@@ -0,0 +1 @@
+../gir.py \ No newline at end of file
diff --git a/lib/mpris/ifaces.vala b/lib/mpris/ifaces.vala
index 4a9d715..298a288 100644
--- a/lib/mpris/ifaces.vala
+++ b/lib/mpris/ifaces.vala
@@ -1,19 +1,18 @@
-namespace AstalMpris {
[DBus (name="org.freedesktop.DBus")]
-internal interface DBusImpl : DBusProxy {
- public abstract string[] list_names () throws GLib.Error;
- public signal void name_owner_changed (string name, string old_owner, string new_owner);
+private interface AstalMpris.DBusImpl : DBusProxy {
+ public abstract string[] list_names() throws GLib.Error;
+ public signal void name_owner_changed(string name, string old_owner, string new_owner);
}
[DBus (name="org.freedesktop.DBus.Properties")]
-internal interface PropsIface : DBusProxy {
- public abstract HashTable<string, Variant> get_all (string iface);
+private interface AstalMpris.PropsIface : DBusProxy {
+ public abstract HashTable<string, Variant> get_all(string iface);
}
[DBus (name="org.mpris.MediaPlayer2")]
-internal interface IMpris : PropsIface {
- public abstract void raise () throws GLib.Error;
- public abstract void quit () throws GLib.Error;
+private interface AstalMpris.IMpris : PropsIface {
+ public abstract void raise() throws GLib.Error;
+ public abstract void quit() throws GLib.Error;
public abstract bool can_quit { get; }
public abstract bool fullscreen { get; set; }
@@ -27,18 +26,18 @@ internal interface IMpris : PropsIface {
}
[DBus (name="org.mpris.MediaPlayer2.Player")]
-internal interface IPlayer : IMpris {
- public abstract void next () throws GLib.Error;
- public abstract void previous () throws GLib.Error;
- public abstract void pause () throws GLib.Error;
- public abstract void play_pause () throws GLib.Error;
- public abstract void stop () throws GLib.Error;
- public abstract void play () throws GLib.Error;
- public abstract void seek (int64 offset) throws GLib.Error;
- public abstract void set_position (ObjectPath track_id, int64 position) throws GLib.Error;
- public abstract void open_uri (string uri) throws GLib.Error;
+private interface AstalMpris.IPlayer : IMpris {
+ public abstract void next() throws GLib.Error;
+ public abstract void previous() throws GLib.Error;
+ public abstract void pause() throws GLib.Error;
+ public abstract void play_pause() throws GLib.Error;
+ public abstract void stop() throws GLib.Error;
+ public abstract void play() throws GLib.Error;
+ public abstract void seek(int64 offset) throws GLib.Error;
+ public abstract void set_position(ObjectPath track_id, int64 position) throws GLib.Error;
+ public abstract void open_uri(string uri) throws GLib.Error;
- public signal void seeked (int64 position);
+ public signal void seeked(int64 position);
public abstract string playback_status { owned get; }
public abstract string loop_status { owned get; set; }
@@ -57,4 +56,3 @@ internal interface IPlayer : IMpris {
public abstract bool can_seek { get; }
public abstract bool can_control { get; }
}
-}
diff --git a/lib/mpris/meson.build b/lib/mpris/meson.build
index c9a5c53..bf215c9 100644
--- a/lib/mpris/meson.build
+++ b/lib/mpris/meson.build
@@ -38,34 +38,40 @@ deps = [
dependency('json-glib-1.0'),
]
-sources = [
- config,
+sources = [config] + files(
'ifaces.vala',
- 'player.vala',
'mpris.vala',
-]
+ 'player.vala',
+)
if get_option('lib')
lib = library(
meson.project_name(),
sources,
dependencies: deps,
+ vala_args: ['--vapi-comments'],
vala_header: meson.project_name() + '.h',
vala_vapi: meson.project_name() + '-' + api_version + '.vapi',
- vala_gir: gir,
version: meson.project_version(),
install: true,
- install_dir: [true, true, true, true],
+ install_dir: [true, true, true],
)
- import('pkgconfig').generate(
- lib,
- name: meson.project_name(),
- filebase: meson.project_name() + '-' + api_version,
- version: meson.project_version(),
- subdirs: meson.project_name(),
- requires: deps,
- install_dir: get_option('libdir') / 'pkgconfig',
+ pkgs = []
+ foreach dep : deps
+ pkgs += ['--pkg=' + dep.name()]
+ endforeach
+
+ gir_tgt = custom_target(
+ gir,
+ command: [find_program('python3'), files('gir.py'), meson.project_name(), gir]
+ + pkgs
+ + sources,
+ input: sources,
+ depends: lib,
+ output: gir,
+ install: true,
+ install_dir: get_option('datadir') / 'gir-1.0',
)
custom_target(
@@ -78,10 +84,20 @@ if get_option('lib')
],
input: lib,
output: typelib,
- depends: lib,
+ depends: [lib, gir_tgt],
install: true,
install_dir: get_option('libdir') / 'girepository-1.0',
)
+
+ import('pkgconfig').generate(
+ lib,
+ name: meson.project_name(),
+ filebase: meson.project_name() + '-' + api_version,
+ version: meson.project_version(),
+ subdirs: meson.project_name(),
+ requires: deps,
+ install_dir: get_option('libdir') / 'pkgconfig',
+ )
endif
if get_option('cli')
diff --git a/lib/mpris/mpris.vala b/lib/mpris/mpris.vala
index 0e55a2e..8039d39 100644
--- a/lib/mpris/mpris.vala
+++ b/lib/mpris/mpris.vala
@@ -1,12 +1,24 @@
namespace AstalMpris {
-public Mpris get_default() {
- return Mpris.get_default();
+ /**
+ * Gets the default singleton Mpris instance.
+ */
+ public Mpris get_default() {
+ return Mpris.get_default();
+ }
}
-public class Mpris : Object {
+/**
+ * Object that monitors dbus for players to appear and disappear.
+ */
+public class AstalMpris.Mpris : Object {
internal static string PREFIX = "org.mpris.MediaPlayer2.";
private static Mpris instance;
+ private DBusImpl proxy;
+
+ /**
+ * Gets the default singleton Mpris instance.
+ */
public static Mpris get_default() {
if (instance == null)
instance = new Mpris();
@@ -14,15 +26,23 @@ public class Mpris : Object {
return instance;
}
- private DBusImpl proxy;
-
private HashTable<string, Player> _players =
new HashTable<string, Player> (str_hash, str_equal);
+ /**
+ * List of currently available players.
+ */
public List<weak Player> players { owned get { return _players.get_values(); } }
- public signal void player_added (Player player);
- public signal void player_closed (Player player);
+ /**
+ * Emitted when a new mpris Player appears.
+ */
+ public signal void player_added(Player player);
+
+ /**
+ * Emitted when a Player disappears.
+ */
+ public signal void player_closed(Player player);
construct {
try {
@@ -63,4 +83,3 @@ public class Mpris : Object {
notify_property("players");
}
}
-}
diff --git a/lib/mpris/player.vala b/lib/mpris/player.vala
index 6764d2b..2050f61 100644
--- a/lib/mpris/player.vala
+++ b/lib/mpris/player.vala
@@ -1,75 +1,184 @@
-namespace AstalMpris {
-public class Player : Object {
+/**
+ * Object which tracks players through their mpris dbus interface.
+ * The most simple way is to use [[email protected]] which tracks every player,
+ * but [[email protected]] can be constructed for a dedicated players too.
+ */
+public class AstalMpris.Player : Object {
private static string COVER_CACHE = Environment.get_user_cache_dir() + "/astal/mpris";
private IPlayer proxy;
+ private uint pollid; // periodically notify position
- public signal void appeared () { available = true; }
- public signal void closed () { available = false; }
+ internal signal void appeared() { available = true; }
+ internal signal void closed() { available = false; }
- // identifiers
- public string bus_name { owned get; construct set; }
- public bool available { get; private set; }
+ /**
+ * Full dbus namae of this player.
+ */
+ public string bus_name { owned get; private set; }
- // periodically notify position
- private uint pollid;
+ /**
+ * Indicates if [[email protected]:bus_name] is available on dbus.
+ */
+ public bool available { get; private set; }
// mpris
+
+ /**
+ * Brings the player's user interface to the front
+ * using any appropriate mechanism available.
+ *
+ * The media player may be unable to control how its user interface is displayed,
+ * or it may not have a graphical user interface at all.
+ * In this case, the [[email protected]:can_raise] is `false` and this method does nothing.
+ */
public void raise() {
- try { proxy.raise(); } catch (Error error) { critical(error.message); }
+ try { proxy.raise(); } catch (Error err) { critical(err.message); }
}
+ /**
+ * Causes the media player to stop running.
+ *
+ * The media player may refuse to allow clients to shut it down.
+ * In this case, the [[email protected]:can_quit] property is false and this method does nothing.
+ */
public void quit() {
- try { proxy.quit(); } catch (Error error) { critical(error.message); }
+ try { proxy.quit(); } catch (Error err) { critical(err.message); }
}
+ /**
+ * Indicates if [[email protected]] has any effect.
+ */
public bool can_quit { get; private set; }
+
+ /**
+ * Indicates if the player is occupying the fullscreen. This is typically used for videos.
+ * Use [[email protected]_fullscreen] to toggle fullscreen state.
+ */
public bool fullscreen { get; private set; }
+
+ /**
+ * Indicates if [[email protected]_fullscreen] has any effect.
+ */
public bool can_set_fullscreen { get; private set; }
+
+ /**
+ * Indicates if [[email protected]] has any effect.
+ */
public bool can_raise { get; private set; }
- public bool has_track_list { get; private set; }
+
+ // TODO: Tracklist interface
+ // public bool has_track_list { get; private set; }
+
+ /**
+ * A human friendly name to identify the player.
+ */
public string identity { owned get; private set; }
+
+ /**
+ * The base name of a .desktop file
+ */
public string entry { owned get; private set; }
+
+ /**
+ * The URI schemes supported by the media player.
+ *
+ * This can be viewed as protocols supported by the player in almost all cases.
+ * Almost every media player will include support for the "file" scheme.
+ * Other common schemes are "http" and "rtsp".
+ */
public string[] supported_uri_schemas { owned get; private set; }
+
+ /**
+ * The mime-types supported by the player.
+ */
public string[] supported_mime_types { owned get; private set; }
+ /**
+ * Toggle [[email protected]:fullscreen] state.
+ */
public void toggle_fullscreen() {
if (!can_set_fullscreen)
- critical("can not set fullscreen on " + bus_name);
+ critical(@"can not set fullscreen on $bus_name");
proxy.fullscreen = !fullscreen;
}
- // player
+ /**
+ * Skips to the next track in the tracklist.
+ * If there is no next track (and endless playback and track repeat are both off), stop playback.
+ * If [[email protected]:can_go_next] is `false` this method has no effect.
+ */
public void next() {
try { proxy.next(); } catch (Error error) { critical(error.message); }
}
+ /**
+ * Skips to the previous track in the tracklist.
+ * If there is no previous track (and endless playback and track repeat are both off), stop playback.
+ * If [[email protected]:can_go_previous] is `false` this method has no effect.
+ */
public void previous() {
try { proxy.previous(); } catch (Error error) { critical(error.message); }
}
+ /**
+ * Pauses playback.
+ * If playback is already paused, this has no effect.
+ * If [[email protected]:can_pause] is `false` this method has no effect.
+ */
public void pause() {
try { proxy.pause(); } catch (Error error) { critical(error.message); }
}
+ /**
+ * Pauses playback.
+ * If playback is already paused, resumes playback.
+ * If playback is stopped, starts playback.
+ */
public void play_pause() {
try { proxy.play_pause(); } catch (Error error) { critical(error.message); }
}
+ /**
+ * Stops playback.
+ * If playback is already stopped, this has no effect.
+ * If [[email protected]:can_control] is `false` this method has no effect.
+ */
public void stop() {
try { proxy.stop(); } catch (Error error) { critical(error.message); }
}
+ /**
+ * Starts or resumes playback.
+ * If already playing, this has no effect.
+ * If paused, playback resumes from the current position.
+ * If [[email protected]:can_play] is `false` this method has no effect.
+ */
public void play() {
try { proxy.play(); } catch (Error error) { critical(error.message); }
}
+ /**
+ * uri scheme should be an element of [[email protected]:supported_uri_schemas]
+ * and the mime-type should match one of the elements of [[email protected]:supported_mime_types].
+ *
+ * @param uri Uri of the track to load.
+ */
public void open_uri(string uri) {
try { proxy.open_uri(uri); } catch (Error error) { critical(error.message); }
}
+ /**
+ * Change [[email protected]:loop_status] from none to track,
+ * from track to playlist, from playlist to none.
+ */
public void loop() {
+ if (loop_status == Loop.UNSUPPORTED) {
+ critical(@"loop is unsupported by $bus_name");
+ return;
+ }
+
switch (loop_status) {
case Loop.NONE:
loop_status = Loop.TRACK;
@@ -85,15 +194,21 @@ public class Player : Object {
}
}
+ /**
+ * Toggle [[email protected]:shuffle_status].
+ */
public void shuffle() {
+ if (shuffle_status == Shuffle.UNSUPPORTED) {
+ critical(@"shuffle is unsupported by $bus_name");
+ return;
+ }
+
shuffle_status = shuffle_status == Shuffle.ON
? Shuffle.OFF
: Shuffle.ON;
}
- public signal void seeked (int64 position);
-
- public double _get_position() {
+ private double _get_position() {
try {
var reply = proxy.call_sync(
"org.freedesktop.DBus.Properties.Get",
@@ -130,63 +245,175 @@ public class Player : Object {
private Shuffle _shuffle_status = Shuffle.UNSUPPORTED;
private double _volume = -1;
+ /**
+ * The current loop/repeat status.
+ */
public Loop loop_status {
get { return _loop_status; }
set { proxy.loop_status = value.to_string(); }
}
+ /**
+ * The current playback rate.
+ */
public double rate {
get { return _rate; }
set { proxy.rate = value; }
}
+ /**
+ * The current shuffle status.
+ */
public Shuffle shuffle_status {
get { return _shuffle_status; }
set { proxy.shuffle = value == Shuffle.ON; }
}
+ /**
+ * The current volume level between 0 and 1.
+ */
public double volume {
get { return _volume; }
set { proxy.volume = value; }
}
+ /**
+ * The current position of the track in seconds.
+ * To get a progress percentage simply divide this with [[email protected]:length].
+ */
public double position {
get { return _get_position(); }
set { _set_position(value); }
}
+ /**
+ * The current playback status.
+ */
public PlaybackStatus playback_status { get; private set; }
+
+ /**
+ * The minimum value which the [[email protected]:rate] can take.
+ */
public double minimum_rate { get; private set; }
+
+ /**
+ * The maximum value which the [[email protected]:rate] can take.
+ */
public double maximum_rate { get; private set; }
+
+ /**
+ * Indicates if invoking [[email protected]] has effect.
+ */
public bool can_go_next { get; private set; }
+
+ /**
+ * Indicates if invoking [[email protected]] has effect.
+ */
public bool can_go_previous { get; private set; }
+
+ /**
+ * Indicates if invoking [[email protected]] has effect.
+ */
public bool can_play { get; private set; }
+
+ /**
+ * Indicates if invoking [[email protected]] has effect.
+ */
public bool can_pause { get; private set; }
+
+ /**
+ * Indicates if setting [[email protected]:position] has effect.
+ */
public bool can_seek { get; private set; }
- public bool can_control { get; private set; }
- // metadata
- [CCode (notify = false)]
- public HashTable<string,Variant> metadata { owned get; private set; }
+ /**
+ * Indicates if the player can be controlled with
+ * methods such as [[email protected]_pause].
+ */
+ public bool can_control { get; private set; }
+ /**
+ * Metadata hashtable of this player.
+ * In languages that cannot introspect this
+ * use [[email protected]_meta].
+ */
+ [CCode (notify = false)] // notified manually in sync
+ public HashTable<string, Variant> metadata { owned get; private set; }
+
+ /**
+ * Currently playing track's id.
+ */
public string trackid { owned get; private set; }
+
+ /**
+ * Length of the currently playing track in seconds.
+ */
public double length { get; private set; }
+
+ /**
+ * The location of an image representing the track or album.
+ * You should always prefer to use [[email protected]:cover_art].
+ */
public string art_url { owned get; private set; }
+ /**
+ * Title of the currently playing album.
+ */
public string album { owned get; private set; }
+
+ /**
+ * Artists of the currently playing album.
+ */
public string album_artist { owned get; private set; }
+
+ /**
+ * Artists of the currently playing track.
+ */
public string artist { owned get; private set; }
+
+ /**
+ * Lyrics of the currently playing track.
+ */
public string lyrics { owned get; private set; }
+
+ /**
+ * Title of the currently playing track.
+ */
public string title { owned get; private set; }
+
+ /**
+ * Composers of the currently playing track.
+ */
public string composer { owned get; private set; }
+
+ /**
+ * Comments of the currently playing track.
+ */
public string comments { owned get; private set; }
- // cached cover art
+ /**
+ * Path of the cached [[email protected]:art_url].
+ */
public string cover_art { owned get; private set; }
+ /**
+ * Lookup a key from [[email protected]:metadata].
+ * This method is useful for languages that fail to introspect hashtables.
+ */
+ public Variant? get_meta(string key) {
+ return metadata.lookup(key);
+ }
+
+ /**
+ * Construct a Player that tracks a dbus name. For example "org.mpris.MediaPlayer2.spotify".
+ * The "org.mpris.MediaPlayer2." prefix can be leftout so simply "spotify" would mean the same.
+ * [[email protected]:available] indicates whether the player is actually running or not.
+ *
+ * @param name dbus name of the player.
+ */
public Player(string name) {
- Object(bus_name: name.has_prefix("org.mpris.MediaPlayer2.")
- ? name : "org.mpris.MediaPlayer2." + name);
+ bus_name = name.has_prefix("org.mpris.MediaPlayer2.")
+ ? name : @"org.mpris.MediaPlayer2.$name";
}
private void sync() {
@@ -195,7 +422,7 @@ public class Player : Object {
fullscreen = proxy.fullscreen;
can_set_fullscreen = proxy.can_set_fullscreen;
can_raise = proxy.can_raise;
- has_track_list = proxy.has_track_list;
+ // has_track_list = proxy.has_track_list;
identity = proxy.identity;
entry = proxy.desktop_entry;
supported_uri_schemas = proxy.supported_uri_schemas;
@@ -310,10 +537,6 @@ public class Player : Object {
}
}
- public Variant? get_meta(string key) {
- return metadata.lookup(key);
- }
-
private string get_str(string key) {
if (metadata.get(key) == null)
return "";
@@ -341,15 +564,33 @@ public class Player : Object {
}
construct {
- try {
- try_proxy();
- sync();
- } catch (Error error) {
- critical(error.message);
- }
+ notify["bus-name"].connect(() => {
+ try {
+ setup_proxy();
+ setup_position_poll();
+ sync();
+ } catch (Error error) {
+ critical(error.message);
+ }
+ });
}
- public void try_proxy() throws Error {
+ private void setup_position_poll() {
+ var current_position = position;
+
+ pollid = Timeout.add_seconds(1, () => {
+ if (!available)
+ return Source.CONTINUE;
+
+ if (position >= 0 && current_position != position) {
+ current_position = position;
+ notify_property("position");
+ }
+ return Source.CONTINUE;
+ }, Priority.DEFAULT);
+ }
+
+ private void setup_proxy() throws Error {
if (proxy != null)
return;
@@ -359,27 +600,19 @@ public class Player : Object {
"/org/mpris/MediaPlayer2"
);
- if (proxy.g_name_owner != null)
+ if (proxy.g_name_owner != null) {
appeared();
+ }
proxy.notify["g-name-owner"].connect(() => {
- if (proxy.g_name_owner != null)
+ if (proxy.g_name_owner != null) {
appeared();
- else
+ } else {
closed();
+ }
});
proxy.g_properties_changed.connect(sync);
-
- pollid = Timeout.add_seconds(1, () => {
- if (!available)
- return Source.CONTINUE;
-
- if (position >= 0) {
- notify_property("position");
- }
- return Source.CONTINUE;
- }, Priority.DEFAULT);
}
~Player() {
@@ -387,12 +620,12 @@ public class Player : Object {
}
}
-public enum PlaybackStatus {
+public enum AstalMpris.PlaybackStatus {
PLAYING,
PAUSED,
STOPPED;
- public static PlaybackStatus from_string(string? str) {
+ internal static PlaybackStatus from_string(string? str) {
switch (str) {
case "Playing":
return PLAYING;
@@ -403,27 +636,18 @@ public enum PlaybackStatus {
return STOPPED;
}
}
-
- public string to_string() {
- switch (this) {
- case PLAYING:
- return "Playing";
- case PAUSED:
- return "Paused";
- case STOPPED:
- default:
- return "Stopped";
- }
- }
}
-public enum Loop {
+public enum AstalMpris.Loop {
UNSUPPORTED,
+ /** The playback will stop when there are no more tracks to play. */
NONE,
+ /** The current track will start again from the begining once it has finished playing. */
TRACK,
+ /** The playback loops through a list of tracks. */
PLAYLIST;
- public static Loop from_string(string? str) {
+ internal static Loop from_string(string? str) {
switch (str) {
case "None":
return NONE;
@@ -436,7 +660,7 @@ public enum Loop {
}
}
- public string? to_string() {
+ internal string? to_string() {
switch (this) {
case NONE:
return "None";
@@ -450,16 +674,18 @@ public enum Loop {
}
}
-public enum Shuffle {
+public enum AstalMpris.Shuffle {
UNSUPPORTED,
+ /** Playback is progressing through a playlist in some other order. */
ON,
+ /** Playback is progressing linearly through a playlist. */
OFF;
- public static Shuffle from_bool(bool b) {
+ internal static Shuffle from_bool(bool b) {
return b ? Shuffle.ON : Shuffle.OFF;
}
- public string? to_string() {
+ internal string? to_string() {
switch (this) {
case OFF:
return "Off";
@@ -470,4 +696,3 @@ public enum Shuffle {
}
}
}
-}
diff --git a/lib/notifd/notification.vala b/lib/notifd/notification.vala
index 527a352..29c6c56 100644
--- a/lib/notifd/notification.vala
+++ b/lib/notifd/notification.vala
@@ -158,12 +158,6 @@ public class AstalNotifd.Notification : Object {
*/
public signal void resolved(ClosedReason reason);
- /**
- * Emitted when the user dismisses this notification.
- *
- * @see dismiss
- */
- public signal void dismissed();
/**
* Emitted when an [[email protected]] of this notification is invoked.
@@ -173,12 +167,10 @@ public class AstalNotifd.Notification : Object {
public signal void invoked(string action_id);
/**
- * Dismiss this notification popup
- *
- * This method doesn't have any functionality on its own, but should be handled
- * by frontend implementation to hide notification popups.
+ * Resolve this notification with [[email protected]_BY_USER].
*/
public void dismiss() { dismissed(); }
+ internal signal void dismissed();
/**
* Invoke an [[email protected]] of this notification.
diff --git a/lib/notifd/proxy.vala b/lib/notifd/proxy.vala
index bedb8b9..95e7105 100644
--- a/lib/notifd/proxy.vala
+++ b/lib/notifd/proxy.vala
@@ -32,10 +32,6 @@ internal class AstalNotifd.DaemonProxy : Object {
set { proxy.dont_disturb = value; }
}
- public uint[] notification_ids() throws DBusError, IOError {
- return proxy.notification_ids();
- }
-
public Notification get_notification(uint id) {
return notifs.get(id);
}
diff --git a/lib/notifd/signals.md b/lib/notifd/signals.md
index cdc6688..e13b92c 100644
--- a/lib/notifd/signals.md
+++ b/lib/notifd/signals.md
@@ -5,7 +5,7 @@ ignore this, I'm just dumb and can't follow where signals go or get emitted from
## Notification
* resolved(reason) - by daemon/proxy
-* dismissed() - by user with `.dismiss()`
+* dismissed() - by user with `.dismiss()`, used to emit resolved from proxy/daemon
* invoked(action) - by user with `.invoke()`
## Deamon
diff --git a/nix/libcava.nix b/nix/libcava.nix
new file mode 100644
index 0000000..866599d
--- /dev/null
+++ b/nix/libcava.nix
@@ -0,0 +1,60 @@
+{
+ stdenv,
+ fetchFromGitHub,
+ autoreconfHook,
+ autoconf-archive,
+ alsa-lib,
+ fftw,
+ iniparser,
+ libpulseaudio,
+ portaudio,
+ sndio,
+ SDL2,
+ libGL,
+ pipewire,
+ jack2,
+ ncurses,
+ pkgconf,
+ meson,
+ ninja,
+}:
+stdenv.mkDerivation rec {
+ pname = "cava";
+ version = "0.10.3";
+
+ src = fetchFromGitHub {
+ owner = "LukashonakV";
+ repo = "cava";
+ rev = "0.10.3";
+ hash = "sha256-ZDFbI69ECsUTjbhlw2kHRufZbQMu+FQSMmncCJ5pagg=";
+ };
+
+ buildInputs = [
+ alsa-lib
+ libpulseaudio
+ ncurses
+ iniparser
+ sndio
+ SDL2
+ libGL
+ portaudio
+ jack2
+ pipewire
+ ];
+
+ propagatedBuildInputs = [
+ fftw
+ ];
+
+ nativeBuildInputs = [
+ autoreconfHook
+ autoconf-archive
+ pkgconf
+ meson
+ ninja
+ ];
+
+ preAutoreconf = ''
+ echo ${version} > version
+ '';
+}
diff --git a/nix/lua.nix b/nix/lua.nix
index 549c6c3..d4221f1 100644
--- a/nix/lua.nix
+++ b/nix/lua.nix
@@ -12,7 +12,7 @@ defaults: {
ps.lgi
(ps.luaPackages.toLuaModule (pkgs.stdenvNoCC.mkDerivation {
name = "astal";
- src = "${astal}/lang/lua";
+ src = "${astal}/lang/lua/astal";
dontBuild = true;
installPhase = ''
mkdir -p $out/share/lua/${ps.lua.luaversion}/astal