diff options
| -rw-r--r-- | Sources/bar/bar.docc/bar.md | 1 | ||||
| -rw-r--r-- | Sources/bar/bar.docc/daemons.md | 82 | ||||
| -rw-r--r-- | Sources/bar/bar.docc/daemons/daemon-mpd.md | 34 | ||||
| -rw-r--r-- | Sources/bar/bar.docc/daemons/daemon-volumed.md | 31 | ||||
| -rw-r--r-- | daemons/mpd.c | 156 | ||||
| -rw-r--r-- | daemons/volume/house.panini.bar.volumed.plist | 25 | ||||
| -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; } |
