diff --git a/.builds/alpine-x64.yml b/.builds/alpine-x64.yml index e4ad80c..1703a3d 100644 --- a/.builds/alpine-x64.yml +++ b/.builds/alpine-x64.yml @@ -37,10 +37,6 @@ sources: # to: tasks: - - codespell: | - pip install codespell - cd yambar - ~/.local/bin/codespell README.md CHANGELOG.md *.c *.h doc/*.scd - fcft: | cd yambar/subprojects git clone https://codeberg.org/dnkl/fcft.git diff --git a/.woodpecker.yaml b/.woodpecker.yaml index 8aee7ee..e7c9151 100644 --- a/.woodpecker.yaml +++ b/.woodpecker.yaml @@ -12,7 +12,7 @@ steps: - python3 -m venv codespell-venv - source codespell-venv/bin/activate - pip install codespell - - codespell README.md CHANGELOG.md *.c *.h doc/*.scd + - codespell README.md CHANGELOG.md *.c *.h doc/*.scd bar decorations modules particles examples - deactivate - name: subprojects diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e07a35..0e0e772 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # Changelog +* [Unreleased](#unreleased) * [1.11.0](#1-11-0) * [1.10.0](#1-10-0) * [1.9.0](#1-9-0) @@ -11,6 +12,64 @@ * [1.5.0](#1-5-0) +## Unreleased +### Added + +* environment variable substitution in config files ([#96][96]). +* Log output now respects the [`NO_COLOR`](http://no-color.org/) + environment variable. +* network: `type` tag ([#380][380]). +* network: `type` and `kind` tags ([#380][380]). +* tags: `/` tag formatter: divides the tag's decimal value with `N` + ([#392][392]). +* i3/sway: `output` tag, reflecting the output (monitor) a workspace + is on. +* Added "string like" `~~` operator to Map particle. Allows glob-style + matching on strings using `*` and `?` characters ([#400][400]). +* Added "single" mode flag to the `mpd` module ([#428][428]). +* niri: add a new module for niri-workspaces and niri-language + ([#404][404]). +* pipewire: added `spacing`, `left-spacing` and `right-spacing` + attributes. +* mpris: new module ([#53][53]). + +[96]: https://codeberg.org/dnkl/yambar/issues/96 +[380]: https://codeberg.org/dnkl/yambar/issues/380 +[392]: https://codeberg.org/dnkl/yambar/issues/392 +[400]: https://codeberg.org/dnkl/yambar/pulls/400 +[428]: https://codeberg.org/dnkl/yambar/pulls/428 +[404]: https://codeberg.org/dnkl/yambar/issues/404 +[53]: https://codeberg.org/dnkl/yambar/issues/53 + + +### Changed + +* `river`: expand to an empty list of particles when river is not + running ([#384][384]). + +[384]: https://codeberg.org/dnkl/yambar/issues/384 + + +### Deprecated +### Removed +### Fixed + +* network: fix missing break in switch statement ([#377][377]). +* i3/sway: crash when output is turned off an on ([#300][300]). +* mpd: yambar never attempting to reconnect after MPD closed the + connection (for example, when MPD is restarted). +* Bar positioning on multi-monitor setups, when `location=bottom`. +* pipewire: Improve handling of node switching ([#424][424]). + +[377]: https://codeberg.org/dnkl/yambar/issues/377 +[300]: https://codeberg.org/dnkl/yambar/issues/300 +[424]: https://codeberg.org/dnkl/yambar/pulls/424 + + +### Security +### Contributors + + ## 1.11.0 ### Added diff --git a/README.md b/README.md index 2887f53..4e825b3 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,28 @@ # Yambar -[![Packaging status](https://repology.org/badge/vertical-allrepos/yambar.svg)](https://repology.org/project/yambar/versions) +**This project is not developed anymore, and this repository will be +archived in a near future**. + +I do not have neither the time nor the will to work on this +anymore. Mainly because I do not use a bar myself anymore. + +There are also technical difficulties, caused by the fact that yambar +was initially X11, and only later was Wayland support added. + +This means I have to maintain a backend (X11) I have not used myself +in many years, as well as trying to work around technical limitations +imposed by the way both X11 and Wayland is supported, As my own use of +a bar has dwindled, the will to refactor and improve the backends has +disappeared. + +Yambar has seen a lot of contributions, for which I am very +grateful. I hope that means someone is willing to pick up where I left +of, and continue working on yambar. If not, we at least had a good +run; the first commit was in late 2018, roughly 6½ years ago! + + +[![Packaging status](https://repology.org/badge/vertical-allrepos/yambar.svg?columns=4)](https://repology.org/project/yambar/versions) ## Index diff --git a/bar/bar.c b/bar/bar.c index 3890bc0..109f210 100644 --- a/bar/bar.c +++ b/bar/bar.c @@ -28,7 +28,7 @@ #define max(x, y) ((x) > (y) ? (x) : (y)) /* - * Calculate total width of left/center/rigth groups. + * Calculate total width of left/center/right groups. * Note: begin_expose() must have been called */ static void diff --git a/bar/wayland.c b/bar/wayland.c index 3d8e4e0..86ab252 100644 --- a/bar/wayland.c +++ b/bar/wayland.c @@ -1223,7 +1223,7 @@ loop(struct bar *_bar, void (*expose)(const struct bar *bar), bool do_expose = false; /* Coalesce “refresh” commands */ - size_t count = 0; + __attribute__((unused)) size_t count = 0; while (true) { uint8_t command; ssize_t r = read(backend->pipe_fds[0], &command, sizeof(command)); diff --git a/bar/xcb.c b/bar/xcb.c index 2552fe6..ae52bf3 100644 --- a/bar/xcb.c +++ b/bar/xcb.c @@ -101,7 +101,7 @@ setup(struct bar *_bar) backend->x = mon->x; backend->y = mon->y; bar->width = mon->width; - backend->y += bar->location == BAR_TOP ? 0 : screen->height_in_pixels - bar->height_with_border; + backend->y += bar->location == BAR_TOP ? 0 : mon->height - bar->height_with_border; found_monitor = true; @@ -369,7 +369,7 @@ refresh(const struct bar *_bar) /* Send an event to handle refresh from main thread */ - /* Note: docs say that all X11 events are 32 bytes, reglardless of + /* Note: docs say that all X11 events are 32 bytes, regardless of * the size of the event structure */ xcb_expose_event_t *evt = calloc(32, 1); diff --git a/doc/meson.build b/doc/meson.build index e5728ab..e801bf1 100644 --- a/doc/meson.build +++ b/doc/meson.build @@ -34,6 +34,9 @@ endif if plugin_mpd_enabled plugin_pages += ['yambar-modules-mpd.5.scd'] endif +if plugin_mpris_enabled + plugin_pages += ['yambar-modules-mpris.5.scd'] +endif if plugin_i3_enabled plugin_pages += ['yambar-modules-i3.5.scd'] plugin_pages += ['yambar-modules-sway.5.scd'] @@ -44,6 +47,12 @@ endif if plugin_network_enabled plugin_pages += ['yambar-modules-network.5.scd'] endif +if plugin_niri_language_enabled + plugin_pages += ['yambar-modules-niri-language.5.scd'] +endif +if plugin_niri_workspaces_enabled + plugin_pages += ['yambar-modules-niri-workspaces.5.scd'] +endif if plugin_pipewire_enabled plugin_pages += ['yambar-modules-pipewire.5.scd'] endif diff --git a/doc/yambar-decorations.5.scd b/doc/yambar-decorations.5.scd index 9dd21b8..3d7c379 100644 --- a/doc/yambar-decorations.5.scd +++ b/doc/yambar-decorations.5.scd @@ -137,7 +137,7 @@ content: # STACK -This particles combines multiple decorations. +This particle combines multiple decorations. ## CONFIGURATION diff --git a/doc/yambar-modules-disk-io.5.scd b/doc/yambar-modules-disk-io.5.scd index 5203316..3f51e79 100644 --- a/doc/yambar-modules-disk-io.5.scd +++ b/doc/yambar-modules-disk-io.5.scd @@ -17,8 +17,8 @@ currently present in the machine. for the machine | is_disk : boolean -: whether or not the device is a disk (e.g. sda, sdb) or a partition - (e.g. sda1, sda2, ...). "Total" is advertised as a disk. +: whether or not the device is a disk (e.g., sda, sdb) or a partition + (e.g., sda1, sda2, ...). "Total" is advertised as a disk. | read_speed : int : bytes read, in bytes/s diff --git a/doc/yambar-modules-i3.5.scd b/doc/yambar-modules-i3.5.scd index 296a2da..2014a3c 100644 --- a/doc/yambar-modules-i3.5.scd +++ b/doc/yambar-modules-i3.5.scd @@ -26,6 +26,9 @@ with the _application_ and _title_ tags to replace the X11-only | name : string : The workspace name +| output +: string +: The output (monitor) the workspace is on | visible : bool : True if the workspace is currently visible (on any output) diff --git a/doc/yambar-modules-mpd.5.scd b/doc/yambar-modules-mpd.5.scd index aff6227..d89407a 100644 --- a/doc/yambar-modules-mpd.5.scd +++ b/doc/yambar-modules-mpd.5.scd @@ -20,6 +20,9 @@ mpd - This module provides MPD status such as currently playing artist/album/son | consume : bool : True if the *consume* flag is set +| single +: bool +: True if the *single* flag is set | volume : range : Volume of MPD in percentage diff --git a/doc/yambar-modules-mpris.5.scd b/doc/yambar-modules-mpris.5.scd new file mode 100644 index 0000000..510dc8f --- /dev/null +++ b/doc/yambar-modules-mpris.5.scd @@ -0,0 +1,101 @@ +yambar-modules-mpris(5) + +# NAME +mpris - This module provides MPRIS status such as currently playing artist/album/song + +# TAGS + +[[ *Name* +:[ *Type* +:< *Description* +| state +: string +: One of *offline*, *stopped*, *paused* or *playing* +| shuffle +: bool +: True if the *shuffle* flag is set +| repeat +: string +: One of *none*, *track* or *paylist* +| volume +: range +: Volume in percentage +| album +: string +: Currently playing album +| artist +: string +: Artist of currently playing song +| title +: string +: Title of currently playing song +| file +: string +: Filename or URL of currently playing song +| pos +: string +: *%M:%S*-formatted string describing the song's current position + (also see _elapsed_) +| end +: string +: *%M:%S*-formatted string describing the song's total length (also + see _duration_) +| elapsed +: realtime +: Position in currently playing song, in milliseconds. Can be used + with a _progress-bar_ particle. + +# CONFIGURATION + +[[ *Name* +:[ *Type* +:[ *Req* +:< *Description* +| identities +: list of string +: yes +: A list of MPRIS client identities +| query_timeout +: int +: no +: Dbus/MPRIS client connection timeout in ms. Try setting/incrementing + this value if the module reports a timeout error. Defaults to 500. + +# EXAMPLES + +``` +bar: + center: + - mpris: + identities: + - "spotify" + - "firefox" + content: + map: + conditions: + state != offline && state != stopped: + - string: {text: "{artist}", max: 30 } + - string: {text: "-" } + - string: {text: "{title}", max: 30 } +``` + +# NOTE + +The 'identity' refers a part of your clients DBus bus name. +You can obtain a list of active client names using: + +``` +Systemd: > busctl --user --list +Playerctl: > playerctl --list-all +Libdbus: > dbus-send --session --print-reply --type=method_call \ + --dest='org.freedesktop.DBus' /org org.freedesktop.DBus.ListNames +``` + +MPRIS client bus names start with 'org.mpris.MediaPlayer2.'. +For example, firefox may use the bus name: +'org.mpris.MediaPlayer2.firefox.instance_1_7' which +gives us the identity 'firefox' + +# SEE ALSO + +*yambar-modules*(5), *yambar-particles*(5), *yambar-tags*(5), *yambar-decorations*(5) diff --git a/doc/yambar-modules-network.5.scd b/doc/yambar-modules-network.5.scd index cdc5530..afbbae3 100644 --- a/doc/yambar-modules-network.5.scd +++ b/doc/yambar-modules-network.5.scd @@ -21,6 +21,16 @@ address per network interface. | name : string : Network interface name +| type +: string +: Interface type (*ether*, *wlan*, *loopback*, or *ARPHRD_NNN*, where + *N* is a number). +| kind +: string +: Interface kind. Empty for non-virtual interfaces. For virtual + interfaces, this value is taken from the _IFLA\_INFO\_KIND_ netlink + attribute. Examples of valid values are *bond*, *bridge*, *gre*, *tun* + and *veth*. | index : int : Network interface index @@ -91,17 +101,23 @@ address per network interface. # EXAMPLES +Display all Ethernet (including WLAN) devices. This excludes loopback, +bridges etc. + ``` bar: left: - network: content: map: - default: - string: {text: "{name}: {state} ({ipv4})"} conditions: - ipv4 == "": - string: {text: "{name}: {state}"} + type == ether || type == wlan: + map: + default: + string: {text: "{name}: {state} ({ipv4})"} + conditions: + ipv4 == "": + string: {text: "{name}: {state}"} ``` # SEE ALSO diff --git a/doc/yambar-modules-niri-language.5.scd b/doc/yambar-modules-niri-language.5.scd new file mode 100644 index 0000000..befa41e --- /dev/null +++ b/doc/yambar-modules-niri-language.5.scd @@ -0,0 +1,34 @@ +yambar-modules-niri-language(5) + +# NAME +niri-language - This module provides information about niri's currently +selected language. + +# TAGS + +[[ *Name* +:[ *Type* +:< *Description* +| language +: string +: The currently selected language. + +# CONFIGURATION + +No additional attributes supported, only the generic ones (see +*GENERIC CONFIGURATION* in *yambar-modules*(5)) + +# EXAMPLES + +``` +bar: + left: + - niri-language: + content: + string: {text: "{language}"} +``` + +# SEE ALSO + +*yambar-modules*(5), *yambar-particles*(5), *yambar-tags*(5), *yambar-decorations*(5) + diff --git a/doc/yambar-modules-niri-workspaces.5.scd b/doc/yambar-modules-niri-workspaces.5.scd new file mode 100644 index 0000000..812bade --- /dev/null +++ b/doc/yambar-modules-niri-workspaces.5.scd @@ -0,0 +1,60 @@ +yambar-modules-niri-workspaces(5) + +# NAME +niri-workspaces - This module provides information about niri workspaces. + +# DESCRIPTION + +This module provides a map of each workspace present in niri. + +Each workspace has its _id_, _name_, and its status (_focused_, +_active_, _empty_). The workspaces are sorted by their ids. + +This module will *only* track the monitor where yambar was launched. +If you have a multi monitor setup, please launch yambar on each +individual monitor to track its workspaces. + +# TAGS + +[[ *Name* +:[ *Type* +:< *Description* +| id +: int +: The workspace id. +| name +: string +: The name of the workspace. +| active +: bool +: True if the workspace is currently visible on the current output. +| focused +: bool +: True if the workspace is currently focused. +| empty +: bool +: True if the workspace contains no window. + +# CONFIGURATION + +No additional attributes supported, only the generic ones (see +*GENERIC CONFIGURATION* in *yambar-modules*(5)) + +# EXAMPLES + +``` +bar: + left: + - niri-workspaces: + content: + map: + default: {string: {text: "| {id}"}} + conditions: + active: {string: {text: "-> {id}"}} + ~empty: {string: {text: "@ {id}"}} +``` + +# SEE ALSO + +*yambar-modules*(5), *yambar-particles*(5), *yambar-tags*(5), *yambar-decorations*(5) + diff --git a/doc/yambar-modules-pipewire.5.scd b/doc/yambar-modules-pipewire.5.scd index be94489..8010449 100644 --- a/doc/yambar-modules-pipewire.5.scd +++ b/doc/yambar-modules-pipewire.5.scd @@ -19,10 +19,10 @@ pipewire - Monitors pipewire for volume, mute/unmute, device change : Current device description | form_factor : string -: Current device form factor (headset, speaker, mic, etc) +: Current device form factor (headset, speaker, mic, etc.) | bus : string -: Current device bus (bluetooth, alsa, etc) +: Current device bus (bluetooth, alsa, etc.) | icon : string : Current device icon name @@ -43,6 +43,18 @@ pipewire - Monitors pipewire for volume, mute/unmute, device change :[ *Type* :[ *Req* :< *Description* +| left-spacing +: int +: no +: Space, in pixels, in the left side of each rendered volume +| right-spacing +: int +: no +: Space, in pixels, on the right side of each rendered volume +| spacing +: int +: no +: Short-hand for setting both _left-spacing_ and _right-spacing_ | content : particle : yes diff --git a/doc/yambar-modules-script.5.scd b/doc/yambar-modules-script.5.scd index d27a006..48722cf 100644 --- a/doc/yambar-modules-script.5.scd +++ b/doc/yambar-modules-script.5.scd @@ -16,7 +16,7 @@ configurable amount of time. In continuous mode, the script is executed once. It will typically run in a loop, sending an updated tag set whenever it needs, or wants to. The last tag set is used (displayed) by yambar until a new tag set -is received. This mode is intended to be used by scripts that depends +is received. This mode is intended to be used by scripts that depend on non-polling methods to update their state. Tag sets, or _transactions_, are separated by an empty line diff --git a/doc/yambar-modules.5.scd b/doc/yambar-modules.5.scd index 765d06f..1ec4871 100644 --- a/doc/yambar-modules.5.scd +++ b/doc/yambar-modules.5.scd @@ -174,6 +174,10 @@ Available modules have their own pages: *yambar-modules-sway*(5) +*yambar-modules-niri-language*(5) + +*yambar-modules-niri-workspaces*(5) + *yambar-modules-xkb*(5) *yambar-modules-xwindow*(5) diff --git a/doc/yambar-particles.5.scd b/doc/yambar-particles.5.scd index 5dc858b..325ef89 100644 --- a/doc/yambar-particles.5.scd +++ b/doc/yambar-particles.5.scd @@ -155,7 +155,7 @@ content: This particle is a list (or sequence, if you like) of other particles. It can be used to render e.g. _string_ particles with -different font and/or color formatting. Or ay other particle +different font and/or color formatting. Or any other particle combinations. But note that this means you *cannot* set any attributes on the _list_ @@ -214,6 +214,11 @@ content: - string: ... ``` +Note that the short form has a hard-coded *right-spacing* of 2. This +cannot be changed. If you want a different spacing, you must use an +explicit list particle (i.e. the long form). + + # MAP This particle maps the values of a specific tag to different @@ -260,6 +265,26 @@ To match for empty strings, use ' "" ': == "" ``` +String glob matching + +To perform string matching using globbing with "\*" & "?" characters: +\* Match any zero or more characters. ? Match exactly any one +character. + +``` + ~~ "hello*" +``` + +Will match any string starting with "hello", including "hello", +"hello1", "hello123", etc. + +``` + ~~ "hello?" +``` + +Will match any string starting with "hello" followed by any single +character, including "hello1", "hello-", but not "hello". + Furthermore, you may use the boolean operators: [- && @@ -451,7 +476,7 @@ itself when needed. ``` content: - progres-bar: + progress-bar: tag: tag_name length: 20 start: {string: {text: ├}} diff --git a/doc/yambar-tags.5.scd b/doc/yambar-tags.5.scd index b778154..b6b8b56 100644 --- a/doc/yambar-tags.5.scd +++ b/doc/yambar-tags.5.scd @@ -86,11 +86,15 @@ be used. : format : Range tags : Renders a range tag's value as a percentage value +| /N +: format +: All tag types +: Renders a tag's value (in decimal) divided by N | kb, mb, gb : format : All tag types : Renders a tag's value (in decimal) divided by 1000, 1000^2 or - 1000^3. Note: no unit suffix is appended) + 1000^3. Note: no unit suffix is appended | kib, mib, gib : format : All tag types diff --git a/doc/yambar.1.scd b/doc/yambar.1.scd index 549b980..2aaa46f 100644 --- a/doc/yambar.1.scd +++ b/doc/yambar.1.scd @@ -25,7 +25,7 @@ yambar - modular status panel for X11 and Wayland *-p*,*--print-pid*=_FILE_|_FD_ Print PID to this file, or FD, when successfully started. The file (or FD) is closed immediately after writing the PID. When a _FILE_ - as been specified, the file is unlinked exit. + as been specified, the file is unlinked upon exiting. *-d*,*--log-level*={*info*,*warning*,*error*,*none*} Log level, used both for log output on stderr as well as diff --git a/doc/yambar.5.scd b/doc/yambar.5.scd index 1b06939..38c521d 100644 --- a/doc/yambar.5.scd +++ b/doc/yambar.5.scd @@ -23,6 +23,11 @@ types that are frequently used: - 000000ff: black, no transparency - 00ff00ff: green, no transparency - ff000099: red, semi-transparent +- *environment reference*: a string that contains format ${VAR}. This will be + replaced by the value of the environment variable VAR. Example: + - ${HOME} + - ${HOME}/.config/yambar + - ENV is ${ENV}, ENV2 is ${ENV2} # FORMAT [[ *Name* diff --git a/examples/configurations/laptop.conf b/examples/configurations/laptop.conf index e57ebb3..1bdd16c 100644 --- a/examples/configurations/laptop.conf +++ b/examples/configurations/laptop.conf @@ -4,7 +4,7 @@ # For X11/i3, you'll want to replace calls to swaymsg with i3-msg, and # the sway-xkb module with the xkb module. -# fonts we'll be re-using here and there +# fonts we'll be reusing here and there awesome: &awesome Font Awesome 6 Free:style=solid:pixelsize=14 awesome_brands: &awesome_brands Font Awesome 6 Brands:pixelsize=16 diff --git a/examples/river-minimal.yml b/examples/river-minimal.yml new file mode 100644 index 0000000..2ca225f --- /dev/null +++ b/examples/river-minimal.yml @@ -0,0 +1,69 @@ +bg_default: &bg_default {stack: [{background: {color: 81A1C1ff}}, {underline: {size: 4, color: D8DEE9ff}}]} +bar: + height: 32 + location: top + background: 000000ff + font: NotoSans:pixelsize=16 + + right: + - clock: + content: + - string: {text: , font: "Font Awesome 6 Free:style=solid:size=12"} + - string: {text: "{date}", right-margin: 5} + - string: {text: , font: "Font Awesome 6 Free:style=solid:size=12"} + - string: {text: "{time} "} + left: + - river: + anchors: + - base: &river_base + left-margin: 10 + right-margin: 13 + default: {string: {text: }} + conditions: + id == 1: {string: {text: 1}} + id == 2: {string: {text: 2}} + id == 3: {string: {text: 3}} + id == 4: {string: {text: 4}} + id == 5: {string: {text: 5}} + + content: + map: + on-click: + left: sh -c "riverctl set-focused-tags $((1 << ({id} - 1)))" + right: sh -c "riverctl toggle-focused-tags $((1 << ({id} -1)))" + middle: sh -c "riverctl toggle-view-tags $((1 << ({id} -1)))" + conditions: + state == urgent: + map: + <<: *river_base + deco: {background: {color: D08770ff}} + state == focused: + map: + <<: *river_base + deco: *bg_default + state == visible && ~occupied: + map: + <<: *river_base + state == visible && occupied: + map: + <<: *river_base + deco: *bg_default + state == unfocused: + map: + <<: *river_base + state == invisible && ~occupied: {empty: {}} + state == invisible && occupied: + map: + <<: *river_base + deco: {underline: {size: 3, color: ea6962ff}} + + + center: + - foreign-toplevel: + content: + map: + conditions: + ~activated: {empty: {}} + activated: + - string: {text: " {app-id}", foreground: ffa0a0ff} + - string: {text: ": {title}"} diff --git a/examples/scripts/dwl-tags.sh b/examples/scripts/dwl-tags.sh index b1dbe4c..4999548 100755 --- a/examples/scripts/dwl-tags.sh +++ b/examples/scripts/dwl-tags.sh @@ -19,7 +19,7 @@ # # Now the fun part # -# Exemple configuration: +# Example configuration: # # - script: # path: /absolute/path/to/dwl-tags.sh diff --git a/examples/scripts/pacman.sh b/examples/scripts/pacman.sh index 83e2a3f..5026b5a 100755 --- a/examples/scripts/pacman.sh +++ b/examples/scripts/pacman.sh @@ -12,7 +12,7 @@ # {aur} int number of aur packages # {pkg} int sum of both # -# Exemples configuration: +# Examples configuration: # - script: # path: /absolute/path/to/pacman.sh # args: [] diff --git a/log.c b/log.c index 7ba4193..ba4ebd9 100644 --- a/log.c +++ b/log.c @@ -39,9 +39,15 @@ log_init(enum log_colorize _colorize, bool _do_syslog, enum log_facility syslog_ [LOG_FACILITY_DAEMON] = LOG_DAEMON, }; - colorize = _colorize == LOG_COLORIZE_NEVER ? false - : _colorize == LOG_COLORIZE_ALWAYS ? true - : isatty(STDERR_FILENO); + /* Don't use colors if NO_COLOR is defined and not empty */ + const char *no_color_str = getenv("NO_COLOR"); + const bool no_color = no_color_str != NULL && no_color_str[0] != '\0'; + + colorize = _colorize == LOG_COLORIZE_NEVER + ? false + : _colorize == LOG_COLORIZE_ALWAYS + ? true + : !no_color && isatty(STDERR_FILENO); do_syslog = _do_syslog; log_level = _log_level; diff --git a/main.c b/main.c index a9c6932..c355843 100644 --- a/main.c +++ b/main.c @@ -87,7 +87,7 @@ get_config_path(void) static struct bar * load_bar(const char *config_path, enum bar_backend backend) { - FILE *conf_file = fopen(config_path, "r"); + FILE *conf_file = fopen(config_path, "re"); if (conf_file == NULL) { LOG_ERRNO("%s: failed to open", config_path); return NULL; diff --git a/meson.build b/meson.build index d760e94..67d3096 100644 --- a/meson.build +++ b/meson.build @@ -1,18 +1,22 @@ project('yambar', 'c', version: '1.11.0', license: 'MIT', - meson_version: '>=0.59.0', + meson_version: '>=0.60.0', default_options: ['c_std=c18', 'warning_level=1', - 'werror=true', 'b_ndebug=if-release']) is_debug_build = get_option('buildtype').startswith('debug') plugs_as_libs = get_option('core-plugins-as-shared-libraries') cc = meson.get_compiler('c') +cc_flags = [ + '-Werror=all' + ] -if cc.has_function('memfd_create') +if cc.has_function('memfd_create', + args: ['-D_GNU_SOURCE=200809L'], + prefix: '#include ') add_project_arguments('-DMEMFD_CREATE', language: 'c') endif @@ -74,6 +78,7 @@ tllist = dependency('tllist', version: '>=1.0.1', fallback: 'tllist') fcft = dependency('fcft', version: ['>=3.0.0', '<4.0.0'], fallback: 'fcft') add_project_arguments( + cc_flags + ['-D_GNU_SOURCE'] + (is_debug_build ? ['-D_DEBUG'] : []) + (backend_x11 ? ['-DENABLE_X11'] : []) + @@ -178,6 +183,7 @@ summary( 'Foreign toplevel (window tracking for Wayland)': plugin_foreign_toplevel_enabled, 'Memory monitoring': plugin_mem_enabled, 'Music Player Daemon (MPD)': plugin_mpd_enabled, + 'Media Player Remote Interface Specificaion (MPRIS)': plugin_mpris_enabled, 'i3+Sway': plugin_i3_enabled, 'Label': plugin_label_enabled, 'Network monitoring': plugin_network_enabled, @@ -187,6 +193,8 @@ summary( 'River': plugin_river_enabled, 'Script': plugin_script_enabled, 'Sway XKB keyboard': plugin_sway_xkb_enabled, + 'Niri language': plugin_niri_language_enabled, + 'Niri workspaces': plugin_niri_workspaces_enabled, 'XKB keyboard (for X11)': plugin_xkb_enabled, 'XWindow (window tracking for X11)': plugin_xwindow_enabled, }, diff --git a/meson_options.txt b/meson_options.txt index a9aac05..23a8e11 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -26,6 +26,8 @@ option('plugin-mem', type: 'feature', value: 'auto', description: 'Memory monitoring support') option('plugin-mpd', type: 'feature', value: 'auto', description: 'Music Player Daemon (MPD) support') +option('plugin-mpris', type: 'feature', value: 'enabled', + description: 'Media Player Remote Interface Specificaion (MPRIS) support') option('plugin-i3', type: 'feature', value: 'auto', description: 'i3+Sway support') option('plugin-label', type: 'feature', value: 'auto', @@ -44,6 +46,10 @@ option('plugin-script', type: 'feature', value: 'auto', description: 'Script support') option('plugin-sway-xkb', type: 'feature', value: 'auto', description: 'keyboard support for Sway') +option('plugin-niri-language', type: 'feature', value: 'auto', + description: 'language support for Niri') +option('plugin-niri-workspaces', type: 'feature', value: 'auto', + description: 'workspaces support for Niri') option('plugin-xkb', type: 'feature', value: 'auto', description: 'keyboard support for X11') option('plugin-xwindow', type: 'feature', value: 'auto', diff --git a/modules/backlight.c b/modules/backlight.c index 0fa1787..1495c5c 100644 --- a/modules/backlight.c +++ b/modules/backlight.c @@ -112,13 +112,13 @@ readint_from_fd(int fd) static int initialize(struct private *m) { - int backlight_fd = open("/sys/class/backlight", O_RDONLY); + int backlight_fd = open("/sys/class/backlight", O_RDONLY | O_CLOEXEC); if (backlight_fd == -1) { LOG_ERRNO("/sys/class/backlight"); return -1; } - int base_dir_fd = openat(backlight_fd, m->device, O_RDONLY); + int base_dir_fd = openat(backlight_fd, m->device, O_RDONLY | O_CLOEXEC); close(backlight_fd); if (base_dir_fd == -1) { @@ -126,7 +126,7 @@ initialize(struct private *m) return -1; } - int max_fd = openat(base_dir_fd, "max_brightness", O_RDONLY); + int max_fd = openat(base_dir_fd, "max_brightness", O_RDONLY | O_CLOEXEC); if (max_fd == -1) { LOG_ERRNO("/sys/class/backlight/%s/max_brightness", m->device); close(base_dir_fd); @@ -136,7 +136,7 @@ initialize(struct private *m) m->max_brightness = readint_from_fd(max_fd); close(max_fd); - int current_fd = openat(base_dir_fd, "brightness", O_RDONLY); + int current_fd = openat(base_dir_fd, "brightness", O_RDONLY | O_CLOEXEC); close(base_dir_fd); if (current_fd == -1) { diff --git a/modules/battery.c b/modules/battery.c index c3507d7..34b98c8 100644 --- a/modules/battery.c +++ b/modules/battery.c @@ -259,13 +259,13 @@ initialize(struct private *m) { char line_buf[512]; - int pw_fd = open("/sys/class/power_supply", O_RDONLY); + int pw_fd = open("/sys/class/power_supply", O_RDONLY | O_CLOEXEC); if (pw_fd < 0) { LOG_ERRNO("/sys/class/power_supply"); return false; } - int base_dir_fd = openat(pw_fd, m->battery, O_RDONLY); + int base_dir_fd = openat(pw_fd, m->battery, O_RDONLY | O_CLOEXEC); close(pw_fd); if (base_dir_fd < 0) { @@ -274,7 +274,7 @@ initialize(struct private *m) } { - int fd = openat(base_dir_fd, "manufacturer", O_RDONLY); + int fd = openat(base_dir_fd, "manufacturer", O_RDONLY | O_CLOEXEC); if (fd == -1) { LOG_WARN("/sys/class/power_supply/%s/manufacturer: %s", m->battery, strerror(errno)); m->manufacturer = NULL; @@ -285,7 +285,7 @@ initialize(struct private *m) } { - int fd = openat(base_dir_fd, "model_name", O_RDONLY); + int fd = openat(base_dir_fd, "model_name", O_RDONLY | O_CLOEXEC); if (fd == -1) { LOG_WARN("/sys/class/power_supply/%s/model_name: %s", m->battery, strerror(errno)); m->model = NULL; @@ -298,7 +298,7 @@ initialize(struct private *m) if (faccessat(base_dir_fd, "energy_full_design", O_RDONLY, 0) == 0 && faccessat(base_dir_fd, "energy_full", O_RDONLY, 0) == 0) { { - int fd = openat(base_dir_fd, "energy_full_design", O_RDONLY); + int fd = openat(base_dir_fd, "energy_full_design", O_RDONLY | O_CLOEXEC); if (fd == -1) { LOG_ERRNO("/sys/class/power_supply/%s/energy_full_design", m->battery); goto err; @@ -309,7 +309,7 @@ initialize(struct private *m) } { - int fd = openat(base_dir_fd, "energy_full", O_RDONLY); + int fd = openat(base_dir_fd, "energy_full", O_RDONLY | O_CLOEXEC); if (fd == -1) { LOG_ERRNO("/sys/class/power_supply/%s/energy_full", m->battery); goto err; @@ -325,7 +325,7 @@ initialize(struct private *m) if (faccessat(base_dir_fd, "charge_full_design", O_RDONLY, 0) == 0 && faccessat(base_dir_fd, "charge_full", O_RDONLY, 0) == 0) { { - int fd = openat(base_dir_fd, "charge_full_design", O_RDONLY); + int fd = openat(base_dir_fd, "charge_full_design", O_RDONLY | O_CLOEXEC); if (fd == -1) { LOG_ERRNO("/sys/class/power_supply/%s/charge_full_design", m->battery); goto err; @@ -336,7 +336,7 @@ initialize(struct private *m) } { - int fd = openat(base_dir_fd, "charge_full", O_RDONLY); + int fd = openat(base_dir_fd, "charge_full", O_RDONLY | O_CLOEXEC); if (fd == -1) { LOG_ERRNO("/sys/class/power_supply/%s/charge_full", m->battery); goto err; @@ -362,13 +362,13 @@ update_status(struct module *mod) { struct private *m = mod->private; - int pw_fd = open("/sys/class/power_supply", O_RDONLY); + int pw_fd = open("/sys/class/power_supply", O_RDONLY | O_CLOEXEC); if (pw_fd < 0) { LOG_ERRNO("/sys/class/power_supply"); return false; } - int base_dir_fd = openat(pw_fd, m->battery, O_RDONLY); + int base_dir_fd = openat(pw_fd, m->battery, O_RDONLY | O_CLOEXEC); close(pw_fd); if (base_dir_fd < 0) { @@ -376,14 +376,14 @@ update_status(struct module *mod) return false; } - int status_fd = openat(base_dir_fd, "status", O_RDONLY); + int status_fd = openat(base_dir_fd, "status", O_RDONLY | O_CLOEXEC); if (status_fd < 0) { LOG_ERRNO("/sys/class/power_supply/%s/status", m->battery); close(base_dir_fd); return false; } - int capacity_fd = openat(base_dir_fd, "capacity", O_RDONLY); + int capacity_fd = openat(base_dir_fd, "capacity", O_RDONLY | O_CLOEXEC); if (capacity_fd < 0) { LOG_ERRNO("/sys/class/power_supply/%s/capacity", m->battery); close(status_fd); @@ -391,12 +391,12 @@ update_status(struct module *mod) return false; } - int energy_fd = openat(base_dir_fd, "energy_now", O_RDONLY); - int power_fd = openat(base_dir_fd, "power_now", O_RDONLY); - int charge_fd = openat(base_dir_fd, "charge_now", O_RDONLY); - int current_fd = openat(base_dir_fd, "current_now", O_RDONLY); - int time_to_empty_fd = openat(base_dir_fd, "time_to_empty_now", O_RDONLY); - int time_to_full_fd = openat(base_dir_fd, "time_to_full_now", O_RDONLY); + int energy_fd = openat(base_dir_fd, "energy_now", O_RDONLY | O_CLOEXEC); + int power_fd = openat(base_dir_fd, "power_now", O_RDONLY | O_CLOEXEC); + int charge_fd = openat(base_dir_fd, "charge_now", O_RDONLY | O_CLOEXEC); + int current_fd = openat(base_dir_fd, "current_now", O_RDONLY | O_CLOEXEC); + int time_to_empty_fd = openat(base_dir_fd, "time_to_empty_now", O_RDONLY | O_CLOEXEC); + int time_to_full_fd = openat(base_dir_fd, "time_to_full_now", O_RDONLY | O_CLOEXEC); long capacity = readint_from_fd(capacity_fd); long energy = energy_fd >= 0 ? readint_from_fd(energy_fd) : -1; diff --git a/modules/cpu.c b/modules/cpu.c index 833c188..118361e 100644 --- a/modules/cpu.c +++ b/modules/cpu.c @@ -124,7 +124,7 @@ refresh_cpu_stats(struct cpu_stats *cpu_stats, size_t core_count) size_t len = 0; ssize_t read; - fp = fopen("/proc/stat", "r"); + fp = fopen("/proc/stat", "re"); if (NULL == fp) { LOG_ERRNO("unable to open /proc/stat"); return; diff --git a/modules/dbus.h b/modules/dbus.h new file mode 100644 index 0000000..6517cef --- /dev/null +++ b/modules/dbus.h @@ -0,0 +1,13 @@ +#pragma once + +// This header provides an generic interface for different versions of +// systemd-sdbus. + +#if defined(HAVE_LIBSYSTEMD) +#include +#elif defined(HAVE_LIBELOGIND) +#include +#elif defined(HAVE_BASU) +#include +#endif + diff --git a/modules/disk-io.c b/modules/disk-io.c index 90abdba..c33cbef 100644 --- a/modules/disk-io.c +++ b/modules/disk-io.c @@ -105,7 +105,7 @@ refresh_device_stats(struct private *m) size_t len = 0; ssize_t read; - fp = fopen("/proc/diskstats", "r"); + fp = fopen("/proc/diskstats", "re"); if (NULL == fp) { LOG_ERRNO("unable to open /proc/diskstats"); return; @@ -129,7 +129,7 @@ refresh_device_stats(struct private *m) while ((read = getline(&line, &len, fp)) != -1) { /* - * For an explanation of the fields bellow, see + * For an explanation of the fields below, see * https://www.kernel.org/doc/Documentation/ABI/testing/procfs-diskstats */ uint8_t major_number = 0; diff --git a/modules/dwl.c b/modules/dwl.c index 4d9e8fa..3b1bdcc 100644 --- a/modules/dwl.c +++ b/modules/dwl.c @@ -231,7 +231,7 @@ process_line(char *line, struct module *module) /* No need to check error IMHO */ *target = strtoul(string, NULL, 10); - /* Populate informations */ + /* Populate information */ if (index == 6) { for (size_t id = 1; id <= private->number_of_tags; ++id) { uint32_t mask = 1 << (id - 1); @@ -330,7 +330,7 @@ run_init(int *inotify_fd, int *inotify_wd, FILE **file, char *dwl_info_filename) return 1; } - *file = fopen(dwl_info_filename, "r"); + *file = fopen(dwl_info_filename, "re"); if (*file == NULL) { inotify_rm_watch(*inotify_fd, *inotify_wd); close(*inotify_fd); diff --git a/modules/i3.c b/modules/i3.c index 73bd9d6..cbdafaf 100644 --- a/modules/i3.c +++ b/modules/i3.c @@ -514,7 +514,6 @@ handle_workspace_event(int sock, int type, const struct json_object *json, void else if (is_move) { struct workspace *w = workspace_lookup(m, current_id); - assert(w != NULL); struct json_object *_current_output; if (!json_object_object_get_ex(current, "output", &_current_output)) { @@ -522,16 +521,22 @@ handle_workspace_event(int sock, int type, const struct json_object *json, void mtx_unlock(&mod->lock); return false; } + const char *current_output_string = json_object_get_string(_current_output); - free(w->output); - w->output = strdup(json_object_get_string(_current_output)); + /* Ignore fallback_output ("For when there's no connected outputs") */ + if (strcmp(current_output_string, "FALLBACK") != 0) { - /* - * If the moved workspace was focused, schedule a full update because - * visibility for other workspaces may have changed. - */ - if (w->focused) { - i3_send_pkg(sock, I3_IPC_MESSAGE_TYPE_GET_WORKSPACES, NULL); + assert(w != NULL); + free(w->output); + w->output = strdup(current_output_string); + + /* + * If the moved workspace was focused, schedule a full update because + * visibility for other workspaces may have changed. + */ + if (w->focused) { + i3_send_pkg(sock, I3_IPC_MESSAGE_TYPE_GET_WORKSPACES, NULL); + } } } @@ -589,7 +594,7 @@ handle_window_event(int sock, int type, const struct json_object *json, void *_m mtx_lock(&mod->lock); struct workspace *ws = NULL; - size_t focused = 0; + __attribute__((unused)) size_t focused = 0; tll_foreach(m->workspaces, it) { if (it->item.focused) { @@ -659,7 +664,7 @@ handle_window_event(int sock, int type, const struct json_object *json, void *_m char path[64]; snprintf(path, sizeof(path), "/proc/%u/comm", ws->window.pid); - int fd = open(path, O_RDONLY); + int fd = open(path, O_RDONLY | O_CLOEXEC); if (fd == -1) { /* Application may simply have terminated */ free(ws->window.application); @@ -871,6 +876,7 @@ content(struct module *mod) struct tag_set tags = { .tags = (struct tag *[]){ tag_new_string(mod, "name", name), + tag_new_string(mod, "output", ws->output), tag_new_bool(mod, "visible", ws->visible), tag_new_bool(mod, "focused", ws->focused), tag_new_bool(mod, "urgent", ws->urgent), @@ -882,7 +888,7 @@ content(struct module *mod) tag_new_string(mod, "mode", m->mode), }, - .count = 9, + .count = 10, }; if (ws->focused) { diff --git a/modules/mem.c b/modules/mem.c index dc9bcf8..de4e133 100644 --- a/modules/mem.c +++ b/modules/mem.c @@ -54,7 +54,7 @@ get_mem_stats(uint64_t *mem_free, uint64_t *mem_total) size_t len = 0; ssize_t read = 0; - fp = fopen("/proc/meminfo", "r"); + fp = fopen("/proc/meminfo", "re"); if (NULL == fp) { LOG_ERRNO("unable to open /proc/meminfo"); return false; diff --git a/modules/meson.build b/modules/meson.build index e2ed56e..f6d53d8 100644 --- a/modules/meson.build +++ b/modules/meson.build @@ -22,6 +22,10 @@ plugin_mem_enabled = get_option('plugin-mem').allowed() mpd = dependency('libmpdclient', required: get_option('plugin-mpd')) plugin_mpd_enabled = mpd.found() +# DBus dependency. Used by 'mpris' +sdbus_library = dependency('libsystemd', 'libelogind', 'basu', required: get_option('plugin-mpris')) +plugin_mpris_enabled = sdbus_library.found() + json_i3 = dependency('json-c', required: get_option('plugin-i3')) plugin_i3_enabled = json_i3.found() @@ -45,6 +49,12 @@ plugin_script_enabled = get_option('plugin-script').allowed() json_sway_xkb = dependency('json-c', required: get_option('plugin-sway-xkb')) plugin_sway_xkb_enabled = json_sway_xkb.found() +json_niri_language = dependency('json-c', required: get_option('plugin-niri-language')) +plugin_niri_language_enabled = json_niri_language.found() + +json_niri_workspaces = dependency('json-c', required: get_option('plugin-niri-workspaces')) +plugin_niri_workspaces_enabled = json_niri_workspaces.found() + xcb_xkb = dependency('xcb-xkb', required: get_option('plugin-xkb')) plugin_xkb_enabled = backend_x11 and xcb_xkb.found() @@ -89,6 +99,11 @@ if plugin_mpd_enabled mod_data += {'mpd': [[], [mpd]]} endif +if plugin_mpris_enabled + sdbus = declare_dependency(compile_args: ['-DHAVE_' + sdbus_library.name().to_upper()], dependencies:[sdbus_library]) + mod_data += {'mpris': [[], [sdbus]]} +endif + if plugin_i3_enabled mod_data += {'i3': [['i3-common.c', 'i3-common.h'], [dynlist, json_i3]]} endif @@ -121,6 +136,14 @@ if plugin_sway_xkb_enabled mod_data += {'sway-xkb': [['i3-common.c', 'i3-common.h'], [dynlist, json_sway_xkb]]} endif +if plugin_niri_language_enabled + mod_data += {'niri-language': [['niri-common.c', 'niri-common.h'], [dynlist, json_niri_language]]} +endif + +if plugin_niri_workspaces_enabled + mod_data += {'niri-workspaces': [['niri-common.c', 'niri-common.h'], [dynlist, json_niri_workspaces]]} +endif + if plugin_xkb_enabled mod_data += {'xkb': [[], [xcb_stuff, xcb_xkb]]} endif diff --git a/modules/mpd.c b/modules/mpd.c index 22c0e6b..e70e41f 100644 --- a/modules/mpd.c +++ b/modules/mpd.c @@ -39,6 +39,7 @@ struct private bool repeat; bool random; bool consume; + bool single; int volume; char *album; char *artist; @@ -176,6 +177,7 @@ content(struct module *mod) tag_new_bool(mod, "repeat", m->repeat), tag_new_bool(mod, "random", m->random), tag_new_bool(mod, "consume", m->consume), + tag_new_bool(mod, "single", m->single), tag_new_int_range(mod, "volume", m->volume, 0, 100), tag_new_string(mod, "album", m->album), tag_new_string(mod, "artist", m->artist), @@ -187,7 +189,7 @@ content(struct module *mod) tag_new_int_realtime( mod, "elapsed", elapsed, 0, m->duration, realtime), }, - .count = 13, + .count = 14, }; mtx_unlock(&mod->lock); @@ -336,6 +338,7 @@ update_status(struct module *mod) m->repeat = mpd_status_get_repeat(status); m->random = mpd_status_get_random(status); m->consume = mpd_status_get_consume(status); + m->single = mpd_status_get_single_state(status) == MPD_SINGLE_ONESHOT; m->volume = mpd_status_get_volume(status); m->duration = mpd_status_get_total_time(status) * 1000; m->elapsed.value = mpd_status_get_elapsed_ms(status); @@ -437,7 +440,7 @@ run(struct module *mod) */ while (!aborted) { struct pollfd fds[] = {{.fd = mod->abort_fd, .events = POLLIN}}; - int res = poll(fds, sizeof(fds) / sizeof(fds[0]), 10 * 1000); + int res = poll(fds, sizeof(fds) / sizeof(fds[0]), 2 * 1000); if (res < 0) { if (errno == EINTR) @@ -448,10 +451,16 @@ run(struct module *mod) break; } - if (res == 1) { + if (res == 0) { + ret = 0; + break; + } + + else if (res == 1) { assert(fds[0].revents & POLLIN); aborted = true; } + } } diff --git a/modules/mpris.c b/modules/mpris.c new file mode 100644 index 0000000..5ddf6e0 --- /dev/null +++ b/modules/mpris.c @@ -0,0 +1,1100 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include + +#define LOG_MODULE "mpris" +#define LOG_ENABLE_DBG 0 +#include "../bar/bar.h" +#include "../config-verify.h" +#include "../config.h" +#include "../log.h" +#include "../plugin.h" + +#include "dbus.h" +#include "yml.h" + +#define is_empty_string(str) ((str) == NULL || (str)[0] == '\0') + +#define DEFAULT_QUERY_TIMEOUT_MS (500 * 1000) + +#define MPRIS_PATH "/org/mpris/MediaPlayer2" +#define MPRIS_BUS_NAME "org.mpris.MediaPlayer2" +#define MPRIS_SERVICE "org.mpris.MediaPlayer2" +#define MPRIS_INTERFACE_PLAYER "org.mpris.MediaPlayer2.Player" + +#define DBUS_PATH "/org/freedesktop/DBus" +#define DBUS_BUS_NAME "org.freedesktop.DBus" +#define DBUS_SERVICE "org.freedesktop.DBus" +#define DBUS_INTERFACE_MONITORING "org.freedesktop.DBus.Monitoring" +#define DBUS_INTERFACE_PROPERTIES "org.freedesktop.DBus.Properties" + +enum status { + STATUS_OFFLINE, + STATUS_PLAYING, + STATUS_PAUSED, + STATUS_STOPPED, + STATUS_ERROR, +}; + +typedef tll(char *) string_array; + +struct metadata { + uint64_t length_us; + char *trackid; + string_array artists; + char *album; + char *title; +}; + +struct property { + struct metadata metadata; + char *playback_status; + char *loop_status; + uint64_t position_us; + double rate; + double volume; + bool shuffle; +}; + +struct client { + bool has_seeked_support; + enum status status; + const char *bus_name; + const char *bus_unique_name; + + struct property property; + + /* The unix timestamp of the last position change (ie. + * seeking, pausing) */ + struct timespec seeked_when; +}; + +struct context { + const struct private *mpd_config; + + sd_bus *monitor_connection; + sd_bus_message *update_message; + + tll(struct client *) clients; + struct client *current_client; + + bool has_update; +}; + +struct private +{ + thrd_t refresh_thread_id; + int refresh_abort_fd; + + size_t timeout_ms; + string_array identity_list; + struct context context; + struct particle *label; +}; + +#if defined(LOG_ENABLE_DBG) && LOG_ENABLE_DBG +static void __attribute__((unused)) +debug_print_argument_type(sd_bus_message *message) +{ + char type; + const char *content; + sd_bus_message_peek_type(message, &type, &content); + LOG_DBG("argument type: %c -> %s", type, content); +} +#endif + +static void +metadata_clear(struct metadata *metadata) +{ + tll_free_and_free(metadata->artists, free); + + if (metadata->album != NULL) { + free(metadata->album); + metadata->album = NULL; + } + + if (metadata->title != NULL) { + free(metadata->title); + metadata->title = NULL; + } + + if (metadata->trackid != NULL) { + free(metadata->trackid); + metadata->trackid = NULL; + } +} + +static void +client_free(struct client *client) +{ + free((void *)client->bus_name); + free((void *)client->bus_unique_name); + free(client); +} + +static void +client_free_by_unique_name(struct context *context, const char *unique_name) +{ + tll_foreach(context->clients, it) + { + struct client *client = it->item; + if (strcmp(client->bus_unique_name, unique_name) == 0) { + LOG_DBG("client_remove: Removing client %s", client->bus_name); + + client_free(client); + tll_remove(context->clients, it); + } + } +} + +static void +client_add(struct context *context, const char *name, const char *unique_name) +{ + struct client *client = malloc(sizeof(*client)); + (*client) = (struct client){ + .bus_name = strdup(name), + .bus_unique_name = strdup(unique_name), + }; + + tll_push_back(context->clients, client); + LOG_DBG("client_add: name='%s' unique_name='%s'", name, unique_name); +} + +static struct client * +client_lookup_by_unique_name(struct context *context, const char *unique_name) +{ + tll_foreach(context->clients, it) + { + struct client *client = it->item; + if (strcmp(client->bus_unique_name, unique_name) == 0) { + LOG_DBG("client_lookup: name: %s", client->bus_name); + return client; + } + } + + return NULL; +} + +static void +client_change_unique_name(struct client *client, const char *new_name) +{ + if (client->bus_unique_name != NULL) { + free((void *)client->bus_unique_name); + } + + client->bus_unique_name = strdup(new_name); +} + +static bool +verify_bus_name(const string_array *identity_list, const char *name) +{ + tll_foreach(*identity_list, it) + { + const char *ident = it->item; + + if (strlen(name) < strlen(MPRIS_BUS_NAME ".") + strlen(ident)) { + continue; + } + + const char *cmp = name + strlen(MPRIS_BUS_NAME "."); + if (strncmp(cmp, ident, strlen(ident)) != 0) { + continue; + } + + return true; + } + + return false; +} + +static bool +read_string_array(sd_bus_message *message, string_array *list) +{ + int status = 0; + + /* message argument layout: 'vas' */ + /* enter variant */ + status = sd_bus_message_enter_container(message, SD_BUS_TYPE_VARIANT, "as"); + if (status <= 0) { + LOG_DBG("unexpected layout: errno=%d (%s)", status, strerror(-status)); + return false; + } + + /* enter array */ + status = sd_bus_message_enter_container(message, SD_BUS_TYPE_ARRAY, "s"); + assert(status >= 0); + + const char *string; + while ((status = sd_bus_message_read_basic(message, SD_BUS_TYPE_STRING, &string)) > 0) { + if (!is_empty_string(string)) { + tll_push_back(*list, strdup(string)); + } + } + + if (status < 0) { + LOG_ERR("metadata: unexpected layout: errno=%d (%s)", status, strerror(-status)); + return false; + } + + /* close array */ + sd_bus_message_exit_container(message); + /* close variant */ + sd_bus_message_exit_container(message); + + return true; +} + +static bool +metadata_parse_property(const char *property_name, sd_bus_message *message, struct metadata *buffer) +{ + int status = 0; + const char *string = NULL; + + char argument_type = 0; + const char *argument_layout = NULL; + sd_bus_message_peek_type(message, &argument_type, &argument_layout); + assert(argument_type == SD_BUS_TYPE_VARIANT); + assert(!is_empty_string(argument_layout)); + + if (strcmp(property_name, "mpris:trackid") == 0) { + if (argument_layout[0] != SD_BUS_TYPE_STRING && argument_layout[0] != SD_BUS_TYPE_OBJECT_PATH) + goto unexpected_type; + + status = sd_bus_message_read(message, "v", argument_layout, &string); + if (status > 0 && !is_empty_string(string)) + buffer->trackid = strdup(string); + + /* FIXME: "strcmp matches both 'album' as well as 'albumArtist'" */ + } else if (strcmp(property_name, "xesam:album") == 0) { + status = sd_bus_message_read(message, "v", argument_layout, &string); + if (status > 0 && !is_empty_string(string)) + buffer->album = strdup(string); + + } else if (strcmp(property_name, "xesam:artist") == 0) { + status = read_string_array(message, &buffer->artists); + + } else if (strcmp(property_name, "xesam:title") == 0) { + status = sd_bus_message_read(message, "v", "s", &string); + if (status > 0 && !is_empty_string(string)) + buffer->title = strdup(string); + + } else if (strcmp(property_name, "mpris:length") == 0) { + /* MPRIS requires 'mpris:length' to be an i64 (the wording is a bit ambiguous), however some client + * use a u64 instead. */ + if (argument_layout[0] != SD_BUS_TYPE_INT64 && argument_layout[0] != SD_BUS_TYPE_UINT64) + goto unexpected_type; + + status = sd_bus_message_read(message, "v", argument_layout, &buffer->length_us); + + } else { + LOG_DBG("metadata: ignoring property: %s", property_name); + sd_bus_message_skip(message, NULL); + return true; + } + + if (status < 0) { + LOG_ERR("metadata: failed to read property: arg_type='%c' arg_layout='%s' errno=%d (%s)", argument_type, + argument_layout, status, strerror(-status)); + return false; + } + + return true; +unexpected_type: + LOG_ERR("metadata: unexpected type for '%s'", property_name); + return false; +} + +static bool +metadata_parse_array(struct metadata *metadata, sd_bus_message *message) +{ + int status = sd_bus_message_enter_container(message, SD_BUS_TYPE_VARIANT, "a{sv}"); + if (status <= 0) { + LOG_DBG("unexpected layout: errno=%d (%s)", status, strerror(-status)); + return false; + } + status = sd_bus_message_enter_container(message, SD_BUS_TYPE_ARRAY, "{sv}"); + assert(status >= 0); + + while ((status = sd_bus_message_enter_container(message, SD_BUS_TYPE_DICT_ENTRY, "sv")) > 0) { + const char *property_name = NULL; + status = sd_bus_message_read_basic(message, SD_BUS_TYPE_STRING, &property_name); + if (status <= 0) { + LOG_DBG("unexpected layout: errno=%d (%s)", status, strerror(-status)); + return false; + } + + status = metadata_parse_property(property_name, message, metadata); + if (status == 0) { + return false; + } + + status = sd_bus_message_exit_container(message); + assert(status >= 0); + } + + /* close array */ + sd_bus_message_exit_container(message); + /* close variant */ + sd_bus_message_exit_container(message); + + return status >= 0; +} + +static bool +property_parse(struct property *prop, const char *property_name, sd_bus_message *message) +{ + /* This function is called in two different ways: + * 1. update_status(): The property is passed directly + * 2. update_status_from_message(): The property is passed wrapped + * inside a variant and has to be unpacked */ + const char *argument_layout = NULL; + char argument_type = 0; + int status = sd_bus_message_peek_type(message, &argument_type, &argument_layout); + + assert(status > 0); + assert(argument_type == SD_BUS_TYPE_VARIANT); + assert(!is_empty_string(argument_layout)); + + const char *string; + if (strcmp(property_name, "PlaybackStatus") == 0) { + status = sd_bus_message_read(message, "v", "s", &string); + if (status && !is_empty_string(string)) + prop->playback_status = strdup(string); + + } else if (strcmp(property_name, "LoopStatus") == 0) { + status = sd_bus_message_read(message, "v", "s", &string); + if (status && !is_empty_string(string)) + prop->loop_status = strdup(string); + + } else if (strcmp(property_name, "Position") == 0) { + /* MPRIS requires 'Position' to be a i64, however some client + * use a u64 instead. */ + if (argument_layout[0] != SD_BUS_TYPE_INT64 && argument_layout[0] != SD_BUS_TYPE_UINT64) { + LOG_ERR("property: unexpected type for '%s'", property_name); + return false; + } + status = sd_bus_message_read(message, "v", argument_layout[0], &prop->position_us); + + } else if (strcmp(property_name, "Shuffle") == 0) { + status = sd_bus_message_read(message, "v", "b", &prop->shuffle); + + } else if (strcmp(property_name, "Metadata") == 0) { + metadata_clear(&prop->metadata); + status = metadata_parse_array(&prop->metadata, message); + + } else { + LOG_DBG("property: ignoring property: %s", property_name); + sd_bus_message_skip(message, NULL); + return true; + } + + return status > 0; +} + +/* ------------- */ + +static void +format_usec_timestamp(unsigned usec, char *s, size_t sz) +{ + uint32_t secs = usec / 1000 / 1000; + uint32_t hours = secs / (60 * 60); + uint32_t minutes = secs % (60 * 60) / 60; + secs %= 60; + + if (hours > 0) + snprintf(s, sz, "%02u:%02u:%02u", hours, minutes, secs); + else + snprintf(s, sz, "%02u:%02u", minutes, secs); +} + +static void +destroy(struct module *mod) +{ + struct private *m = mod->private; + + tll_free_and_free(m->context.clients, client_free); + sd_bus_close(m->context.monitor_connection); + + tll_free_and_free(m->identity_list, free); + m->label->destroy(m->label); + free(m); + + module_default_destroy(mod); +} + +static void +context_event_handle_name_owner_changed(sd_bus_message *message, struct context *context) +{ + /* NameOwnerChanged (STRING name, STRING old_owner, STRING new_owner) */ + /* This signal indicates that the owner of a name has changed, ie. + * it was acquired, lost or changed */ + + const char *bus_name = NULL, *old_owner = NULL, *new_owner = NULL; + int status __attribute__((unused)) = sd_bus_message_read(message, "sss", &bus_name, &old_owner, &new_owner); + assert(status > 0); + + LOG_DBG("event_handler: 'NameOwnerChanged': bus_name: '%s' old_owner: '%s' new_ower: '%s'", bus_name, old_owner, + new_owner); + + if (is_empty_string(new_owner) && !is_empty_string(old_owner)) { + /* Target bus has been lost */ + struct client *client = client_lookup_by_unique_name(context, old_owner); + + if (client == NULL) + return; + + LOG_DBG("event_handler: 'NameOwnerChanged': Target bus disappeared: %s", client->bus_name); + client_free_by_unique_name(context, client->bus_unique_name); + + if (context->current_client == client) + context->current_client = NULL; + + return; + } else if (is_empty_string(old_owner) && !is_empty_string(new_owner)) { + /* New unique name registered. Not used */ + return; + } + + /* Name changed */ + assert(!is_empty_string(new_owner)); + assert(!is_empty_string(old_owner)); + + struct client *client = client_lookup_by_unique_name(context, old_owner); + LOG_DBG("'NameOwnerChanged': Name changed from '%s' to '%s' for client '%s'", old_owner, new_owner, + client->bus_name); + client_change_unique_name(client, new_owner); +} + +static void +context_event_handle_name_acquired(sd_bus_message *message, struct context *context) +{ + /* Spy on applications that requested an "MPRIS style" bus name */ + + /* NameAcquired (STRING name) */ + /* " This signal is sent to a specific application when it gains ownership of a name. " */ + const char *name = NULL; + int status __attribute__((unused)) = sd_bus_message_read_basic(message, SD_BUS_TYPE_STRING, &name); + assert(status > 0); + + LOG_DBG("event_handler: 'NameAcquired': name: '%s'", name); + + if (strncmp(name, MPRIS_BUS_NAME, strlen(MPRIS_BUS_NAME)) != 0) { + return; + } + + if (verify_bus_name(&context->mpd_config->identity_list, name)) { + const char *unique_name = sd_bus_message_get_destination(message); + LOG_DBG("'NameAcquired': Acquired new client: %s unique: %s", name, unique_name); + client_add(context, name, unique_name); + } +} + +static int +context_event_handler(sd_bus_message *message, void *userdata, sd_bus_error *ret_error) +{ + struct context *context = userdata; + + const char *member = sd_bus_message_get_member(message); + const char *sender = sd_bus_message_get_sender(message); + const char *path_name = sd_bus_message_get_path(message); + +#if 0 + const char *destination = sd_bus_message_get_destination(message); + const char *self = sd_bus_message_get_sender(message); + LOG_DBG("member: '%s' self: '%s' dest: '%s' sender: '%s'", member, self, + destination, sender); +#endif + + if (tll_length(context->clients) == 0 && strcmp(member, "NameAcquired") != 0) { + return 1; + } + + /* TODO: Allow multiple clients to connect */ + if (strcmp(path_name, DBUS_PATH) == 0 && strcmp(member, "NameAcquired") == 0) { + context_event_handle_name_acquired(message, context); + } + + if (strcmp(path_name, DBUS_PATH) == 0 && strcmp(member, "NameOwnerChanged") == 0) { + context_event_handle_name_owner_changed(message, context); + return 1; + } + + /* Copy the 'PropertiesChanged/Seeked' message, so it can be parsed + * later on */ + if (strcmp(path_name, MPRIS_PATH) == 0 + && (strcmp(member, "PropertiesChanged") == 0 || strcmp(member, "Seeked") == 0)) { + struct client *client = client_lookup_by_unique_name(context, sender); + if (client == NULL) + return 1; + + LOG_DBG("event_handler: '%s': name: '%s' unique_name: '%s'", member, client->bus_name, client->bus_unique_name); + + context->has_update = true; + context->current_client = client; + context->update_message = sd_bus_message_ref(message); + + assert(context->update_message != NULL); + } + + return 1; +} + +static bool +context_process_events(struct context *context, uint32_t timeout_ms) +{ + int status = -1; + + status = sd_bus_wait(context->monitor_connection, timeout_ms); + if (status < 0) { + if (status == -ENOTCONN) + LOG_DBG("Disconnect signal has been processed"); + else + LOG_ERR("Failed to query monitor connection: errno=%d", status); + + return false; + } + + /* 'sd_bus_process' processes one 'action' per call. + * This includes: connection, authentication, message processing */ + status = sd_bus_process(context->monitor_connection, NULL); + + if (status < 0) { + if (status == -ENOTCONN) + LOG_DBG("Disconnect signal has been processed"); + else + LOG_ERR("Failed to query monitor connection: errno=%d", status); + + return false; + } + + return true; +} + +static bool +context_setup(struct context *context) +{ + int status = true; + sd_bus *connection; + if ((status = sd_bus_default_user(&connection)) < 0) { + LOG_ERR("Failed to connect to the desktop bus. errno: %d", status); + return false; + } + + context->monitor_connection = connection; + + /* Turn this connection into a monitor */ + sd_bus_message *message; + status = sd_bus_message_new_method_call(connection, &message, DBUS_SERVICE, DBUS_PATH, DBUS_INTERFACE_MONITORING, + "BecomeMonitor"); + + const char *matching_rules[] = { + /* Listen for... */ + /* ... new MPRIS clients */ + "type='signal',interface='org.freedesktop.DBus',member='NameAcquired',path='/org/freedesktop/" + "DBus',arg0namespace='org.mpris.MediaPlayer2'", + /* ... name changes */ + "type='signal',interface='org.freedesktop.DBus',member='NameOwnerChanged'," + "path='/org/freedesktop/DBus'", + /* ... property changes */ + "type='signal',interface='org.freedesktop.DBus.Properties',member='PropertiesChanged', " + "path='/org/mpris/MediaPlayer2'", + /* ... changes in playback position */ + "type='signal',interface='org.mpris.MediaPlayer2.Player',member='Seeked', " + "path='/org/mpris/MediaPlayer2'", + }; + + /* TODO: Error handling */ + /* "BecomeMonitor" ('asu'): (Rules: String[], Flags: UINT32) */ + /* https://dbus.freedesktop.org/doc/dbus-specification.html#bus-messages-become-monitor */ + status = sd_bus_message_open_container(message, SD_BUS_TYPE_ARRAY, "s"); + for (uint32_t i = 0; i < sizeof(matching_rules) / sizeof(matching_rules[0]); i++) { + status = sd_bus_message_append(message, "s", matching_rules[i]); + } + status = sd_bus_message_close_container(message); + status = sd_bus_message_append_basic(message, SD_BUS_TYPE_UINT32, &(uint32_t){0}); + + sd_bus_message *reply = NULL; + sd_bus_error error = {}; + status = sd_bus_call(NULL, message, context->mpd_config->timeout_ms, &error, &reply); + + if (status < 0 && sd_bus_error_is_set(&error)) { + LOG_ERR("context_setup: got error: %s: %s (%d)", error.name, error.message, sd_bus_error_get_errno(&error)); + return false; + } + + sd_bus_message_unref(message); + sd_bus_message_unref(reply); + + sd_bus_add_filter(connection, NULL, context_event_handler, context); + + return status >= 0; +} + +static uint64_t +timespec_diff_us(const struct timespec *a, const struct timespec *b) +{ + uint64_t nsecs_a = a->tv_sec * 1000000000 + a->tv_nsec; + uint64_t nsecs_b = b->tv_sec * 1000000000 + b->tv_nsec; + + assert(nsecs_a >= nsecs_b); + uint64_t nsec_diff = nsecs_a - nsecs_b; + return nsec_diff / 1000; +} + +static bool +update_status_from_message(struct module *mod, sd_bus_message *message) +{ + struct private *m = mod->private; + mtx_lock(&mod->lock); + + struct client *client = m->context.current_client; + int status = 1; + + /* Player.Seeked (UINT64 position)*/ + if (strcmp(sd_bus_message_get_member(message), "Seeked") == 0) { + client->has_seeked_support = true; + + status = sd_bus_message_read_basic(message, SD_BUS_TYPE_INT64, &client->property.position_us); + if (status <= 0) + return status; + + clock_gettime(CLOCK_MONOTONIC, &client->seeked_when); + return true; + } + + /* Properties.PropertiesChanged (STRING interface_name, + * ARRAY of DICT_ENTRY changed_properties, + * ARRAY invalidated_properties); */ + assert(strcmp(sd_bus_message_get_member(message), "PropertiesChanged") == 0); + assert(strcmp(sd_bus_message_get_signature(message, 1), "sa{sv}as") == 0); + + /* argument: 'interface_name' layout: 's' */ + const char *interface_name = NULL; + sd_bus_message_read_basic(message, SD_BUS_TYPE_STRING, &interface_name); + + if (strcmp(interface_name, MPRIS_INTERFACE_PLAYER) != 0) { + LOG_DBG("Ignoring interface: %s", interface_name); + mtx_unlock(&mod->lock); + return true; + } + + /* argument: 'changed_properties' layout: 'a{sv}' */ + + /* Make sure we reset the position on metadata change unless the + * update contains its own position value */ + bool should_reset_position = true; + bool has_entries = sd_bus_message_enter_container(message, SD_BUS_TYPE_ARRAY, "{sv}"); + + while ((has_entries = sd_bus_message_enter_container(message, SD_BUS_TYPE_DICT_ENTRY, "sv")) > 0) { + const char *property_name = NULL; + int status __attribute__((unused)) = sd_bus_message_read_basic(message, SD_BUS_TYPE_STRING, &property_name); + assert(status > 0); + + if (!property_parse(&client->property, property_name, message)) { + return false; + } + + status = sd_bus_message_exit_container(message); + assert(status >= 0); + + if (strcmp(property_name, "PlaybackStatus") == 0) { + if (strcmp(client->property.playback_status, "Stopped") == 0) { + client->status = STATUS_STOPPED; + + } else if (strcmp(client->property.playback_status, "Playing") == 0) { + clock_gettime(CLOCK_MONOTONIC, &client->seeked_when); + client->status = STATUS_PLAYING; + + } else if (strcmp(client->property.playback_status, "Paused") == 0) { + /* Update our position to include the elapsed time */ + struct timespec now; + clock_gettime(CLOCK_MONOTONIC, &now); + client->status = STATUS_PAUSED; + client->property.position_us += timespec_diff_us(&now, &client->seeked_when); + } + } + + /* Make sure to reset the position upon metadata/song changes */ + if (should_reset_position && strcmp(property_name, "Metadata") == 0) { + client->property.position_us = 0; + + if (client->property.playback_status == NULL) { + client->property.playback_status = "Paused"; + client->status = STATUS_PAUSED; + } + } + + if (strcmp(property_name, "Position") == 0) { + should_reset_position = false; + } + } + + status = sd_bus_message_exit_container(message); + assert(status > 0); + + mtx_unlock(&mod->lock); + return true; +} + +static struct exposable * +content_empty(struct module *mod) +{ + struct private *m = mod->private; + mtx_lock(&mod->lock); + + struct tag_set tags = { + .tags = (struct tag *[]){ + tag_new_bool(mod, "has-seeked-support", "false"), + tag_new_string(mod, "state", "offline"), + tag_new_bool(mod, "shuffle", "false"), + tag_new_string(mod, "loop", "None"), + tag_new_int_range(mod, "volume", 0, 0, 100), + tag_new_string(mod, "album", ""), + tag_new_string(mod, "artist", ""), + tag_new_string(mod, "title", ""), + tag_new_string(mod, "pos", ""), + tag_new_string(mod, "end", ""), + tag_new_int_realtime( + mod, "elapsed", 0, 0, 0, TAG_REALTIME_NONE), + }, + .count = 10, + }; + + struct exposable *exposable = m->label->instantiate(m->label, &tags); + tag_set_destroy(&tags); + mtx_unlock(&mod->lock); + + return exposable; +} + +static struct exposable * +content(struct module *mod) +{ + const struct private *m = mod->private; + const struct client *client = m->context.current_client; + + if (client == NULL) { + return content_empty(mod); + } + + const struct metadata *metadata = &client->property.metadata; + const struct property *property = &client->property; + + /* Calculate the current playback position */ + struct timespec now; + clock_gettime(CLOCK_MONOTONIC, &now); + + uint64_t elapsed_us = client->property.position_us; + uint64_t length_us = metadata->length_us; + + if (client->has_seeked_support && client->status == STATUS_PLAYING) { + elapsed_us += timespec_diff_us(&now, &client->seeked_when); + if (elapsed_us > length_us) { + LOG_DBG("dynamic update of elapsed overflowed: " + "elapsed=%" PRIu64 ", duration=%" PRIu64, + elapsed_us, length_us); + elapsed_us = length_us; + } + } + + /* Some clients can report misleading or incomplete updates to the + * playback position, potentially causing the position to exceed + * the length */ + if (elapsed_us > length_us) + elapsed_us = length_us = 0; + + char tag_pos_value[16] = {0}, tag_end_value[16] = {0}; + if (length_us > 0) { + format_usec_timestamp(elapsed_us, tag_pos_value, sizeof(tag_pos_value)); + format_usec_timestamp(length_us, tag_end_value, sizeof(tag_end_value)); + } + + char *tag_state_value = NULL; + switch (client->status) { + case STATUS_ERROR: + tag_state_value = "error"; + break; + case STATUS_OFFLINE: + tag_state_value = "offline"; + break; + case STATUS_PLAYING: + tag_state_value = "playing"; + break; + case STATUS_PAUSED: + tag_state_value = "paused"; + break; + case STATUS_STOPPED: + tag_state_value = "stopped"; + break; + } + + const char *tag_loop_value = (property->loop_status == NULL) ? "" : property->loop_status; + const char *tag_album_value = (metadata->album == NULL) ? "" : metadata->album; + const char *tag_artists_value = (tll_length(metadata->artists) <= 0) ? "" : tll_front(metadata->artists); + const char *tag_title_value = (metadata->title == NULL) ? "" : metadata->title; + const uint32_t tag_volume_value = (property->volume >= 0.995) ? 100 : 100 * property->volume; + const bool tag_shuffle_value = property->shuffle; + const enum tag_realtime_unit realtime_unit + = (client->has_seeked_support && client->status == STATUS_PLAYING) ? TAG_REALTIME_MSECS : TAG_REALTIME_NONE; + + mtx_lock(&mod->lock); + struct tag_set tags = { + .tags = (struct tag *[]){ + tag_new_bool(mod, "has_seeked_support", client->has_seeked_support), + tag_new_bool(mod, "shuffle", tag_shuffle_value), + tag_new_int_range(mod, "volume", tag_volume_value, 0, 100), + tag_new_string(mod, "album", tag_album_value), + tag_new_string(mod, "artist", tag_artists_value), + tag_new_string(mod, "end", tag_end_value), + tag_new_string(mod, "loop", tag_loop_value), + tag_new_string(mod, "pos", tag_pos_value), + tag_new_string(mod, "state", tag_state_value), + tag_new_string(mod, "title", tag_title_value), + tag_new_int_realtime( + mod, "elapsed", elapsed_us, 0, length_us, realtime_unit), + }, + .count = 11, + }; + + struct exposable *exposable = m->label->instantiate(m->label, &tags); + tag_set_destroy(&tags); + mtx_unlock(&mod->lock); + + return exposable; +} + +struct refresh_context { + struct module *mod; + int abort_fd; + long milli_seconds; +}; + +static int +refresh_in_thread(void *arg) +{ + struct refresh_context *ctx = arg; + struct module *mod = ctx->mod; + + /* Extract data from context so that we can free it */ + int abort_fd = ctx->abort_fd; + long milli_seconds = ctx->milli_seconds; + free(ctx); + + /*LOG_DBG("going to sleep for %ldms", milli_seconds);*/ + + /* Wait for timeout, or abort signal */ + struct pollfd fds[] = {{.fd = abort_fd, .events = POLLIN}}; + int r = poll(fds, 1, milli_seconds); + + if (r < 0) { + LOG_ERRNO("failed to poll() in refresh thread"); + return 1; + } + + /* Aborted? */ + if (r == 1) { + assert(fds[0].revents & POLLIN); + /*LOG_DBG("refresh thread aborted");*/ + return 0; + } + + LOG_DBG("timed refresh"); + mod->bar->refresh(mod->bar); + + return 0; +} + +static bool +refresh_in(struct module *mod, long milli_seconds) +{ + struct private *m = mod->private; + + /* Abort currently running refresh thread */ + if (m->refresh_thread_id != 0) { + /*LOG_DBG("aborting current refresh thread");*/ + + /* Signal abort to thread */ + assert(m->refresh_abort_fd != -1); + if (write(m->refresh_abort_fd, &(uint64_t){1}, sizeof(uint64_t)) != sizeof(uint64_t)) { + LOG_ERRNO("failed to signal abort to refresher thread"); + return false; + } + + /* Wait for it to finish */ + int res; + thrd_join(m->refresh_thread_id, &res); + + /* Close and cleanup */ + close(m->refresh_abort_fd); + m->refresh_abort_fd = -1; + m->refresh_thread_id = 0; + } + + /* Create a new eventfd, to be able to signal abort to the thread */ + int abort_fd = eventfd(0, EFD_CLOEXEC); + if (abort_fd == -1) { + LOG_ERRNO("failed to create eventfd"); + return false; + } + + /* Thread context */ + struct refresh_context *ctx = malloc(sizeof(*ctx)); + ctx->mod = mod; + ctx->abort_fd = m->refresh_abort_fd = abort_fd; + ctx->milli_seconds = milli_seconds; + + /* Create thread */ + int r = thrd_create(&m->refresh_thread_id, &refresh_in_thread, ctx); + + if (r != thrd_success) { + LOG_ERR("failed to create refresh thread"); + close(m->refresh_abort_fd); + m->refresh_abort_fd = -1; + m->refresh_thread_id = 0; + free(ctx); + } + + /* Detach - we don't want to have to thrd_join() it */ + // thrd_detach(tid); + return r == 0; +} + +static int +run(struct module *mod) +{ + const struct bar *bar = mod->bar; + struct private *m = mod->private; + + if (!context_setup(&m->context)) { + LOG_ERR("Failed to setup context"); + return -1; + } + + struct context *context = &m->context; + + int ret = 0; + bool aborted = false; + while (ret == 0 && !aborted) { + const uint32_t timeout_ms = 50; + struct pollfd fds[] = {{.fd = mod->abort_fd, .events = POLLIN}}; + + /* Check for abort event */ + if (poll(fds, 1, timeout_ms) < 0) { + if (errno == EINTR) + continue; + + LOG_ERRNO("failed to poll"); + break; + } + + if (fds[0].revents & POLLIN) { + aborted = true; + break; + } + + if (!context_process_events(context, m->timeout_ms)) { + aborted = true; + break; + } + + /* Process dynamic updates, received through the contexts + * monitor connection. The 'upate_message' attribute is set + * inside the contexts event callback, if there are any + * updates to be processed. */ + if (context->has_update) { + assert(context->current_client != NULL); + assert(context->update_message != NULL); + + context->has_update = false; + aborted = !update_status_from_message(mod, context->update_message); + context->update_message = sd_bus_message_unref(context->update_message); + } + + bar->refresh(bar); + } + + LOG_DBG("exiting"); + return ret; +} + +static const char * +description(const struct module *mod) +{ + return "mpris"; +} + +static struct module * +mpris_new(const struct yml_node *ident_list, size_t timeout_ms, struct particle *label) +{ + struct private *priv = calloc(1, sizeof(*priv)); + priv->label = label; + priv->timeout_ms = timeout_ms; + priv->context.mpd_config = priv; + + size_t i = 0; + for (struct yml_list_iter iter = yml_list_iter(ident_list); iter.node != NULL; yml_list_next(&iter), i++) { + char *string = strdup(yml_value_as_string(iter.node)); + tll_push_back(priv->identity_list, string); + } + + struct module *mod = module_common_new(); + mod->private = priv; + mod->run = &run; + mod->destroy = &destroy; + mod->content = &content; + mod->description = &description; + mod->refresh_in = &refresh_in; + return mod; +} + +static struct module * +from_conf(const struct yml_node *node, struct conf_inherit inherited) +{ + const struct yml_node *ident_list = yml_get_value(node, "identities"); + const struct yml_node *query_timeout = yml_get_value(node, "query_timeout"); + const struct yml_node *c = yml_get_value(node, "content"); + + size_t timeout_ms = DEFAULT_QUERY_TIMEOUT_MS; + if (query_timeout != NULL) + timeout_ms = yml_value_as_int(query_timeout) * 1000; + + return mpris_new(ident_list, timeout_ms, conf_to_particle(c, inherited)); +} + +static bool +conf_verify_indentities(keychain_t *chain, const struct yml_node *node) +{ + return conf_verify_list(chain, node, &conf_verify_string); +} + +static bool +verify_conf(keychain_t *chain, const struct yml_node *node) +{ + static const struct attr_info attrs[] = { + {"identities", true, &conf_verify_indentities}, + {"query_timeout", false, &conf_verify_unsigned}, + MODULE_COMMON_ATTRS, + }; + + return conf_verify_dict(chain, node, attrs); +} + +const struct module_iface module_mpris_iface = { + .verify_conf = &verify_conf, + .from_conf = &from_conf, +}; + +#if defined(CORE_PLUGINS_AS_SHARED_LIBRARIES) +extern const struct module_iface iface __attribute__((weak, alias("module_mpris_iface"))); +#endif diff --git a/modules/network.c b/modules/network.c index 4bb0fb2..46a3148 100644 --- a/modules/network.c +++ b/modules/network.c @@ -17,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -24,7 +25,7 @@ #include #define LOG_MODULE "network" -#define LOG_ENABLE_DBG 0 +#define LOG_ENABLE_DBG 1 #include "../bar/bar.h" #include "../config-verify.h" #include "../config.h" @@ -52,6 +53,8 @@ struct af_addr { struct iface { char *name; + char *type; /* ARPHRD_NNN */ + char *kind; /* IFLA_LINKINFO::IFLA_INFO_KIND */ uint32_t get_stats_seq_nr; @@ -104,6 +107,8 @@ free_iface(struct iface iface) { tll_free(iface.addrs); free(iface.ssid); + free(iface.kind); + free(iface.type); free(iface.name); } @@ -119,7 +124,10 @@ destroy(struct module *mod) if (m->urandom_fd >= 0) close(m->urandom_fd); - tll_foreach(m->ifaces, it) free_iface(it->item); + tll_foreach(m->ifaces, it) { + free_iface(it->item); + tll_remove(m->ifaces, it); + } free(m); module_default_destroy(mod); @@ -204,6 +212,8 @@ content(struct module *mod) struct tag_set tags = { .tags = (struct tag *[]){ tag_new_string(mod, "name", iface->name), + tag_new_string(mod, "type", iface->type), + tag_new_string(mod, "kind", iface->kind), tag_new_int(mod, "index", iface->index), tag_new_bool(mod, "carrier", iface->carrier), tag_new_string(mod, "state", state), @@ -218,7 +228,7 @@ content(struct module *mod) tag_new_float(mod, "dl-speed", iface->dl_speed), tag_new_float(mod, "ul-speed", iface->ul_speed), }, - .count = 14, + .count = 16, }; exposables[idx++] = m->label->instantiate(m->label, &tags); tag_set_destroy(&tags); @@ -546,6 +556,79 @@ send_nl80211_get_scan(struct private *m) return true; } +static bool +foreach_nlattr(struct module *mod, struct iface *iface, const struct genlmsghdr *genl, size_t len, + bool (*cb)(struct module *mod, struct iface *iface, uint16_t type, bool nested, const void *payload, + size_t len, void *ctx), + void *ctx) +{ + const uint8_t *raw = (const uint8_t *)genl + GENL_HDRLEN; + const uint8_t *end = (const uint8_t *)genl + len; + + for (const struct nlattr *attr = (const struct nlattr *)raw; raw < end; + raw += NLA_ALIGN(attr->nla_len), attr = (const struct nlattr *)raw) { + uint16_t type = attr->nla_type & NLA_TYPE_MASK; + bool nested = (attr->nla_type & NLA_F_NESTED) != 0; + ; + const void *payload = raw + NLA_HDRLEN; + + if (!cb(mod, iface, type, nested, payload, attr->nla_len - NLA_HDRLEN, ctx)) + return false; + } + + return true; +} + +static bool +foreach_nlattr_nested(struct module *mod, struct iface *iface, const void *parent_payload, size_t len, + bool (*cb)(struct module *mod, struct iface *iface, uint16_t type, bool nested, + const void *payload, size_t len, void *ctx), + void *ctx) +{ + const uint8_t *raw = parent_payload; + const uint8_t *end = parent_payload + len; + + for (const struct nlattr *attr = (const struct nlattr *)raw; raw < end; + raw += NLA_ALIGN(attr->nla_len), attr = (const struct nlattr *)raw) { + uint16_t type = attr->nla_type & NLA_TYPE_MASK; + bool nested = (attr->nla_type & NLA_F_NESTED) != 0; + const void *payload = raw + NLA_HDRLEN; + + if (!cb(mod, iface, type, nested, payload, attr->nla_len - NLA_HDRLEN, ctx)) + return false; + } + + return true; +} + +static bool +parse_linkinfo(struct module *mod, struct iface *iface, uint16_t type, + bool nested, const void *payload, size_t len, void *_void) +{ + switch (type) { + case IFLA_INFO_KIND: { + const char *kind = payload; + free(iface->kind); + iface->kind = strndup(kind, len); + + LOG_DBG("%s: IFLA_INFO_KIND: %s", iface->name, iface->kind); + break; + } + + case IFLA_INFO_DATA: + //LOG_DBG("%s: IFLA_INFO_DATA", iface->name); + break; + + default: + LOG_WARN("unrecognized IFLA_LINKINFO attribute: " + "type=%hu, nested=%d, len=%zu", + type, nested, len); + break; + } + + return true; +} + static void handle_link(struct module *mod, uint16_t type, const struct ifinfomsg *msg, size_t len) { @@ -578,9 +661,31 @@ handle_link(struct module *mod, uint16_t type, const struct ifinfomsg *msg, size } if (iface == NULL) { + char *type = NULL; + + switch (msg->ifi_type) { + case ARPHRD_ETHER: + type = strdup("ether"); + break; + + case ARPHRD_LOOPBACK: + type = strdup("loopback"); + break; + + case ARPHRD_IEEE80211: + type = strdup("wlan"); + break; + + default: + if (asprintf(&type, "ARPHRD_%hu", msg->ifi_type) < 0) + type = strdup("unknown"); + break; + } + mtx_lock(&mod->lock); tll_push_back(m->ifaces, ((struct iface){ .index = msg->ifi_index, + .type = type, .state = IF_OPER_DOWN, .addrs = tll_init(), })); @@ -593,8 +698,10 @@ handle_link(struct module *mod, uint16_t type, const struct ifinfomsg *msg, size case IFLA_IFNAME: mtx_lock(&mod->lock); iface->name = strdup((const char *)RTA_DATA(attr)); - LOG_DBG("%s: index=%d", iface->name, iface->index); + LOG_DBG("%s: index=%d, type=%s", iface->name, iface->index, iface->type); mtx_unlock(&mod->lock); + break; + case IFLA_OPERSTATE: { uint8_t operstate = *(const uint8_t *)RTA_DATA(attr); if (iface->state == operstate) @@ -629,7 +736,8 @@ handle_link(struct module *mod, uint16_t type, const struct ifinfomsg *msg, size if (memcmp(iface->mac, mac, sizeof(iface->mac)) == 0) break; - LOG_DBG("%s: IFLA_ADDRESS: %02x:%02x:%02x:%02x:%02x:%02x", iface->name, mac[0], mac[1], mac[2], mac[3], + LOG_DBG("%s: IFLA_ADDRESS: %02x:%02x:%02x:%02x:%02x:%02x", + iface->name, mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); mtx_lock(&mod->lock); @@ -637,6 +745,13 @@ handle_link(struct module *mod, uint16_t type, const struct ifinfomsg *msg, size mtx_unlock(&mod->lock); break; } + + case IFLA_LINKINFO: { + foreach_nlattr_nested( + mod, iface, RTA_DATA(attr), RTA_PAYLOAD(attr), + &parse_linkinfo, NULL); + break; + } } } @@ -669,7 +784,7 @@ handle_address(struct module *mod, uint16_t type, const struct ifaddrmsg *msg, s } if (iface == NULL) { - LOG_ERR("failed to find network interface with index %d. Probaly a yambar bug", msg->ifa_index); + LOG_ERR("failed to find network interface with index %d. Probably a yambar bug", msg->ifa_index); return; } @@ -719,51 +834,6 @@ handle_address(struct module *mod, uint16_t type, const struct ifaddrmsg *msg, s mod->bar->refresh(mod->bar); } -static bool -foreach_nlattr(struct module *mod, struct iface *iface, const struct genlmsghdr *genl, size_t len, - bool (*cb)(struct module *mod, struct iface *iface, uint16_t type, bool nested, const void *payload, - size_t len, void *ctx), - void *ctx) -{ - const uint8_t *raw = (const uint8_t *)genl + GENL_HDRLEN; - const uint8_t *end = (const uint8_t *)genl + len; - - for (const struct nlattr *attr = (const struct nlattr *)raw; raw < end; - raw += NLA_ALIGN(attr->nla_len), attr = (const struct nlattr *)raw) { - uint16_t type = attr->nla_type & NLA_TYPE_MASK; - bool nested = (attr->nla_type & NLA_F_NESTED) != 0; - ; - const void *payload = raw + NLA_HDRLEN; - - if (!cb(mod, iface, type, nested, payload, attr->nla_len - NLA_HDRLEN, ctx)) - return false; - } - - return true; -} - -static bool -foreach_nlattr_nested(struct module *mod, struct iface *iface, const void *parent_payload, size_t len, - bool (*cb)(struct module *mod, struct iface *iface, uint16_t type, bool nested, - const void *payload, size_t len, void *ctx), - void *ctx) -{ - const uint8_t *raw = parent_payload; - const uint8_t *end = parent_payload + len; - - for (const struct nlattr *attr = (const struct nlattr *)raw; raw < end; - raw += NLA_ALIGN(attr->nla_len), attr = (const struct nlattr *)raw) { - uint16_t type = attr->nla_type & NLA_TYPE_MASK; - bool nested = (attr->nla_type & NLA_F_NESTED) != 0; - const void *payload = raw + NLA_HDRLEN; - - if (!cb(mod, iface, type, nested, payload, attr->nla_len - NLA_HDRLEN, ctx)) - return false; - } - - return true; -} - struct mcast_group { uint32_t id; const char *name; @@ -1300,6 +1370,8 @@ parse_genl_reply(struct module *mod, const struct nlmsghdr *hdr, size_t len) continue; LOG_DBG("%s: got interface information", iface->name); + free(iface->type); + iface->type = strdup("wlan"); foreach_nlattr(mod, iface, genl, msg_size, &handle_nl80211_new_interface, NULL); break; @@ -1504,7 +1576,7 @@ out: static struct module * network_new(struct particle *label, int poll_interval, int left_spacing, int right_spacing) { - int urandom_fd = open("/dev/urandom", O_RDONLY); + int urandom_fd = open("/dev/urandom", O_RDONLY | O_CLOEXEC); if (urandom_fd < 0) { LOG_ERRNO("failed to open /dev/urandom"); return NULL; diff --git a/modules/niri-common.c b/modules/niri-common.c new file mode 100644 index 0000000..ac53921 --- /dev/null +++ b/modules/niri-common.c @@ -0,0 +1,377 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../log.h" +#include "niri-common.h" + +#define LOG_MODULE "niri:common" +#define LOG_ENABLE_DBG 0 + +static struct niri_socket instance = { + .fd = -1, + .abort_fd = -1, +}; + +static void +workspace_free(struct niri_workspace *workspace) +{ + free(workspace->name); + free(workspace); +} + +static void +parser(char *response) +{ + enum json_tokener_error error = json_tokener_success; + struct json_object *json = json_tokener_parse_verbose(response, &error); + if (error != json_tokener_success) { + LOG_WARN("failed to parse niri socket's response"); + return; + } + + enum niri_event events = 0; + struct json_object_iterator it = json_object_iter_begin(json); + struct json_object_iterator end = json_object_iter_end(json); + while (!json_object_iter_equal(&it, &end)) { + char const *key = json_object_iter_peek_name(&it); + + // "WorkspacesChanged": { + // "workspaces": [ + // { + // "id": 3, + // "idx": 1, + // "name": null, + // "output": "DP-4", + // "is_active": true, + // "is_focused": true, + // "active_window_id": 24 + // }, + // ... + // ] + // } + if (strcmp(key, "WorkspacesChanged") == 0) { + mtx_lock(&instance.mtx); + tll_foreach(instance.workspaces, it) { tll_remove_and_free(instance.workspaces, it, workspace_free); } + mtx_unlock(&instance.mtx); + + json_object *obj = json_object_iter_peek_value(&it); + json_object *workspaces = json_object_object_get(obj, "workspaces"); + + size_t length = json_object_array_length(workspaces); + for (size_t i = 0; i < length; ++i) { + json_object *ws_obj = json_object_array_get_idx(workspaces, i); + + // only add workspaces on the current yambar's monitor + struct json_object *output = json_object_object_get(ws_obj, "output"); + if (strcmp(instance.monitor, json_object_get_string(output)) != 0) + continue; + + struct niri_workspace *ws = calloc(1, sizeof(*ws)); + ws->idx = json_object_get_int(json_object_object_get(ws_obj, "idx")); + ws->id = json_object_get_int(json_object_object_get(ws_obj, "id")); + ws->active = json_object_get_boolean(json_object_object_get(ws_obj, "is_active")); + ws->focused = json_object_get_boolean(json_object_object_get(ws_obj, "is_focused")); + ws->empty = json_object_get_int(json_object_object_get(ws_obj, "active_window_id")) == 0; + + char const *name = json_object_get_string(json_object_object_get(ws_obj, "name")); + if (name) + ws->name = strdup(name); + + mtx_lock(&instance.mtx); + bool inserted = false; + tll_foreach(instance.workspaces, it) + { + if (it->item->idx > ws->idx) { + tll_insert_before(instance.workspaces, it, ws); + inserted = true; + break; + } + } + if (!inserted) + tll_push_back(instance.workspaces, ws); + mtx_unlock(&instance.mtx); + + events |= workspaces_changed; + } + } + + // "WorkspaceActivated": { + // "id": 7, + // "focused":true + // } + else if (strcmp(key, "WorkspaceActivated") == 0) { + json_object *obj = json_object_iter_peek_value(&it); + int id = json_object_get_int(json_object_object_get(obj, "id")); + + mtx_lock(&instance.mtx); + tll_foreach(instance.workspaces, it) + { + bool b = it->item->id == id; + it->item->focused = b; + it->item->active = b; + } + mtx_unlock(&instance.mtx); + + events |= workspace_activated; + } + + // "WorkspaceActiveWindowChanged": { + // "workspace_id": 3, + // "active_window_id": 8 + // } + else if (strcmp(key, "WorkspaceActiveWindowChanged") == 0) { + json_object *obj = json_object_iter_peek_value(&it); + int id = json_object_get_int(json_object_object_get(obj, "id")); + bool empty = json_object_get_int(json_object_object_get(obj, "active_window_id")) == 0; + + mtx_lock(&instance.mtx); + tll_foreach(instance.workspaces, it) + { + if (it->item->id == id) { + it->item->empty = empty; + break; + } + } + mtx_unlock(&instance.mtx); + + events |= workspace_active_window_changed; + } + + // + // "KeyboardLayoutsChanged": { + // "keyboard_layouts": { + // "names": [ + // "English (US)", + // "Russian" + // ], + // "current_idx": 0 + // } + // } + else if (strcmp(key, "KeyboardLayoutsChanged") == 0) { + tll_foreach(instance.keyboard_layouts, it) { tll_remove_and_free(instance.keyboard_layouts, it, free); } + + json_object *obj = json_object_iter_peek_value(&it); + json_object *kb_layouts = json_object_object_get(obj, "keyboard_layouts"); + + instance.keyboard_layout_index = json_object_get_int(json_object_object_get(kb_layouts, "current_idx")); + + json_object *names = json_object_object_get(kb_layouts, "names"); + size_t names_length = json_object_array_length(names); + for (size_t i = 0; i < names_length; ++i) { + char const *name = json_object_get_string(json_object_array_get_idx(names, i)); + tll_push_back(instance.keyboard_layouts, strdup(name)); + } + + events |= keyboard_layouts_changed; + } + + // "KeyboardLayoutSwitched": { + // "idx": 1 + // } + else if (strcmp(key, "KeyboardLayoutSwitched") == 0) { + json_object *obj = json_object_iter_peek_value(&it); + instance.keyboard_layout_index = json_object_get_int(json_object_object_get(obj, "idx")); + + events |= keyboard_layouts_switched; + } + + json_object_iter_next(&it); + } + + json_object_put(json); + + mtx_lock(&instance.mtx); + tll_foreach(instance.subscribers, it) + { + if (it->item->events & events) + if (write(it->item->fd, &(uint64_t){1}, sizeof(uint64_t)) == -1) + LOG_ERRNO("failed to write"); + } + mtx_unlock(&instance.mtx); +} + +static int +run(void *userdata) +{ + static char msg[] = "\"EventStream\"\n"; + static char expected[] = "{\"Ok\":\"Handled\"}"; + + if (write(instance.fd, msg, sizeof(msg) / sizeof(msg[0])) == -1) { + LOG_ERRNO("failed to sent message to niri socket"); + return thrd_error; + } + + static char buffer[8192]; + if (read(instance.fd, buffer, sizeof(buffer) / sizeof(buffer[0]) - 1) == -1) { + LOG_ERRNO("failed to read response of niri socket"); + return thrd_error; + } + + char *saveptr; + char *response = strtok_r(buffer, "\n", &saveptr); + if (response == NULL || strcmp(expected, response) != 0) { + // unexpected first response, something went wrong + LOG_ERR("unexpected response of niri socket"); + return thrd_error; + } + + while ((response = strtok_r(NULL, "\n", &saveptr)) != NULL) + parser(response); + + while (true) { + struct pollfd fds[] = { + (struct pollfd){.fd = instance.abort_fd, .events = POLLIN}, + (struct pollfd){.fd = instance.fd, .events = POLLIN}, + }; + + if (poll(fds, sizeof(fds) / sizeof(fds[0]), -1) == -1) { + if (errno == EINTR) + continue; + + LOG_ERRNO("failed to poll"); + break; + } + + if (fds[0].revents & POLLIN) + break; + + static char buffer[8192]; + ssize_t length = read(fds[1].fd, buffer, sizeof(buffer) / sizeof(buffer[0])); + + if (length == 0) + break; + + if (length == -1) { + if (errno == EAGAIN || errno == EINTR) + continue; + + LOG_ERRNO("unable to read niri socket"); + break; + } + + buffer[length] = '\0'; + saveptr = NULL; + response = strtok_r(buffer, "\n", &saveptr); + do { + parser(response); + } while ((response = strtok_r(NULL, "\n", &saveptr)) != NULL); + } + + return thrd_success; +} + +struct niri_socket * +niri_socket_open(char const *monitor) +{ + if (instance.fd >= 0) + return &instance; + + char const *path = getenv("NIRI_SOCKET"); + if (path == NULL) { + LOG_ERR("NIRI_SOCKET is empty. Is niri running?"); + return NULL; + } + + if ((instance.fd = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0)) == -1) { + LOG_ERRNO("failed to create socket"); + goto error; + } + + struct sockaddr_un addr; + addr.sun_family = AF_UNIX; + addr.sun_path[sizeof(addr.sun_path) - 1] = '\0'; + strncpy(addr.sun_path, path, sizeof(addr.sun_path) - 1); + + if (connect(instance.fd, (struct sockaddr *)&addr, sizeof(addr)) == -1) { + LOG_ERRNO("failed to connect to niri socket"); + goto error; + } + + if ((instance.abort_fd = eventfd(0, EFD_CLOEXEC)) == -1) { + LOG_ERRNO("failed to create abort_fd"); + goto error; + } + + if (mtx_init(&instance.mtx, mtx_plain) != thrd_success) { + LOG_ERR("failed to initialize mutex"); + goto error; + } + + if (thrd_create(&instance.thrd, run, NULL) != thrd_success) { + LOG_ERR("failed to create thread"); + mtx_destroy(&instance.mtx); + goto error; + } + + instance.monitor = monitor; + + return &instance; + +error: + if (instance.fd >= 0) + close(instance.fd); + if (instance.abort_fd >= 0) + close(instance.abort_fd); + instance.fd = -1; + instance.abort_fd = -1; + instance.monitor = NULL; + + return NULL; +} + +static void +socket_close(void) +{ + if (write(instance.abort_fd, &(uint64_t){1}, sizeof(uint64_t)) != sizeof(uint64_t)) + LOG_ERRNO("failed to write to abort_fd"); + + thrd_join(instance.thrd, NULL); + + close(instance.abort_fd); + close(instance.fd); + instance.abort_fd = -1; + instance.fd = -1; + + mtx_destroy(&instance.mtx); + + tll_free_and_free(instance.subscribers, free); + tll_free_and_free(instance.workspaces, workspace_free); + tll_free_and_free(instance.keyboard_layouts, free); +} + +void +niri_socket_close(void) +{ + static once_flag flag = ONCE_FLAG_INIT; + call_once(&flag, socket_close); +} + +int +niri_socket_subscribe(enum niri_event events) +{ + int fd = eventfd(0, EFD_CLOEXEC); + if (fd == -1) { + LOG_ERRNO("failed to create eventfd"); + return -1; + } + + struct niri_subscriber *subscriber = calloc(1, sizeof(*subscriber)); + subscriber->events = events; + subscriber->fd = fd; + + mtx_lock(&instance.mtx); + tll_push_back(instance.subscribers, subscriber); + mtx_unlock(&instance.mtx); + + return subscriber->fd; +} diff --git a/modules/niri-common.h b/modules/niri-common.h new file mode 100644 index 0000000..18afe38 --- /dev/null +++ b/modules/niri-common.h @@ -0,0 +1,45 @@ +#pragma once + +#include +#include +#include + +enum niri_event { + workspaces_changed = (1 << 0), + workspace_activated = (1 << 1), + workspace_active_window_changed = (1 << 2), + keyboard_layouts_changed = (1 << 3), + keyboard_layouts_switched = (1 << 4), +}; + +struct niri_subscriber { + int events; + int fd; +}; + +struct niri_workspace { + int id; + int idx; + char *name; + bool active; + bool focused; + bool empty; +}; + +struct niri_socket { + char const *monitor; + int abort_fd; + int fd; + + tll(struct niri_subscriber *) subscribers; + tll(struct niri_workspace *) workspaces; + tll(char *) keyboard_layouts; + size_t keyboard_layout_index; + + thrd_t thrd; + mtx_t mtx; +}; + +struct niri_socket *niri_socket_open(char const *monitor); +void niri_socket_close(void); +int niri_socket_subscribe(enum niri_event events); diff --git a/modules/niri-language.c b/modules/niri-language.c new file mode 100644 index 0000000..f8138ee --- /dev/null +++ b/modules/niri-language.c @@ -0,0 +1,160 @@ +#include +#include +#include +#include +#include + +#define LOG_MODULE "niri-language" +#define LOG_ENABLE_DBG 0 +#include "niri-common.h" + +#include "../log.h" +#include "../particles/dynlist.h" +#include "../plugin.h" + +struct private +{ + struct particle *label; + struct niri_socket *niri; +}; + +static void +destroy(struct module *module) +{ + struct private *private = module->private; + private->label->destroy(private->label); + + free(private); + + module_default_destroy(module); +} + +static const char * +description(const struct module *module) +{ + return "niri-lang"; +} + +static struct exposable * +content(struct module *module) +{ + const struct private *private = module->private; + + if (private->niri == NULL) + return dynlist_exposable_new(&((struct exposable *){0}), 0, 0, 0); + + mtx_lock(&module->lock); + mtx_lock(&private->niri->mtx); + + char *name = "???"; + size_t i = 0; + tll_foreach(private->niri->keyboard_layouts, it) + { + if (i++ == private->niri->keyboard_layout_index) + name = it->item; + } + + struct tag_set tags = { + .tags = (struct tag *[]){tag_new_string(module, "language", name)}, + .count = 1, + }; + + struct exposable *exposable = private->label->instantiate(private->label, &tags); + tag_set_destroy(&tags); + mtx_unlock(&private->niri->mtx); + mtx_unlock(&module->lock); + return exposable; +} + +static int +run(struct module *module) +{ + struct private *private = module->private; + + /* Ugly, but I didn't find better way for waiting + * the monitor's name to be set */ + char const *monitor; + do { + monitor = module->bar->output_name(module->bar); + usleep(50); + } while (monitor == NULL); + + private->niri = niri_socket_open(monitor); + if (private->niri == NULL) + return 1; + + int fd = niri_socket_subscribe(keyboard_layouts_changed | keyboard_layouts_switched); + if (fd == -1) { + niri_socket_close(); + return 1; + } + + module->bar->refresh(module->bar); + + while (true) { + struct pollfd fds[] = { + (struct pollfd){.fd = module->abort_fd, .events = POLLIN}, + (struct pollfd){.fd = fd, .events = POLLIN}, + }; + + if (poll(fds, sizeof(fds) / sizeof(fds[0]), -1) == -1) { + if (errno == EINTR) + continue; + + LOG_ERRNO("failed to poll"); + break; + } + + if (fds[0].revents & POLLIN) + break; + + if (read(fds[1].fd, &(uint64_t){0}, sizeof(uint64_t)) == -1) + LOG_ERRNO("failed to read from eventfd"); + + module->bar->refresh(module->bar); + } + + niri_socket_close(); + return 0; +} + +static struct module * +niri_language_new(struct particle *label) +{ + struct private *private = calloc(1, sizeof(struct private)); + private->label = label; + + struct module *module = module_common_new(); + module->private = private; + module->run = &run; + module->destroy = &destroy; + module->content = &content; + module->description = &description; + + return module; +} + +static struct module * +from_conf(struct yml_node const *node, struct conf_inherit inherited) +{ + struct yml_node const *content = yml_get_value(node, "content"); + return niri_language_new(conf_to_particle(content, inherited)); +} + +static bool +verify_conf(keychain_t *chain, const struct yml_node *node) +{ + static struct attr_info const attrs[] = { + MODULE_COMMON_ATTRS, + }; + return conf_verify_dict(chain, node, attrs); +} + +const struct module_iface module_niri_language_iface = { + .verify_conf = &verify_conf, + .from_conf = &from_conf, +}; + +#if defined(CORE_PLUGINS_AS_SHARED_LIBRARIES) +extern const struct module_iface iface __attribute__((weak, alias("module_niri_language_iface"))); +#endif diff --git a/modules/niri-workspaces.c b/modules/niri-workspaces.c new file mode 100644 index 0000000..bca0150 --- /dev/null +++ b/modules/niri-workspaces.c @@ -0,0 +1,163 @@ +#include +#include +#include +#include + +#define LOG_MODULE "niri-workspaces" +#define LOG_ENABLE_DBG 0 +#include "niri-common.h" + +#include "../log.h" +#include "../particles/dynlist.h" +#include "../plugin.h" + +struct private +{ + struct particle *label; + struct niri_socket *niri; +}; + +static void +destroy(struct module *module) +{ + struct private *private = module->private; + private->label->destroy(private->label); + + free(private); + + module_default_destroy(module); +} + +static const char * +description(const struct module *module) +{ + return "niri-ws"; +} + +static struct exposable * +content(struct module *module) +{ + struct private const *private = module->private; + + if (private->niri == NULL) + return dynlist_exposable_new(&((struct exposable *){0}), 0, 0, 0); + + mtx_lock(&module->lock); + mtx_lock(&private->niri->mtx); + + size_t i = 0; + struct exposable *exposable[tll_length(private->niri->workspaces)]; + tll_foreach(private->niri->workspaces, it) + { + struct tag_set tags = { + .tags = (struct tag*[]){ + tag_new_int(module, "id", it->item->idx), + tag_new_string(module, "name", it->item->name), + tag_new_bool(module, "active", it->item->active), + tag_new_bool(module, "focused", it->item->focused), + tag_new_bool(module, "empty", it->item->empty), + }, + .count = 5, + }; + + exposable[i++] = private->label->instantiate(private->label, &tags); + tag_set_destroy(&tags); + } + + mtx_unlock(&private->niri->mtx); + mtx_unlock(&module->lock); + return dynlist_exposable_new(exposable, i, 0, 0); +} + +static int +run(struct module *module) +{ + struct private *private = module->private; + + /* Ugly, but I didn't find better way for waiting + * the monitor's name to be set */ + char const *monitor; + do { + monitor = module->bar->output_name(module->bar); + usleep(50); + } while (monitor == NULL); + + private->niri = niri_socket_open(monitor); + if (private->niri == NULL) + return 1; + + int fd = niri_socket_subscribe(workspaces_changed | workspace_activated | workspace_active_window_changed); + if (fd == -1) { + niri_socket_close(); + return 1; + } + + module->bar->refresh(module->bar); + + while (true) { + struct pollfd fds[] = { + (struct pollfd){.fd = module->abort_fd, .events = POLLIN}, + (struct pollfd){.fd = fd, .events = POLLIN}, + }; + + if (poll(fds, sizeof(fds) / sizeof(fds[0]), -1) == -1) { + if (errno == EINTR) + continue; + + LOG_ERRNO("failed to poll"); + break; + } + + if (fds[0].revents & POLLIN) + break; + + if (read(fds[1].fd, &(uint64_t){0}, sizeof(uint64_t)) == -1) + LOG_ERRNO("failed to read from eventfd"); + + module->bar->refresh(module->bar); + } + + niri_socket_close(); + return 0; +} + +static struct module * +niri_workspaces_new(struct particle *label) +{ + struct private *private = calloc(1, sizeof(struct private)); + private->label = label; + + struct module *module = module_common_new(); + module->private = private; + module->run = &run; + module->destroy = &destroy; + module->content = &content; + module->description = &description; + + return module; +} + +static struct module * +from_conf(struct yml_node const *node, struct conf_inherit inherited) +{ + struct yml_node const *content = yml_get_value(node, "content"); + return niri_workspaces_new(conf_to_particle(content, inherited)); +} + +static bool +verify_conf(keychain_t *chain, const struct yml_node *node) +{ + static struct attr_info const attrs[] = { + MODULE_COMMON_ATTRS, + }; + return conf_verify_dict(chain, node, attrs); +} + +const struct module_iface module_niri_workspaces_iface = { + .verify_conf = &verify_conf, + .from_conf = &from_conf, +}; + +#if defined(CORE_PLUGINS_AS_SHARED_LIBRARIES) +extern const struct module_iface iface __attribute__((weak, alias("module_niri_workspaces_iface"))); +#endif diff --git a/modules/pipewire.c b/modules/pipewire.c index 1a6ab1f..1ff3642 100644 --- a/modules/pipewire.c +++ b/modules/pipewire.c @@ -33,7 +33,7 @@ struct output_informations { uint32_t device_id; uint32_t card_profile_device_id; - /* informations */ + /* information */ bool muted; uint16_t linear_volume; /* classic volume */ uint16_t cubic_volume; /* volume a la pulseaudio */ @@ -45,11 +45,23 @@ struct output_informations { }; static struct output_informations const output_informations_null; +static void +output_informations_destroy(struct output_informations *output_informations) +{ + free(output_informations->name); + free(output_informations->description); + free(output_informations->icon); + free(output_informations->form_factor); + free(output_informations->bus); +} + struct data; struct private { struct particle *label; struct data *data; + int left_spacing; + int right_spacing; /* pipewire related */ struct output_informations sink_informations; @@ -213,18 +225,23 @@ node_find_route(struct data *data, bool is_sink) static void node_unhook_binded_node(struct data *data, bool is_sink) { + struct private *private = data->module->private; + struct node **target_node = NULL; struct spa_hook *target_listener = NULL; void **target_proxy = NULL; + struct output_informations *output_informations = NULL; if (is_sink) { target_node = &data->binded_sink; target_listener = &data->node_sink_listener; target_proxy = &data->node_sink; + output_informations = &private->sink_informations; } else { target_node = &data->binded_source; target_listener = &data->node_source_listener; target_proxy = &data->node_source; + output_informations = &private->source_informations; } if (*target_node == NULL) @@ -235,6 +252,9 @@ node_unhook_binded_node(struct data *data, bool is_sink) *target_node = NULL; *target_proxy = NULL; + + output_informations_destroy(output_informations); + *output_informations = output_informations_null; } static void @@ -333,7 +353,7 @@ device_events_param(void *userdata, int seq, uint32_t id, uint32_t index, uint32 X_FREE_SET(route->icon_name, X_STRDUP(data.icon_name)); route->direction = data.direction; - /* set missing informations if possible */ + /* set missing information if possible */ struct private *private = device->data->module->private; struct node *binded_node = NULL; struct output_informations *output_informations = NULL; @@ -350,7 +370,7 @@ device_events_param(void *userdata, int seq, uint32_t id, uint32_t index, uint32 if (binded_node == NULL) return; - /* Node's device is the the same as route's device */ + /* Node's device is the same as route's device */ if (output_informations->device_id != route->device->id) return; @@ -358,7 +378,7 @@ device_events_param(void *userdata, int seq, uint32_t id, uint32_t index, uint32 if (output_informations->card_profile_device_id != route->profile_device_id) return; - /* Update missing informations */ + /* Update missing information */ X_FREE_SET(output_informations->form_factor, X_STRDUP(route->form_factor)); X_FREE_SET(output_informations->icon, X_STRDUP(route->icon_name)); @@ -384,7 +404,7 @@ node_events_info(void *userdata, struct pw_node_info const *info) for (size_t i = 0; i < info->n_params; ++i) { if (info->params[i].id == SPA_PARAM_Props) { void *target_node = (node_data->is_sink ? data->node_sink : data->node_source); - /* Found it, will emit a param event, the parem will then be handled + /* Found it, will emit a param event, the param will then be handled * in node_events_param */ pw_node_enum_params(target_node, 0, info->params[i].id, 0, -1, NULL); break; @@ -398,18 +418,18 @@ node_events_info(void *userdata, struct pw_node_info const *info) struct spa_dict_item const *item = NULL; item = spa_dict_lookup_item(info->props, "node.name"); - if (item != NULL) - X_FREE_SET(output_informations->name, X_STRDUP(item->value)); + X_FREE_SET(output_informations->name, item != NULL ? X_STRDUP(item->value) : NULL); item = spa_dict_lookup_item(info->props, "node.description"); - if (item != NULL) - X_FREE_SET(output_informations->description, X_STRDUP(item->value)); + X_FREE_SET(output_informations->description, item != NULL ? X_STRDUP(item->value) : NULL); item = spa_dict_lookup_item(info->props, "device.id"); if (item != NULL) { uint32_t value = 0; spa_atou32(item->value, &value, 10); output_informations->device_id = value; + } else { + output_informations->device_id = 0; } item = spa_dict_lookup_item(info->props, "card.profile.device"); @@ -417,30 +437,29 @@ node_events_info(void *userdata, struct pw_node_info const *info) uint32_t value = 0; spa_atou32(item->value, &value, 10); output_informations->card_profile_device_id = value; + } else { + output_informations->card_profile_device_id = 0; } - /* Device's informations has an more important priority than node's informations */ + /* Device's information has an more important priority than node's information */ /* icon_name */ struct route *route = node_find_route(data, node_data->is_sink); if (route != NULL && route->icon_name != NULL) - output_informations->icon = X_STRDUP(route->icon_name); + X_FREE_SET(output_informations->icon, X_STRDUP(route->icon_name)); else { item = spa_dict_lookup_item(info->props, "device.icon-name"); - if (item != NULL) - X_FREE_SET(output_informations->icon, X_STRDUP(item->value)); + X_FREE_SET(output_informations->icon, item != NULL ? X_STRDUP(item->value) : NULL); } /* form_factor */ if (route != NULL && route->form_factor != NULL) - output_informations->form_factor = X_STRDUP(route->form_factor); + X_FREE_SET(output_informations->form_factor, X_STRDUP(route->form_factor)); else { item = spa_dict_lookup_item(info->props, "device.form-factor"); - if (item != NULL) - X_FREE_SET(output_informations->form_factor, X_STRDUP(item->value)); + X_FREE_SET(output_informations->form_factor, item != NULL ? X_STRDUP(item->value) : NULL); } item = spa_dict_lookup_item(info->props, "device.bus"); - if (item != NULL) - X_FREE_SET(output_informations->bus, X_STRDUP(item->value)); + X_FREE_SET(output_informations->bus, item != NULL ? X_STRDUP(item->value) : NULL); data->module->bar->refresh(data->module->bar); } @@ -659,7 +678,7 @@ static void try_to_bind_node(struct node_data *node_data, char const *target_name, struct node **target_node, void **target_proxy, struct spa_hook *target_listener) { - /* profile deactived */ + /* profile deactivated */ if (target_name == NULL) return; @@ -827,18 +846,8 @@ destroy(struct module *module) pipewire_deinit(private->data); private->label->destroy(private->label); - /* sink */ - free(private->sink_informations.name); - free(private->sink_informations.description); - free(private->sink_informations.icon); - free(private->sink_informations.form_factor); - free(private->sink_informations.bus); - /* source */ - free(private->source_informations.name); - free(private->source_informations.description); - free(private->source_informations.icon); - free(private->source_informations.form_factor); - free(private->source_informations.bus); + output_informations_destroy(&private->sink_informations); + output_informations_destroy(&private->source_informations); free(private); module_default_destroy(module); @@ -911,7 +920,7 @@ content(struct module *module) mtx_unlock(&module->lock); - return dynlist_exposable_new(exposables, exposables_length, 0, 0); + return dynlist_exposable_new(exposables, exposables_length, private->left_spacing, private->right_spacing); } static int @@ -958,11 +967,13 @@ run(struct module *module) } static struct module * -pipewire_new(struct particle *label) +pipewire_new(struct particle *label, int left_spacing, int right_spacing) { struct private *private = calloc(1, sizeof(struct private)); assert(private != NULL); private->label = label; + private->left_spacing = left_spacing; + private->right_spacing = right_spacing; struct module *module = module_common_new(); module->private = private; @@ -980,13 +991,25 @@ static struct module * from_conf(struct yml_node const *node, struct conf_inherit inherited) { struct yml_node const *content = yml_get_value(node, "content"); - return pipewire_new(conf_to_particle(content, inherited)); + struct yml_node const *spacing = yml_get_value(node, "spacing"); + struct yml_node const *left_spacing = yml_get_value(node, "left-spacing"); + struct yml_node const *right_spacing = yml_get_value(node, "right-spacing"); + + int left = spacing != NULL ? yml_value_as_int(spacing) : left_spacing != NULL ? yml_value_as_int(left_spacing) : 0; + int right = spacing != NULL ? yml_value_as_int(spacing) + : right_spacing != NULL ? yml_value_as_int(right_spacing) + : 0; + + return pipewire_new(conf_to_particle(content, inherited), left, right); } static bool verify_conf(keychain_t *keychain, struct yml_node const *node) { static struct attr_info const attrs[] = { + {"spacing", false, &conf_verify_unsigned}, + {"left-spacing", false, &conf_verify_unsigned}, + {"right-spacing", false, &conf_verify_unsigned}, MODULE_COMMON_ATTRS, }; return conf_verify_dict(keychain, node, attrs); diff --git a/modules/pulse.c b/modules/pulse.c index e605dea..f6c7f69 100644 --- a/modules/pulse.c +++ b/modules/pulse.c @@ -438,7 +438,7 @@ run(struct module *mod) } // Create refresh timer. - priv->refresh_timer_fd = timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK); + priv->refresh_timer_fd = timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK | TFD_CLOEXEC); if (priv->refresh_timer_fd < 0) { LOG_ERRNO("failed to create timerfd"); pa_mainloop_free(priv->mainloop); diff --git a/modules/removables.c b/modules/removables.c index e4ef98e..df4ade4 100644 --- a/modules/removables.c +++ b/modules/removables.c @@ -161,13 +161,10 @@ content(struct module *mod) static void find_mount_points(const char *dev_path, mount_point_list_t *mount_points) { - int fd = open("/proc/self/mountinfo", O_RDONLY | O_CLOEXEC); - FILE *f = fd >= 0 ? fdopen(fd, "r") : NULL; + FILE *f = fopen("/proc/self/mountinfo", "re"); - if (fd < 0 || f == NULL) { + if (f == NULL) { LOG_ERRNO("failed to open /proc/self/mountinfo"); - if (fd >= 0) - close(fd); return; } diff --git a/modules/river.c b/modules/river.c index 2619c62..ec25f9f 100644 --- a/modules/river.c +++ b/modules/river.c @@ -52,6 +52,7 @@ struct seat { struct private { struct module *mod; + bool is_running; struct zxdg_output_manager_v1 *xdg_output_manager; struct zriver_status_manager_v1 *status_manager; struct particle *template; @@ -88,6 +89,11 @@ content(struct module *mod) mtx_lock(&m->mod->lock); + if (!m->is_running) { + mtx_unlock(&m->mod->lock); + return dynlist_exposable_new(NULL, 0, 0, 0); + } + uint32_t urgent = 0; uint32_t occupied = 0; uint32_t output_focused = 0; @@ -685,6 +691,8 @@ run(struct module *mod) goto out; } + m->is_running = true; + wl_display_roundtrip(display); while (true) { diff --git a/modules/xwindow.c b/modules/xwindow.c index ffae527..c730128 100644 --- a/modules/xwindow.c +++ b/modules/xwindow.c @@ -130,7 +130,7 @@ update_application(struct module *mod) char path[1024]; snprintf(path, sizeof(path), "/proc/%d/cmdline", pid); - int fd = open(path, O_RDONLY); + int fd = open(path, O_RDONLY | O_CLOEXEC); if (fd == -1) return; diff --git a/particles/map.c b/particles/map.c index 51fc744..c5510ff 100644 --- a/particles/map.c +++ b/particles/map.c @@ -13,6 +13,61 @@ #include "map.h" +// String globbing match. +// Note: Uses "non-greedy" implementation for "*" wildcard matching +static bool +string_like(const char* name, const char* pattern) +{ + LOG_DBG("pattern:%s name:%s", pattern, name); + int px = 0, nx = 0; + int nextpx = 0, nextnx = 0; + + while (px < strlen(pattern) || nx < strlen(name)) { + if (px < strlen(pattern)) { + char c = pattern[px]; + switch (c) { + case '?': { + // single character + px++; + nx++; + continue; + } + case '*': { + // zero or more glob + nextpx=px; + nextnx=nx+1; + px++; + continue; + } + default: { + // normal character + if (nx < strlen(name) && name[nx] == c) + { + px++; + nx++; + continue; + } + } + } + + } + + // mismatch + if (0 < nextnx && nextnx <= strlen(name)) { + px = nextpx; + nx = nextnx; + continue; + } + + return false; + + } + + LOG_DBG("map: name %s matched all the pattern %s", name, pattern); + // Matched all of pattern to all of name. Success. + return true; +} + static bool int_condition(const long tag_value, const long cond_value, enum map_op op) { @@ -75,6 +130,8 @@ str_condition(const char *tag_value, const char *cond_value, enum map_op op) return strcmp(tag_value, cond_value) >= 0; case MAP_OP_GT: return strcmp(tag_value, cond_value) > 0; + case MAP_OP_LIKE: + return string_like(tag_value, cond_value) != 0; case MAP_OP_SELF: LOG_WARN("using String tag as bool"); default: @@ -166,6 +223,7 @@ free_map_condition(struct map_condition *c) case MAP_OP_LE: case MAP_OP_LT: case MAP_OP_GE: + case MAP_OP_LIKE: case MAP_OP_GT: free(c->value); /* FALLTHROUGH */ diff --git a/particles/map.h b/particles/map.h index 23670a5..1256744 100644 --- a/particles/map.h +++ b/particles/map.h @@ -9,6 +9,7 @@ enum map_op { MAP_OP_GT, MAP_OP_SELF, MAP_OP_NOT, + MAP_OP_LIKE, MAP_OP_AND, MAP_OP_OR, diff --git a/particles/map.l b/particles/map.l index d34f086..034353c 100644 --- a/particles/map.l +++ b/particles/map.l @@ -69,6 +69,7 @@ void yyerror(const char *s); \< yylval.op = MAP_OP_LT; return CMP_OP; >= yylval.op = MAP_OP_GE; return CMP_OP; > yylval.op = MAP_OP_GT; return CMP_OP; +~~ yylval.op = MAP_OP_LIKE; return CMP_OP; && yylval.op = MAP_OP_AND; return BOOL_OP; \|\| yylval.op = MAP_OP_OR; return BOOL_OP; ~ return NOT; diff --git a/particles/map.y b/particles/map.y index ee426da..8f3f46b 100644 --- a/particles/map.y +++ b/particles/map.y @@ -35,27 +35,27 @@ result: condition { MAP_CONDITION_PARSE_RESULT = $1; }; condition: WORD { $$ = malloc(sizeof(struct map_condition)); - $$->tag = $1; + $$->tag = $1; $$->op = MAP_OP_SELF; } | WORD CMP_OP WORD { $$ = malloc(sizeof(struct map_condition)); - $$->tag = $1; + $$->tag = $1; $$->op = $2; - $$->value = $3; + $$->value = $3; } | WORD CMP_OP STRING { $$ = malloc(sizeof(struct map_condition)); - $$->tag = $1; + $$->tag = $1; $$->op = $2; - $$->value = $3; + $$->value = $3; } | L_PAR condition R_PAR { $$ = $2; } | - NOT condition { + NOT condition { $$ = malloc(sizeof(struct map_condition)); $$->cond1 = $2; $$->op = MAP_OP_NOT; @@ -79,7 +79,7 @@ static char const* token_to_str(yysymbol_kind_t tkn) { switch (tkn) { - case YYSYMBOL_CMP_OP: return "==, !=, <=, <, >=, >"; + case YYSYMBOL_CMP_OP: return "==, !=, <=, <, >=, >, ~~"; case YYSYMBOL_BOOL_OP: return "||, &&"; case YYSYMBOL_L_PAR: return "("; case YYSYMBOL_R_PAR: return ")"; diff --git a/plugin.c b/plugin.c index 8e75389..2ed0a4f 100644 --- a/plugin.c +++ b/plugin.c @@ -57,6 +57,9 @@ EXTERN_MODULE(mem); #if defined(HAVE_PLUGIN_mpd) EXTERN_MODULE(mpd); #endif +#if defined(HAVE_PLUGIN_mpris) +EXTERN_MODULE(mpris); +#endif #if defined(HAVE_PLUGIN_i3) EXTERN_MODULE(i3); #endif @@ -84,6 +87,12 @@ EXTERN_MODULE(script); #if defined(HAVE_PLUGIN_sway_xkb) EXTERN_MODULE(sway_xkb); #endif +#if defined(HAVE_PLUGIN_niri_language) +EXTERN_MODULE(niri_language); +#endif +#if defined(HAVE_PLUGIN_niri_workspaces) +EXTERN_MODULE(niri_workspaces); +#endif #if defined(HAVE_PLUGIN_xkb) EXTERN_MODULE(xkb); #endif @@ -187,6 +196,9 @@ static void __attribute__((constructor)) init(void) #if defined(HAVE_PLUGIN_mpd) REGISTER_CORE_MODULE(mpd, mpd); #endif +#if defined(HAVE_PLUGIN_mpris) + REGISTER_CORE_MODULE(mpris, mpris); +#endif #if defined(HAVE_PLUGIN_i3) REGISTER_CORE_MODULE(i3, i3); #endif @@ -214,6 +226,12 @@ static void __attribute__((constructor)) init(void) #if defined(HAVE_PLUGIN_sway_xkb) REGISTER_CORE_MODULE(sway-xkb, sway_xkb); #endif +#if defined(HAVE_PLUGIN_niri_language) + REGISTER_CORE_MODULE(niri-language, niri_language); +#endif +#if defined(HAVE_PLUGIN_niri_workspaces) + REGISTER_CORE_MODULE(niri-workspaces, niri_workspaces); +#endif #if defined(HAVE_PLUGIN_xkb) REGISTER_CORE_MODULE(xkb, xkb); #endif diff --git a/tag.c b/tag.c index e95b1c7..d6609af 100644 --- a/tag.c +++ b/tag.c @@ -430,12 +430,12 @@ sbuf_append(struct sbuf *s1, const char *s2) // stores the number in "*value" on success static bool -is_number(const char *str, int *value) +is_number(const char *str, long *value) { errno = 0; char *end; - int v = strtol(str, &end, 10); + long v = strtol(str, &end, 10); if (errno != 0 || *end != '\0') return false; @@ -510,12 +510,7 @@ tags_expand_template(const char *template, const struct tag_set *tags) FMT_HEX, FMT_OCT, FMT_PERCENT, - FMT_KBYTE, - FMT_MBYTE, - FMT_GBYTE, - FMT_KIBYTE, - FMT_MIBYTE, - FMT_GIBYTE, + FMT_DIVIDE, } format = FMT_DEFAULT; @@ -527,8 +522,9 @@ tags_expand_template(const char *template, const struct tag_set *tags) } kind = VALUE_VALUE; - int digits = 0; - int decimals = 2; + long digits = 0; + long decimals = 2; + long divider = 1; bool zero_pad = false; char *point = NULL; @@ -541,18 +537,38 @@ tags_expand_template(const char *template, const struct tag_set *tags) format = FMT_OCT; else if (strcmp(tag_args[i], "%") == 0) format = FMT_PERCENT; - else if (strcmp(tag_args[i], "kb") == 0) - format = FMT_KBYTE; - else if (strcmp(tag_args[i], "mb") == 0) - format = FMT_MBYTE; - else if (strcmp(tag_args[i], "gb") == 0) - format = FMT_GBYTE; - else if (strcmp(tag_args[i], "kib") == 0) - format = FMT_KIBYTE; - else if (strcmp(tag_args[i], "mib") == 0) - format = FMT_MIBYTE; - else if (strcmp(tag_args[i], "gib") == 0) - format = FMT_GIBYTE; + else if (*tag_args[i] == '/') { + format = FMT_DIVIDE; + const char *divider_str = tag_args[i] + 1; + if (!is_number(divider_str, ÷r) || divider == 0) { + divider = 1; + LOG_WARN("tag `%s`: invalid divider %s, reset to 1", tag_name, divider_str); + } + } + else if (strcmp(tag_args[i], "kb") == 0) { + format = FMT_DIVIDE; + divider = 1000; + } + else if (strcmp(tag_args[i], "mb") == 0) { + format = FMT_DIVIDE; + divider = 1000 * 1000; + } + else if (strcmp(tag_args[i], "gb") == 0) { + format = FMT_DIVIDE; + divider = 1000 * 1000 * 1000; + } + else if (strcmp(tag_args[i], "kib") == 0) { + format = FMT_DIVIDE; + divider = 1024; + } + else if (strcmp(tag_args[i], "mib") == 0) { + format = FMT_DIVIDE; + divider = 1024 * 1024; + } + else if (strcmp(tag_args[i], "gib") == 0) { + format = FMT_DIVIDE; + divider = 1024 * 1024 * 1024; + } else if (strcmp(tag_args[i], "min") == 0) kind = VALUE_MIN; else if (strcmp(tag_args[i], "max") == 0) @@ -634,20 +650,7 @@ tags_expand_template(const char *template, const struct tag_set *tags) break; } - case FMT_KBYTE: - case FMT_MBYTE: - case FMT_GBYTE: - case FMT_KIBYTE: - case FMT_MIBYTE: - case FMT_GIBYTE: { - const long divider = format == FMT_KBYTE ? 1000 - : format == FMT_MBYTE ? 1000 * 1000 - : format == FMT_GBYTE ? 1000 * 1000 * 1000 - : format == FMT_KIBYTE ? 1024 - : format == FMT_MIBYTE ? 1024 * 1024 - : format == FMT_GIBYTE ? 1024 * 1024 * 1024 - : 1; - + case FMT_DIVIDE: { char str[24]; if (tag->type(tag) == TAG_TYPE_FLOAT) { const char *fmt = zero_pad ? "%0*.*f" : "%*.*f"; @@ -684,19 +687,7 @@ tags_expand_template(const char *template, const struct tag_set *tags) fmt = zero_pad ? "%0*lu" : "%*lu"; break; - case FMT_KBYTE: - case FMT_MBYTE: - case FMT_GBYTE: - case FMT_KIBYTE: - case FMT_MIBYTE: - case FMT_GIBYTE: { - const long divider = format == FMT_KBYTE ? 1024 - : format == FMT_MBYTE ? 1024 * 1024 - : format == FMT_GBYTE ? 1024 * 1024 * 1024 - : format == FMT_KIBYTE ? 1000 - : format == FMT_MIBYTE ? 1000 * 1000 - : format == FMT_GIBYTE ? 1000 * 1000 * 1000 - : 1; + case FMT_DIVIDE: { value /= divider; fmt = zero_pad ? "%0*lu" : "%*lu"; break; diff --git a/test/meson.build b/test/meson.build index e8c7138..a3635a2 100644 --- a/test/meson.build +++ b/test/meson.build @@ -7,4 +7,9 @@ test('no-config', yambar, args: ['-C', '-c', 'xyz'], should_fail: true) test('config-isnt-file', yambar, args: ['-C', '-c', '.'], should_fail: true) test('config-no-bar', yambar, args: ['-C', '-c', join_paths(pwd, 'no-bar.yml')], should_fail: true) -test('full-conf-good', yambar, args: ['-C', '-c', join_paths(pwd, 'full-conf-good.yml')]) +if plugin_alsa_enabled and plugin_backlight_enabled and \ + plugin_battery_enabled and plugin_clock_enabled and \ + plugin_i3_enabled and plugin_mpd_enabled and plugin_network_enabled \ + and plugin_removables_enabled + test('full-conf-good', yambar, args: ['-C', '-c', join_paths(pwd, 'full-conf-good.yml')]) +endif diff --git a/yml.c b/yml.c index ec08101..4769d38 100644 --- a/yml.c +++ b/yml.c @@ -366,6 +366,47 @@ format_error(enum yml_error err, const struct yml_node *parent, const struct yml return err_str; } +static char * +replace_env_variables(const char *str, size_t len) +{ + char *result = strndup(str, len); + char *start, *key; + const char *end, *env_value; + const char* prefix = "${"; + const char* suffix = "}"; + const size_t pref_len = 2; + const size_t suff_len = 1; + size_t key_len; + + while ((start = strstr(result, prefix)) != NULL && + (end = strstr(start, suffix)) != NULL) + { + key_len = end - start - pref_len; + key = strndup(start + pref_len, key_len); + env_value = getenv(key); + + if (env_value) { + size_t result_len = strlen(result); + size_t new_len = result_len - key_len - pref_len - suff_len + strlen(env_value); + char *new_result = malloc(new_len + 1); + + strncpy(new_result, result, start - result); + new_result[start - result] = '\0'; + strcat(new_result, env_value); + strcat(new_result, end + 1); + + free(result); + result = new_result; + } else { + memmove(start, end + 1, strlen(end + 1) + 1); + } + + free(key); + } + + return result; +} + struct yml_node * yml_load(FILE *yml, char **error) { @@ -456,7 +497,7 @@ yml_load(FILE *yml, char **error) case YAML_SCALAR_EVENT: { struct yml_node *new_scalar = calloc(1, sizeof(*new_scalar)); new_scalar->type = SCALAR; - new_scalar->scalar.value = strndup((const char *)event.data.scalar.value, event.data.scalar.length); + new_scalar->scalar.value = replace_env_variables((const char *)event.data.scalar.value, event.data.scalar.length); enum yml_error err = add_node(n, new_scalar, event.start_mark); if (err != YML_ERR_NONE) {