aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Sources/bar/bar.docc/bar.md1
-rw-r--r--Sources/bar/bar.docc/daemons.md82
-rw-r--r--Sources/bar/bar.docc/daemons/daemon-mpd.md34
-rw-r--r--Sources/bar/bar.docc/daemons/daemon-volumed.md31
-rw-r--r--daemons/mpd.c156
-rw-r--r--daemons/volume/house.panini.bar.volumed.plist25
-rw-r--r--daemons/volumed.c (renamed from daemons/volume/volumed.c)20
7 files changed, 307 insertions, 42 deletions
diff --git a/Sources/bar/bar.docc/bar.md b/Sources/bar/bar.docc/bar.md
index eb4ff73..e49e402 100644
--- a/Sources/bar/bar.docc/bar.md
+++ b/Sources/bar/bar.docc/bar.md
@@ -29,6 +29,7 @@ If you specify the interval, the block will re-run the command with such frequen
- <doc:customisation>
- <doc:patches>
+- <doc:daemons>
- ``Config``
- ``HEX``
diff --git a/Sources/bar/bar.docc/daemons.md b/Sources/bar/bar.docc/daemons.md
new file mode 100644
index 0000000..e99ecfc
--- /dev/null
+++ b/Sources/bar/bar.docc/daemons.md
@@ -0,0 +1,82 @@
+# Daemons
+
+Extend the program with companion processes.
+
+A *daemon* is a separate program that runs alongside *bar* and communicates with it via Darwin notifications (see ``Notifier``).
+Unlike <doc:patches>, daemons do not modify *bar*'s source code, they are independent binaries that you compile and run on their own.
+
+This approach keeps *bar* minimal and focused while still allowing it to react to system events like volume changes, network events, or battery status.
+Daemons are written in C by default (though any language works), and each one is a single `.c` file that compiles with `cc`.
+
+> Tip: Daemons are a good fit for events that require system-level APIs (CoreAudio, IOKit, CGEvent, etc.) without pulling that complexity into *bar* itself.
+
+## How it works
+
+A daemon listens for some system signal and, when it fires, posts a Darwin notification:
+
+```c
+notify_post("bar.touch.<block-name>");
+```
+
+The ``Notifier`` system in *bar* receives the notification and re-runs the corresponding block's command.
+The block itself stays stateless, it just reads and prints the current state whenever it is called.
+
+For example, the <doc:daemon-bar-volumed> daemon listens for CoreAudio volume change events and posts `bar.touch.volume`.
+The volume block in ``Config`` runs once per notification, prints the current volume, and the bar updates.
+
+## How to install
+
+Each daemon in the collection can be installed following the same pattern:
+
+1. Edit the daemon file to send the correct notification/s:
+```c
+// Search in the file for a line like
+notify_post("bar.touch.<block-name>");
+
+// Replace it with the correct block name
+notify_post("bar.touch.volume");
+
+// If you want to update multiple blocks, just add
+// another line below the first one
+notify_post("bar.touch.volume");
+notify_post("bar.touch.music");
+```
+
+
+2. Compile the daemon:
+```shell
+cc -o ~/.local/bin/<daemon-name> daemons/<daemon-name>.c -framework <dependency>
+```
+
+3. (Optional) Create a launch agent for automatic startup.
+The process is very similar to the one described in the <doc:run-at-login> article.
+
+## Topics
+
+### Audio
+
+- <doc:daemon-volumed>
+<!-- - <doc:daemon-bar-micd> -->
+
+<!-- ### Display -->
+<!---->
+<!-- - <doc:daemon-bar-brightnessd> -->
+<!-- - <doc:daemon-bar-spaced> -->
+<!---->
+<!-- ### Power -->
+<!---->
+<!-- - <doc:daemon-bar-batteryd> -->
+<!---->
+<!-- ### Network -->
+<!---->
+<!-- - <doc:daemon-bar-networkd> -->
+
+### Media
+
+- <doc:daemon-mpd>
+<!-- - <doc:daemon-bar-nowplayingd> -->
+
+<!-- ### System -->
+<!---->
+<!-- - <doc:daemon-bar-focusd> -->
+<!-- - <doc:daemon-bar-devicesd> -->
diff --git a/Sources/bar/bar.docc/daemons/daemon-mpd.md b/Sources/bar/bar.docc/daemons/daemon-mpd.md
new file mode 100644
index 0000000..000d7a1
--- /dev/null
+++ b/Sources/bar/bar.docc/daemons/daemon-mpd.md
@@ -0,0 +1,34 @@
+# mpd
+
+React to MPD (Music Player Daemon) track changes.
+
+This daemon connects to MPD via TCP and uses the `idle` command to block until the player state or playlist changes. When it does, it posts a Darwin notification that tells *bar* to refresh the desired block.
+
+## How it works
+
+MPD's protocol has an `idle` command designed exactly for this purpose. You send `idle player playlist options` and the connection blocks until something changes. No polling, no busy-waiting.
+
+When the `idle` command returns, the daemon calls `notify_post("bar.touch.<block-name>")` and immediately re-enters idle mode.
+
+The ``Notifier`` system in *bar* picks this up and re-runs the block's command.
+
+### Dependencies
+
+None — uses only standard POSIX sockets and `notify.h`, both available on every Mac.
+
+### Configuration
+
+The daemon respects the same environment variables as `mpc`:
+
+- `MPD_HOST` — MPD server hostname (default: `localhost`)
+- `MPD_PORT` — MPD server port (default: `6600`)
+
+## Compile
+
+```shell
+cc -o bar-mpd daemons/mpd.c
+```
+
+## Author
+
+Tommaso Negri [@tommasongr](https://github.com/tommasongr)
diff --git a/Sources/bar/bar.docc/daemons/daemon-volumed.md b/Sources/bar/bar.docc/daemons/daemon-volumed.md
new file mode 100644
index 0000000..1b06102
--- /dev/null
+++ b/Sources/bar/bar.docc/daemons/daemon-volumed.md
@@ -0,0 +1,31 @@
+# volumed
+
+React to volume and mute changes.
+
+This daemon listens for system volume changes via **CoreAudio** and posts a Darwin notification that tells *bar* to refresh the desired block.
+Because it uses CoreAudio's property listeners instead of an event tap, **no Accessibility permission is required**.
+
+## How it works
+
+The daemon registers a CoreAudio property listener on the default output device for:
+- `kAudioDevicePropertyVolumeScalar` — fires when the volume level changes.
+- `kAudioDevicePropertyMute` — fires when the mute state toggles.
+
+When either property changes, the daemon calls `notify_post("bar.touch.<block-name>")`.
+The ``Notifier`` system in *bar* picks this up and re-runs the block's command.
+
+The daemon also watches for output device changes (e.g. plugging in headphones) and re-registers the listener on the new device.
+
+### Dependencies
+
+- CoreAudio.framework — system framework, shipped with macOS.
+
+## Compile
+
+```shell
+cc -o bar-volumed daemons/volumed.c -framework CoreAudio
+```
+
+## Author
+
+Tommaso Negri [@tommasongr](https://github.com/tommasongr)
diff --git a/daemons/mpd.c b/daemons/mpd.c
new file mode 100644
index 0000000..f8dbb64
--- /dev/null
+++ b/daemons/mpd.c
@@ -0,0 +1,156 @@
+// mpd — MPD (Music Player Daemon) daemon for bar
+//
+// Connects to MPD via TCP and uses the `idle` command to block until
+// the player state or playlist changes. When it does, posts a Darwin
+// notification. No polling, no special permissions — just a TCP socket.
+//
+// MPD_HOST and MPD_PORT environment variables are respected (same as
+// mpc's convention). Defaults: localhost:6600.
+//
+// Compile:
+// cc -o bar-mpd daemons/mpd.c
+
+#include <notify.h>
+#include <signal.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <netdb.h>
+#include <sys/socket.h>
+#include <errno.h>
+
+static volatile int keep_running = 1;
+
+static void handle_signal(int sig) {
+ (void)sig;
+ keep_running = 0;
+}
+
+// ── MPD helpers ──────────────────────────────────────────────────────
+
+/// Read a line from the MPD socket (up to newline). Returns the number
+/// of bytes read, or -1 on error. The line is null-terminated.
+static int read_line(int fd, char *buf, size_t size) {
+ size_t i = 0;
+ while (i < size - 1) {
+ ssize_t n = read(fd, &buf[i], 1);
+ if (n <= 0) return -1;
+ if (buf[i] == '\n') break;
+ i++;
+ }
+ buf[i] = '\0';
+ return (int)i;
+}
+
+/// Connect to MPD. Returns a socket fd, or -1 on failure.
+static int connect_mpd(const char *host, int port) {
+ struct addrinfo hints = {0}, *ai = NULL;
+ hints.ai_family = AF_UNSPEC;
+ hints.ai_socktype = SOCK_STREAM;
+
+ char port_str[16];
+ snprintf(port_str, sizeof(port_str), "%d", port);
+
+ int err = getaddrinfo(host, port_str, &hints, &ai);
+ if (err) return -1;
+
+ int fd = -1;
+ for (struct addrinfo *rp = ai; rp; rp = rp->ai_next) {
+ fd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol);
+ if (fd < 0) continue;
+ if (connect(fd, rp->ai_addr, rp->ai_addrlen) == 0) break;
+ close(fd);
+ fd = -1;
+ }
+ freeaddrinfo(ai);
+
+ if (fd < 0) return -1;
+
+ // Read the MPD greeting line (e.g. "OK MPD 0.23.0")
+ char buf[256];
+ if (read_line(fd, buf, sizeof(buf)) < 0) {
+ close(fd);
+ return -1;
+ }
+
+ return fd;
+}
+
+/// Send a command to MPD and read the response. Returns 0 on success.
+static int mpd_command(int fd, const char *cmd) {
+ char buf[1024];
+ snprintf(buf, sizeof(buf), "%s\n", cmd);
+ ssize_t len = strlen(buf);
+
+ if (write(fd, buf, len) != len) return -1;
+
+ // Read until we see "OK" or "ACK"
+ while (1) {
+ int n = read_line(fd, buf, sizeof(buf));
+ if (n < 0) return -1;
+ if (strcmp(buf, "OK") == 0) return 0;
+ if (strncmp(buf, "ACK", 3) == 0) return -1;
+ // ignore other response lines (e.g. "changed: player")
+ }
+}
+
+// ── Main ─────────────────────────────────────────────────────────────
+
+int main(void) {
+ // Use sigaction so that read() is NOT restarted after a signal
+ // (macOS's signal() enables SA_RESTART by default, which would
+ // cause the program to ignore Ctrl+C while blocked on read).
+ struct sigaction sa = {0};
+ sa.sa_handler = handle_signal;
+ sigemptyset(&sa.sa_mask);
+ sa.sa_flags = 0; // no SA_RESTART → read() returns EINTR
+ sigaction(SIGINT, &sa, NULL);
+ sigaction(SIGTERM, &sa, NULL);
+
+ // Read MPD host/port from environment (same convention as mpc)
+ const char *mpd_host = getenv("MPD_HOST");
+ if (!mpd_host || !*mpd_host) mpd_host = "localhost";
+
+ int mpd_port = 6600;
+ const char *port_env = getenv("MPD_PORT");
+ if (port_env && *port_env) mpd_port = atoi(port_env);
+
+ while (keep_running) {
+ int fd = connect_mpd(mpd_host, mpd_port);
+ if (fd < 0) {
+ // MPD not running yet — wait and retry
+ for (int i = 0; i < 10 && keep_running; i++) sleep(1);
+ continue;
+ }
+
+ // Enter idle mode — blocks until something changes
+ while (keep_running) {
+ const char *idle_cmd = "idle player playlist options\n";
+ ssize_t len = strlen(idle_cmd);
+
+ if (write(fd, idle_cmd, len) != len) break;
+
+ // Read response — will block until MPD sends something
+ char buf[256];
+ int n = read_line(fd, buf, sizeof(buf));
+ if (n < 0) break;
+
+ // Drain the rest of the response
+ while (1) {
+ n = read_line(fd, buf, sizeof(buf));
+ if (n < 0) break;
+ if (strcmp(buf, "OK") == 0) break;
+ }
+
+ notify_post("bar.touch.<block-name>");
+ }
+
+ close(fd);
+
+ // Brief pause before reconnecting
+ if (keep_running) sleep(1);
+ }
+
+ return 0;
+}
diff --git a/daemons/volume/house.panini.bar.volumed.plist b/daemons/volume/house.panini.bar.volumed.plist
deleted file mode 100644
index 09e1084..0000000
--- a/daemons/volume/house.panini.bar.volumed.plist
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
-<plist version="1.0">
-<dict>
- <key>Label</key>
- <string>house.panini.bar.volumed</string>
-
- <key>ProgramArguments</key>
- <array>
- <string>/Users/your-username/.local/bin/bar-volumed</string>
- </array>
-
- <key>RunAtLoad</key>
- <true/>
-
- <key>KeepAlive</key>
- <true/>
-
- <key>StandardOutPath</key>
- <string>/tmp/bar/volumed.log</string>
-
- <key>StandardErrorPath</key>
- <string>/tmp/bar/volumed.log</string>
-</dict>
-</plist>
diff --git a/daemons/volume/volumed.c b/daemons/volumed.c
index 3681b60..ca6afa8 100644
--- a/daemons/volume/volumed.c
+++ b/daemons/volumed.c
@@ -1,4 +1,4 @@
-// bar-volumed — volume daemon for bar (no permissions needed)
+// volumed — volume daemon for bar
//
// Uses CoreAudio to listen for system volume/mute changes instead of
// a CGEvent tap, so no Accessibility permission is required.
@@ -6,22 +6,8 @@
// When the volume changes, it posts a Darwin notification that tells
// bar to refresh the "volume" block.
//
-// ── Setup ──
-//
-// 1. Add a volume block to Config (no interval needed):
-// Block(name: "volume", placement: .right, command: "<your-volume-script>")
-//
-// 2. Compile and run the daemon:
-// cc -o ~/.local/bin/bar-volumed daemons/volume/volumed.c -framework CoreAudio
-// ~/.local/bin/bar-volumed &
-//
-// 3. Or install as a luanch agent:
-// cp daemons/volume/house.panini.bar.volumed.plist ~/Library/LaunchAgents/
-// # Make sure to update the ProgramArguments with the correct path to the program
-// launchctl load ~/Library/LaunchAgents/house.panini.bar.volumed.plist
-//
// Compile:
-// cc -o bar-volumed daemons/volume/volumed.c -framework CoreAudio
+// cc -o bar-volumed daemons/volumed.c -framework CoreAudio
#include <CoreAudio/CoreAudio.h>
#include <notify.h>
@@ -46,7 +32,7 @@ static OSStatus volume_changed(
void *context
) {
(void)object; (void)num_addresses; (void)addresses; (void)context;
- notify_post("bar.touch.volume");
+ notify_post("bar.touch.<block-name>");
return noErr;
}