diff --git a/.build.yml b/.builds/alpine-x64.yml similarity index 84% rename from .build.yml rename to .builds/alpine-x64.yml index 22b6766..1703a3d 100644 --- a/.build.yml +++ b/.builds/alpine-x64.yml @@ -1,4 +1,4 @@ -image: alpine/edge +image: alpine/latest packages: - musl-dev - eudev-libs @@ -19,22 +19,28 @@ packages: - json-c-dev - libmpdclient-dev - alsa-lib-dev + - pulseaudio-dev + - pipewire-dev - ttf-dejavu - gcovr + - python3 + - py3-pip + - flex + - bison sources: - https://git.sr.ht/~dnkl/yambar -triggers: - - action: email - condition: failure - to: daniel@ekloef.se +# triggers: +# - action: email +# condition: failure +# to: tasks: - - install-gcovr: | - python2 -m ensurepip --user --upgrade - python2 -m pip install --user --upgrade pip - python2 -m pip install --user --upgrade setuptools + - fcft: | + cd yambar/subprojects + git clone https://codeberg.org/dnkl/fcft.git + cd ../.. - setup: | mkdir -p bld/debug bld/release bld/x11-only bld/wayland-only bld/plugs-are-shared meson --buildtype=debug -Db_coverage=true yambar bld/debug diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..2436e90 --- /dev/null +++ b/.clang-format @@ -0,0 +1,24 @@ +--- +BasedOnStyle: GNU +IndentWidth: 4 +--- +Language: Cpp +Standard: Auto +PointerAlignment: Right +ColumnLimit: 120 +BreakBeforeBraces: Custom +BraceWrapping: + AfterEnum: false + AfterClass: false + SplitEmptyFunction: true + AfterFunction: true + AfterStruct: false + +SpaceBeforeParens: ControlStatements +Cpp11BracedListStyle: true + +WhitespaceSensitiveMacros: + - REGISTER_CORE_PARTICLE + - REGISTER_CORE_DECORATION + - REGISTER_CORE_PLUGIN + - REGISTER_CORE_MODULE diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..ef74858 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,17 @@ +root = true + +[*] +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +charset = utf-8 +indent_style = space +indent_size = 4 +max_line_length = 70 + +[{meson.build,PKGBUILD}] +indent_size = 2 + +[*.scd] +indent_style = tab +trim_trailing_whitespace = false diff --git a/.gitignore b/.gitignore index 1e67045..6630775 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /bld/ /pkg/ /src/ -/subprojects/ +/subprojects/* +!/subprojects/*.wrap diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml deleted file mode 100644 index cb34bae..0000000 --- a/.gitlab-ci.yml +++ /dev/null @@ -1,94 +0,0 @@ -image: alpine:edge - -stages: - - info - - build - -variables: - GIT_SUBMODULE_STRATEGY: normal - -before_script: - - echo "http://dl-cdn.alpinelinux.org/alpine/edge/testing" >> /etc/apk/repositories - - apk update - - apk add musl-dev eudev-libs eudev-dev linux-headers meson ninja gcc scdoc - - apk add pixman-dev freetype-dev fontconfig-dev - - apk add libxcb-dev xcb-util-wm-dev xcb-util-cursor-dev yaml-dev - - apk add wayland-dev wayland-protocols wlroots-dev - - apk add json-c-dev libmpdclient-dev alsa-lib-dev - - apk add ttf-dejavu - - apk add git - - mkdir -p subprojects && cd subprojects - - git clone https://codeberg.org/dnkl/tllist.git - - git clone https://codeberg.org/dnkl/fcft.git - - cd .. - -versions: - stage: info - script: - - meson --version - - ninja --version - - cc --version - -debug: - stage: build - script: - - apk add gcovr - - mkdir -p bld/debug - - cd bld/debug - - meson --buildtype=debug -Db_coverage=true ../.. - - ninja -k0 - - meson test --print-errorlogs - - ninja coverage-html - - mv meson-logs/coveragereport ../../coverage - - ninja coverage-text - - tail -2 meson-logs/coverage.txt - artifacts: - paths: - - coverage - coverage: '/^TOTAL.*\s+(\d+\%)$/' - -# valgrind: -# stage: build -# script: -# - apk add valgrind -# - mkdir -p bld/debug -# - cd bld/debug -# - meson --buildtype=debug ../.. -# - ninja -k0 -# - meson test --verbose --wrapper "valgrind --leak-check=full --show-leak-kinds=all --error-exitcode=3" - -release: - stage: build - script: - - mkdir -p bld/release - - cd bld/release - - meson --buildtype=minsize ../../ - - ninja -k0 - - meson test --print-errorlogs - -x11_only: - stage: build - script: - - mkdir -p bld/debug - - cd bld/debug - - meson --buildtype=debug -Dbackend-x11=enabled -Dbackend-wayland=disabled ../../ - - ninja -k0 - - meson test --print-errorlogs - -wayland_only: - stage: build - script: - - mkdir -p bld/debug - - cd bld/debug - - meson --buildtype=debug -Dbackend-x11=disabled -Dbackend-wayland=enabled ../../ - - ninja -k0 - - meson test --print-errorlogs - -plugins_as_shared_modules: - stage: build - script: - - mkdir -p bld/debug - - cd bld/debug - - meson --buildtype=debug -Dcore-plugins-as-shared-libraries=true ../../ - - ninja -k0 - - meson test --print-errorlogs diff --git a/.woodpecker.yaml b/.woodpecker.yaml new file mode 100644 index 0000000..e7c9151 --- /dev/null +++ b/.woodpecker.yaml @@ -0,0 +1,132 @@ +steps: + - name: codespell + when: + - event: [manual, pull_request] + - event: [push, tag] + branch: [master, releases/*] + image: alpine:latest + commands: + - apk add openssl + - apk add python3 + - apk add py3-pip + - python3 -m venv codespell-venv + - source codespell-venv/bin/activate + - pip install codespell + - codespell README.md CHANGELOG.md *.c *.h doc/*.scd bar decorations modules particles examples + - deactivate + + - name: subprojects + when: + - event: [manual, pull_request] + - event: [push, tag] + branch: [master, releases/*] + image: alpine:latest + commands: + - apk add git + - mkdir -p subprojects && cd subprojects + - git clone https://codeberg.org/dnkl/tllist.git + - git clone https://codeberg.org/dnkl/fcft.git + - cd .. + + - name: x64 + when: + - event: [manual, pull_request] + - event: [push, tag] + branch: [master, releases/*] + depends_on: [subprojects] + image: alpine:latest + commands: + - apk update + - apk add musl-dev eudev-libs eudev-dev linux-headers meson ninja gcc scdoc + - apk add pixman-dev freetype-dev fontconfig-dev + - apk add libxcb-dev xcb-util-wm-dev xcb-util-cursor-dev yaml-dev + - apk add wayland-dev wayland-protocols wlroots-dev + - apk add json-c-dev libmpdclient-dev alsa-lib-dev pulseaudio-dev pipewire-dev + - apk add ttf-dejavu + - apk add git + - apk add flex bison + + # Debug + - apk add gcovr + - mkdir -p bld/debug-x64 + - cd bld/debug-x64 + - meson --buildtype=debug -Db_coverage=true ../.. + - ninja -k0 + - meson test --print-errorlogs + - ninja coverage-html + - mv meson-logs/coveragereport ../../coverage + - ninja coverage-text + - tail -2 meson-logs/coverage.txt + - ./yambar --version + - cd ../.. + + # Release + - mkdir -p bld/release-x64 + - cd bld/release-x64 + - meson --buildtype=minsize ../../ + - ninja -k0 + - meson test --print-errorlogs + - ./yambar --version + - cd ../.. + + # X11 only + - mkdir -p bld/x11-only + - cd bld/x11-only + - meson --buildtype=debug -Dbackend-x11=enabled -Dbackend-wayland=disabled ../../ + - ninja -k0 + - meson test --print-errorlogs + - ./yambar --version + - cd ../.. + + # Wayland only + - mkdir -p bld/wayland-only + - cd bld/wayland-only + - meson --buildtype=debug -Dbackend-x11=disabled -Dbackend-wayland=enabled ../../ + - ninja -k0 + - meson test --print-errorlogs + - ./yambar --version + - cd ../.. + + - name: x86 + when: + - event: [manual, pull_request] + - event: [push, tag] + branch: [master, releases/*] + depends_on: [subprojects] + image: i386/alpine:latest + commands: + - apk add musl-dev eudev-libs eudev-dev linux-headers meson ninja gcc scdoc + - apk add pixman-dev freetype-dev fontconfig-dev + - apk add libxcb-dev xcb-util-wm-dev xcb-util-cursor-dev yaml-dev + - apk add wayland-dev wayland-protocols wlroots-dev + - apk add json-c-dev libmpdclient-dev alsa-lib-dev pulseaudio-dev pipewire-dev + - apk add ttf-dejavu + - apk add git + - apk add flex bison + + # Debug + - mkdir -p bld/debug-x86 + - cd bld/debug-x86 + - meson --buildtype=debug ../../ + - ninja -k0 + - meson test --print-errorlogs + - ./yambar --version + - cd ../.. + + # Release + - mkdir -p bld/release-x86 + - cd bld/release-x86 + - meson --buildtype=minsize ../../ + - ninja -k0 + - meson test --print-errorlogs + - ./yambar --version + - cd ../.. + + # Plugins as shared modules + - mkdir -p bld/shared-modules + - cd bld/shared-modules + - meson --buildtype=debug -Dcore-plugins-as-shared-libraries=true ../../ + - ninja -k0 + - meson test --print-errorlogs + - ./yambar --version + - cd ../.. diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d833aa..0e0e772 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,16 +1,608 @@ # Changelog +* [Unreleased](#unreleased) +* [1.11.0](#1-11-0) +* [1.10.0](#1-10-0) +* [1.9.0](#1-9-0) +* [1.8.0](#1-8-0) +* [1.7.0](#1-7-0) +* [1.6.2](#1-6-2) +* [1.6.1](#1-6-1) +* [1.6.0](#1-6-0) * [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 + +* battery: current smoothing, for improved discharge estimates. +* battery: scale option, for batteries that report 'charge' at a + different scale than 'current'. +* network: new `quality` tag (Wi-Fi only). +* Read alternative config from pipes and FIFOs (e.g. `--config + /dev/stdin`) ([#340][340]). +* Added `overlay` and `background` as possible `layer` values + ([#372][372]). + +[340]: https://codeberg.org/dnkl/yambar/pulls/340 +[372]: https://codeberg.org/dnkl/yambar/issues/372 + + +### Changed + +* log-level: default to `warning` +* network: use dynlist instead of fixed name ([#355][355]) + +[355]: https://codeberg.org/dnkl/yambar/pulls/355 + + +### Fixed + +* Compiler error _‘fmt’ may be used uninitialized_ ([#311][311]). +* map: conditions failing to match when they contain multiple, quoted + tag values ([#302][302]). +* Crash when hidden by an opaque window. +* Bar not resizing itself when the screen resolution is changed + ([#330][330]). +* i3/sway: incorrect empty/title state of workspaces ([#343][343]). +* mem: state updated on each bar redraw ([#352][352]). +* script: buffer overflow when reading large amounts of data. +* i3/sway: module fails when reloading config file ([#361][361]). +* Worked around bug in gcc causing a compilation error ([#350][350]). +* Miscalculation of list width in presence of empty particles ([#369][369]). +* Log-level not respected by syslog. + +[311]: https://codeberg.org/dnkl/yambar/issues/311 +[302]: https://codeberg.org/dnkl/yambar/issues/302 +[330]: https://codeberg.org/dnkl/yambar/issues/330 +[343]: https://codeberg.org/dnkl/yambar/issues/343 +[352]: https://codeberg.org/dnkl/yambar/issues/352 +[361]: https://codeberg.org/dnkl/yambar/issues/361 +[350]: https://codeberg.org/dnkl/yambar/issues/350 +[369]: https://codeberg.org/dnkl/yambar/issues/369 + + +### Contributors + +* Delgan +* Haden Collins +* Jordan Isaacs +* kotyk +* Leonardo Hernández Hernández +* oob +* rdbo +* Sertonix +* steovd +* Väinö Mäkelä +* Yiyu Zhou + + +## 1.10.0 + +### Added + +* Field width tag format option ([#246][246]) +* river: support for ‘layout’ events. +* dwl: support for specifying name of tags ([#256][256]) +* i3/sway: extend option `sort`; use `native` to sort numbered workspaces only. +* modules/dwl: handle the appid status ([#284][284]) +* battery: also show estimation for time to full ([#303][303]). +* on-click: tilde expansion ([#307][307]) +* script: tilde expansion of `path` ([#307][307]). + +[246]: https://codeberg.org/dnkl/yambar/issues/246 +[256]: https://codeberg.org/dnkl/yambar/pulls/256 +[284]: https://codeberg.org/dnkl/yambar/pulls/284 +[307]: https://codeberg.org/dnkl/yambar/issues/307 + + +### Changed + +* disk-io: `interval` renamed to `poll-interval` +* mem: `interval` renamed to `poll-interval` +* battery/network/script: `poll-interval` unit changed from seconds to + milliseconds ([#244][244]). +* all modules: minimum poll interval changed from 500ms to 250ms. +* network: do not use IPv6 link-local ([#281][281]) + +[244]: https://codeberg.org/dnkl/yambar/issues/244 +[281]: https://codeberg.org/dnkl/yambar/pulls/281 + + +### Fixed + +* Build failures for certain combinations of enabled and disabled + plugins ([#239][239]). +* Documentation for the `cpu` module; `interval` has been renamed to + `poll-interval` ([#241][241]). +* battery: module was not thread safe. +* dwl module reporting only the last part of the title ([#251][251]) +* i3/sway: regression; persistent workspaces shown twice + ([#253][253]). +* pipewire: use `roundf()` instead of `ceilf()` for more accuracy + ([#262][262]) +* Crash when a yaml anchor has a value that already exists in the + target yaml node ([#286][286]). +* battery: Fix time conversion in battery estimation ([#303][303]). +* battery: poll timeout being reset when receiving irrelevant udev + notification (leading to battery status never updating, in worst + case) ([#305][305]). + +[239]: https://codeberg.org/dnkl/yambar/issues/239 +[241]: https://codeberg.org/dnkl/yambar/issues/241 +[251]: https://codeberg.org/dnkl/yambar/pulls/251 +[253]: https://codeberg.org/dnkl/yambar/issues/253 +[262]: https://codeberg.org/dnkl/yambar/issues/262 +[286]: https://codeberg.org/dnkl/yambar/issues/286 +[305]: https://codeberg.org/dnkl/yambar/issues/305 + + +### Contributors + +* Leonardo Gibrowski Faé (Horus) +* Armin Fisslthaler +* Ben Brown +* David Bimmler +* Leonardo Hernández Hernández +* Ogromny +* Oleg Hahm +* Stanislav Ochotnický +* tiosgz +* Yutaro Ohno + + +## 1.9.0 + +### Added + +* Support for specifying number of decimals when printing a float tag + ([#200][200]). +* Support for custom font fallbacks ([#153][153]). +* overline: new decoration ([#153][153]). +* i3/sway: boolean option `strip-workspace-numbers`. +* font-shaping: new inheritable configuration option, allowing you to + configure whether strings should be _shaped_ using HarfBuzz, or not + ([#159][159]). +* river: support for the new “mode” event present in version 3 of the + river status manager protocol, in the form of a new tag, _”mode”_, + in the `title` particle. +* network: request link stats and expose under tags `dl-speed` and + `ul-speed` when `poll-interval` is set. +* new module: disk-io. +* new module: pulse ([#223][223]). +* alsa: `dB` tag ([#202][202]). +* mpd: `file` tag ([#219][219]). +* pipewire: add a new module for pipewire ([#224][224]) +* on-click: support `next`/`previous` mouse buttons ([#228][228]). +* dwl: add a new module for DWL ([#218][218]) +* sway: support for workspace ‘rename’ and ‘move’ events + ([#216][216]). + +[153]: https://codeberg.org/dnkl/yambar/issues/153 +[159]: https://codeberg.org/dnkl/yambar/issues/159 +[200]: https://codeberg.org/dnkl/yambar/issues/200 +[202]: https://codeberg.org/dnkl/yambar/issues/202 +[218]: https://codeberg.org/dnkl/yambar/pulls/218 +[219]: https://codeberg.org/dnkl/yambar/pulls/219 +[223]: https://codeberg.org/dnkl/yambar/pulls/223 +[224]: https://codeberg.org/dnkl/yambar/pulls/224 +[228]: https://codeberg.org/dnkl/yambar/pulls/228 +[216]: https://codeberg.org/dnkl/yambar/issues/216 + + +### Changed + +* All modules are now compile-time optional. +* Minimum required meson version is now 0.59. +* Float tags are now treated as floats instead of integers when + formatted with the `kb`/`kib`/`mb`/`mib`/`gb`/`gib` string particle + formatters. +* network: `tx-bitrate` and `rx-bitrate` are now in bits/s instead of + Mb/s. Use the `mb` string formatter to render these tags as before + (e.g. `string: {text: "{tx-bitrate:mb}"}`). +* i3: newly created, and **unfocused** workspaces are now considered + non-empty ([#191][191]) +* alsa: use dB instead of raw volume values, if possible, when + calculating the `percent` tag ([#202][202]) +* cpu: `content` particle is now a template instantiated once for each + core, and once for the total CPU usage. See + **yambar-modules-cpu**(5) for more information ([#207][207]). +* **BREAKING CHANGE**: overhaul of the `map` particle. Instead of + specifying a `tag` and then an array of `values`, you must now + simply use an array of `conditions`, that consist of: + + ` ` + + where `` is one of: + + `== != < <= > >=` + + Note that boolean tags must be used as is: + + `online` + + `~online # use '~' to match for their falsehood` + + As an example, if you previously had something like: + + ``` + map: + tag: State + values: + unrecognized: + ... + ``` + + You would now write it as: + + ``` + map: + conditions: + State == unrecognized: + ... + ``` + + Note that if `` contains any non-alphanumerical characters, + it **must** be surrounded by `""`: + + `State == "very confused!!!"` + + Finally, you can mix and match conditions using the boolean + operators `&&` and `||`: + + ``` + && + && ( || ) # parenthesis work + ~( && ) # '~' can be applied to any condition + ``` + + For a more thorough explanation, see the updated map section in the + man page for yambar-particles([#137][137], [#175][175] and [#][182]). + +[137]: https://codeberg.org/dnkl/yambar/issues/137 +[175]: https://codeberg.org/dnkl/yambar/issues/172 +[182]: https://codeberg.org/dnkl/yambar/issues/182 +[191]: https://codeberg.org/dnkl/yambar/issues/191 +[202]: https://codeberg.org/dnkl/yambar/issues/202 +[207]: https://codeberg.org/dnkl/yambar/issues/207 + + +### Fixed + +* i3: fixed “missing workspace indicator” (_err: modules/i3.c:94: + workspace reply/event without 'name' and/or 'output', and/or 'focus' + properties_). +* Slow/laggy behavior when quickly spawning many `on-click` handlers, + e.g. when handling mouse wheel events ([#169][169]). +* cpu: don’t error out on systems where SMT has been disabled + ([#172][172]). +* examples/dwl-tags: updated parsing of `output` name ([#178][178]). +* sway-xkb: don’t crash when Sway sends an _”added”_ event for a + device yambar is already tracking ([#177][177]). +* Crash when a particle is “too wide”, and tries to render outside the + bar ([#198][198]). +* string: crash when failing to convert string to UTF-32. +* script: only first transaction processed when receiving multiple + transactions in a single batch ([#221][221]). +* network: missing SSID (recent kernels, or possibly wireless drivers, + no longer provide the SSID in the `NL80211_CMD_NEW_STATION` + response) ([#226][226]). +* sway-xkb: crash when compositor presents multiple inputs with + identical IDs ([#229][229]). + +[169]: https://codeberg.org/dnkl/yambar/issues/169 +[172]: https://codeberg.org/dnkl/yambar/issues/172 +[178]: https://codeberg.org/dnkl/yambar/issues/178 +[177]: https://codeberg.org/dnkl/yambar/issues/177 +[198]: https://codeberg.org/dnkl/yambar/issues/198 +[221]: https://codeberg.org/dnkl/yambar/issues/221 +[226]: https://codeberg.org/dnkl/yambar/issues/226 +[229]: https://codeberg.org/dnkl/yambar/issues/229 + + +### Contributors + +* Baptiste Daroussin +* Horus +* Johannes +* Leonardo Gibrowski Faé +* Leonardo Neumann +* Midgard +* Ogromny +* Peter Rice +* Timur Celik +* Willem van de Krol +* hiog + + +## 1.8.0 + +### Added + +* ramp: can now have custom min and max values + ([#103](https://codeberg.org/dnkl/yambar/issues/103)). +* border: new decoration. +* i3/sway: new boolean tag: `empty` + ([#139](https://codeberg.org/dnkl/yambar/issues/139)). +* mem: a module handling system memory monitoring +* cpu: a module offering cpu usage monitoring +* removables: support for audio CDs + ([#146](https://codeberg.org/dnkl/yambar/issues/146)). +* removables: new boolean tag: `audio`. + + +### Changed + +* fcft >= 3.0 is now required. +* Made `libmpdclient` an optional dependency +* battery: unknown battery states are now mapped to ‘unknown’, instead + of ‘discharging’. +* Wayland: the bar no longer exits when the monitor is + disabled/unplugged ([#106](https://codeberg.org/dnkl/yambar/issues/106)). + + +### Fixed + +* `left-margin` and `right-margin` from being rejected as invalid + options. +* Crash when `udev_monitor_receive_device()` returned `NULL`. This + affected the “backlight”, “battery” and “removables” modules + ([#109](https://codeberg.org/dnkl/yambar/issues/109)). +* foreign-toplevel: update bar when a top-level is closed. +* Bar not being mapped on an output before at least one module has + “refreshed” it ([#116](https://codeberg.org/dnkl/yambar/issues/116)). +* network: failure to retrieve wireless attributes (SSID, RX/TX + bitrate, signal strength etc). +* Integer options that were supposed to be >= 0 were incorrectly + allowed, leading to various bad things; including yambar crashing, + or worse, the compositor crashing + ([#129](https://codeberg.org/dnkl/yambar/issues/129)). +* kib/kb, mib/mb and gib/gb formatters were inverted. + + +### Contributors + +* [sochotnicky](https://codeberg.org/sochotnicky) +* Alexandre Acebedo +* anb +* Baptiste Daroussin +* Catterwocky +* horus645 +* Jan Beich +* mz +* natemaia +* nogerine +* Soc Virnyl S. Estela +* Vincent Fischer + + +## 1.7.0 + +### Added + +* i3: `persistent` attribute, allowing persistent workspaces + ([#72](https://codeberg.org/dnkl/yambar/issues/72)). +* bar: `border.{left,right,top,bottom}-width`, allowing the width of + each side of the border to be configured + individually. `border.width` is now a short-hand for setting all + four borders to the same value + ([#77](https://codeberg.org/dnkl/yambar/issues/77)). +* bar: `layer: top|bottom`, allowing the layer which the bar is + rendered on to be changed. Wayland only - ignored on X11. +* river: `all-monitors: false|true`. +* `-d,--log-level=info|warning|error|none` command line option + ([#84](https://codeberg.org/dnkl/yambar/issues/84)). +* river: support for the river-status protocol, version 2 (‘urgent’ + views). +* `online` tag to the `alsa` module. +* alsa: `volume` and `muted` options, allowing you to configure which + channels to use as source for the volume level and muted state. +* foreign-toplevel: Wayland module that provides information about + currently opened windows. +* alsa: support for capture devices. +* network: `ssid`, `signal`, `rx-bitrate` and `rx-bitrate` tags. +* network: `poll-interval` option (for the new `signal` and + `*-bitrate` tags). +* tags: percentage tag formatter, for range tags: `{tag_name:%}`. +* tags: kb/mb/gb, and kib/mib/gib tag formatters. +* clock: add a config option to show UTC time. + +### Changed + +* bar: do not add `spacing` around empty (zero-width) modules. +* alsa: do not error out if we fail to connect to the ALSA device, or + if we get disconnected. Instead, keep retrying until we succeed + ([#86](https://codeberg.org/dnkl/yambar/issues/86)). + + +### Fixed + +* `yambar --backend=wayland` always erroring out with _”yambar was + compiled without the Wayland backend”_. +* Regression: `{where}` tag not being expanded in progress-bar + `on-click` handlers. +* `alsa` module causing yambar to use 100% CPU if the ALSA device is + disconnected ([#61](https://codeberg.org/dnkl/yambar/issues/61)). + + +### Contributors + +* [paemuri](https://codeberg.org/paemuri) +* [ericonr](https://codeberg.org/ericonr) +* [Nulo](https://nulo.in) + + +## 1.6.2 + +### Added + +* Text shaping support. +* Support for middle and right mouse buttons, mouse wheel and trackpad + scrolling ([#39](https://codeberg.org/dnkl/yambar/issues/39)). +* script: polling mode. See the new `poll-interval` option + ([#67](https://codeberg.org/dnkl/yambar/issues/67)). + + +### Changed + +* doc: split up **yambar-modules**(5) into multiple man pages, one for + each module ([#15](https://codeberg.org/dnkl/yambar/issues/15)). +* fcft >= 2.4.0 is now required. +* sway-xkb: non-keyboard inputs are now ignored + ([#51](https://codeberg.org/dnkl/yambar/issues/51)). +* battery: don’t terminate (causing last status to “freeze”) when + failing to update; retry again later + ([#44](https://codeberg.org/dnkl/yambar/issues/44)). +* battery: differentiate "Not Charging" and "Discharging" in state + tag of battery module. + ([#57](https://codeberg.org/dnkl/yambar/issues/57)). +* string: use HORIZONTAL ELLIPSIS instead of three regular periods + when truncating a string + ([#73](https://codeberg.org/dnkl/yambar/issues/73)). + + +### Fixed + +* Crash when merging non-dictionary anchors in the YAML configuration + ([#32](https://codeberg.org/dnkl/yambar/issues/32)). +* Crash in the `ramp` particle when the tag’s value was out-of-bounds + ([#45](https://codeberg.org/dnkl/yambar/issues/45)). +* Crash when a string particle contained `{}` + ([#48](https://codeberg.org/dnkl/yambar/issues/48)). +* `script` module rejecting range tag end values containing the digit + `9` ([#60](https://codeberg.org/dnkl/yambar/issues/60)). + + +### Contributors + +* [novakane](https://codeberg.org/novakane) +* [mz](https://codeberg.org/mz) + + +## 1.6.1 + +### Changed + +* i3: workspaces with numerical names are sorted separately from + non-numerically named workspaces + ([#30](https://codeberg.org/dnkl/yambar/issues/30)). + + +### Fixed + +* mpd: `elapsed` tag not working (regression, introduced in 1.6.0). +* Wrong background color for (semi-) transparent backgrounds. +* battery: stats sometimes getting stuck at 0, or impossibly large + values ([#25](https://codeberg.org/dnkl/yambar/issues/25)). + + +## 1.6.0 + +### Added + +* alsa: `percent` tag. This is an integer tag that represents the + current volume as a percentage value + ([#10](https://codeberg.org/dnkl/yambar/issues/10)). +* river: added documentation + ([#9](https://codeberg.org/dnkl/yambar/issues/9)). +* script: new module, adds support for custom user scripts + ([#11](https://codeberg.org/dnkl/yambar/issues/11)). +* mpd: `volume` tag. This is a range tag that represents MPD's current + volume in percentage (0-100) +* i3: `sort` configuration option, that controls how the workspace + list is sorted. Can be set to one of `none`, `ascending` or + `descending`. Default is `none` + ([#17](https://codeberg.org/dnkl/yambar/issues/17)). +* i3: `mode` tag: the name of the currently active mode + + +### Fixed + +* YAML parsing error messages being replaced with a generic _“unknown + error”_. +* Memory leak when a YAML parsing error was encountered. +* clock: update every second when necessary + ([#12](https://codeberg.org/dnkl/yambar/issues/12)). +* mpd: fix compilation with clang + ([#16](https://codeberg.org/dnkl/yambar/issues/16)). +* Crash when the alpha component in a color value was 0. +* XCB: Fallback to non-primary monitor when the primary monitor is + disconnected ([#20](https://codeberg.org/dnkl/yambar/issues/20)) + + +### Contributors + +* [JorwLNKwpH](https://codeberg.org/JorwLNKwpH) +* [optimus-prime](https://codeberg.org/optimus-prime) + + ## 1.5.0 ### Added -* battery: support for drivers that use 'charge\_\*' (instead of - 'energy\_\*') sys files. +* battery: support for drivers that use `charge_*` (instead of + `energy_*`) sys files. * removables: SD card support. -* removables: new 'ignore' property. +* removables: new `ignore` property. * Wayland: multi-seat support. * **Experimental**: 'river': new module for the river Wayland compositor. diff --git a/PKGBUILD b/PKGBUILD index e8229fa..6fc2d69 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -1,7 +1,8 @@ pkgname=yambar -pkgver=1.5.0 +pkgver=1.11.0 pkgrel=1 pkgdesc="Simplistic and highly configurable status panel for X and Wayland" +changelog=CHANGELOG.md arch=('x86_64' 'aarch64') url=https://codeberg.org/dnkl/yambar license=(mit) @@ -15,7 +16,9 @@ depends=( 'libudev.so' 'json-c' 'libmpdclient' - 'fcft>=2.0.0') + 'libpulse' + 'pipewire' + 'fcft>=3.0.0' 'fcft<4.0.0') optdepends=('xcb-util-errors: better X error messages') source=() diff --git a/PKGBUILD.wayland-only b/PKGBUILD.wayland-only index 65c5864..266fc06 100644 --- a/PKGBUILD.wayland-only +++ b/PKGBUILD.wayland-only @@ -1,5 +1,5 @@ pkgname=yambar-wayland -pkgver=1.5.0 +pkgver=1.11.0 pkgrel=1 pkgdesc="Simplistic and highly configurable status panel for Wayland" arch=('x86_64' 'aarch64') @@ -16,8 +16,11 @@ depends=( 'libudev.so' 'json-c' 'libmpdclient' - 'fcft>=2.0.0') + 'libpulse' + 'pipewire' + 'fcft>=3.0.0' 'fcft<4.0.0') source=() +changelog=CHANGELOG.md pkgver() { cd ../.git &> /dev/null && git describe --tags --long | sed 's/^v//;s/\([^-]*-g\)/r\1/;s/-/./g' || diff --git a/README.md b/README.md index beb2b17..4e825b3 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,31 @@ +[![CI status](https://ci.codeberg.org/api/badges/dnkl/yambar/status.svg)](https://ci.codeberg.org/dnkl/yambar) + # Yambar +**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 1. [Introduction](#introduction) @@ -54,14 +80,16 @@ bar: right: - clock: content: - - string: {text: , font: "Font Awesome 5 Free:style=solid:size=12"} + - string: {text: , font: "Font Awesome 6 Free:style=solid:size=12"} - string: {text: "{date}", right-margin: 5} - - string: {text: , font: "Font Awesome 5 Free:style=solid:size=12"} + - string: {text: , font: "Font Awesome 6 Free:style=solid:size=12"} - string: {text: "{time}"} ``` For details, see the man pages (**yambar**(5) is a good start). +Example configurations can be found in [examples](examples/configurations). + ## Modules @@ -71,29 +99,27 @@ Available modules: * backlight * battery * clock +* cpu +* disk-io +* dwl +* foreign-toplevel * i3 (and Sway) * label +* mem * mpd * network +* pipewire +* pulse * removables +* river +* script (see script [examples](examples/scripts)) +* sway-xkb * xkb (_XCB backend only_) * xwindow (_XCB backend only_) ## Installation -If you have not installed [tllist](https://codeberg.org/dnkl/tllist) -and [fcft](https://codeberg.org/dnkl/fcft) as system libraries, clone -them into the `subprojects` directory: - -```sh -mkdir -p subprojects -pushd subprojects -git clone https://codeberg.org/dnkl/tllist.git -git clone https://codeberg.org/dnkl/fcft.git -popd -``` - To build, first, create a build directory, and switch to it: ```sh mkdir -p bld/release && cd bld/release @@ -102,9 +128,14 @@ mkdir -p bld/release && cd bld/release Second, configure the build (if you intend to install it globally, you might also want `--prefix=/usr`): ```sh -meson --buildtype=release ../.. +meson setup --buildtype=release ../.. ``` +Optionally, explicitly disable a backend (or enable, if you want a +configuration error if not all dependencies are met) by adding either +`-Dbackend-x11=disabled|enabled` or +`-Dbackend-wayland=disabled|enabled` to the meson command line. + Three, build it: ```sh ninja diff --git a/bar/backend.h b/bar/backend.h index f2681a8..b7a9fcb 100644 --- a/bar/backend.h +++ b/bar/backend.h @@ -7,11 +7,10 @@ struct backend { bool (*setup)(struct bar *bar); void (*cleanup)(struct bar *bar); - void (*loop)(struct bar *bar, - void (*expose)(const struct bar *bar), - void (*on_mouse)(struct bar *bar, enum mouse_event event, - int x, int y)); + void (*loop)(struct bar *bar, void (*expose)(const struct bar *bar), + void (*on_mouse)(struct bar *bar, enum mouse_event event, enum mouse_button btn, int x, int y)); void (*commit)(const struct bar *bar); void (*refresh)(const struct bar *bar); void (*set_cursor)(struct bar *bar, const char *cursor); + const char *(*output_name)(const struct bar *bar); }; diff --git a/bar/bar.c b/bar/bar.c index d3abd14..109f210 100644 --- a/bar/bar.c +++ b/bar/bar.c @@ -1,12 +1,14 @@ #include "bar.h" #include "private.h" +#include +#include +#include +#include +#include #include #include -#include -#include #include -#include #include #include @@ -16,15 +18,17 @@ #include "../log.h" #if defined(ENABLE_X11) - #include "xcb.h" +#include "xcb.h" #endif #if defined(ENABLE_WAYLAND) - #include "wayland.h" +#include "wayland.h" #endif +#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 @@ -36,26 +40,33 @@ calculate_widths(const struct private *b, int *left, int *center, int *right) for (size_t i = 0; i < b->left.count; i++) { struct exposable *e = b->left.exps[i]; - assert(e != NULL); - *left += b->left_spacing + e->width + b->right_spacing; + if (e->width > 0) + *left += b->left_spacing + e->width + b->right_spacing; } for (size_t i = 0; i < b->center.count; i++) { struct exposable *e = b->center.exps[i]; - assert(e != NULL); - *center += b->left_spacing + e->width + b->right_spacing; + if (e->width > 0) + *center += b->left_spacing + e->width + b->right_spacing; } for (size_t i = 0; i < b->right.count; i++) { struct exposable *e = b->right.exps[i]; - assert(e != NULL); - *right += b->left_spacing + e->width + b->right_spacing; + if (e->width > 0) + *right += b->left_spacing + e->width + b->right_spacing; } /* No spacing on the edges (that's what the margins are for) */ - *left -= b->left_spacing + b->right_spacing; - *center -= b->left_spacing + b->right_spacing; - *right -= b->left_spacing + b->right_spacing; + if (*left > 0) + *left -= b->left_spacing + b->right_spacing; + if (*center > 0) + *center -= b->left_spacing + b->right_spacing; + if (*right > 0) + *right -= b->left_spacing + b->right_spacing; + + assert(*left >= 0); + assert(*center >= 0); + assert(*right >= 0); } static void @@ -64,85 +75,94 @@ expose(const struct bar *_bar) const struct private *bar = _bar->private; pixman_image_t *pix = bar->pix; - pixman_image_fill_rectangles( - PIXMAN_OP_SRC, pix, &bar->background, 1, - &(pixman_rectangle16_t){0, 0, bar->width, bar->height_with_border}); + pixman_image_fill_rectangles(PIXMAN_OP_SRC, pix, &bar->background, 1, + &(pixman_rectangle16_t){0, 0, bar->width, bar->height_with_border}); - if (bar->border.width > 0) { - pixman_image_fill_rectangles( - PIXMAN_OP_OVER, pix, &bar->border.color, 4, - (pixman_rectangle16_t[]){ - {0, 0, bar->width, bar->border.width}, - {0, 0, bar->border.width, bar->height_with_border}, - {bar->width - bar->border.width, 0, bar->border.width, bar->height_with_border}, - {0, bar->height_with_border - bar->border.width, bar->width, bar->border.width}, - }); - } + pixman_image_fill_rectangles( + PIXMAN_OP_OVER, pix, &bar->border.color, 4, + (pixman_rectangle16_t[]){ + /* Left */ + {0, 0, bar->border.left_width, bar->height_with_border}, + + /* Right */ + {bar->width - bar->border.right_width, 0, bar->border.right_width, bar->height_with_border}, + + /* Top */ + {bar->border.left_width, 0, bar->width - bar->border.left_width - bar->border.right_width, + bar->border.top_width}, + + /* Bottom */ + {bar->border.left_width, bar->height_with_border - bar->border.bottom_width, + bar->width - bar->border.left_width - bar->border.right_width, bar->border.bottom_width}, + }); for (size_t i = 0; i < bar->left.count; i++) { struct module *m = bar->left.mods[i]; struct exposable *e = bar->left.exps[i]; - if (e != NULL) e->destroy(e); - bar->left.exps[i] = module_begin_expose(m); + assert(bar->left.exps[i]->width >= 0); } for (size_t i = 0; i < bar->center.count; i++) { struct module *m = bar->center.mods[i]; struct exposable *e = bar->center.exps[i]; - if (e != NULL) e->destroy(e); - bar->center.exps[i] = module_begin_expose(m); + assert(bar->center.exps[i]->width >= 0); } for (size_t i = 0; i < bar->right.count; i++) { struct module *m = bar->right.mods[i]; struct exposable *e = bar->right.exps[i]; - if (e != NULL) e->destroy(e); - bar->right.exps[i] = module_begin_expose(m); + assert(bar->right.exps[i]->width >= 0); } int left_width, center_width, right_width; calculate_widths(bar, &left_width, ¢er_width, &right_width); - int y = bar->border.width; - int x = bar->border.width + bar->left_margin - bar->left_spacing; + int y = bar->border.top_width; + int x = bar->border.left_width + bar->left_margin - bar->left_spacing; + pixman_region32_t clip; + pixman_region32_init_rect( + &clip, bar->border.left_width + bar->left_margin, bar->border.top_width, + (bar->width - bar->left_margin - bar->right_margin - bar->border.left_width - bar->border.right_width), + bar->height); + pixman_image_set_clip_region32(pix, &clip); + pixman_region32_fini(&clip); + for (size_t i = 0; i < bar->left.count; i++) { const struct exposable *e = bar->left.exps[i]; e->expose(e, pix, x + bar->left_spacing, y, bar->height); - x += bar->left_spacing + e->width + bar->right_spacing; + if (e->width > 0) + x += bar->left_spacing + e->width + bar->right_spacing; } x = bar->width / 2 - center_width / 2 - bar->left_spacing; for (size_t i = 0; i < bar->center.count; i++) { const struct exposable *e = bar->center.exps[i]; e->expose(e, pix, x + bar->left_spacing, y, bar->height); - x += bar->left_spacing + e->width + bar->right_spacing; + if (e->width > 0) + x += bar->left_spacing + e->width + bar->right_spacing; } - x = bar->width - ( - right_width + - bar->left_spacing + - bar->right_margin + - bar->border.width); + x = bar->width - (right_width + bar->left_spacing + bar->right_margin + bar->border.right_width); for (size_t i = 0; i < bar->right.count; i++) { const struct exposable *e = bar->right.exps[i]; e->expose(e, pix, x + bar->left_spacing, y, bar->height); - x += bar->left_spacing + e->width + bar->right_spacing; + if (e->width > 0) + x += bar->left_spacing + e->width + bar->right_spacing; } bar->backend.iface->commit(_bar); } - static void refresh(const struct bar *bar) { @@ -157,15 +177,20 @@ set_cursor(struct bar *bar, const char *cursor) b->backend.iface->set_cursor(bar, cursor); } +static const char * +output_name(const struct bar *bar) +{ + const struct private *b = bar->private; + return b->backend.iface->output_name(bar); +} + static void -on_mouse(struct bar *_bar, enum mouse_event event, int x, int y) +on_mouse(struct bar *_bar, enum mouse_event event, enum mouse_button btn, int x, int y) { struct private *bar = _bar->private; - if ((y < bar->border.width || - y >= (bar->height_with_border - bar->border.width)) || - (x < bar->border.width || x >= (bar->width - bar->border.width))) - { + if ((y < bar->border.top_width || y >= (bar->height_with_border - bar->border.bottom_width)) + || (x < bar->border.left_width || x >= (bar->width - bar->border.right_width))) { set_cursor(_bar, "left_ptr"); return; } @@ -173,14 +198,17 @@ on_mouse(struct bar *_bar, enum mouse_event event, int x, int y) int left_width, center_width, right_width; calculate_widths(bar, &left_width, ¢er_width, &right_width); - int mx = bar->border.width + bar->left_margin - bar->left_spacing; + int mx = bar->border.left_width + bar->left_margin - bar->left_spacing; for (size_t i = 0; i < bar->left.count; i++) { struct exposable *e = bar->left.exps[i]; + if (e->width == 0) + continue; + mx += bar->left_spacing; if (x >= mx && x < mx + e->width) { if (e->on_mouse != NULL) - e->on_mouse(e, _bar, event, x - mx, y); + e->on_mouse(e, _bar, event, btn, x - mx, y); return; } @@ -191,28 +219,31 @@ on_mouse(struct bar *_bar, enum mouse_event event, int x, int y) for (size_t i = 0; i < bar->center.count; i++) { struct exposable *e = bar->center.exps[i]; + if (e->width == 0) + continue; + mx += bar->left_spacing; if (x >= mx && x < mx + e->width) { if (e->on_mouse != NULL) - e->on_mouse(e, _bar, event, x - mx, y); + e->on_mouse(e, _bar, event, btn, x - mx, y); return; } mx += e->width + bar->right_spacing; } - mx = bar->width - (right_width - + bar->left_spacing + - bar->right_margin + - bar->border.width); + mx = bar->width - (right_width + bar->left_spacing + bar->right_margin + bar->border.right_width); for (size_t i = 0; i < bar->right.count; i++) { struct exposable *e = bar->right.exps[i]; + if (e->width == 0) + continue; + mx += bar->left_spacing; if (x >= mx && x < mx + e->width) { if (e->on_mouse != NULL) - e->on_mouse(e, _bar, event, x - mx, y); + e->on_mouse(e, _bar, event, btn, x - mx, y); return; } @@ -222,13 +253,27 @@ on_mouse(struct bar *_bar, enum mouse_event event, int x, int y) set_cursor(_bar, "left_ptr"); } +static void +set_module_thread_name(thrd_t id, struct module *mod) +{ + char title[16]; + if (mod->description != NULL) + strncpy(title, mod->description(mod), sizeof(title)); + else + strncpy(title, "mod:", sizeof(title)); + + title[15] = '\0'; + + if (pthread_setname_np(id, title) < 0) + LOG_ERRNO("failed to set thread title"); +} static int run(struct bar *_bar) { struct private *bar = _bar->private; - bar->height_with_border = bar->height + 2 * bar->border.width; + bar->height_with_border = bar->height + bar->border.top_width + bar->border.bottom_width; if (!bar->backend.iface->setup(_bar)) { bar->backend.iface->cleanup(_bar); @@ -238,29 +283,33 @@ run(struct bar *_bar) } set_cursor(_bar, "left_ptr"); + expose(_bar); /* Start modules */ - thrd_t thrd_left[bar->left.count]; - thrd_t thrd_center[bar->center.count]; - thrd_t thrd_right[bar->right.count]; + thrd_t thrd_left[max(bar->left.count, 1)]; + thrd_t thrd_center[max(bar->center.count, 1)]; + thrd_t thrd_right[max(bar->right.count, 1)]; for (size_t i = 0; i < bar->left.count; i++) { struct module *mod = bar->left.mods[i]; mod->abort_fd = _bar->abort_fd; thrd_create(&thrd_left[i], (int (*)(void *))bar->left.mods[i]->run, mod); + set_module_thread_name(thrd_left[i], mod); } for (size_t i = 0; i < bar->center.count; i++) { struct module *mod = bar->center.mods[i]; mod->abort_fd = _bar->abort_fd; thrd_create(&thrd_center[i], (int (*)(void *))bar->center.mods[i]->run, mod); + set_module_thread_name(thrd_center[i], mod); } for (size_t i = 0; i < bar->right.count; i++) { struct module *mod = bar->right.mods[i]; mod->abort_fd = _bar->abort_fd; thrd_create(&thrd_right[i], (int (*)(void *))bar->right.mods[i]->run, mod); + set_module_thread_name(thrd_right[i], mod); } LOG_DBG("all modules started"); @@ -274,20 +323,26 @@ run(struct bar *_bar) int mod_ret; for (size_t i = 0; i < bar->left.count; i++) { thrd_join(thrd_left[i], &mod_ret); - if (mod_ret != 0) - LOG_ERR("module: LEFT #%zu: non-zero exit value: %d", i, mod_ret); + if (mod_ret != 0) { + const struct module *m = bar->left.mods[i]; + LOG_ERR("module: LEFT #%zu (%s): non-zero exit value: %d", i, m->description(m), mod_ret); + } ret = ret == 0 && mod_ret != 0 ? mod_ret : ret; } for (size_t i = 0; i < bar->center.count; i++) { thrd_join(thrd_center[i], &mod_ret); - if (mod_ret != 0) - LOG_ERR("module: CENTER #%zu: non-zero exit value: %d", i, mod_ret); + if (mod_ret != 0) { + const struct module *m = bar->center.mods[i]; + LOG_ERR("module: CENTER #%zu (%s): non-zero exit value: %d", i, m->description(m), mod_ret); + } ret = ret == 0 && mod_ret != 0 ? mod_ret : ret; } for (size_t i = 0; i < bar->right.count; i++) { thrd_join(thrd_right[i], &mod_ret); - if (mod_ret != 0) - LOG_ERR("module: RIGHT #%zu: non-zero exit value: %d", i, mod_ret); + if (mod_ret != 0) { + const struct module *m = bar->right.mods[i]; + LOG_ERR("module: RIGHT #%zu (%s): non-zero exit value: %d", i, m->description(m), mod_ret); + } ret = ret == 0 && mod_ret != 0 ? mod_ret : ret; } @@ -307,7 +362,6 @@ destroy(struct bar *bar) for (size_t i = 0; i < b->left.count; i++) { struct module *m = b->left.mods[i]; struct exposable *e = b->left.exps[i]; - if (e != NULL) e->destroy(e); m->destroy(m); @@ -315,7 +369,6 @@ destroy(struct bar *bar) for (size_t i = 0; i < b->center.count; i++) { struct module *m = b->center.mods[i]; struct exposable *e = b->center.exps[i]; - if (e != NULL) e->destroy(e); m->destroy(m); @@ -323,7 +376,6 @@ destroy(struct bar *bar) for (size_t i = 0; i < b->right.count; i++) { struct module *m = b->right.mods[i]; struct exposable *e = b->right.exps[i]; - if (e != NULL) e->destroy(e); m->destroy(m); @@ -378,7 +430,7 @@ bar_new(const struct bar_config *config) break; case BAR_BACKEND_WAYLAND: -#if defined(BAR_WAYLAND) +#if defined(ENABLE_WAYLAND) backend_data = bar_backend_wayland_new(); backend_iface = &wayland_backend_iface; #else @@ -393,6 +445,7 @@ bar_new(const struct bar_config *config) struct private *priv = calloc(1, sizeof(*priv)); priv->monitor = config->monitor != NULL ? strdup(config->monitor) : NULL; + priv->layer = config->layer; priv->location = config->location; priv->height = config->height; priv->background = config->background; @@ -400,7 +453,11 @@ bar_new(const struct bar_config *config) priv->right_spacing = config->right_spacing; priv->left_margin = config->left_margin; priv->right_margin = config->right_margin; - priv->border.width = config->border.width; + priv->trackpad_sensitivity = config->trackpad_sensitivity; + priv->border.left_width = config->border.left_width; + priv->border.right_width = config->border.right_width; + priv->border.top_width = config->border.top_width; + priv->border.bottom_width = config->border.bottom_width; priv->border.color = config->border.color; priv->border.left_margin = config->border.left_margin; priv->border.right_margin = config->border.right_margin; @@ -431,6 +488,7 @@ bar_new(const struct bar_config *config) bar->destroy = &destroy; bar->refresh = &refresh; bar->set_cursor = &set_cursor; + bar->output_name = &output_name; for (size_t i = 0; i < priv->left.count; i++) priv->left.mods[i]->bar = bar; diff --git a/bar/bar.h b/bar/bar.h index 78e2414..ce91247 100644 --- a/bar/bar.h +++ b/bar/bar.h @@ -1,6 +1,7 @@ #pragma once #include "../color.h" +#include "../font-shaping.h" #include "../module.h" struct bar { @@ -12,24 +13,31 @@ struct bar { void (*refresh)(const struct bar *bar); void (*set_cursor)(struct bar *bar, const char *cursor); + + const char *(*output_name)(const struct bar *bar); }; enum bar_location { BAR_TOP, BAR_BOTTOM }; +enum bar_layer { BAR_LAYER_OVERLAY, BAR_LAYER_TOP, BAR_LAYER_BOTTOM, BAR_LAYER_BACKGROUND }; enum bar_backend { BAR_BACKEND_AUTO, BAR_BACKEND_XCB, BAR_BACKEND_WAYLAND }; struct bar_config { enum bar_backend backend; const char *monitor; + enum bar_layer layer; enum bar_location location; + enum font_shaping font_shaping; int height; int left_spacing, right_spacing; int left_margin, right_margin; + int trackpad_sensitivity; pixman_color_t background; struct { - int width; + int left_width, right_width; + int top_width, bottom_width; pixman_color_t color; int left_margin, right_margin; int top_margin, bottom_margin; diff --git a/bar/meson.build b/bar/meson.build index 5df4b09..6ca5ec9 100644 --- a/bar/meson.build +++ b/bar/meson.build @@ -7,17 +7,16 @@ endif if backend_wayland wayland_protocols = dependency('wayland-protocols') - wayland_protocols_datadir = wayland_protocols.get_pkgconfig_variable('pkgdatadir') + wayland_protocols_datadir = wayland_protocols.get_variable('pkgdatadir') wscanner = dependency('wayland-scanner', native: true) wscanner_prog = find_program( - wscanner.get_pkgconfig_variable('wayland_scanner'), native: true) + wscanner.get_variable('wayland_scanner'), native: true) wl_proto_headers = [] wl_proto_src = [] foreach prot : [ '../external/wlr-layer-shell-unstable-v1.xml', - '../external/river-status-unstable-v1.xml', wayland_protocols_datadir + '/stable/xdg-shell/xdg-shell.xml', wayland_protocols_datadir + '/unstable/xdg-output/xdg-output-unstable-v1.xml'] diff --git a/bar/private.h b/bar/private.h index b5d888f..d48b07f 100644 --- a/bar/private.h +++ b/bar/private.h @@ -3,18 +3,22 @@ #include "../bar/bar.h" #include "backend.h" -struct private { +struct private +{ /* From bar_config */ char *monitor; + enum bar_layer layer; enum bar_location location; int height; int left_spacing, right_spacing; int left_margin, right_margin; + int trackpad_sensitivity; pixman_color_t background; struct { - int width; + int left_width, right_width; + int top_width, bottom_width; pixman_color_t color; int left_margin, right_margin; int top_margin, bottom_margin; diff --git a/bar/wayland.c b/bar/wayland.c index 7d87558..86ab252 100644 --- a/bar/wayland.c +++ b/bar/wayland.c @@ -1,22 +1,25 @@ #include "wayland.h" +#include +#include +#include +#include #include #include #include -#include #include -#include +#include +#include #include -#include #include #include #include #include -#include #include +#include #define LOG_MODULE "bar:wayland" #define LOG_ENABLE_DBG 0 @@ -25,6 +28,10 @@ #include "private.h" +#if !defined(MFD_NOEXEC_SEAL) +#define MFD_NOEXEC_SEAL 0 +#endif + struct buffer { bool busy; size_t width; @@ -43,6 +50,7 @@ struct monitor { struct wl_output *output; struct zxdg_output_v1 *xdg; char *name; + uint32_t wl_name; int x; int y; @@ -93,6 +101,7 @@ struct wayland_backend { tll(struct monitor) monitors; const struct monitor *monitor; + char *last_mapped_monitor; int scale; @@ -107,12 +116,15 @@ struct wayland_backend { /* We're already waiting for a frame done callback */ bool render_scheduled; - tll(struct buffer) buffers; /* List of SHM buffers */ - struct buffer *next_buffer; /* Bar is rendering to this one */ - struct buffer *pending_buffer; /* Finished, but not yet rendered */ + tll(struct buffer) buffers; /* List of SHM buffers */ + struct buffer *next_buffer; /* Bar is rendering to this one */ + struct buffer *pending_buffer; /* Finished, but not yet rendered */ + struct wl_callback *frame_callback; - void (*bar_expose)(const struct bar *bar); - void (*bar_on_mouse)(struct bar *bar, enum mouse_event event, int x, int y); + double aggregated_scroll; + bool have_discrete; + + void (*bar_on_mouse)(struct bar *bar, enum mouse_event event, enum mouse_button btn, int x, int y); }; static void @@ -121,26 +133,30 @@ seat_destroy(struct seat *seat) if (seat == NULL) return; + free(seat->name); + if (seat->pointer.theme != NULL) wl_cursor_theme_destroy(seat->pointer.theme); if (seat->wl_pointer != NULL) - wl_pointer_destroy(seat->wl_pointer); + wl_pointer_release(seat->wl_pointer); if (seat->pointer.surface != NULL) wl_surface_destroy(seat->pointer.surface); if (seat->seat != NULL) - wl_seat_destroy(seat->seat); + wl_seat_release(seat->seat); } void * bar_backend_wayland_new(void) { - return calloc(1, sizeof(struct wayland_backend)); + struct wayland_backend *backend = calloc(1, sizeof(struct wayland_backend)); + backend->pipe_fds[0] = backend->pipe_fds[1] = -1; + return backend; } static void shm_format(void *data, struct wl_shm *wl_shm, uint32_t format) { - //printf("SHM format: 0x%08x\n", format); + // printf("SHM format: 0x%08x\n", format); } static const struct wl_shm_listener shm_listener = { @@ -150,10 +166,7 @@ static const struct wl_shm_listener shm_listener = { static void update_cursor_surface(struct wayland_backend *backend, struct seat *seat) { - if (seat->pointer.serial == 0 || - seat->pointer.cursor == NULL || - seat->pointer.surface == NULL) - { + if (seat->pointer.serial == 0 || seat->pointer.cursor == NULL || seat->pointer.surface == NULL) { return; } @@ -162,17 +175,12 @@ update_cursor_surface(struct wayland_backend *backend, struct seat *seat) const int scale = seat->pointer.scale; wl_surface_set_buffer_scale(seat->pointer.surface, scale); - wl_surface_attach( - seat->pointer.surface, wl_cursor_image_get_buffer(image), 0, 0); + wl_surface_attach(seat->pointer.surface, wl_cursor_image_get_buffer(image), 0, 0); - wl_pointer_set_cursor( - seat->wl_pointer, seat->pointer.serial, - seat->pointer.surface, - image->hotspot_x / scale, image->hotspot_y / scale); + wl_pointer_set_cursor(seat->wl_pointer, seat->pointer.serial, seat->pointer.surface, image->hotspot_x / scale, + image->hotspot_y / scale); - - wl_surface_damage_buffer( - seat->pointer.surface, 0, 0, INT32_MAX, INT32_MAX); + wl_surface_damage_buffer(seat->pointer.surface, 0, 0, INT32_MAX, INT32_MAX); wl_surface_commit(seat->pointer.surface); wl_display_flush(backend->display); @@ -202,11 +210,9 @@ reload_cursor_theme(struct seat *seat, int new_scale) } } - LOG_INFO("%s: cursor theme: %s, size: %u, scale: %d", - seat->name, cursor_theme, cursor_size, new_scale); + LOG_INFO("%s: cursor theme: %s, size: %u, scale: %d", seat->name, cursor_theme, cursor_size, new_scale); - struct wl_cursor_theme *theme = wl_cursor_theme_load( - cursor_theme, cursor_size * new_scale, seat->backend->shm); + struct wl_cursor_theme *theme = wl_cursor_theme_load(cursor_theme, cursor_size * new_scale, seat->backend->shm); if (theme == NULL) { LOG_ERR("%s: failed to load cursor theme", seat->name); @@ -218,8 +224,7 @@ reload_cursor_theme(struct seat *seat, int new_scale) } static void -wl_pointer_enter(void *data, struct wl_pointer *wl_pointer, - uint32_t serial, struct wl_surface *surface, +wl_pointer_enter(void *data, struct wl_pointer *wl_pointer, uint32_t serial, struct wl_surface *surface, wl_fixed_t surface_x, wl_fixed_t surface_y) { struct seat *seat = data; @@ -235,19 +240,19 @@ wl_pointer_enter(void *data, struct wl_pointer *wl_pointer, } static void -wl_pointer_leave(void *data, struct wl_pointer *wl_pointer, - uint32_t serial, struct wl_surface *surface) +wl_pointer_leave(void *data, struct wl_pointer *wl_pointer, uint32_t serial, struct wl_surface *surface) { struct seat *seat = data; struct wayland_backend *backend = seat->backend; + backend->have_discrete = false; + if (backend->active_seat == seat) backend->active_seat = NULL; } static void -wl_pointer_motion(void *data, struct wl_pointer *wl_pointer, - uint32_t time, wl_fixed_t surface_x, wl_fixed_t surface_y) +wl_pointer_motion(void *data, struct wl_pointer *wl_pointer, uint32_t time, wl_fixed_t surface_x, wl_fixed_t surface_y) { struct seat *seat = data; struct wayland_backend *backend = seat->backend; @@ -256,52 +261,119 @@ wl_pointer_motion(void *data, struct wl_pointer *wl_pointer, seat->pointer.y = wl_fixed_to_int(surface_y) * backend->scale; backend->active_seat = seat; - backend->bar_on_mouse( - backend->bar, ON_MOUSE_MOTION, seat->pointer.x, seat->pointer.y); + backend->bar_on_mouse(backend->bar, ON_MOUSE_MOTION, MOUSE_BTN_NONE, seat->pointer.x, seat->pointer.y); } static void -wl_pointer_button(void *data, struct wl_pointer *wl_pointer, - uint32_t serial, uint32_t time, uint32_t button, uint32_t state) +wl_pointer_button(void *data, struct wl_pointer *wl_pointer, uint32_t serial, uint32_t time, uint32_t button, + uint32_t state) { - if (state != WL_POINTER_BUTTON_STATE_PRESSED) + struct seat *seat = data; + struct wayland_backend *backend = seat->backend; + + if (state == WL_POINTER_BUTTON_STATE_PRESSED) + backend->active_seat = seat; + else { + enum mouse_button btn; + + switch (button) { + case BTN_LEFT: + btn = MOUSE_BTN_LEFT; + break; + case BTN_MIDDLE: + btn = MOUSE_BTN_MIDDLE; + break; + case BTN_RIGHT: + btn = MOUSE_BTN_RIGHT; + break; + case BTN_SIDE: + btn = MOUSE_BTN_PREVIOUS; + break; + case BTN_EXTRA: + btn = MOUSE_BTN_NEXT; + break; + default: + return; + } + + backend->bar_on_mouse(backend->bar, ON_MOUSE_CLICK, btn, seat->pointer.x, seat->pointer.y); + } +} + +static void +wl_pointer_axis(void *data, struct wl_pointer *wl_pointer, uint32_t time, uint32_t axis, wl_fixed_t value) +{ + if (axis != WL_POINTER_AXIS_VERTICAL_SCROLL) return; struct seat *seat = data; struct wayland_backend *backend = seat->backend; + struct private *bar = backend->bar->private; backend->active_seat = seat; - backend->bar_on_mouse( - backend->bar, ON_MOUSE_CLICK, seat->pointer.x, seat->pointer.y); -} -static void -wl_pointer_axis(void *data, struct wl_pointer *wl_pointer, - uint32_t time, uint32_t axis, wl_fixed_t value) -{ + if (backend->have_discrete) + return; + + const double amount = wl_fixed_to_double(value); + + if ((backend->aggregated_scroll > 0 && amount < 0) || (backend->aggregated_scroll < 0 && amount > 0)) { + backend->aggregated_scroll = amount; + } else + backend->aggregated_scroll += amount; + + enum mouse_button btn = backend->aggregated_scroll > 0 ? MOUSE_BTN_WHEEL_DOWN : MOUSE_BTN_WHEEL_UP; + + const double step = bar->trackpad_sensitivity; + const double adjust = backend->aggregated_scroll > 0 ? -step : step; + + while (fabs(backend->aggregated_scroll) >= step) { + backend->bar_on_mouse(backend->bar, ON_MOUSE_CLICK, btn, seat->pointer.x, seat->pointer.y); + backend->aggregated_scroll += adjust; + } } static void wl_pointer_frame(void *data, struct wl_pointer *wl_pointer) { + struct seat *seat = data; + struct wayland_backend *backend = seat->backend; + backend->have_discrete = false; } static void -wl_pointer_axis_source(void *data, struct wl_pointer *wl_pointer, - uint32_t axis_source) +wl_pointer_axis_source(void *data, struct wl_pointer *wl_pointer, uint32_t axis_source) { } static void -wl_pointer_axis_stop(void *data, struct wl_pointer *wl_pointer, - uint32_t time, uint32_t axis) +wl_pointer_axis_stop(void *data, struct wl_pointer *wl_pointer, uint32_t time, uint32_t axis) { + if (axis != WL_POINTER_AXIS_VERTICAL_SCROLL) + return; + + struct seat *seat = data; + struct wayland_backend *backend = seat->backend; + backend->aggregated_scroll = 0.; } static void -wl_pointer_axis_discrete(void *data, struct wl_pointer *wl_pointer, - uint32_t axis, int32_t discrete) +wl_pointer_axis_discrete(void *data, struct wl_pointer *wl_pointer, uint32_t axis, int32_t discrete) { + if (axis != WL_POINTER_AXIS_VERTICAL_SCROLL) + return; + + struct seat *seat = data; + struct wayland_backend *backend = seat->backend; + backend->have_discrete = true; + + enum mouse_button btn = discrete > 0 ? MOUSE_BTN_WHEEL_DOWN : MOUSE_BTN_WHEEL_UP; + + int count = abs(discrete); + + for (int32_t i = 0; i < count; i++) { + backend->bar_on_mouse(backend->bar, ON_MOUSE_CLICK, btn, seat->pointer.x, seat->pointer.y); + } } static const struct wl_pointer_listener pointer_listener = { @@ -317,16 +389,14 @@ static const struct wl_pointer_listener pointer_listener = { }; static void -seat_handle_capabilities(void *data, struct wl_seat *wl_seat, - enum wl_seat_capability caps) +seat_handle_capabilities(void *data, struct wl_seat *wl_seat, enum wl_seat_capability caps) { struct seat *seat = data; if (caps & WL_SEAT_CAPABILITY_POINTER) { if (seat->wl_pointer == NULL) { assert(seat->pointer.surface == NULL); - seat->pointer.surface = wl_compositor_create_surface( - seat->backend->compositor); + seat->pointer.surface = wl_compositor_create_surface(seat->backend->compositor); if (seat->pointer.surface == NULL) { LOG_ERR("%s: failed to create pointer surface", seat->name); @@ -366,10 +436,8 @@ static const struct wl_seat_listener seat_listener = { }; static void -output_geometry(void *data, struct wl_output *wl_output, int32_t x, int32_t y, - int32_t physical_width, int32_t physical_height, - int32_t subpixel, const char *make, const char *model, - int32_t transform) +output_geometry(void *data, struct wl_output *wl_output, int32_t x, int32_t y, int32_t physical_width, + int32_t physical_height, int32_t subpixel, const char *make, const char *model, int32_t transform) { struct monitor *mon = data; mon->width_mm = physical_width; @@ -377,19 +445,29 @@ output_geometry(void *data, struct wl_output *wl_output, int32_t x, int32_t y, } static void -output_mode(void *data, struct wl_output *wl_output, uint32_t flags, - int32_t width, int32_t height, int32_t refresh) -{ -} - -static void -output_done(void *data, struct wl_output *wl_output) +output_mode(void *data, struct wl_output *wl_output, uint32_t flags, int32_t width, int32_t height, int32_t refresh) { } static bool update_size(struct wayland_backend *backend); static void refresh(const struct bar *_bar); +static void +output_done(void *data, struct wl_output *wl_output) +{ + struct monitor *mon = data; + + if (mon->backend->monitor == mon) { + int old_scale = mon->backend->scale; + int old_width = mon->backend->width; + + update_size(mon->backend); + + if (mon->backend->scale != old_scale || mon->backend->width != old_width) + refresh(mon->backend->bar); + } +} + static void output_scale(void *data, struct wl_output *wl_output, int32_t factor) { @@ -398,28 +476,40 @@ output_scale(void *data, struct wl_output *wl_output, int32_t factor) return; mon->scale = factor; - - if (mon->backend->monitor == mon) { - int old_scale = mon->backend->scale; - update_size(mon->backend); - - if (mon->backend->scale != old_scale) - refresh(mon->backend->bar); - } } +#if defined(WL_OUTPUT_NAME_SINCE_VERSION) +static void +output_name(void *data, struct wl_output *wl_output, const char *name) +{ + struct monitor *mon = data; + free(mon->name); + mon->name = name != NULL ? strdup(name) : NULL; +} +#endif + +#if defined(WL_OUTPUT_DESCRIPTION_SINCE_VERSION) +static void +output_description(void *data, struct wl_output *wl_output, const char *description) +{ +} +#endif static const struct wl_output_listener output_listener = { .geometry = &output_geometry, .mode = &output_mode, .done = &output_done, .scale = &output_scale, +#if defined(WL_OUTPUT_NAME_SINCE_VERSION) + .name = &output_name, +#endif +#if defined(WL_OUTPUT_DESCRIPTION_SINCE_VERSION) + .description = &output_description, +#endif }; static void -xdg_output_handle_logical_position(void *data, - struct zxdg_output_v1 *xdg_output, - int32_t x, int32_t y) +xdg_output_handle_logical_position(void *data, struct zxdg_output_v1 *xdg_output, int32_t x, int32_t y) { struct monitor *mon = data; mon->x = x; @@ -427,46 +517,69 @@ xdg_output_handle_logical_position(void *data, } static void -xdg_output_handle_logical_size(void *data, struct zxdg_output_v1 *xdg_output, - int32_t width, int32_t height) +xdg_output_handle_logical_size(void *data, struct zxdg_output_v1 *xdg_output, int32_t width, int32_t height) { struct monitor *mon = data; mon->width_px = width; mon->height_px = height; } +static bool create_surface(struct wayland_backend *backend); +static void destroy_surface(struct wayland_backend *backend); + static void xdg_output_handle_done(void *data, struct zxdg_output_v1 *xdg_output) { const struct monitor *mon = data; - LOG_INFO("monitor: %s: %dx%d+%d+%d (%dx%dmm)", - mon->name, mon->width_px, mon->height_px, - mon->x, mon->y, mon->width_mm, mon->height_mm); + LOG_INFO("monitor: %s: %dx%d+%d+%d (%dx%dmm)", mon->name, mon->width_px, mon->height_px, mon->x, mon->y, + mon->width_mm, mon->height_mm); struct wayland_backend *backend = mon->backend; struct private *bar = backend->bar->private; - if (bar->monitor != NULL && mon->name != NULL && - strcmp(bar->monitor, mon->name) == 0) - { - /* User specified a monitor, and this is one */ - backend->monitor = mon; + const bool is_mapped = backend->monitor != NULL; + if (is_mapped) { + assert(backend->surface != NULL); + assert(backend->last_mapped_monitor == NULL); + return; } + const bool output_is_our_configured_monitor + = (bar->monitor != NULL && mon->name != NULL && strcmp(bar->monitor, mon->name) == 0); + + const bool output_is_last_mapped = (backend->last_mapped_monitor != NULL && mon->name != NULL + && strcmp(backend->last_mapped_monitor, mon->name) == 0); + + if (output_is_our_configured_monitor) + LOG_DBG("%s: using this monitor (user configured)", mon->name); + else if (output_is_last_mapped) + LOG_DBG("%s: using this monitor (last mapped)", mon->name); + + if (output_is_our_configured_monitor || output_is_last_mapped) { + /* User specified a monitor, and this is one */ + backend->monitor = mon; + + free(backend->last_mapped_monitor); + backend->last_mapped_monitor = NULL; + + if (create_surface(backend) && update_size(backend)) { + if (backend->pipe_fds[1] >= 0) + refresh(backend->bar); + } + } } static void -xdg_output_handle_name(void *data, struct zxdg_output_v1 *xdg_output, - const char *name) +xdg_output_handle_name(void *data, struct zxdg_output_v1 *xdg_output, const char *name) { struct monitor *mon = data; + free(mon->name); mon->name = strdup(name); } static void -xdg_output_handle_description(void *data, struct zxdg_output_v1 *xdg_output, - const char *description) +xdg_output_handle_description(void *data, struct zxdg_output_v1 *xdg_output, const char *description) { } @@ -484,14 +597,12 @@ verify_iface_version(const char *iface, uint32_t version, uint32_t wanted) if (version >= wanted) return true; - LOG_ERR("%s: need interface version %u, but compositor only implements %u", - iface, wanted, version); + LOG_ERR("%s: need interface version %u, but compositor only implements %u", iface, wanted, version); return false; } static void -handle_global(void *data, struct wl_registry *registry, - uint32_t name, const char *interface, uint32_t version) +handle_global(void *data, struct wl_registry *registry, uint32_t name, const char *interface, uint32_t version) { LOG_DBG("global: 0x%08x, interface=%s, version=%u", name, interface, version); struct wayland_backend *backend = data; @@ -501,8 +612,7 @@ handle_global(void *data, struct wl_registry *registry, if (!verify_iface_version(interface, version, required)) return; - backend->compositor = wl_registry_bind( - registry, name, &wl_compositor_interface, required); + backend->compositor = wl_registry_bind(registry, name, &wl_compositor_interface, required); } else if (strcmp(interface, wl_shm_interface.name) == 0) { @@ -510,8 +620,7 @@ handle_global(void *data, struct wl_registry *registry, if (!verify_iface_version(interface, version, required)) return; - backend->shm = wl_registry_bind( - registry, name, &wl_shm_interface, required); + backend->shm = wl_registry_bind(registry, name, &wl_shm_interface, required); wl_shm_add_listener(backend->shm, &shm_listener, backend); } @@ -520,12 +629,9 @@ handle_global(void *data, struct wl_registry *registry, if (!verify_iface_version(interface, version, required)) return; - struct wl_output *output = wl_registry_bind( - registry, name, &wl_output_interface, required); + struct wl_output *output = wl_registry_bind(registry, name, &wl_output_interface, required); - tll_push_back(backend->monitors, ((struct monitor){ - .backend = backend, - .output = output})); + tll_push_back(backend->monitors, ((struct monitor){.backend = backend, .wl_name = name, .output = output})); struct monitor *mon = &tll_back(backend->monitors); wl_output_add_listener(output, &output_listener, mon); @@ -538,8 +644,7 @@ handle_global(void *data, struct wl_registry *registry, assert(backend->xdg_output_manager != NULL); if (backend->xdg_output_manager != NULL) { - mon->xdg = zxdg_output_manager_v1_get_xdg_output( - backend->xdg_output_manager, mon->output); + mon->xdg = zxdg_output_manager_v1_get_xdg_output(backend->xdg_output_manager, mon->output); zxdg_output_v1_add_listener(mon->xdg, &xdg_output_listener, mon); } @@ -550,21 +655,18 @@ handle_global(void *data, struct wl_registry *registry, if (!verify_iface_version(interface, version, required)) return; - backend->layer_shell = wl_registry_bind( - registry, name, &zwlr_layer_shell_v1_interface, required); + backend->layer_shell = wl_registry_bind(registry, name, &zwlr_layer_shell_v1_interface, required); } else if (strcmp(interface, wl_seat_interface.name) == 0) { - const uint32_t required = 3; + const uint32_t required = 5; if (!verify_iface_version(interface, version, required)) return; - struct wl_seat *seat = wl_registry_bind( - registry, name, &wl_seat_interface, required); + struct wl_seat *seat = wl_registry_bind(registry, name, &wl_seat_interface, required); assert(seat != NULL); - tll_push_back( - backend->seats, ((struct seat){.backend = backend, .seat = seat, .id = name})); + tll_push_back(backend->seats, ((struct seat){.backend = backend, .seat = seat, .id = name})); wl_seat_add_listener(seat, &seat_listener, &tll_back(backend->seats)); } @@ -574,8 +676,7 @@ handle_global(void *data, struct wl_registry *registry, if (!verify_iface_version(interface, version, required)) return; - backend->xdg_output_manager = wl_registry_bind( - registry, name, &zxdg_output_manager_v1_interface, required); + backend->xdg_output_manager = wl_registry_bind(registry, name, &zxdg_output_manager_v1_interface, required); } } @@ -584,7 +685,8 @@ handle_global_remove(void *data, struct wl_registry *registry, uint32_t name) { struct wayland_backend *backend = data; - tll_foreach(backend->seats, it) { + tll_foreach(backend->seats, it) + { if (it->item.id == name) { if (backend->active_seat == &it->item) backend->active_seat = NULL; @@ -594,9 +696,24 @@ handle_global_remove(void *data, struct wl_registry *registry, uint32_t name) } } - LOG_WARN("unknown global removed: 0x%08x", name); + tll_foreach(backend->monitors, it) + { + struct monitor *mon = &it->item; + if (mon->wl_name == name) { + LOG_INFO("%s disconnected/disabled", mon->name); - /* TODO: need to handle displays and seats */ + if (mon == backend->monitor) { + assert(backend->last_mapped_monitor == NULL); + backend->last_mapped_monitor = strdup(mon->name); + backend->monitor = NULL; + } + + tll_remove(backend->monitors, it); + return; + } + } + + LOG_WARN("unknown global removed: 0x%08x", name); } static const struct wl_registry_listener registry_listener = { @@ -605,8 +722,7 @@ static const struct wl_registry_listener registry_listener = { }; static void -layer_surface_configure(void *data, struct zwlr_layer_surface_v1 *surface, - uint32_t serial, uint32_t w, uint32_t h) +layer_surface_configure(void *data, struct zwlr_layer_surface_v1 *surface, uint32_t serial, uint32_t w, uint32_t h) { struct wayland_backend *backend = data; backend->width = w * backend->scale; @@ -618,22 +734,10 @@ layer_surface_configure(void *data, struct zwlr_layer_surface_v1 *surface, static void layer_surface_closed(void *data, struct zwlr_layer_surface_v1 *surface) { + LOG_DBG("layer surface closed by compositor"); + struct wayland_backend *backend = data; - - /* - * Called e.g. when an output is disabled. We don't get a - * corresponding event if/when that same output re-appears. So, - * for now, we simply shut down. In the future, we _could_ maybe - * destroy the surface, listen for output events and re-create the - * surface if the same output re-appears. - */ - LOG_WARN("compositor requested surface be closed - shutting down"); - - if (write(backend->bar->abort_fd, &(uint64_t){1}, sizeof(uint64_t)) - != sizeof(uint64_t)) - { - LOG_ERRNO("failed to signal abort to modules"); - } + destroy_surface(backend); } static const struct zwlr_layer_surface_v1_listener layer_surface_listener = { @@ -641,10 +745,96 @@ static const struct zwlr_layer_surface_v1_listener layer_surface_listener = { .closed = &layer_surface_closed, }; +static const struct wl_surface_listener surface_listener; + +static bool +create_surface(struct wayland_backend *backend) +{ + assert(tll_length(backend->monitors) > 0); + assert(backend->surface == NULL); + assert(backend->layer_surface == NULL); + + struct bar *_bar = backend->bar; + struct private *bar = _bar->private; + + backend->surface = wl_compositor_create_surface(backend->compositor); + if (backend->surface == NULL) { + LOG_ERR("failed to create panel surface"); + return false; + } + + wl_surface_add_listener(backend->surface, &surface_listener, backend); + + enum zwlr_layer_shell_v1_layer layer = ZWLR_LAYER_SHELL_V1_LAYER_BOTTOM; + + switch (bar->layer) { + case BAR_LAYER_BACKGROUND: + layer = ZWLR_LAYER_SHELL_V1_LAYER_BACKGROUND; + break; + + case BAR_LAYER_BOTTOM: + layer = ZWLR_LAYER_SHELL_V1_LAYER_BOTTOM; + break; + + case BAR_LAYER_TOP: + layer = ZWLR_LAYER_SHELL_V1_LAYER_TOP; + break; + + case BAR_LAYER_OVERLAY: + layer = ZWLR_LAYER_SHELL_V1_LAYER_OVERLAY; + break; + } + + backend->layer_surface = zwlr_layer_shell_v1_get_layer_surface( + backend->layer_shell, backend->surface, backend->monitor != NULL ? backend->monitor->output : NULL, layer, + "panel"); + + if (backend->layer_surface == NULL) { + LOG_ERR("failed to create layer shell surface"); + return false; + } + + zwlr_layer_surface_v1_add_listener(backend->layer_surface, &layer_surface_listener, backend); + + /* Aligned to top, maximum width */ + enum zwlr_layer_surface_v1_anchor top_or_bottom + = bar->location == BAR_TOP ? ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP : ZWLR_LAYER_SURFACE_V1_ANCHOR_BOTTOM; + + zwlr_layer_surface_v1_set_anchor(backend->layer_surface, ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT + | ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT | top_or_bottom); + + return true; +} + +static void +destroy_surface(struct wayland_backend *backend) +{ + if (backend->layer_surface != NULL) + zwlr_layer_surface_v1_destroy(backend->layer_surface); + if (backend->surface != NULL) + wl_surface_destroy(backend->surface); + if (backend->frame_callback != NULL) + wl_callback_destroy(backend->frame_callback); + + if (backend->pending_buffer != NULL) + backend->pending_buffer->busy = false; + if (backend->next_buffer != NULL) + backend->next_buffer->busy = false; + + backend->layer_surface = NULL; + backend->surface = NULL; + backend->frame_callback = NULL; + backend->pending_buffer = NULL; + backend->next_buffer = NULL; + + backend->scale = 0; + backend->render_scheduled = false; +} + static void buffer_release(void *data, struct wl_buffer *wl_buffer) { - //printf("buffer release\n"); + // printf("buffer release\n"); struct buffer *buffer = data; assert(buffer->busy); buffer->busy = false; @@ -657,7 +847,8 @@ static const struct wl_buffer_listener buffer_listener = { static struct buffer * get_buffer(struct wayland_backend *backend) { - tll_foreach(backend->buffers, it) { + tll_foreach(backend->buffers, it) + { if (!it->item.busy && it->item.width == backend->width && it->item.height == backend->height) { it->item.busy = true; return &it->item; @@ -684,15 +875,32 @@ get_buffer(struct wayland_backend *backend) pixman_image_t *pix = NULL; /* Backing memory for SHM */ - pool_fd = memfd_create("yambar-wayland-shm-buffer-pool", MFD_CLOEXEC); +#if defined(MEMFD_CREATE) + /* + * Older kernels reject MFD_NOEXEC_SEAL with EINVAL. Try first + * *with* it, and if that fails, try again *without* it. + */ + errno = 0; + pool_fd = memfd_create("yambar-wayland-shm-buffer-pool", MFD_CLOEXEC | MFD_ALLOW_SEALING | MFD_NOEXEC_SEAL); + + if (pool_fd < 0 && errno == EINVAL) { + pool_fd = memfd_create("yambar-wayland-shm-buffer-pool", MFD_CLOEXEC | MFD_ALLOW_SEALING); + } +#elif defined(__FreeBSD__) + // memfd_create on FreeBSD 13 is SHM_ANON without sealing support + pool_fd = shm_open(SHM_ANON, O_RDWR | O_CLOEXEC, 0600); +#else + char name[] = "/tmp/yambar-wayland-shm-buffer-pool-XXXXXX"; + pool_fd = mkostemp(name, O_CLOEXEC); + unlink(name); +#endif if (pool_fd == -1) { LOG_ERRNO("failed to create SHM backing memory file"); goto err; } /* Total size */ - const uint32_t stride = stride_for_format_and_width( - PIXMAN_a8r8g8b8, backend->width); + const uint32_t stride = stride_for_format_and_width(PIXMAN_a8r8g8b8, backend->width); size = stride * backend->height; if (ftruncate(pool_fd, size) == -1) { @@ -706,43 +914,50 @@ get_buffer(struct wayland_backend *backend) goto err; } +#if defined(MEMFD_CREATE) + /* Seal file - we no longer allow any kind of resizing */ + /* TODO: wayland mmaps(PROT_WRITE), for some unknown reason, hence we cannot use F_SEAL_FUTURE_WRITE */ + if (fcntl(pool_fd, F_ADD_SEALS, F_SEAL_GROW | F_SEAL_SHRINK | /*F_SEAL_FUTURE_WRITE |*/ F_SEAL_SEAL) < 0) { + LOG_ERRNO("failed to seal SHM backing memory file"); + /* This is not a fatal error */ + } +#endif + pool = wl_shm_create_pool(backend->shm, pool_fd, size); if (pool == NULL) { LOG_ERR("failed to create SHM pool"); goto err; } - buf = wl_shm_pool_create_buffer( - pool, 0, backend->width, backend->height, stride, WL_SHM_FORMAT_ARGB8888); + buf = wl_shm_pool_create_buffer(pool, 0, backend->width, backend->height, stride, WL_SHM_FORMAT_ARGB8888); if (buf == NULL) { LOG_ERR("failed to create SHM buffer"); goto err; } /* We use the entire pool for our single buffer */ - wl_shm_pool_destroy(pool); pool = NULL; - close(pool_fd); pool_fd = -1; + wl_shm_pool_destroy(pool); + pool = NULL; + close(pool_fd); + pool_fd = -1; - pix = pixman_image_create_bits_no_clear( - PIXMAN_a8r8g8b8, backend->width, backend->height, (uint32_t *)mmapped, stride); + pix = pixman_image_create_bits_no_clear(PIXMAN_a8r8g8b8, backend->width, backend->height, (uint32_t *)mmapped, + stride); if (pix == NULL) { LOG_ERR("failed to create pixman image"); goto err; } /* Push to list of available buffers, but marked as 'busy' */ - tll_push_back( - backend->buffers, - ((struct buffer){ - .busy = true, - .width = backend->width, - .height = backend->height, - .size = size, - .mmapped = mmapped, - .wl_buf = buf, - .pix = pix, - }) - ); + tll_push_back(backend->buffers, ((struct buffer){ + .busy = true, + .width = backend->width, + .height = backend->height, + .size = size, + .mmapped = mmapped, + .wl_buf = buf, + .pix = pix, + })); struct buffer *ret = &tll_back(backend->buffers); wl_buffer_add_listener(ret->wl_buf, &buffer_listener, ret); @@ -772,7 +987,8 @@ guess_scale(const struct wayland_backend *backend) bool all_have_same_scale = true; int last_scale = -1; - tll_foreach(backend->monitors, it) { + tll_foreach(backend->monitors, it) + { if (last_scale == -1) last_scale = it->item.scale; else if (last_scale != it->item.scale) { @@ -798,40 +1014,31 @@ update_size(struct wayland_backend *backend) const struct monitor *mon = backend->monitor; const int scale = mon != NULL ? mon->scale : guess_scale(backend); - if (backend->scale == scale) - return true; + assert(backend->surface != NULL); backend->scale = scale; int height = bar->height_with_border; height /= scale; height *= scale; - bar->height = height - 2 * bar->border.width; + bar->height = height - bar->border.top_width - bar->border.bottom_width; bar->height_with_border = height; - zwlr_layer_surface_v1_set_size( - backend->layer_surface, 0, bar->height_with_border / scale); + zwlr_layer_surface_v1_set_size(backend->layer_surface, 0, bar->height_with_border / scale); zwlr_layer_surface_v1_set_exclusive_zone( backend->layer_surface, - (bar->height_with_border + (bar->location == BAR_TOP - ? bar->border.bottom_margin - : bar->border.top_margin)) - / scale); + (bar->height_with_border + (bar->location == BAR_TOP ? bar->border.bottom_margin : bar->border.top_margin)) + / scale); - zwlr_layer_surface_v1_set_margin( - backend->layer_surface, - bar->border.top_margin / scale, - bar->border.right_margin / scale, - bar->border.bottom_margin / scale, - bar->border.left_margin / scale - ); + zwlr_layer_surface_v1_set_margin(backend->layer_surface, bar->border.top_margin / scale, + bar->border.right_margin / scale, bar->border.bottom_margin / scale, + bar->border.left_margin / scale); /* Trigger a 'configure' event, after which we'll have the width */ wl_surface_commit(backend->surface); wl_display_roundtrip(backend->display); - if (backend->width == -1 || - backend->height != bar->height_with_border) { + if (backend->width == -1 || backend->height != bar->height_with_border) { LOG_ERR("failed to get panel width"); return false; } @@ -848,8 +1055,6 @@ update_size(struct wayland_backend *backend) return true; } -static const struct wl_surface_listener surface_listener; - static bool setup(struct bar *_bar) { @@ -895,44 +1100,17 @@ setup(struct bar *_bar) /* Trigger listeners registered in previous roundtrip */ wl_display_roundtrip(backend->display); - backend->surface = wl_compositor_create_surface(backend->compositor); - if (backend->surface == NULL) { - LOG_ERR("failed to create panel surface"); - return false; + if (backend->surface == NULL && backend->layer_surface == NULL) { + if (!create_surface(backend)) + return false; + + if (!update_size(backend)) + return false; } - wl_surface_add_listener(backend->surface, &surface_listener, backend); + assert(backend->monitor == NULL || backend->width / backend->monitor->scale <= backend->monitor->width_px); - backend->layer_surface = zwlr_layer_shell_v1_get_layer_surface( - backend->layer_shell, backend->surface, - backend->monitor != NULL ? backend->monitor->output : NULL, - ZWLR_LAYER_SHELL_V1_LAYER_BOTTOM, "panel"); - - if (backend->layer_surface == NULL) { - LOG_ERR("failed to create layer shell surface"); - return false; - } - - zwlr_layer_surface_v1_add_listener( - backend->layer_surface, &layer_surface_listener, backend); - - /* Aligned to top, maximum width */ - enum zwlr_layer_surface_v1_anchor top_or_bottom = bar->location == BAR_TOP - ? ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP - : ZWLR_LAYER_SURFACE_V1_ANCHOR_BOTTOM; - - zwlr_layer_surface_v1_set_anchor( - backend->layer_surface, - ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT | - ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT | - top_or_bottom); - - update_size(backend); - - assert(backend->monitor == NULL || - backend->width / backend->monitor->scale <= backend->monitor->width_px); - - if (pipe(backend->pipe_fds) == -1) { + if (pipe2(backend->pipe_fds, O_CLOEXEC | O_NONBLOCK) == -1) { LOG_ERRNO("failed to create pipe"); return false; } @@ -947,7 +1125,34 @@ cleanup(struct bar *_bar) struct private *bar = _bar->private; struct wayland_backend *backend = bar->backend.data; - tll_foreach(backend->buffers, it) { + if (backend->pipe_fds[0] >= 0) + close(backend->pipe_fds[0]); + if (backend->pipe_fds[1] >= 0) + close(backend->pipe_fds[1]); + + tll_foreach(backend->monitors, it) + { + struct monitor *mon = &it->item; + free(mon->name); + + if (mon->xdg != NULL) + zxdg_output_v1_destroy(mon->xdg); + if (mon->output != NULL) + wl_output_release(mon->output); + tll_remove(backend->monitors, it); + } + free(backend->last_mapped_monitor); + + if (backend->xdg_output_manager != NULL) + zxdg_output_manager_v1_destroy(backend->xdg_output_manager); + + tll_foreach(backend->seats, it) seat_destroy(&it->item); + tll_free(backend->seats); + + destroy_surface(backend); + + tll_foreach(backend->buffers, it) + { if (it->item.wl_buf != NULL) wl_buffer_destroy(it->item.wl_buf); if (it->item.pix != NULL) @@ -957,57 +1162,42 @@ cleanup(struct bar *_bar) tll_remove(backend->buffers, it); } - tll_foreach(backend->monitors, it) { - struct monitor *mon = &it->item; - free(mon->name); - - if (mon->xdg != NULL) - zxdg_output_v1_destroy(mon->xdg); - if (mon->output != NULL) - wl_output_destroy(mon->output); - tll_remove(backend->monitors, it); - } - - if (backend->xdg_output_manager != NULL) - zxdg_output_manager_v1_destroy(backend->xdg_output_manager); - - tll_foreach(backend->seats, it) - seat_destroy(&it->item); - tll_free(backend->seats); - - if (backend->layer_surface != NULL) - zwlr_layer_surface_v1_destroy(backend->layer_surface); if (backend->layer_shell != NULL) zwlr_layer_shell_v1_destroy(backend->layer_shell); - if (backend->surface != NULL) - wl_surface_destroy(backend->surface); if (backend->compositor != NULL) wl_compositor_destroy(backend->compositor); if (backend->shm != NULL) wl_shm_destroy(backend->shm); if (backend->registry != NULL) wl_registry_destroy(backend->registry); - if (backend->display != NULL) + if (backend->display != NULL) { + wl_display_flush(backend->display); wl_display_disconnect(backend->display); + } /* Destroyed when freeing buffer list */ bar->pix = NULL; - } static void -loop(struct bar *_bar, - void (*expose)(const struct bar *bar), - void (*on_mouse)(struct bar *bar, enum mouse_event event, int x, int y)) +loop(struct bar *_bar, void (*expose)(const struct bar *bar), + void (*on_mouse)(struct bar *bar, enum mouse_event event, enum mouse_button btn, int x, int y)) { struct private *bar = _bar->private; struct wayland_backend *backend = bar->backend.data; + bool send_abort_to_modules = true; + + pthread_setname_np(pthread_self(), "bar(wayland)"); - backend->bar_expose = expose; backend->bar_on_mouse = on_mouse; - while (wl_display_prepare_read(backend->display) != 0) - wl_display_dispatch_pending(backend->display); + while (wl_display_prepare_read(backend->display) != 0) { + if (wl_display_dispatch_pending(backend->display) < 0) { + LOG_ERRNO("failed to dispatch pending Wayland events"); + goto out; + } + } + wl_display_flush(backend->display); while (true) { @@ -1019,51 +1209,82 @@ loop(struct bar *_bar, poll(fds, sizeof(fds) / sizeof(fds[0]), -1); if (fds[0].revents & POLLIN) { + /* Already done by the bar */ + send_abort_to_modules = false; break; } if (fds[1].revents & POLLHUP) { LOG_INFO("disconnected from wayland"); - if (write(_bar->abort_fd, &(uint64_t){1}, sizeof(uint64_t)) - != sizeof(uint64_t)) - { - LOG_ERRNO("failed to signal abort to modules"); - } break; } if (fds[2].revents & POLLIN) { - uint8_t command; - if (read(backend->pipe_fds[0], &command, sizeof(command)) - != sizeof(command)) - { - LOG_ERRNO("failed to read from command pipe"); - break; + bool do_expose = false; + + /* Coalesce “refresh” commands */ + __attribute__((unused)) size_t count = 0; + while (true) { + uint8_t command; + ssize_t r = read(backend->pipe_fds[0], &command, sizeof(command)); + if (r < 0 && errno == EAGAIN) + break; + + if (r != sizeof(command)) { + LOG_ERRNO("failed to read from command pipe"); + goto out; + } + + assert(command == 1); + if (command == 1) { + count++; + do_expose = true; + } } - assert(command == 1); - expose(_bar); + LOG_DBG("coalesced %zu expose commands", count); + if (do_expose) + expose(_bar); } if (fds[1].revents & POLLIN) { - wl_display_read_events(backend->display); + if (wl_display_read_events(backend->display) < 0) { + LOG_ERRNO("failed to read events from the Wayland socket"); + goto out; + } + + while (wl_display_prepare_read(backend->display) != 0) { + if (wl_display_dispatch_pending(backend->display) < 0) { + LOG_ERRNO("failed to dispatch pending Wayland events"); + goto out; + } + } - while (wl_display_prepare_read(backend->display) != 0) - wl_display_dispatch_pending(backend->display); wl_display_flush(backend->display); } } - wl_display_cancel_read(backend->display); +out: + if (!send_abort_to_modules) + return; + + if (write(_bar->abort_fd, &(uint64_t){1}, sizeof(uint64_t)) != sizeof(uint64_t)) { + LOG_ERRNO("failed to signal abort to modules"); + } + + // wl_display_cancel_read(backend->display); } static void -surface_enter(void *data, struct wl_surface *wl_surface, - struct wl_output *wl_output) +surface_enter(void *data, struct wl_surface *wl_surface, struct wl_output *wl_output) { struct wayland_backend *backend = data; - tll_foreach(backend->monitors, it) { + free(backend->last_mapped_monitor); + backend->last_mapped_monitor = NULL; + + tll_foreach(backend->monitors, it) + { struct monitor *mon = &it->item; if (mon->output != wl_output) @@ -1083,11 +1304,18 @@ surface_enter(void *data, struct wl_surface *wl_surface, } static void -surface_leave(void *data, struct wl_surface *wl_surface, - struct wl_output *wl_output) +surface_leave(void *data, struct wl_surface *wl_surface, struct wl_output *wl_output) { struct wayland_backend *backend = data; + const struct monitor *mon = backend->monitor; + + assert(mon != NULL); + assert(mon->output == wl_output); + backend->monitor = NULL; + + assert(backend->last_mapped_monitor == NULL); + backend->last_mapped_monitor = mon->name != NULL ? strdup(mon->name) : NULL; } static const struct wl_surface_listener surface_listener = { @@ -1095,8 +1323,7 @@ static const struct wl_surface_listener surface_listener = { .leave = &surface_leave, }; -static void frame_callback( - void *data, struct wl_callback *wl_callback, uint32_t callback_data); +static void frame_callback(void *data, struct wl_callback *wl_callback, uint32_t callback_data); static const struct wl_callback_listener frame_listener = { .done = &frame_callback, @@ -1105,13 +1332,15 @@ static const struct wl_callback_listener frame_listener = { static void frame_callback(void *data, struct wl_callback *wl_callback, uint32_t callback_data) { - //printf("frame callback\n"); + // printf("frame callback\n"); struct private *bar = data; struct wayland_backend *backend = bar->backend.data; backend->render_scheduled = false; + assert(wl_callback == backend->frame_callback); wl_callback_destroy(wl_callback); + backend->frame_callback = NULL; if (backend->pending_buffer != NULL) { struct buffer *buffer = backend->pending_buffer; @@ -1126,10 +1355,11 @@ frame_callback(void *data, struct wl_callback *wl_callback, uint32_t callback_da wl_surface_commit(backend->surface); wl_display_flush(backend->display); + backend->frame_callback = cb; backend->pending_buffer = NULL; backend->render_scheduled = true; } else - ;//printf("nothing more to do\n"); + ; // printf("nothing more to do\n"); } static void @@ -1138,13 +1368,16 @@ commit(const struct bar *_bar) struct private *bar = _bar->private; struct wayland_backend *backend = bar->backend.data; - //printf("commit: %dxl%d\n", backend->width, backend->height); + // printf("commit: %dxl%d\n", backend->width, backend->height); + + if (backend->next_buffer == NULL) + return; assert(backend->next_buffer != NULL); assert(backend->next_buffer->busy); if (backend->render_scheduled) { - //printf("already scheduled\n"); + // printf("already scheduled\n"); if (backend->pending_buffer != NULL) backend->pending_buffer->busy = false; @@ -1153,7 +1386,7 @@ commit(const struct bar *_bar) backend->next_buffer = NULL; } else { - //printf("scheduling new frame callback\n"); + // printf("scheduling new frame callback\n"); struct buffer *buffer = backend->next_buffer; assert(buffer->busy); @@ -1167,6 +1400,7 @@ commit(const struct bar *_bar) wl_display_flush(backend->display); backend->render_scheduled = true; + backend->frame_callback = cb; } backend->next_buffer = get_buffer(backend); @@ -1180,9 +1414,7 @@ refresh(const struct bar *_bar) const struct private *bar = _bar->private; const struct wayland_backend *backend = bar->backend.data; - if (write(backend->pipe_fds[1], &(uint8_t){1}, sizeof(uint8_t)) - != sizeof(uint8_t)) - { + if (write(backend->pipe_fds[1], &(uint8_t){1}, sizeof(uint8_t)) != sizeof(uint8_t)) { LOG_ERRNO("failed to signal 'refresh' to main thread"); } } @@ -1202,8 +1434,7 @@ set_cursor(struct bar *_bar, const char *cursor) seat->pointer.xcursor = cursor; - seat->pointer.cursor = wl_cursor_theme_get_cursor( - seat->pointer.theme, cursor); + seat->pointer.cursor = wl_cursor_theme_get_cursor(seat->pointer.theme, cursor); if (seat->pointer.cursor == NULL) { LOG_ERR("%s: failed to load cursor '%s'", seat->name, cursor); @@ -1213,6 +1444,15 @@ set_cursor(struct bar *_bar, const char *cursor) update_cursor_surface(backend, seat); } +static const char * +bar_output_name(const struct bar *_bar) +{ + const struct private *bar = _bar->private; + const struct wayland_backend *backend = bar->backend.data; + + return backend->monitor != NULL ? backend->monitor->name : NULL; +} + const struct backend wayland_backend_iface = { .setup = &setup, .cleanup = &cleanup, @@ -1220,4 +1460,5 @@ const struct backend wayland_backend_iface = { .commit = &commit, .refresh = &refresh, .set_cursor = &set_cursor, + .output_name = &bar_output_name, }; diff --git a/bar/xcb.c b/bar/xcb.c index f95c445..ae52bf3 100644 --- a/bar/xcb.c +++ b/bar/xcb.c @@ -1,15 +1,16 @@ #include "xcb.h" -#include #include +#include -#include #include +#include +#include #include -#include #include #include +#include #include #include #include @@ -38,7 +39,6 @@ struct xcb_backend { void *client_pixmap; size_t client_pixmap_size; pixman_image_t *pix; - }; void * @@ -54,11 +54,8 @@ setup(struct bar *_bar) struct private *bar = _bar->private; struct xcb_backend *backend = bar->backend.data; - if (bar->border.left_margin != 0 || - bar->border.right_margin != 0 || - bar->border.top_margin != 0 || - bar->border.bottom_margin) - { + if (bar->border.left_margin != 0 || bar->border.right_margin != 0 || bar->border.top_margin != 0 + || bar->border.bottom_margin) { LOG_WARN("non-zero border margins ignored in X11 backend"); } @@ -75,10 +72,8 @@ setup(struct bar *_bar) xcb_screen_t *screen = xcb_aux_get_screen(backend->conn, default_screen); - xcb_randr_get_monitors_reply_t *monitors = xcb_randr_get_monitors_reply( - backend->conn, - xcb_randr_get_monitors(backend->conn, screen->root, 0), - &e); + xcb_randr_get_monitors_reply_t *monitors + = xcb_randr_get_monitors_reply(backend->conn, xcb_randr_get_monitors(backend->conn, screen->root, 0), &e); if (e != NULL) { LOG_ERR("failed to get monitor list: %s", xcb_error(e)); @@ -89,39 +84,43 @@ setup(struct bar *_bar) /* Find monitor coordinates and width/height */ bool found_monitor = false; - for (xcb_randr_monitor_info_iterator_t it = - xcb_randr_get_monitors_monitors_iterator(monitors); - it.rem > 0; - xcb_randr_monitor_info_next(&it)) - { + for (xcb_randr_monitor_info_iterator_t it = xcb_randr_get_monitors_monitors_iterator(monitors); it.rem > 0; + xcb_randr_monitor_info_next(&it)) { const xcb_randr_monitor_info_t *mon = it.data; char *name = get_atom_name(backend->conn, mon->name); - LOG_INFO("monitor: %s: %ux%u+%u+%u (%ux%umm)", name, - mon->width, mon->height, mon->x, mon->y, - mon->width_in_millimeters, mon->height_in_millimeters); + LOG_INFO("monitor: %s: %ux%u+%u+%u (%ux%umm)", name, mon->width, mon->height, mon->x, mon->y, + mon->width_in_millimeters, mon->height_in_millimeters); - if (!((bar->monitor == NULL && mon->primary) || - (bar->monitor != NULL && strcmp(bar->monitor, name) == 0))) - { + /* User wants a specific monitor, and this is not the one */ + if (bar->monitor != NULL && strcmp(bar->monitor, name) != 0) { free(name); continue; } - free(name); - 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; - break; + + if ((bar->monitor != NULL && strcmp(bar->monitor, name) == 0) || (bar->monitor == NULL && mon->primary)) { + /* Exact match */ + free(name); + break; + } + + free(name); } free(monitors); if (!found_monitor) { - LOG_ERR("no matching monitor"); + if (bar->monitor == NULL) + LOG_ERR("no monitors found"); + else + LOG_ERR("no monitor '%s'", bar->monitor); + /* TODO: cleanup */ return false; } @@ -143,74 +142,47 @@ setup(struct bar *_bar) LOG_DBG("using a %hhu-bit visual", depth); backend->colormap = xcb_generate_id(backend->conn); - xcb_create_colormap( - backend->conn, 0, backend->colormap, screen->root, vis->visual_id); + xcb_create_colormap(backend->conn, 0, backend->colormap, screen->root, vis->visual_id); backend->win = xcb_generate_id(backend->conn); xcb_create_window( - backend->conn, - depth, backend->win, screen->root, - backend->x, backend->y, bar->width, bar->height_with_border, - 0, - XCB_WINDOW_CLASS_INPUT_OUTPUT, vis->visual_id, - (XCB_CW_BACK_PIXEL | - XCB_CW_BORDER_PIXEL | - XCB_CW_EVENT_MASK | - XCB_CW_COLORMAP), - (const uint32_t []){ - screen->black_pixel, - screen->white_pixel, - (XCB_EVENT_MASK_EXPOSURE | - XCB_EVENT_MASK_BUTTON_RELEASE | - XCB_EVENT_MASK_BUTTON_PRESS | - XCB_EVENT_MASK_POINTER_MOTION | - XCB_EVENT_MASK_STRUCTURE_NOTIFY), - backend->colormap} - ); + backend->conn, depth, backend->win, screen->root, backend->x, backend->y, bar->width, bar->height_with_border, + 0, XCB_WINDOW_CLASS_INPUT_OUTPUT, vis->visual_id, + (XCB_CW_BACK_PIXEL | XCB_CW_BORDER_PIXEL | XCB_CW_EVENT_MASK | XCB_CW_COLORMAP), + (const uint32_t[]){screen->black_pixel, screen->white_pixel, + (XCB_EVENT_MASK_EXPOSURE | XCB_EVENT_MASK_BUTTON_RELEASE | XCB_EVENT_MASK_BUTTON_PRESS + | XCB_EVENT_MASK_POINTER_MOTION | XCB_EVENT_MASK_STRUCTURE_NOTIFY), + backend->colormap}); const char *title = "yambar"; - xcb_change_property( - backend->conn, - XCB_PROP_MODE_REPLACE, backend->win, - XCB_ATOM_WM_NAME, XCB_ATOM_STRING, 8, - strlen(title), title); + xcb_change_property(backend->conn, XCB_PROP_MODE_REPLACE, backend->win, XCB_ATOM_WM_NAME, XCB_ATOM_STRING, 8, + strlen(title), title); - xcb_change_property( - backend->conn, - XCB_PROP_MODE_REPLACE, backend->win, - _NET_WM_PID, XCB_ATOM_CARDINAL, 32, 1, (const uint32_t []){getpid()}); - xcb_change_property( - backend->conn, - XCB_PROP_MODE_REPLACE, backend->win, - _NET_WM_WINDOW_TYPE, XCB_ATOM_ATOM, 32, - 1, (const uint32_t []){_NET_WM_WINDOW_TYPE_DOCK}); - xcb_change_property( - backend->conn, - XCB_PROP_MODE_REPLACE, backend->win, - _NET_WM_STATE, XCB_ATOM_ATOM, 32, - 2, (const uint32_t []){_NET_WM_STATE_ABOVE, _NET_WM_STATE_STICKY}); - xcb_change_property( - backend->conn, - XCB_PROP_MODE_REPLACE, backend->win, - _NET_WM_DESKTOP, XCB_ATOM_CARDINAL, 32, 1, (const uint32_t []){0xffffffff}); + xcb_change_property(backend->conn, XCB_PROP_MODE_REPLACE, backend->win, _NET_WM_PID, XCB_ATOM_CARDINAL, 32, 1, + (const uint32_t[]){getpid()}); + xcb_change_property(backend->conn, XCB_PROP_MODE_REPLACE, backend->win, _NET_WM_WINDOW_TYPE, XCB_ATOM_ATOM, 32, 1, + (const uint32_t[]){_NET_WM_WINDOW_TYPE_DOCK}); + xcb_change_property(backend->conn, XCB_PROP_MODE_REPLACE, backend->win, _NET_WM_STATE, XCB_ATOM_ATOM, 32, 2, + (const uint32_t[]){_NET_WM_STATE_ABOVE, _NET_WM_STATE_STICKY}); + xcb_change_property(backend->conn, XCB_PROP_MODE_REPLACE, backend->win, _NET_WM_DESKTOP, XCB_ATOM_CARDINAL, 32, 1, + (const uint32_t[]){0xffffffff}); /* Always on top */ - xcb_configure_window( - backend->conn, backend->win, XCB_CONFIG_WINDOW_STACK_MODE, - (const uint32_t []){XCB_STACK_MODE_ABOVE}); + xcb_configure_window(backend->conn, backend->win, XCB_CONFIG_WINDOW_STACK_MODE, + (const uint32_t[]){XCB_STACK_MODE_ABOVE}); uint32_t top_strut, bottom_strut; uint32_t top_pair[2], bottom_pair[2]; if (bar->location == BAR_TOP) { - top_strut = backend->y + bar->height_with_border; + top_strut = bar->height_with_border; top_pair[0] = backend->x; top_pair[1] = backend->x + bar->width - 1; bottom_strut = 0; bottom_pair[0] = bottom_pair[1] = 0; } else { - bottom_strut = screen->height_in_pixels - backend->y; + bottom_strut = bar->height_with_border; bottom_pair[0] = backend->x; bottom_pair[1] = backend->x + bar->width - 1; @@ -220,42 +192,38 @@ setup(struct bar *_bar) uint32_t strut[] = { /* left/right/top/bottom */ - 0, 0, + 0, + 0, top_strut, bottom_strut, /* start/end pairs for left/right/top/bottom */ - 0, 0, - 0, 0, - top_pair[0], top_pair[1], - bottom_pair[0], bottom_pair[1], + 0, + 0, + 0, + 0, + top_pair[0], + top_pair[1], + bottom_pair[0], + bottom_pair[1], }; - xcb_change_property( - backend->conn, - XCB_PROP_MODE_REPLACE, backend->win, - _NET_WM_STRUT, XCB_ATOM_CARDINAL, 32, - 4, strut); + xcb_change_property(backend->conn, XCB_PROP_MODE_REPLACE, backend->win, _NET_WM_STRUT, XCB_ATOM_CARDINAL, 32, 4, + strut); - xcb_change_property( - backend->conn, - XCB_PROP_MODE_REPLACE, backend->win, - _NET_WM_STRUT_PARTIAL, XCB_ATOM_CARDINAL, 32, - 12, strut); + xcb_change_property(backend->conn, XCB_PROP_MODE_REPLACE, backend->win, _NET_WM_STRUT_PARTIAL, XCB_ATOM_CARDINAL, + 32, 12, strut); backend->gc = xcb_generate_id(backend->conn); - xcb_create_gc(backend->conn, backend->gc, backend->win, - XCB_GC_FOREGROUND | XCB_GC_GRAPHICS_EXPOSURES, - (const uint32_t []){screen->white_pixel, 0}); + xcb_create_gc(backend->conn, backend->gc, backend->win, XCB_GC_FOREGROUND | XCB_GC_GRAPHICS_EXPOSURES, + (const uint32_t[]){screen->white_pixel, 0}); - const uint32_t stride = stride_for_format_and_width( - PIXMAN_a8r8g8b8, bar->width); + const uint32_t stride = stride_for_format_and_width(PIXMAN_a8r8g8b8, bar->width); backend->client_pixmap_size = stride * bar->height_with_border; backend->client_pixmap = malloc(backend->client_pixmap_size); - backend->pix = pixman_image_create_bits_no_clear( - PIXMAN_a8r8g8b8, bar->width, bar->height_with_border, - (uint32_t *)backend->client_pixmap, stride); + backend->pix = pixman_image_create_bits_no_clear(PIXMAN_a8r8g8b8, bar->width, bar->height_with_border, + (uint32_t *)backend->client_pixmap, stride); bar->pix = backend->pix; xcb_map_window(backend->conn, backend->win); @@ -298,20 +266,18 @@ cleanup(struct bar *_bar) } static void -loop(struct bar *_bar, - void (*expose)(const struct bar *bar), - void (*on_mouse)(struct bar *bar, enum mouse_event event, int x, int y)) +loop(struct bar *_bar, void (*expose)(const struct bar *bar), + void (*on_mouse)(struct bar *bar, enum mouse_event event, enum mouse_button btn, int x, int y)) { struct private *bar = _bar->private; struct xcb_backend *backend = bar->backend.data; + pthread_setname_np(pthread_self(), "bar(xcb)"); + const int fd = xcb_get_file_descriptor(backend->conn); while (true) { - struct pollfd fds[] = { - {.fd = _bar->abort_fd, .events = POLLIN}, - {.fd = fd, .events = POLLIN} - }; + struct pollfd fds[] = {{.fd = _bar->abort_fd, .events = POLLIN}, {.fd = fd, .events = POLLIN}}; poll(fds, sizeof(fds) / sizeof(fds[0]), -1); @@ -320,18 +286,14 @@ loop(struct bar *_bar, if (fds[1].revents & POLLHUP) { LOG_WARN("disconnected from XCB"); - if (write(_bar->abort_fd, &(uint64_t){1}, sizeof(uint64_t)) - != sizeof(uint64_t)) - { + if (write(_bar->abort_fd, &(uint64_t){1}, sizeof(uint64_t)) != sizeof(uint64_t)) { LOG_ERRNO("failed to signal abort to modules"); } break; } - for (xcb_generic_event_t *e = xcb_wait_for_event(backend->conn); - e != NULL; - e = xcb_poll_for_event(backend->conn)) - { + for (xcb_generic_event_t *e = xcb_wait_for_event(backend->conn); e != NULL; + e = xcb_poll_for_event(backend->conn)) { switch (XCB_EVENT_RESPONSE_TYPE(e)) { case 0: LOG_ERR("XCB: %s", xcb_error((const xcb_generic_error_t *)e)); @@ -343,7 +305,7 @@ loop(struct bar *_bar, case XCB_MOTION_NOTIFY: { const xcb_motion_notify_event_t *evt = (void *)e; - on_mouse(_bar, ON_MOUSE_MOTION, evt->event_x, evt->event_y); + on_mouse(_bar, ON_MOUSE_MOTION, MOUSE_BTN_NONE, evt->event_x, evt->event_y); break; } @@ -352,7 +314,16 @@ loop(struct bar *_bar, case XCB_BUTTON_RELEASE: { const xcb_button_release_event_t *evt = (void *)e; - on_mouse(_bar, ON_MOUSE_CLICK, evt->event_x, evt->event_y); + + switch (evt->detail) { + case 1: + case 2: + case 3: + case 4: + case 5: + on_mouse(_bar, ON_MOUSE_CLICK, evt->detail, evt->event_x, evt->event_y); + break; + } break; } @@ -384,10 +355,9 @@ commit(const struct bar *_bar) const struct private *bar = _bar->private; const struct xcb_backend *backend = bar->backend.data; - xcb_put_image( - backend->conn, XCB_IMAGE_FORMAT_Z_PIXMAP, backend->win, backend->gc, - bar->width, bar->height_with_border, 0, 0, 0, - backend->depth, backend->client_pixmap_size, backend->client_pixmap); + xcb_put_image(backend->conn, XCB_IMAGE_FORMAT_Z_PIXMAP, backend->win, backend->gc, bar->width, + bar->height_with_border, 0, 0, 0, backend->depth, backend->client_pixmap_size, + backend->client_pixmap); xcb_flush(backend->conn); } @@ -399,23 +369,19 @@ 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); - *evt = (xcb_expose_event_t){ - .response_type = XCB_EXPOSE, - .window = backend->win, - .x = 0, - .y = 0, - .width = bar->width, - .height = bar->height, - .count = 1 - }; + *evt = (xcb_expose_event_t){.response_type = XCB_EXPOSE, + .window = backend->win, + .x = 0, + .y = 0, + .width = bar->width, + .height = bar->height, + .count = 1}; - xcb_send_event( - backend->conn, false, backend->win, XCB_EVENT_MASK_EXPOSURE, - (char *)evt); + xcb_send_event(backend->conn, false, backend->win, XCB_EVENT_MASK_EXPOSURE, (char *)evt); xcb_flush(backend->conn); free(evt); @@ -437,8 +403,14 @@ set_cursor(struct bar *_bar, const char *cursor) xcb_free_cursor(backend->conn, backend->cursor); backend->cursor = xcb_cursor_load_cursor(backend->cursor_ctx, cursor); - xcb_change_window_attributes( - backend->conn, backend->win, XCB_CW_CURSOR, &backend->cursor); + xcb_change_window_attributes(backend->conn, backend->win, XCB_CW_CURSOR, &backend->cursor); +} + +static const char * +output_name(const struct bar *_bar) +{ + /* Not implemented */ + return NULL; } const struct backend xcb_backend_iface = { @@ -448,4 +420,5 @@ const struct backend xcb_backend_iface = { .commit = &commit, .refresh = &refresh, .set_cursor = &set_cursor, + .output_name = &output_name, }; diff --git a/char32.c b/char32.c new file mode 100644 index 0000000..eb3abb4 --- /dev/null +++ b/char32.c @@ -0,0 +1,83 @@ +#include "char32.h" + +#include +#include +#include + +#include + +#if defined __has_include +#if __has_include() +#include +#endif +#endif + +#define LOG_MODULE "char32" +#define LOG_ENABLE_DBG 0 +#include "log.h" + +/* + * For now, assume we can map directly to the corresponding wchar_t + * functions. This is true if: + * + * - both data types have the same size + * - both use the same encoding (though we require that encoding to be UTF-32) + */ + +_Static_assert(sizeof(wchar_t) == sizeof(char32_t), "wchar_t vs. char32_t size mismatch"); + +#if !defined(__STDC_UTF_32__) || !__STDC_UTF_32__ +#error "char32_t does not use UTF-32" +#endif +#if (!defined(__STDC_ISO_10646__) || !__STDC_ISO_10646__) && !defined(__FreeBSD__) +#error "wchar_t does not use UTF-32" +#endif + +size_t +c32len(const char32_t *s) +{ + return wcslen((const wchar_t *)s); +} + +char32_t * +ambstoc32(const char *src) +{ + if (src == NULL) + return NULL; + + const size_t src_len = strlen(src); + + char32_t *ret = malloc((src_len + 1) * sizeof(ret[0])); + if (ret == NULL) + return NULL; + + mbstate_t ps = {0}; + char32_t *out = ret; + const char *in = src; + const char *const end = src + src_len + 1; + + size_t chars = 0; + size_t rc; + + while ((rc = mbrtoc32(out, in, end - in, &ps)) != 0) { + switch (rc) { + case (size_t)-1: + case (size_t)-2: + case (size_t)-3: + goto err; + } + + in += rc; + out++; + chars++; + } + + *out = U'\0'; + + ret = realloc(ret, (chars + 1) * sizeof(ret[0])); + return ret; + +err: + free(ret); + return NULL; +} diff --git a/char32.h b/char32.h new file mode 100644 index 0000000..1e7a9de --- /dev/null +++ b/char32.h @@ -0,0 +1,7 @@ +#pragma once + +#include +#include + +size_t c32len(const char32_t *s); +char32_t *ambstoc32(const char *src); diff --git a/completions/zsh/_yambar b/completions/zsh/_yambar index aa8172f..90b1117 100644 --- a/completions/zsh/_yambar +++ b/completions/zsh/_yambar @@ -8,5 +8,6 @@ _arguments \ '(-c --config)'{-c,--config}'[alternative configuration file]:filename:_files' \ '(-C --validate)'{-C,--validate}'[verify configuration then quit]' \ '(-p --print-pid)'{-p,--print-pid}'[print PID to this file or FD when up and running]:pidfile:_files' \ + '(-d --log-level)'{-d,--log-level}'[log level (warning)]:loglevel:(info warning error none)' \ '(-l --log-colorize)'{-l,--log-colorize}'[enable or disable colorization of log output on stderr]:logcolor:(never always auto)' \ '(-s --log-no-syslog)'{-s,--log-no-syslog}'[disable syslog logging]' diff --git a/config-verify.c b/config-verify.c index 7e87d42..ed7d2f5 100644 --- a/config-verify.c +++ b/config-verify.c @@ -1,7 +1,7 @@ #include "config.h" -#include #include +#include #include @@ -16,11 +16,9 @@ conf_err_prefix(const keychain_t *chain, const struct yml_node *node) static char msg[4096]; int idx = 0; - idx += snprintf(&msg[idx], sizeof(msg) - idx, "%zu:%zu: ", - yml_source_line(node), yml_source_column(node)); + idx += snprintf(&msg[idx], sizeof(msg) - idx, "%zu:%zu: ", yml_source_line(node), yml_source_column(node)); - tll_foreach(*chain, key) - idx += snprintf(&msg[idx], sizeof(msg) - idx, "%s.", key->item); + tll_foreach(*chain, key) idx += snprintf(&msg[idx], sizeof(msg) - idx, "%s.", key->item); /* Remove trailing "." */ msg[idx - 1] = '\0'; @@ -45,8 +43,27 @@ conf_verify_int(keychain_t *chain, const struct yml_node *node) if (yml_value_is_int(node)) return true; - LOG_ERR("%s: value is not an integer: '%s'", - conf_err_prefix(chain, node), yml_value_as_string(node)); + LOG_ERR("%s: value is not an integer: '%s'", conf_err_prefix(chain, node), yml_value_as_string(node)); + return false; +} + +bool +conf_verify_unsigned(keychain_t *chain, const struct yml_node *node) +{ + if (yml_value_is_int(node) && yml_value_as_int(node) >= 0) + return true; + + LOG_ERR("%s: value is not a positive integer: '%s'", conf_err_prefix(chain, node), yml_value_as_string(node)); + return false; +} + +bool +conf_verify_bool(keychain_t *chain, const struct yml_node *node) +{ + if (yml_value_is_bool(node)) + return true; + + LOG_ERR("%s: value is not a boolean: '%s'", conf_err_prefix(chain, node), yml_value_as_string(node)); return false; } @@ -59,10 +76,7 @@ conf_verify_list(keychain_t *chain, const struct yml_node *node, return false; } - for (struct yml_list_iter iter = yml_list_iter(node); - iter.node != NULL; - yml_list_next(&iter)) - { + for (struct yml_list_iter iter = yml_list_iter(node); iter.node != NULL; yml_list_next(&iter)) { if (!verify(chain, iter.node)) return false; } @@ -71,8 +85,7 @@ conf_verify_list(keychain_t *chain, const struct yml_node *node, } bool -conf_verify_enum(keychain_t *chain, const struct yml_node *node, - const char *values[], size_t count) +conf_verify_enum(keychain_t *chain, const struct yml_node *node, const char *values[], size_t count) { const char *s = yml_value_as_string(node); if (s == NULL) { @@ -93,8 +106,7 @@ conf_verify_enum(keychain_t *chain, const struct yml_node *node, } bool -conf_verify_dict(keychain_t *chain, const struct yml_node *node, - const struct attr_info info[]) +conf_verify_dict(keychain_t *chain, const struct yml_node *node, const struct attr_info info[]) { if (!yml_is_dict(node)) { LOG_ERR("%s: must be a dictionary", conf_err_prefix(chain, node)); @@ -109,10 +121,7 @@ conf_verify_dict(keychain_t *chain, const struct yml_node *node, bool exists[count]; memset(exists, 0, sizeof(exists)); - for (struct yml_dict_iter it = yml_dict_iter(node); - it.key != NULL; - yml_dict_next(&it)) - { + for (struct yml_dict_iter it = yml_dict_iter(node); it.key != NULL; yml_dict_next(&it)) { const char *key = yml_value_as_string(it.key); if (key == NULL) { LOG_ERR("%s: key must be a string", conf_err_prefix(chain, it.key)); @@ -152,6 +161,48 @@ conf_verify_dict(keychain_t *chain, const struct yml_node *node, return true; } +static bool +verify_on_click_path(keychain_t *chain, const struct yml_node *node) +{ + if (!conf_verify_string(chain, node)) + return false; + +#if 1 + /* We allow non-absolute paths in on-click handlers */ + return true; +#else + const char *path = yml_value_as_string(node); + + const bool is_absolute = path[0] == '/'; + const bool is_tilde = path[0] == '~' && path[1] == '/'; + + if (!is_absolute && !is_tilde) { + LOG_ERR("%s: path must be either absolute, or begin with '~/", conf_err_prefix(chain, node)); + return false; + } + + return true; +#endif +} + +bool +conf_verify_on_click(keychain_t *chain, const struct yml_node *node) +{ + /* on-click: */ + const char *s = yml_value_as_string(node); + if (s != NULL) + return verify_on_click_path(chain, node); + + static const struct attr_info info[] = { + {"left", false, &verify_on_click_path}, {"middle", false, &verify_on_click_path}, + {"right", false, &verify_on_click_path}, {"wheel-up", false, &verify_on_click_path}, + {"wheel-down", false, &verify_on_click_path}, {"previous", false, &verify_on_click_path}, + {"next", false, &verify_on_click_path}, {NULL, false, NULL}, + }; + + return conf_verify_dict(chain, node, info); +} + bool conf_verify_color(keychain_t *chain, const struct yml_node *node) { @@ -165,27 +216,30 @@ conf_verify_color(keychain_t *chain, const struct yml_node *node) int v = sscanf(s, "%02x%02x%02x%02x", &r, &g, &b, &a); if (strlen(s) != 8 || v != 4) { - LOG_ERR("%s: value must be a color ('rrggbbaa', e.g ff00ffff)", - conf_err_prefix(chain, node)); + LOG_ERR("%s: value must be a color ('rrggbbaa', e.g ff00ffff)", conf_err_prefix(chain, node)); return false; } return true; } - bool conf_verify_font(keychain_t *chain, const struct yml_node *node) { if (!yml_is_scalar(node)) { - LOG_ERR("%s: font must be a fontconfig-formatted string", - conf_err_prefix(chain, node)); + LOG_ERR("%s: font must be a fontconfig-formatted string", conf_err_prefix(chain, node)); return false; } return true; } +bool +conf_verify_font_shaping(keychain_t *chain, const struct yml_node *node) +{ + return conf_verify_enum(chain, node, (const char *[]){"full", /*"graphemes",*/ "none"}, 2); +} + bool conf_verify_decoration(keychain_t *chain, const struct yml_node *node) { @@ -193,7 +247,8 @@ conf_verify_decoration(keychain_t *chain, const struct yml_node *node) if (yml_dict_length(node) != 1) { LOG_ERR("%s: decoration must be a dictionary with a single key; " - "the name of the particle", conf_err_prefix(chain, node)); + "the name of the particle", + conf_err_prefix(chain, node)); return false; } @@ -209,8 +264,7 @@ conf_verify_decoration(keychain_t *chain, const struct yml_node *node) const struct deco_iface *iface = plugin_load_deco(deco_name); if (iface == NULL) { - LOG_ERR("%s: invalid decoration name: %s", - conf_err_prefix(chain, deco), deco_name); + LOG_ERR("%s: invalid decoration name: %s", conf_err_prefix(chain, deco), deco_name); return false; } @@ -227,10 +281,7 @@ conf_verify_particle_list_items(keychain_t *chain, const struct yml_node *node) { assert(yml_is_list(node)); - for (struct yml_list_iter it = yml_list_iter(node); - it.node != NULL; - yml_list_next(&it)) - { + for (struct yml_list_iter it = yml_list_iter(node); it.node != NULL; yml_list_next(&it)) { if (!conf_verify_particle(chain, it.node)) return false; } @@ -245,7 +296,8 @@ conf_verify_particle_dictionary(keychain_t *chain, const struct yml_node *node) if (yml_dict_length(node) != 1) { LOG_ERR("%s: particle must be a dictionary with a single key; " - "the name of the particle", conf_err_prefix(chain, node)); + "the name of the particle", + conf_err_prefix(chain, node)); return false; } @@ -261,8 +313,7 @@ conf_verify_particle_dictionary(keychain_t *chain, const struct yml_node *node) const struct particle_iface *iface = plugin_load_particle(particle_name); if (iface == NULL) { - LOG_ERR("%s: invalid particle name: %s", - conf_err_prefix(chain, particle), particle_name); + LOG_ERR("%s: invalid particle name: %s", conf_err_prefix(chain, particle), particle_name); return false; } @@ -284,19 +335,18 @@ conf_verify_particle(keychain_t *chain, const struct yml_node *node) else if (yml_is_list(node)) return conf_verify_particle_list_items(chain, node); else { - LOG_ERR("%s: particle must be either a dictionary or a list", - conf_err_prefix(chain, node)); + LOG_ERR("%s: particle must be either a dictionary or a list", conf_err_prefix(chain, node)); return false; } } - static bool verify_module(keychain_t *chain, const struct yml_node *node) { if (!yml_is_dict(node) || yml_dict_length(node) != 1) { LOG_ERR("%s: module must be a dictionary with a single key; " - "the name of the module", conf_err_prefix(chain, node)); + "the name of the module", + conf_err_prefix(chain, node)); return false; } @@ -312,8 +362,7 @@ verify_module(keychain_t *chain, const struct yml_node *node) const struct module_iface *iface = plugin_load_module(mod_name); if (iface == NULL) { - LOG_ERR( - "%s: invalid module name: %s", conf_err_prefix(chain, node), mod_name); + LOG_ERR("%s: invalid module name: %s", conf_err_prefix(chain, node), mod_name); return false; } @@ -335,10 +384,7 @@ verify_module_list(keychain_t *chain, const struct yml_node *node) return false; } - for (struct yml_list_iter it = yml_list_iter(node); - it.node != NULL; - yml_list_next(&it)) - { + for (struct yml_list_iter it = yml_list_iter(node); it.node != NULL; yml_list_next(&it)) { if (!verify_module(chain, it.node)) return false; } @@ -350,14 +396,12 @@ static bool verify_bar_border(keychain_t *chain, const struct yml_node *node) { static const struct attr_info attrs[] = { - {"width", false, &conf_verify_int}, - {"color", false, &conf_verify_color}, - {"margin", false, &conf_verify_int}, - {"left-margin", false, &conf_verify_int}, - {"right-margin", false, &conf_verify_int}, - {"top-margin", false, &conf_verify_int}, - {"bottom-margin", false, &conf_verify_int}, - {NULL, false, NULL}, + {"width", false, &conf_verify_unsigned}, {"left-width", false, &conf_verify_unsigned}, + {"right-width", false, &conf_verify_unsigned}, {"top-width", false, &conf_verify_unsigned}, + {"bottom-width", false, &conf_verify_unsigned}, {"color", false, &conf_verify_color}, + {"margin", false, &conf_verify_unsigned}, {"left-margin", false, &conf_verify_unsigned}, + {"right-margin", false, &conf_verify_unsigned}, {"top-margin", false, &conf_verify_unsigned}, + {"bottom-margin", false, &conf_verify_unsigned}, {NULL, false, NULL}, }; return conf_verify_dict(chain, node, attrs); @@ -369,6 +413,12 @@ verify_bar_location(keychain_t *chain, const struct yml_node *node) return conf_verify_enum(chain, node, (const char *[]){"top", "bottom"}, 2); } +static bool +verify_bar_layer(keychain_t *chain, const struct yml_node *node) +{ + return conf_verify_enum(chain, node, (const char *[]){"overlay", "top", "bottom", "background"}, 4); +} + bool conf_verify_bar(const struct yml_node *bar) { @@ -381,28 +431,32 @@ conf_verify_bar(const struct yml_node *bar) chain_push(&chain, "bar"); static const struct attr_info attrs[] = { - {"height", true, &conf_verify_int}, + {"height", true, &conf_verify_unsigned}, {"location", true, &verify_bar_location}, {"background", true, &conf_verify_color}, {"monitor", false, &conf_verify_string}, + {"layer", false, &verify_bar_layer}, - {"spacing", false, &conf_verify_int}, - {"left-spacing", false, &conf_verify_int}, - {"right-spacing", false, &conf_verify_int}, + {"spacing", false, &conf_verify_unsigned}, + {"left-spacing", false, &conf_verify_unsigned}, + {"right-spacing", false, &conf_verify_unsigned}, - {"margin", false, &conf_verify_int}, - {"left_margin", false, &conf_verify_int}, - {"right_margin", false, &conf_verify_int}, + {"margin", false, &conf_verify_unsigned}, + {"left-margin", false, &conf_verify_unsigned}, + {"right-margin", false, &conf_verify_unsigned}, {"border", false, &verify_bar_border}, {"font", false, &conf_verify_font}, + {"font-shaping", false, &conf_verify_font_shaping}, {"foreground", false, &conf_verify_color}, {"left", false, &verify_module_list}, {"center", false, &verify_module_list}, {"right", false, &verify_module_list}, + {"trackpad-sensitivity", false, &conf_verify_unsigned}, + {NULL, false, NULL}, }; diff --git a/config-verify.h b/config-verify.h index 44a6a66..8ad44ce 100644 --- a/config-verify.h +++ b/config-verify.h @@ -26,22 +26,23 @@ chain_pop(keychain_t *chain) tll_pop_back(*chain); } -const char *conf_err_prefix( - const keychain_t *chain, const struct yml_node *node); - +const char *conf_err_prefix(const keychain_t *chain, const struct yml_node *node); bool conf_verify_string(keychain_t *chain, const struct yml_node *node); bool conf_verify_int(keychain_t *chain, const struct yml_node *node); +bool conf_verify_unsigned(keychain_t *chain, const struct yml_node *node); +bool conf_verify_bool(keychain_t *chain, const struct yml_node *node); -bool conf_verify_enum(keychain_t *chain, const struct yml_node *node, - const char *values[], size_t count); +bool conf_verify_enum(keychain_t *chain, const struct yml_node *node, const char *values[], size_t count); bool conf_verify_list(keychain_t *chain, const struct yml_node *node, bool (*verify)(keychain_t *chain, const struct yml_node *node)); bool conf_verify_dict(keychain_t *chain, const struct yml_node *node, const struct attr_info info[]); /* NULL-terminated list */ +bool conf_verify_on_click(keychain_t *chain, const struct yml_node *node); bool conf_verify_color(keychain_t *chain, const struct yml_node *node); bool conf_verify_font(keychain_t *chain, const struct yml_node *node); +bool conf_verify_font_shaping(keychain_t *chain, const struct yml_node *node); bool conf_verify_particle(keychain_t *chain, const struct yml_node *node); bool conf_verify_particle_list_items(keychain_t *chain, const struct yml_node *node); diff --git a/config.c b/config.c index 556891c..0f80364 100644 --- a/config.c +++ b/config.c @@ -1,9 +1,10 @@ #include "config.h" -#include -#include -#include #include +#include +#include +#include +#include #include @@ -20,9 +21,7 @@ static uint8_t hex_nibble(char hex) { - assert((hex >= '0' && hex <= '9') || - (hex >= 'a' && hex <= 'f') || - (hex >= 'A' && hex <= 'F')); + assert((hex >= '0' && hex <= '9') || (hex >= 'a' && hex <= 'f') || (hex >= 'A' && hex <= 'F')); if (hex >= '0' && hex <= '9') return hex - '0'; @@ -54,12 +53,11 @@ conf_to_color(const struct yml_node *node) uint16_t alpha = hex_byte(&hex[6]); alpha |= alpha << 8; - int alpha_div = 0xffff / alpha; return (pixman_color_t){ - .red = (red << 8 | red) / alpha_div, - .green = (green << 8 | green) / alpha_div, - .blue = (blue << 8 | blue) / alpha_div, + .red = (uint32_t)(red << 8 | red) * alpha / 0xffff, + .green = (uint32_t)(green << 8 | green) * alpha / 0xffff, + .blue = (uint32_t)(blue << 8 | blue) * alpha / 0xffff, .alpha = alpha, }; } @@ -67,7 +65,69 @@ conf_to_color(const struct yml_node *node) struct fcft_font * conf_to_font(const struct yml_node *node) { - return fcft_from_name(1, &(const char *){yml_value_as_string(node)}, NULL); + const char *font_spec = yml_value_as_string(node); + + size_t count = 0; + size_t size = 0; + const char **fonts = NULL; + + char *copy = strdup(font_spec); + for (const char *font = strtok(copy, ","); font != NULL; font = strtok(NULL, ",")) { + /* Trim spaces, strictly speaking not necessary, but looks nice :) */ + while (isspace(font[0])) + font++; + + if (font[0] == '\0') + continue; + + if (count + 1 > size) { + size += 4; + fonts = realloc(fonts, size * sizeof(fonts[0])); + } + + assert(count + 1 <= size); + fonts[count++] = font; + } + + struct fcft_font *ret = fcft_from_name(count, fonts, NULL); + + free(fonts); + free(copy); + return ret; +} + +enum font_shaping +conf_to_font_shaping(const struct yml_node *node) +{ + const char *v = yml_value_as_string(node); + + if (strcmp(v, "none") == 0) + return FONT_SHAPE_NONE; + + else if (strcmp(v, "graphemes") == 0) { + static bool have_warned = false; + + if (!have_warned && !(fcft_capabilities() & FCFT_CAPABILITY_GRAPHEME_SHAPING)) { + have_warned = true; + LOG_WARN("cannot enable grapheme shaping; no support in fcft"); + } + return FONT_SHAPE_GRAPHEMES; + } + + else if (strcmp(v, "full") == 0) { + static bool have_warned = false; + + if (!have_warned && !(fcft_capabilities() & FCFT_CAPABILITY_TEXT_RUN_SHAPING)) { + have_warned = true; + LOG_WARN("cannot enable full text shaping; no support in fcft"); + } + return FONT_SHAPE_FULL; + } + + else { + assert(false); + return FONT_SHAPE_NONE; + } } struct deco * @@ -85,25 +145,20 @@ conf_to_deco(const struct yml_node *node) } static struct particle * -particle_simple_list_from_config(const struct yml_node *node, - struct conf_inherit inherited) +particle_simple_list_from_config(const struct yml_node *node, struct conf_inherit inherited) { size_t count = yml_list_length(node); struct particle *parts[count]; size_t idx = 0; - for (struct yml_list_iter it = yml_list_iter(node); - it.node != NULL; - yml_list_next(&it), idx++) - { + for (struct yml_list_iter it = yml_list_iter(node); it.node != NULL; yml_list_next(&it), idx++) { parts[idx] = conf_to_particle(it.node, inherited); } /* Lazy-loaded function pointer to particle_list_new() */ - static struct particle *(*particle_list_new)( - struct particle *common, - struct particle *particles[], size_t count, - int left_spacing, int right_spacing) = NULL; + static struct particle *(*particle_list_new)(struct particle *common, struct particle *particles[], size_t count, + int left_spacing, int right_spacing) + = NULL; if (particle_list_new == NULL) { const struct plugin *plug = plugin_load("list", PLUGIN_PARTICLE); @@ -112,8 +167,8 @@ particle_simple_list_from_config(const struct yml_node *node, assert(particle_list_new != NULL); } - struct particle *common = particle_common_new( - 0, 0, NULL, fcft_clone(inherited.font), inherited.foreground, NULL); + struct particle *common = particle_common_new(0, 0, NULL, fcft_clone(inherited.font), inherited.font_shaping, + inherited.foreground, NULL); return particle_list_new(common, parts, count, 0, 2); } @@ -132,16 +187,74 @@ conf_to_particle(const struct yml_node *node, struct conf_inherit inherited) const struct yml_node *right_margin = yml_get_value(pair.value, "right-margin"); const struct yml_node *on_click = yml_get_value(pair.value, "on-click"); const struct yml_node *font_node = yml_get_value(pair.value, "font"); + const struct yml_node *font_shaping_node = yml_get_value(pair.value, "font-shaping"); const struct yml_node *foreground_node = yml_get_value(pair.value, "foreground"); const struct yml_node *deco_node = yml_get_value(pair.value, "deco"); - int left = margin != NULL ? yml_value_as_int(margin) : - left_margin != NULL ? yml_value_as_int(left_margin) : 0; - int right = margin != NULL ? yml_value_as_int(margin) : - right_margin != NULL ? yml_value_as_int(right_margin) : 0; + int left = margin != NULL ? yml_value_as_int(margin) : left_margin != NULL ? yml_value_as_int(left_margin) : 0; + int right = margin != NULL ? yml_value_as_int(margin) : right_margin != NULL ? yml_value_as_int(right_margin) : 0; + + char *on_click_templates[MOUSE_BTN_COUNT] = {NULL}; + if (on_click != NULL) { + const char *yml_legacy = yml_value_as_string(on_click); + + if (yml_legacy != NULL) { + char *legacy = NULL; + + if (yml_legacy[0] == '~' && yml_legacy[1] == '/') { + const char *home_dir = getenv("HOME"); + + if (home_dir != NULL) + if (asprintf(&legacy, "%s/%s", home_dir, yml_legacy + 2) < 0) + legacy = NULL; + + if (legacy == NULL) + legacy = strdup(yml_legacy); + } else + legacy = strdup(yml_legacy); + + on_click_templates[MOUSE_BTN_LEFT] = legacy; + } + + else if (yml_is_dict(on_click)) { + for (struct yml_dict_iter it = yml_dict_iter(on_click); it.key != NULL; yml_dict_next(&it)) { + const char *key = yml_value_as_string(it.key); + const char *yml_template = yml_value_as_string(it.value); + + char *template = NULL; + + if (yml_template[0] == '~' && yml_template[1] == '/') { + const char *home_dir = getenv("HOME"); + + if (home_dir != NULL) + if (asprintf(&template, "%s/%s", home_dir, yml_template + 2) < 0) + template = NULL; + + if (template == NULL) + template = strdup(yml_template); + } else + template = strdup(yml_template); + + if (strcmp(key, "left") == 0) + on_click_templates[MOUSE_BTN_LEFT] = template; + else if (strcmp(key, "middle") == 0) + on_click_templates[MOUSE_BTN_MIDDLE] = template; + else if (strcmp(key, "right") == 0) + on_click_templates[MOUSE_BTN_RIGHT] = template; + else if (strcmp(key, "wheel-up") == 0) + on_click_templates[MOUSE_BTN_WHEEL_UP] = template; + else if (strcmp(key, "wheel-down") == 0) + on_click_templates[MOUSE_BTN_WHEEL_DOWN] = template; + else if (strcmp(key, "previous") == 0) + on_click_templates[MOUSE_BTN_PREVIOUS] = template; + else if (strcmp(key, "next") == 0) + on_click_templates[MOUSE_BTN_NEXT] = template; + else + assert(false); + } + } + } - const char *on_click_template - = on_click != NULL ? yml_value_as_string(on_click) : NULL; struct deco *deco = deco_node != NULL ? conf_to_deco(deco_node) : NULL; /* @@ -153,14 +266,14 @@ conf_to_particle(const struct yml_node *node, struct conf_inherit inherited) * clone the font, since each particle takes ownership of its own * font. */ - struct fcft_font *font = font_node != NULL - ? conf_to_font(font_node) : fcft_clone(inherited.font); - pixman_color_t foreground = foreground_node != NULL - ? conf_to_color(foreground_node) : inherited.foreground; + struct fcft_font *font = font_node != NULL ? conf_to_font(font_node) : fcft_clone(inherited.font); + enum font_shaping font_shaping + = font_shaping_node != NULL ? conf_to_font_shaping(font_shaping_node) : inherited.font_shaping; + pixman_color_t foreground = foreground_node != NULL ? conf_to_color(foreground_node) : inherited.foreground; /* Instantiate base/common particle */ - struct particle *common = particle_common_new( - left, right, on_click_template, font, foreground, deco); + struct particle *common + = particle_common_new(left, right, on_click_templates, font, font_shaping, foreground, deco); const struct particle_iface *iface = plugin_load_particle(type); @@ -176,6 +289,8 @@ conf_to_bar(const struct yml_node *bar, enum bar_backend backend) struct bar_config conf = { .backend = backend, + .layer = BAR_LAYER_BOTTOM, + .font_shaping = FONT_SHAPE_FULL, }; /* @@ -186,8 +301,7 @@ conf_to_bar(const struct yml_node *bar, enum bar_backend backend) conf.height = yml_value_as_int(height); const struct yml_node *location = yml_get_value(bar, "location"); - conf.location = strcmp(yml_value_as_string(location), "top") == 0 - ? BAR_TOP : BAR_BOTTOM; + conf.location = strcmp(yml_value_as_string(location), "top") == 0 ? BAR_TOP : BAR_BOTTOM; const struct yml_node *background = yml_get_value(bar, "background"); conf.background = conf_to_color(background); @@ -200,6 +314,21 @@ conf_to_bar(const struct yml_node *bar, enum bar_backend backend) if (monitor != NULL) conf.monitor = yml_value_as_string(monitor); + const struct yml_node *layer = yml_get_value(bar, "layer"); + if (layer != NULL) { + const char *tmp = yml_value_as_string(layer); + if (strcmp(tmp, "overlay") == 0) + conf.layer = BAR_LAYER_OVERLAY; + else if (strcmp(tmp, "top") == 0) + conf.layer = BAR_LAYER_TOP; + else if (strcmp(tmp, "bottom") == 0) + conf.layer = BAR_LAYER_BOTTOM; + else if (strcmp(tmp, "background") == 0) + conf.layer = BAR_LAYER_BACKGROUND; + else + assert(false); + } + const struct yml_node *spacing = yml_get_value(bar, "spacing"); if (spacing != NULL) conf.left_spacing = conf.right_spacing = yml_value_as_int(spacing); @@ -224,9 +353,16 @@ conf_to_bar(const struct yml_node *bar, enum bar_backend backend) if (right_margin != NULL) conf.right_margin = yml_value_as_int(right_margin); + const struct yml_node *trackpad_sensitivity = yml_get_value(bar, "trackpad-sensitivity"); + conf.trackpad_sensitivity = trackpad_sensitivity != NULL ? yml_value_as_int(trackpad_sensitivity) : 30; + const struct yml_node *border = yml_get_value(bar, "border"); if (border != NULL) { const struct yml_node *width = yml_get_value(border, "width"); + const struct yml_node *left_width = yml_get_value(border, "left-width"); + const struct yml_node *right_width = yml_get_value(border, "right-width"); + const struct yml_node *top_width = yml_get_value(border, "top-width"); + const struct yml_node *bottom_width = yml_get_value(border, "bottom-width"); const struct yml_node *color = yml_get_value(border, "color"); const struct yml_node *margin = yml_get_value(border, "margin"); const struct yml_node *left_margin = yml_get_value(border, "left-margin"); @@ -235,16 +371,24 @@ conf_to_bar(const struct yml_node *bar, enum bar_backend backend) const struct yml_node *bottom_margin = yml_get_value(border, "bottom-margin"); if (width != NULL) - conf.border.width = yml_value_as_int(width); + conf.border.left_width = conf.border.right_width = conf.border.top_width = conf.border.bottom_width + = yml_value_as_int(width); + + if (left_width != NULL) + conf.border.left_width = yml_value_as_int(left_width); + if (right_width != NULL) + conf.border.right_width = yml_value_as_int(right_width); + if (top_width != NULL) + conf.border.top_width = yml_value_as_int(top_width); + if (bottom_width != NULL) + conf.border.bottom_width = yml_value_as_int(bottom_width); if (color != NULL) conf.border.color = conf_to_color(color); if (margin != NULL) - conf.border.left_margin = - conf.border.right_margin = - conf.border.top_margin = - conf.border.bottom_margin = yml_value_as_int(margin); + conf.border.left_margin = conf.border.right_margin = conf.border.top_margin = conf.border.bottom_margin + = yml_value_as_int(margin); if (left_margin != NULL) conf.border.left_margin = yml_value_as_int(left_margin); @@ -264,6 +408,7 @@ conf_to_bar(const struct yml_node *bar, enum bar_backend backend) * foreground color at top-level. */ struct fcft_font *font = fcft_from_name(1, &(const char *){"sans"}, NULL); + enum font_shaping font_shaping = FONT_SHAPE_FULL; pixman_color_t foreground = {0xffff, 0xffff, 0xffff, 0xffff}; /* White */ const struct yml_node *font_node = yml_get_value(bar, "font"); @@ -272,12 +417,17 @@ conf_to_bar(const struct yml_node *bar, enum bar_backend backend) font = conf_to_font(font_node); } + const struct yml_node *font_shaping_node = yml_get_value(bar, "font-shaping"); + if (font_shaping_node != NULL) + font_shaping = conf_to_font_shaping(font_shaping_node); + const struct yml_node *foreground_node = yml_get_value(bar, "foreground"); if (foreground_node != NULL) foreground = conf_to_color(foreground_node); struct conf_inherit inherited = { .font = font, + .font_shaping = font_shaping, .foreground = foreground, }; @@ -293,10 +443,7 @@ conf_to_bar(const struct yml_node *bar, enum bar_backend backend) struct module **mods = calloc(count, sizeof(*mods)); size_t idx = 0; - for (struct yml_list_iter it = yml_list_iter(node); - it.node != NULL; - yml_list_next(&it), idx++) - { + for (struct yml_list_iter it = yml_list_iter(node); it.node != NULL; yml_list_next(&it), idx++) { struct yml_dict_iter m = yml_dict_iter(it.node); const char *mod_name = yml_value_as_string(m.key); @@ -307,14 +454,14 @@ conf_to_bar(const struct yml_node *bar, enum bar_backend backend) * applied to all its particles. */ const struct yml_node *mod_font = yml_get_value(m.value, "font"); - const struct yml_node *mod_foreground = yml_get_value( - m.value, "foreground"); + const struct yml_node *mod_font_shaping = yml_get_value(m.value, "font-shaping"); + const struct yml_node *mod_foreground = yml_get_value(m.value, "foreground"); struct conf_inherit mod_inherit = { - .font = mod_font != NULL - ? conf_to_font(mod_font) : inherited.font, - .foreground = mod_foreground != NULL - ? conf_to_color(mod_foreground) : inherited.foreground, + .font = mod_font != NULL ? conf_to_font(mod_font) : inherited.font, + .font_shaping + = mod_font_shaping != NULL ? conf_to_font_shaping(mod_font_shaping) : inherited.font_shaping, + .foreground = mod_foreground != NULL ? conf_to_color(mod_foreground) : inherited.foreground, }; const struct module_iface *iface = plugin_load_module(mod_name); diff --git a/config.h b/config.h index 56f5b2e..86c2f4e 100644 --- a/config.h +++ b/config.h @@ -1,8 +1,9 @@ #pragma once -#include -#include "yml.h" #include "bar/bar.h" +#include "font-shaping.h" +#include "yml.h" +#include struct bar; struct particle; @@ -16,12 +17,13 @@ struct bar *conf_to_bar(const struct yml_node *bar, enum bar_backend backend); pixman_color_t conf_to_color(const struct yml_node *node); struct fcft_font *conf_to_font(const struct yml_node *node); +enum font_shaping conf_to_font_shaping(const struct yml_node *node); struct conf_inherit { const struct fcft_font *font; + enum font_shaping font_shaping; pixman_color_t foreground; }; -struct particle *conf_to_particle( - const struct yml_node *node, struct conf_inherit inherited); +struct particle *conf_to_particle(const struct yml_node *node, struct conf_inherit inherited); struct deco *conf_to_deco(const struct yml_node *node); diff --git a/decoration.h b/decoration.h index efb79e2..ba44e43 100644 --- a/decoration.h +++ b/decoration.h @@ -4,10 +4,11 @@ struct deco { void *private; - void (*expose)(const struct deco *deco, pixman_image_t *pix, - int x, int y, int width, int height); + void (*expose)(const struct deco *deco, pixman_image_t *pix, int x, int y, int width, int height); void (*destroy)(struct deco *deco); }; -#define DECORATION_COMMON_ATTRS \ - {NULL, false, NULL} +#define DECORATION_COMMON_ATTRS \ + { \ + NULL, false, NULL \ + } diff --git a/decorations/background.c b/decorations/background.c index b3b9ed2..f1430f1 100644 --- a/decorations/background.c +++ b/decorations/background.c @@ -1,12 +1,13 @@ #include -#include "../config.h" #include "../config-verify.h" +#include "../config.h" #include "../decoration.h" #include "../plugin.h" -struct private { - //struct rgba color; +struct private +{ + // struct rgba color; pixman_color_t color; }; @@ -22,9 +23,7 @@ static void expose(const struct deco *deco, pixman_image_t *pix, int x, int y, int width, int height) { const struct private *d = deco->private; - pixman_image_fill_rectangles( - PIXMAN_OP_OVER, pix, &d->color, 1, - &(pixman_rectangle16_t){x, y, width, height}); + pixman_image_fill_rectangles(PIXMAN_OP_OVER, pix, &d->color, 1, &(pixman_rectangle16_t){x, y, width, height}); } static struct deco * diff --git a/decorations/border.c b/decorations/border.c new file mode 100644 index 0000000..e93fc4e --- /dev/null +++ b/decorations/border.c @@ -0,0 +1,91 @@ +#include + +#include "../config-verify.h" +#include "../config.h" +#include "../decoration.h" +#include "../plugin.h" + +#define LOG_MODULE "border" +#define LOG_ENABLE_DBG 0 +#include "../log.h" + +#define min(x, y) ((x) < (y) ? (x) : (y)) +#define max(x, y) ((x) > (y) ? (x) : (y)) + +struct private +{ + pixman_color_t color; + int size; +}; + +static void +destroy(struct deco *deco) +{ + struct private *d = deco->private; + free(d); + free(deco); +} + +static void +expose(const struct deco *deco, pixman_image_t *pix, int x, int y, int width, int height) +{ + const struct private *d = deco->private; + pixman_image_fill_rectangles(PIXMAN_OP_OVER, pix, &d->color, 4, + (pixman_rectangle16_t[]){ + /* Top */ + {x, y, width, min(d->size, height)}, + + /* Bottom */ + {x, max(y + height - d->size, y), width, min(d->size, height)}, + + /* Left */ + {x, y, min(d->size, width), height}, + + /* Right */ + {max(x + width - d->size, x), y, min(d->size, width), height}, + }); +} + +static struct deco * +border_new(pixman_color_t color, int size) +{ + struct private *priv = calloc(1, sizeof(*priv)); + priv->color = color; + priv->size = size; + + struct deco *deco = calloc(1, sizeof(*deco)); + deco->private = priv; + deco->expose = &expose; + deco->destroy = &destroy; + + return deco; +} + +static struct deco * +from_conf(const struct yml_node *node) +{ + const struct yml_node *color = yml_get_value(node, "color"); + const struct yml_node *size = yml_get_value(node, "size"); + return border_new(conf_to_color(color), size != NULL ? yml_value_as_int(size) : 1); +} + +static bool +verify_conf(keychain_t *chain, const struct yml_node *node) +{ + static const struct attr_info attrs[] = { + {"color", true, &conf_verify_color}, + {"size", false, &conf_verify_unsigned}, + DECORATION_COMMON_ATTRS, + }; + + return conf_verify_dict(chain, node, attrs); +} + +const struct deco_iface deco_border_iface = { + .verify_conf = &verify_conf, + .from_conf = &from_conf, +}; + +#if defined(CORE_PLUGINS_AS_SHARED_LIBRARIES) +extern const struct deco_iface iface __attribute__((weak, alias("deco_border_iface"))); +#endif diff --git a/decorations/meson.build b/decorations/meson.build index 708267e..c64164c 100644 --- a/decorations/meson.build +++ b/decorations/meson.build @@ -1,7 +1,7 @@ deco_sdk = declare_dependency(dependencies: [pixman, tllist, fcft]) decorations = [] -foreach deco : ['background', 'stack', 'underline'] +foreach deco : ['background', 'border', 'stack', 'underline', 'overline'] if plugs_as_libs shared_module('@0@'.format(deco), '@0@.c'.format(deco), dependencies: deco_sdk, diff --git a/decorations/overline.c b/decorations/overline.c new file mode 100644 index 0000000..e9ff8be --- /dev/null +++ b/decorations/overline.c @@ -0,0 +1,71 @@ +#include + +#include "../config-verify.h" +#include "../config.h" +#include "../decoration.h" +#include "../plugin.h" + +struct private +{ + int size; + pixman_color_t color; +}; + +static void +destroy(struct deco *deco) +{ + struct private *d = deco->private; + free(d); + free(deco); +} + +static void +expose(const struct deco *deco, pixman_image_t *pix, int x, int y, int width, int height) +{ + const struct private *d = deco->private; + pixman_image_fill_rectangles(PIXMAN_OP_OVER, pix, &d->color, 1, &(pixman_rectangle16_t){x, y, width, d->size}); +} + +static struct deco * +overline_new(int size, pixman_color_t color) +{ + struct private *priv = calloc(1, sizeof(*priv)); + priv->size = size; + priv->color = color; + + struct deco *deco = calloc(1, sizeof(*deco)); + deco->private = priv; + deco->expose = &expose; + deco->destroy = &destroy; + + return deco; +} + +static struct deco * +from_conf(const struct yml_node *node) +{ + const struct yml_node *size = yml_get_value(node, "size"); + const struct yml_node *color = yml_get_value(node, "color"); + return overline_new(yml_value_as_int(size), conf_to_color(color)); +} + +static bool +verify_conf(keychain_t *chain, const struct yml_node *node) +{ + static const struct attr_info attrs[] = { + {"size", true, &conf_verify_unsigned}, + {"color", true, &conf_verify_color}, + DECORATION_COMMON_ATTRS, + }; + + return conf_verify_dict(chain, node, attrs); +} + +const struct deco_iface deco_overline_iface = { + .verify_conf = &verify_conf, + .from_conf = &from_conf, +}; + +#if defined(CORE_PLUGINS_AS_SHARED_LIBRARIES) +extern const struct deco_iface iface __attribute__((weak, alias("deco_overline_iface"))); +#endif diff --git a/decorations/stack.c b/decorations/stack.c index 16c58ee..d8420d2 100644 --- a/decorations/stack.c +++ b/decorations/stack.c @@ -1,13 +1,14 @@ #include #define LOG_MODULE "stack" -#include "../log.h" -#include "../config.h" #include "../config-verify.h" +#include "../config.h" #include "../decoration.h" +#include "../log.h" #include "../plugin.h" -struct private { +struct private +{ struct deco **decos; size_t count; }; @@ -57,10 +58,7 @@ from_conf(const struct yml_node *node) struct deco *decos[count]; size_t idx = 0; - for (struct yml_list_iter it = yml_list_iter(node); - it.node != NULL; - yml_list_next(&it), idx++) - { + for (struct yml_list_iter it = yml_list_iter(node); it.node != NULL; yml_list_next(&it), idx++) { decos[idx] = conf_to_deco(it.node); } @@ -75,10 +73,7 @@ verify_conf(keychain_t *chain, const struct yml_node *node) return false; } - for (struct yml_list_iter it = yml_list_iter(node); - it.node != NULL; - yml_list_next(&it)) - { + for (struct yml_list_iter it = yml_list_iter(node); it.node != NULL; yml_list_next(&it)) { if (!conf_verify_decoration(chain, it.node)) return false; } diff --git a/decorations/underline.c b/decorations/underline.c index a700bec..0175116 100644 --- a/decorations/underline.c +++ b/decorations/underline.c @@ -1,11 +1,12 @@ #include -#include "../config.h" #include "../config-verify.h" +#include "../config.h" #include "../decoration.h" #include "../plugin.h" -struct private { +struct private +{ int size; pixman_color_t color; }; @@ -22,9 +23,8 @@ static void expose(const struct deco *deco, pixman_image_t *pix, int x, int y, int width, int height) { const struct private *d = deco->private; - pixman_image_fill_rectangles( - PIXMAN_OP_OVER, pix, &d->color, 1, - &(pixman_rectangle16_t){x, y + height - d->size, width, d->size}); + pixman_image_fill_rectangles(PIXMAN_OP_OVER, pix, &d->color, 1, + &(pixman_rectangle16_t){x, y + height - d->size, width, d->size}); } static struct deco * @@ -54,7 +54,7 @@ static bool verify_conf(keychain_t *chain, const struct yml_node *node) { static const struct attr_info attrs[] = { - {"size", true, &conf_verify_int}, + {"size", true, &conf_verify_unsigned}, {"color", true, &conf_verify_color}, DECORATION_COMMON_ATTRS, }; diff --git a/doc/meson.build b/doc/meson.build index b4dccb3..e801bf1 100644 --- a/doc/meson.build +++ b/doc/meson.build @@ -1,11 +1,86 @@ sh = find_program('sh', native: true) scdoc = dependency('scdoc', native: true) -scdoc_prog = find_program(scdoc.get_pkgconfig_variable('scdoc'), native: true) +scdoc_prog = find_program(scdoc.get_variable('scdoc'), native: true) -foreach man_src : ['yambar.1.scd', 'yambar.5.scd', 'yambar-decorations.5.scd', - 'yambar-modules.5.scd', 'yambar-particles.5.scd', - 'yambar-tags.5.scd'] +plugin_pages = [] +if plugin_alsa_enabled + plugin_pages += ['yambar-modules-alsa.5.scd'] +endif +if plugin_backlight_enabled + plugin_pages += ['yambar-modules-backlight.5.scd'] +endif +if plugin_battery_enabled + plugin_pages += ['yambar-modules-battery.5.scd'] +endif +if plugin_clock_enabled + plugin_pages += ['yambar-modules-clock.5.scd'] +endif +if plugin_cpu_enabled + plugin_pages += ['yambar-modules-cpu.5.scd'] +endif +if plugin_disk_io_enabled + plugin_pages += ['yambar-modules-disk-io.5.scd'] +endif +if plugin_dwl_enabled + plugin_pages += ['yambar-modules-dwl.5.scd'] +endif +if plugin_foreign_toplevel_enabled + plugin_pages += ['yambar-modules-foreign-toplevel.5.scd'] +endif +if plugin_mem_enabled + plugin_pages += ['yambar-modules-mem.5.scd'] +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'] +endif +if plugin_label_enabled + plugin_pages += ['yambar-modules-label.5.scd'] +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 +if plugin_pulse_enabled + plugin_pages += ['yambar-modules-pulse.5.scd'] +endif +if plugin_removables_enabled + plugin_pages += ['yambar-modules-removables.5.scd'] +endif +if plugin_river_enabled + plugin_pages += ['yambar-modules-river.5.scd'] +endif +if plugin_script_enabled + plugin_pages += ['yambar-modules-script.5.scd'] +endif +if plugin_sway_xkb_enabled + plugin_pages += ['yambar-modules-sway-xkb.5.scd'] +endif +if plugin_xkb_enabled + plugin_pages += ['yambar-modules-xkb.5.scd'] +endif + +foreach man_src : ['yambar.1.scd', + 'yambar.5.scd', + 'yambar-decorations.5.scd', + 'yambar-modules.5.scd', + 'yambar-particles.5.scd', + 'yambar-tags.5.scd'] + plugin_pages parts = man_src.split('.') name = parts[-3] section = parts[-2] @@ -15,7 +90,7 @@ foreach man_src : ['yambar.1.scd', 'yambar.5.scd', 'yambar-decorations.5.scd', out, output: out, input: man_src, - command: [sh, '-c', '@0@ < @INPUT@'.format(scdoc_prog.path())], + command: [sh, '-c', '@0@ < @INPUT@'.format(scdoc_prog.full_path())], capture: true, install: true, install_dir: join_paths(get_option('mandir'), 'man@0@'.format(section))) diff --git a/doc/yambar-decorations.5.scd b/doc/yambar-decorations.5.scd index 48dcc87..3d7c379 100644 --- a/doc/yambar-decorations.5.scd +++ b/doc/yambar-decorations.5.scd @@ -23,7 +23,7 @@ This decoration sets the particles background color. [[ *Name* :[ *Type* :[ *Req* -:[ *Description* +:< *Description* | color : color : yes @@ -49,7 +49,7 @@ bottom of the particle. [[ *Name* :[ *Type* :[ *Req* -:[ *Description* +:< *Description* | size : int : yes @@ -70,9 +70,74 @@ content: color: ff0000ff ``` + +# OVERLINE + +Similar to _underline_, this decoration renders a line of configurable +size and color at the top of the particle. + +## CONFIGURATION + +[[ *Name* +:[ *Type* +:[ *Req* +:< *Description* +| size +: int +: yes +: The size (height/thickness) of the line, in pixels +| color +: color +: yes +: The color of the line. See *yambar*(5) for format. + +## EXAMPLES + +``` +content: + string: + deco: + overline: + size: 2 + color: ff0000ff +``` + + +# BORDER + +This decoration renders a border of configurable size (i.e border +width) around the particle. + +## CONFIGURATION + +[[ *Name* +:[ *Type* +:[ *Req* +:< *Description* +| color +: color +: yes +: The color of the line. See *yambar*(5) for format. +| size +: int +: no +: Border width, in pixels. Defaults to 1px. + +## EXAMPLES + +``` +content: + string: + deco: + border: + size: 2 + color: ff0000ff +``` + + # STACK -This particles combines multiple decorations. +This particle combines multiple decorations. ## CONFIGURATION diff --git a/doc/yambar-modules-alsa.5.scd b/doc/yambar-modules-alsa.5.scd new file mode 100644 index 0000000..14804e5 --- /dev/null +++ b/doc/yambar-modules-alsa.5.scd @@ -0,0 +1,70 @@ +yambar-modules-alsa(5) + +# NAME +alsa - Monitors an alsa soundcard for volume and mute/unmute changes + +# TAGS + +[[ *Name* +:[ *Type* +:< *Description* +| online +: bool +: True when the ALSA device has successfully been opened +| dB +: range +: Volume level (in dB), with min and max as start and end range + values. +| volume +: range +: Volume level (raw), with min and max as start and end range values +| percent +: range +: Volume level, as a percentage. This value is based on the *dB* tag + if available, otherwise the *volume* tag. +| muted +: bool +: True if muted, otherwise false + + +# CONFIGURATION + +[[ *Name* +:[ *Type* +:[ *Req* +:< *Description* +| card +: string +: yes +: The soundcard name. *default* might work. +| mixer +: string +: yes +: Mixer channel to monitor. _Master_ might work. +| volume +: string +: no +: The name of the channel to use as source for the volume level + (default: first available channel, usually "Front Left"). +| muted +: string +: no +: The name of the channel to use as source for the muted state + (default: first available channel, usually "Front Left"). + + +# EXAMPLES + +``` +bar: + left: + - alsa: + card: hw:PCH + mixer: Master + content: {string: {text: "{volume}"}} +``` + +# SEE ALSO + +*yambar-modules*(5), *yambar-particles*(5), *yambar-tags*(5), *yambar-decorations*(5) + diff --git a/doc/yambar-modules-backlight.5.scd b/doc/yambar-modules-backlight.5.scd new file mode 100644 index 0000000..6fa3a9a --- /dev/null +++ b/doc/yambar-modules-backlight.5.scd @@ -0,0 +1,47 @@ +yambar-modules-backlight(5) + +# NAME +backlight - This module reads monitor backlight status + +# DESCRIPTION +This module reads monitor backlight status from +_/sys/class/backlight_, and uses *udev* to monitor for changes. + +# TAGS + +[[ *Name* +:[ *Type* +:< *Description* +| brightness +: range +: The current brightness level, in absolute value +| percent +: range +: The current brightness level, in percent + +# CONFIGURATION + +[[ *Name* +:[ *Type* +:[ *Req* +:[ *Description* +| name +: string +: yes +: The backlight device's name (one of the names in */sys/class/backlight*) + +# EXAMPLES + +``` +bar: + left: + - backlight: + name: intel_backlight + content: + string: {text: "backlight: {percent}%"} +``` + +# SEE ALSO + +*yambar-modules*(5), *yambar-particles*(5), *yambar-tags*(5), *yambar-decorations*(5) + diff --git a/doc/yambar-modules-battery.5.scd b/doc/yambar-modules-battery.5.scd new file mode 100644 index 0000000..aab2106 --- /dev/null +++ b/doc/yambar-modules-battery.5.scd @@ -0,0 +1,90 @@ +yambar-modules-battery(5) + +# NAME +battery - This module reads battery status + +# DESCRIPTION + +This module reads battery status from _/sys/class/power_supply_ and +uses *udev* to monitor for changes. + +Note that it is common (and "normal") for batteries to be in the state +*unknown* under certain conditions. + +For example, some have been seen to enter the *unknown* state when +charging and the capacity reaches ~90%. The battery then stays in +*unknown*, rather than *charging*, until it has been fully charged and +enters the state *full*. + +This does not happen with all batteries, and other batteries may enter +the state *unknown* under other conditions. + +# TAGS + +[[ *Name* +:[ *Type* +:< *Description* +| name +: string +: Battery device name +| manufacturer +: string +: Name of the battery manufacturer +| model +: string +: Battery model name +| state +: string +: One of *full*, *not charging*, *charging*, *discharging* or *unknown* +| capacity +: range +: capacity left, in percent +| estimate +: string +: Estimated time left (to empty while discharging, or to full while + charging), formatted as HH:MM. + +# CONFIGURATION + +[[ *Name* +:[ *Type* +:[ *Req* +:< *Description* +| name +: string +: yes +: Battery device name (one of the names in */sys/class/power_supply*) +| poll-interval +: int +: no +: How often, in milliseconds, to poll for capacity changes + (default=*60000*). Set to `0` to disable polling (*warning*: many + batteries do not support asynchronous reporting). Cannot be less + than 250ms. +| battery-scale +: int +: no +: How much to scale down the battery charge amount. Some batteries + report too high resulting in bad discharge estimates. Default=1. +| smoothing-secs +: int +: no +: How many seconds to perform smoothing over for battery discharge + estimates. Default=100s. + +# EXAMPLES + +``` +bar: + left: + - battery: + name: BAT0 + poll-interval: 30000 + content: + string: {text: "BAT: {capacity}% {estimate}"} +``` + +# SEE ALSO + +*yambar-modules*(5), *yambar-particles*(5), *yambar-tags*(5), *yambar-decorations*(5) + diff --git a/doc/yambar-modules-clock.5.scd b/doc/yambar-modules-clock.5.scd new file mode 100644 index 0000000..bf3506b --- /dev/null +++ b/doc/yambar-modules-clock.5.scd @@ -0,0 +1,51 @@ +yambar-modules-clock(5) + +# NAME +clock - This module provides the current date and time + +# TAGS + +[[ *Name* +:[ *Type* +:< *Description* +| time +: string +: Current time, formatted using the _time-format_ attribute +| date +: string +: Current date, formatted using the _date-format_ attribute + +# CONFIGURATION + +[[ *Name* +:[ *Type* +:[ *Req* +:< *Description* +| time-format +: string +: no +: *strftime* formatter for the _time_ tag (default=*%H:%M*) +| date-format +: string +: no +: *strftime* formatter for the _date_ date (default=*%x*) +| utc +: bool +: no +: Use GMT instead of local timezone (default=false) + +# EXAMPLES + +``` +bar: + left: + - clock: + time-format: "%H:%M %Z" + content: + string: {text: "{date} {time}"} +``` + +# SEE ALSO + +*yambar-modules*(5), *yambar-particles*(5), *yambar-tags*(5), *yambar-decorations*(5) + diff --git a/doc/yambar-modules-cpu.5.scd b/doc/yambar-modules-cpu.5.scd new file mode 100644 index 0000000..090ccdd --- /dev/null +++ b/doc/yambar-modules-cpu.5.scd @@ -0,0 +1,79 @@ +yambar-modules-cpu(5) + +# NAME +cpu - This module provides the CPU usage + +# DESCRIPTION + +This module reports CPU usage, in percent. The _content_ particle is a +template that is instantiated once for each core, and once for the +total CPU usage. + +# TAGS + +[[ *Name* +:[ *Type* +:< *Description* +| id +: int +: Core ID. 0..n represents individual cores, and -1 represents the + total usage +| cpu +: range +: Current usage of CPU core {id}, in percent + +# CONFIGURATION + +[[ *Name* +:[ *Type* +:[ *Req* +:< *Description* +| poll-interval +: int +: no +: Refresh interval of the CPU usage stats in milliseconds + (default=500). Cannot be less then 250ms. + +# EXAMPLES + +## Display total CPU usage as a number +``` +bar: + left: + - cpu: + poll-interval: 2500 + content: + map: + conditions: + id < 0: + - string: {text: , font: Font Awesome 6 Free:style=solid} + - string: {text: "{cpu}%"} +``` + +## Display a vertical bar for each core +``` +bar: + left: + - cpu: + poll-interval: 2500 + content: + map: + conditions: + id >= 0: + - ramp: + tag: cpu + items: + - string: {text: ▁} + - string: {text: ▂} + - string: {text: ▃} + - string: {text: ▄} + - string: {text: ▅} + - string: {text: ▆} + - string: {text: ▇} + - string: {text: █} +``` + + +# SEE ALSO + +*yambar-modules*(5), *yambar-particles*(5), *yambar-tags*(5), *yambar-decorations*(5) diff --git a/doc/yambar-modules-disk-io.5.scd b/doc/yambar-modules-disk-io.5.scd new file mode 100644 index 0000000..3f51e79 --- /dev/null +++ b/doc/yambar-modules-disk-io.5.scd @@ -0,0 +1,85 @@ +yambar-modules-disk-io(5) + +# NAME +disk-io - This module keeps track of the amount of bytes being +read/written from/to disk. It can distinguish between all partitions +currently present in the machine. + +# TAGS + +[[ *Name* +:[ *Type* +:< *Description* +| device +: string +: Name of the device being tracked (use the command *lsblk* to see these). + There is a special device, "Total", that reports the total stats + 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. +| read_speed +: int +: bytes read, in bytes/s +| write_speed +: int +: bytes written, in bytes/s +| ios_in_progress +: int +: number of ios that are happening at the time of polling + +# CONFIGURATION + +[[ *Name* +:[ *Type* +:[ *Req* +:< *Description* +| poll-interval +: int +: no +: Refresh interval of disk's stats in milliseconds (default=500). + Cannot be less then 250ms. + +# EXAMPLES + +This reports the total amount of bytes being read and written every second, +formatting in b/s, kb/s, mb/s, or gb/s, as appropriate. + +``` +bar: + left: + - disk-io: + poll-interval: 1000 + content: + map: + conditions: + device == Total: + list: + items: + - string: {text: "Total read: "} + - map: + default: {string: {text: "{read_speed} B/s"}} + conditions: + read_speed > 1073741824: + string: {text: "{read_speed:gib} GB/s"} + read_speed > 1048576: + string: {text: "{read_speed:mib} MB/s"} + read_speed > 1024: + string: {text: "{read_speed:kib} KB/s"} + - string: {text: " | "} + - string: {text: "Total written: "} + - map: + default: {string: {text: "{write_speed} B/s"}} + conditions: + write_speed > 1073741824: + string: {text: "{write_speed:gib} GB/s"} + write_speed > 1048576: + string: {text: "{write_speed:mib} MB/s"} + write_speed > 1024: + string: {text: "{write_speed:kib} KB/s"} +``` + +# SEE ALSO + +*yambar-modules*(5), *yambar-particles*(5), *yambar-tags*(5), *yambar-decorations*(5) diff --git a/doc/yambar-modules-dwl.5.scd b/doc/yambar-modules-dwl.5.scd new file mode 100644 index 0000000..1562e56 --- /dev/null +++ b/doc/yambar-modules-dwl.5.scd @@ -0,0 +1,107 @@ +yambar-modules-dwl(5) + +# NAME +dwl - This module provides information about dwl tags, and information. + +# DESCRIPTION + +This module provides a map of each tags present in dwl. + +Each tags has its _id_, its _name_, its status (_selected_, _empty_, _urgent_) +and the global data like _title_, _appid_, _fullscreen_, _floating_, +_selmon_, and _layout_). The tags start a 1. For needs where +you only want information about the global data and not the _tags_, +there is a tag with the id _0_ that contains only the global data. + +This module will track *only* the monitor where yambar was launched on. +If you have a multi monitor setup, please launch yambar on each of your +monitors. + +Please, be aware that only *one instance* of this module is supported. +Running multiple instances at the same time may result in +*undefined behavior*. + +# TAGS + +[[ *Name* +:[ *Type* +:< *Description* +| id +: int +: dwl tag id. +| name +: string +: The name of the tag (defaults to _id_ if not set). +| selected +: bool +: True if the tag is currently selected. +| empty +: bool +: True if there are no windows in the tag. +| urgent +: bool +: True if the tag has the urgent flag set. +| title +: string +: The currently focused window's title. +| appid +: string +: The currently focused window's application id. +| fullscreen +: bool +: True if there is a fullscreen window in the current tag. +| floating +: bool +: True if there is a floating window in the current tag. +| selmon +: bool +: True if the monitor is actually focused. +| layout +: string +: The actual layout name of the tag. + +# CONFIGURATION + +[[ *Name* +:[ *Type* +:[ *Req* +:< *Description* +| number-of-tags +: int +: yes +: The number of defined tags in the dwl `config.def.h`. +| name-of-tags +: list +: false +: The name of the tags (must have the same length that _number-of-tags_). +| dwl-info-filename +: string +: yes +: The filepath to the log emitted by dwl when running. + +# EXAMPLES + +``` +bar: + left: + - dwl: + number-of-tags: 9 + dwl-info-filename: "/home/ogromny/dwl_info" + name-of-tags: [ , , , , , , , ,  ] + content: + list: + items: + - map: + conditions: + # default tag + id == 0: {string: {text: "{layout} {title}"}} + + selected: {string: {text: "-> {name}"}} + ~empty: {string: {text: "{name}"}} + urgent: {string: {text: "=> {name} <="}} +``` + +# SEE ALSO + +*yambar-modules*(5), *yambar-particles*(5), *yambar-tags*(5), *yambar-decorations*(5) + diff --git a/doc/yambar-modules-foreign-toplevel.5.scd b/doc/yambar-modules-foreign-toplevel.5.scd new file mode 100644 index 0000000..8b1d576 --- /dev/null +++ b/doc/yambar-modules-foreign-toplevel.5.scd @@ -0,0 +1,78 @@ +yambar-modules-foreign-toplevel(5) + +# NAME +foreign-toplevel - This module provides information about toplevel windows in Wayland + +# DESCRIPTION + +This module uses the _wlr foreign toplevel management_ Wayland +protocol to provide information about currently open windows, such as +their application ID, window title, and their current state +(maximized/minimized/fullscreen/activated). + +The configuration for the foreign-toplevel module specifies a +_template_ particle. This particle will be instantiated once for each +window. + +Note: Wayland only. + + +# TAGS + +[[ *Name* +:[ *Type* +:< *Description* +| app-id +: string +: The application ID (typically the application name) +| title +: string +: The window title +| maximized +: bool +: True if the window is currently maximized +| minimized +: bool +: True if the window is currently minimized +| fullscreen +: bool +: True if the window is currently fullscreened +| activated +: bool +: True if the window is currently activated (i.e. has focus) + + +# CONFIGURATION + +[[ *Name* +:[ *Type* +:[ *Req* +:< *Description* +| content +: particle +: yes +: Template particle that will be instantiated once for each window +| all-monitors +: bool +: no +: When set to true, only windows on the same monitor the bar will be + used. The default is false. + + +# EXAMPLES + +``` +bar: + left: + - foreign-toplevel: + content: + map: + conditions: + ~activated: {empty: {}} + activated: + - string: {text: "{app-id}: {title}"} +``` + +# SEE ALSO + +*yambar-modules*(5), *yambar-particles*(5), *yambar-tags*(5), *yambar-decorations*(5) diff --git a/doc/yambar-modules-i3.5.scd b/doc/yambar-modules-i3.5.scd new file mode 100644 index 0000000..2014a3c --- /dev/null +++ b/doc/yambar-modules-i3.5.scd @@ -0,0 +1,117 @@ +yambar-modules-i3(5) + +# NAME +i3 - This module monitors i3 and sway workspaces + +# DESCRIPTION + +Unlike other modules where the _content_ attribute is just a single +*particle*, the i3 module's _content_ is an associative array mapping +i3/sway workspace names to a particle. + +You can add an empty workspace name, *""*, as a catch-all workspace +particle. The *i3* module will fallback to this entry if it cannot +find the workspace name in the _content_ map. + +It also recognizes the special name *current*, which always represents +the currently focused workspace. On Sway, this can be used together +with the _application_ and _title_ tags to replace the X11-only +*xwindow* module. + +# TAGS + +[[ *Name* +:[ *Type* +:< *Description* +| 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) +| focused +: bool +: True if the workspace is currently focused +| urgent +: bool +: True if the workspace has the urgent flag set +| empty +: bool +: True if the workspace is empty (Sway only) +| state +: string +: One of *urgent*, *focused*, *unfocused* or *invisible* (note: + *unfocused* is when it is visible, but neither focused nor urgent). +| application +: string +: Name of application currently focused on this workspace (Sway only - use the *xwindow* module in i3) +| title +: string +: This workspace's focused window's title +| mode +: string +: The name of the current mode + +# CONFIGURATION + +[[ *Name* +:[ *Type* +:[ *Req* +:< *Description* +| content +: associative array +: yes +: Unlike other modules, _content_ is an associative array mapping + workspace names to particles. Use *""* to specify a default + fallback particle, or *current* for the currently active workspace. +| sort +: enum +: no +: How to sort the list of workspaces; one of _none_, _native_, _ascending_ or _descending_, defaults to _none_. Use _native_ to sort numbered workspaces only. +| strip-workspace-numbers +: bool +: no +: If true, *N:* prefixes will be stripped from workspace names. Useful together with *sort*, to have the workspace order fixed. +| persistent +: list of strings +: no +: Persistent workspaces. I.e. workspaces that are never removed, even if empty. +| left-spacing +: int +: no +: Space, in pixels, on the left-side of each rendered workspace particle +| right-spacing +: int +: no +: Space, in pixels, on the right-side of each rendered workspace particle +| spacing +: int +: no +: Short-hand for setting both _left-spacing_ and _right-spacing_ + +# EXAMPLES + +This renders all workspace names, with an *\** indicating the +currently focused one. It also renders the currently focused +application name and window title. + +``` +bar: + left: + - i3: + content: + "": + map: + default: {string: {text: "{name}"}} + conditions: + state == focused: {string: {text: "{name}*"}} + current: { string: {text: "{application}: {title}"}} +``` + +# SEE ALSO + +*yambar-modules*(5), *yambar-particles*(5), *yambar-tags*(5), *yambar-decorations*(5) + diff --git a/doc/yambar-modules-label.5.scd b/doc/yambar-modules-label.5.scd new file mode 100644 index 0000000..a6516f1 --- /dev/null +++ b/doc/yambar-modules-label.5.scd @@ -0,0 +1,32 @@ +yambar-modules-label(5) + +# NAME +label - This module renders the provided _content_ particle + +# DESCRIPTION + +This module renders the provided _content_ particle, but provides no +additional data. + +# TAGS + +None + +# CONFIGURATION + +No additional attributes supported, only the generic ones (see +*GENERIC CONFIGURATION* in *yambar-modules*(5)) + +# EXAMPLES + +``` +bar: + left: + - label: + content: {string: {text: hello world}} +``` + +# SEE ALSO + +*yambar-modules*(5), *yambar-particles*(5), *yambar-tags*(5), *yambar-decorations*(5) + diff --git a/doc/yambar-modules-mem.5.scd b/doc/yambar-modules-mem.5.scd new file mode 100644 index 0000000..fc0a9eb --- /dev/null +++ b/doc/yambar-modules-mem.5.scd @@ -0,0 +1,52 @@ +yambar-modules-mem(5) + +# NAME +mem - This module provides the memory usage + +# TAGS + +[[ *Name* +:[ *Type* +:< *Description* +| free +: int +: Free memory in bytes +| used +: int +: Used memory in bytes +| total +: int +: Total memory in bytes +| percent_free +: range +: Free memory in percent +| percent_used +: range +: Used memory in percent + +# CONFIGURATION + +[[ *Name* +:[ *Type* +:[ *Req* +:< *Description* +| poll-interval +: string +: no +: Refresh interval of the memory usage stats in milliseconds + (default=500). Cannot be less then 250ms. + +# EXAMPLES + +``` +bar: + left: + - mem: + poll-interval: 2500 + content: + string: {text: "{used:mb}MB"} +``` + +# SEE ALSO + +*yambar-modules*(5), *yambar-particles*(5), *yambar-tags*(5), *yambar-decorations*(5) diff --git a/doc/yambar-modules-mpd.5.scd b/doc/yambar-modules-mpd.5.scd new file mode 100644 index 0000000..d89407a --- /dev/null +++ b/doc/yambar-modules-mpd.5.scd @@ -0,0 +1,86 @@ +yambar-modules-mpd(5) + +# NAME +mpd - This module provides MPD status such as currently playing artist/album/song + +# TAGS + +[[ *Name* +:[ *Type* +:< *Description* +| state +: string +: One of *offline*, *stopped*, *paused* or *playing* +| repeat +: bool +: True if the *repeat* flag is set +| random +: bool +: True if the *random* flag is set +| 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 +| album +: string +: Currently playing album (also valid in *paused* state) +| artist +: string +: Artist of currently playing song (also valid in *paused* state) +| title +: string +: Title of currently playing song (also valid in *paused* state) +| file +: string +: Filename or URL of currently playing song (also valid in *paused* state) +| 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. +| duration +: int +: Length of currently playing song, in milliseconds + +# CONFIGURATION + +[[ *Name* +:[ *Type* +:[ *Req* +:< *Description* +| host +: string +: yes +: Hostname/IP/unix-socket to connect to +| port +: int +: no +: TCP port to connect to + +# EXAMPLES + +``` +bar: + left: + - mpd: + host: /run/mpd/socket + content: + string: {text: "{artist} - {album} - {title} ({end})"} +``` + +# SEE ALSO + +*yambar-modules*(5), *yambar-particles*(5), *yambar-tags*(5), *yambar-decorations*(5) + 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 new file mode 100644 index 0000000..afbbae3 --- /dev/null +++ b/doc/yambar-modules-network.5.scd @@ -0,0 +1,126 @@ +yambar-modules-network(5) + +# NAME +network - This module monitors network connection state + +# DESCRIPTION + +This module monitors network connection state; disconnected/connected +state and MAC/IP addresses. It instantiates the provided _content_ +particle for each network interface. + +Note: while the module internally tracks all assigned IPv4/IPv6 +addresses, it currently exposes only a single IPv4 and a single IPv6 +address per network interface. + +# TAGS + +[[ *Name* +:[ *Type* +:< *Description* +| 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 +| carrier +: bool +: True if the interface has CARRIER. That is, if it is physically connected. +| state +: string +: One of *unknown*, *not present*, *down*, *lower layers down*, + *testing*, *dormant* or *up*. You are probably interested in *down* and *up*. +| mac +: string +: MAC address +| ipv4 +: string +: IPv4 address assigned to the interface, or *""* if none +| ipv6 +: string +: IPv6 address assigned to the interface, or *""* if none +| ssid +: string +: SSID the adapter is connected to (Wi-Fi only) +| signal +: int +: Signal strength, in dBm (Wi-Fi only) +| quality +: range +: Quality of the signal, in percent (Wi-Fi only) +| rx-bitrate +: int +: RX bitrate, in bits/s +| tx-bitrate +: int +: TX bitrate in bits/s +| dl-speed +: int +: Download speed in bits/s +| ul-speed +: int +: Upload speed in bits/s + + +# CONFIGURATION + +[[ *Name* +:[ *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_ +| poll-interval +: int +: no +: Periodically (in milliseconds) update the signal, quality, rx+tx bitrate, and + ul+dl speed tags (default=0). Setting it to 0 disables updates. Cannot be less + than 250ms. + + +# EXAMPLES + +Display all Ethernet (including WLAN) devices. This excludes loopback, +bridges etc. + +``` +bar: + left: + - network: + content: + map: + conditions: + type == ether || type == wlan: + map: + default: + string: {text: "{name}: {state} ({ipv4})"} + conditions: + ipv4 == "": + string: {text: "{name}: {state}"} +``` + +# SEE ALSO + +*yambar-modules*(5), *yambar-particles*(5), *yambar-tags*(5), *yambar-decorations*(5) + 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 new file mode 100644 index 0000000..8010449 --- /dev/null +++ b/doc/yambar-modules-pipewire.5.scd @@ -0,0 +1,103 @@ +yambar-modules-pipewire(5) + +# NAME +pipewire - Monitors pipewire for volume, mute/unmute, device change + +# TAGS + +[[ *Name* +:[ *Type* +:< *Description* +| type +: string +: Either "source" (capture) or "sink" (speaker) +| name +: string +: Current device name +| description +: string +: Current device description +| form_factor +: string +: Current device form factor (headset, speaker, mic, etc.) +| bus +: string +: Current device bus (bluetooth, alsa, etc.) +| icon +: string +: Current device icon name +| muted +: bool +: True if muted, otherwise false +| linear_volume +: range +: Linear volume in percentage (with 0 as min and 100 as max) +| cubic_volume +: range +: Cubic volume (used by pulseaudio) in percentage (with 0 as min and 100 as max) + + +# CONFIGURATION + +[[ *Name* +:[ *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 +: Unlike other modules, _content_ is a template particle that will be + expanded twice (i.e. into a list of two elements). The first + element is the 'sink', and the second element the 'source'. + + +# EXAMPLES + +``` +bar: + left: + - pipewire: + anchors: + volume: &volume + conditions: + muted: {string: {text: "{linear_volume}%", foreground: ff0000ff}} + ~muted: {string: {text: "{linear_volume}%"}} + content: + list: + items: + - map: + conditions: + type == "sink": + map: + conditions: + icon == "audio-headset-bluetooth": + string: {text: "🎧 "} + default: + - ramp: + tag: linear_volume + items: + - string: {text: "🔈 "} + - string: {text: "🔉 "} + - string: {text: "🔊 "} + type == "source": + - string: {text: "🎙 "} + - map: + <<: *volume +``` + +# SEE ALSO + +*yambar-modules*(5), *yambar-particles*(5), *yambar-tags*(5), *yambar-decorations*(5) + diff --git a/doc/yambar-modules-pulse.5.scd b/doc/yambar-modules-pulse.5.scd new file mode 100644 index 0000000..008ec78 --- /dev/null +++ b/doc/yambar-modules-pulse.5.scd @@ -0,0 +1,67 @@ +yambar-modules-pulse(5) + +# NAME +pulse - Monitors a PulseAudio source and/or sink + +# TAGS + +[[ *Name* +:[ *Type* +:< *Description* +| online +: bool +: True when connected to the PulseAudio server +| sink_online +: bool +: True when the sink is present +| source_online +: bool +: True when the source is present +| sink_percent +: range +: Sink volume level, as a percentage +| source_percent +: range +: Source volume level, as a percentage +| sink_muted +: bool +: True if the sink is muted, otherwise false +| source_muted +: bool +: True if the source is muted, otherwise false +| sink_port +: string +: Description of the active sink port +| source_port +: string +: Description of the active source port + +# CONFIGURATION + +[[ *Name* +:[ *Type* +:[ *Req* +:< *Description* +| sink +: string +: no +: Name of sink to monitor (default: _@DEFAULT\_SINK@_). +| source +: string +: no +: Name of source to monitor (default: _@DEFAULT\_SOURCE@_). + +# EXAMPLES + +``` +bar: + left: + - pulse: + content: + string: {text: "{sink_percent}% ({sink_port})"} +``` + +# SEE ALSO + +*yambar-modules*(5), *yambar-particles*(5), *yambar-tags*(5), *yambar-decorations*(5) + diff --git a/doc/yambar-modules-removables.5.scd b/doc/yambar-modules-removables.5.scd new file mode 100644 index 0000000..0a193d4 --- /dev/null +++ b/doc/yambar-modules-removables.5.scd @@ -0,0 +1,92 @@ +yambar-modules-removables(5) + +# NAME +removables - This module detects removable drives + +# DESCRIPTION + +This module detects removable drives (USB sticks, CD-ROMs) and +instantiates the provided _content_ particle for each detected drive. + +# TAGS + +[[ *Name* +:[ *Type* +:< *Description* +| vendor +: string +: Name of the drive vendor +| model +: string +: Drive model name +| optical +: bool +: True if the drive is an optical drive (CD-ROM, DVD-ROM etc) +| audio +: bool +: True if an optical drive has an audio CD inserted (i.e. this + property is always false for non-optical drives). +| device +: string +: Volume device name (typically */dev/sd?*) +| size +: range +: The volume's size, in bytes. The tag's maximum value is set to the + underlying block device's size +| label +: string +: The volume's label, or its size if it has no label +| mounted +: bool +: True if the volume is mounted +| mount_point +: string +: Path where the volume is mounted, or *""* if it is not mounted + +# CONFIGURATION + +[[ *Name* +:[ *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_ +| ignore +: list of strings +: no +: List of device paths that should be ignored (e.g. /dev/mmcblk0, or /dev/mmcblk0p1) + +# EXAMPLES + +``` +bar: + right: + - removables: + content: + map: + conditions: + ~mounted: + string: + on-click: udisksctl mount -b {device} + text: "{label}" + mounted: + string: + on-click: udisksctl unmount -b {device} + text: "{label}" + deco: {underline: {size: 2, color: ffffffff}} +``` + +# SEE ALSO + +*yambar-modules*(5), *yambar-particles*(5), *yambar-tags*(5), *yambar-decorations*(5) + diff --git a/doc/yambar-modules-river.5.scd b/doc/yambar-modules-river.5.scd new file mode 100644 index 0000000..3bf3b61 --- /dev/null +++ b/doc/yambar-modules-river.5.scd @@ -0,0 +1,109 @@ +yambar-modules-river(5) + +# NAME +river - This module provides information about the river tags + +# DESCRIPTION + +This module uses river's (https://github.com/ifreund/river, a dynamic +tiling Wayland compositor) status protocol to provide information +about the river tags. + +It has an interface similar to the i3/sway module. + +The configuration for the river module specifies one _title_ particle, +which will be instantiated once for each seat, with tags representing +the seats' name, the title of the seats' currently focused view, and +its current river "mode". + +It also specifies a _content_ template particle, which is instantiated +once for all 32 river tags. This means you probably want to use a +*map* particle to hide unused river tags. + +# TAGS (for the "content" particle) + +[[ *Name* +:[ *Type* +:< *Description* +| id +: int +: River tag number +| urgent +: bool +: True if the river tag has at least one urgent view. +| visible +: bool +: True if the river tag is focused by at least one output (i.e. visible on at least one monitor). +| focused +: bool +: True if the river tag is _visible_ and has keyboard focus. +| occupied +: bool +: True if the river tag has views (i.e. windows). +| state +: string +: Set to *urgent* if _urgent_ is true, *focused* if _focused_ is true, + *unfocused* if _visible_ is true, but _focused_ is false, or + *invisible* if the river tag is not visible on any monitors. + + +# TAGS (for the "title" particle) + +[[ *Name* +:[ *Type* +:< *Description* +| seat +: string +: The name of the seat. +| title +: string +: The seat's focused view's title. +| mode +: string +: The seat's current mode (entered with e.g. *riverctl enter-mode foobar*). +| layout +: string +: Current layout of the output currently focused by the seat. + +# CONFIGURATION + +[[ *Name* +:[ *Type* +:[ *Req* +:< *Description* +| title +: particle +: no +: Particle that will be instantiated with the _seat_ and _title_ tags. +| content +: particle +: yes +: Template particle that will be instantiated once for all of the 32 river tags. +| all-monitors +: bool +: no +: When set to false (the default), tags reflect river tags and seats + for the monitor yambar is on only. When set to true, tags reflect + the union of all monitors. + +# EXAMPLES + +``` +bar: + left: + - river: + title: {string: { text: "{seat} - {title} ({layout}/{mode})" }} + content: + map: + conditions: + ~occupied: {empty: {}} + occupied: + string: + margin: 5 + text: "{id}: {state}" +``` + +# SEE ALSO + +*yambar-modules*(5), *yambar-particles*(5), *yambar-tags*(5), *yambar-decorations*(5) + diff --git a/doc/yambar-modules-script.5.scd b/doc/yambar-modules-script.5.scd new file mode 100644 index 0000000..48722cf --- /dev/null +++ b/doc/yambar-modules-script.5.scd @@ -0,0 +1,151 @@ +yambar-modules-script(5) + +# NAME +script - This module executes a user-provided script (or binary!) + +# DESCRIPTION + +This module executes a user-provided script (or binary!) that writes +tags on its stdout. + +Scripts can be run in two modes: yambar polled, or continuously. In the +yambar polled mode, the script is expected to write one set of tags +and then exit. Yambar will execute the script again after a +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 depend +on non-polling methods to update their state. + +Tag sets, or _transactions_, are separated by an empty line +(e.g. *echo ""*). The empty line is required to commit (update) the +tag even for only one transaction. + +Each _tag_ is a single line on the format: + +``` +name|type|value +``` + +Where _name_ is what you also use to refer to the tag in the yambar +configuration, _type_ is one of the tag types defined in +*yambar-tags*(5), and _value_ is the tag’s value. + +Example: + +``` +var1|string|hello +var2|int|13 + +var1|string|world +var2|int|37 + +``` + +The example above consists of two transactions. Each transaction has +two tags: one string tag and one integer tag. The second transaction +replaces the tags from the first transaction. Note that **both** +transactions need to be terminated with an empty line. + +Supported _types_ are: + +- string +- int +- bool +- float +- range:n-m (e.g. *var|range:0-100|57*) + +# TAGS + +User defined. + +# CONFIGURATION + +[[ *Name* +:[ *Type* +:[ *Req* +:< *Description* +| path +: string +: yes +: Path to script/binary to execute. Must either be an absolute path, + or start with *~/*. +| args +: list of strings +: no +: Arguments to pass to the script/binary. +| poll-interval +: integer +: no +: Number of milliseconds between each script run. If unset, or set to + 0, continuous mode is used. + +# EXAMPLES + +Here is an "hello world" example script: + +``` +#!/bin/sh + +while true; do + echo "test|string|hello" + echo "" + sleep 3 + + echo "test|string|world" + echo "" + sleep 3 +done +``` + +This script runs in continuous mode, and will emit a single string tag, +_test_, and alternate its value between *hello* and *world* every +three seconds. + +A corresponding yambar configuration could look like this: + +``` +bar: + left: + - script: + path: /path/to/script.sh + args: [] + content: {string: {text: "{test}"}} +``` + +Another example use case of this module could be to display currently playing +song or other media from players that support MPRIS (Media Player Remote +Interfacing Specification): + +``` +bar: + center: + - script: + path: /usr/bin/playerctl + args: + - "--follow" + - "metadata" + - "-f" + - | + status|string|{{status}} + artist|string|{{artist}} + title|string|{{title}} + content: + map: + conditions: + status == Paused: {empty: {}} + status == Playing: + content: {string: {text: "{artist} - {title}"}} +``` + +The above snippet runs a _playerctl_ utility in _--follow_ mode, reacting to +media updates on DBUS and outputting status, artist and title of media being +played in a format that is recognized by yambar. See _playerctl_ documentation +for more available metadata fields and control over which players get used. + +# SEE ALSO + +*yambar-modules*(5), *yambar-particles*(5), *yambar-tags*(5), *yambar-decorations*(5) + diff --git a/doc/yambar-modules-sway-xkb.5.scd b/doc/yambar-modules-sway-xkb.5.scd new file mode 100644 index 0000000..bcf948c --- /dev/null +++ b/doc/yambar-modules-sway-xkb.5.scd @@ -0,0 +1,70 @@ +yambar-modules-sway-xkb(5) + +# NAME +sway-xkb - This module monitor input devices' active XKB layout + +# DESCRIPTION + +This module uses *Sway* extensions to the I3 IPC API to monitor input +devices' active XKB layout. As such, it requires Sway to be running. + +*Note* that the _content_ configuration option is a *template*; +*sway-xkb* will instantiate a particle list, where each item is +instantiated from this template, and represents an input device. + +# TAGS + +[[ *Name* +:[ *Type* +:< *Description* +| id +: string +: Input device identifier +| layout +: string +: The input device's currently active XKB layout + +# CONFIGURATION + +[[ *Name* +:[ *Type* +:[ *Req* +:< *Description* +| identifiers +: list of strings +: yes +: Identifiers of input devices to monitor. Use _swaymsg -t get_inputs_ to see available devices. +| content +: particle +: yes +: A particle template; each existing input device will be instantiated with this template. +| left-spacing +: int +: no +: Space, in pixels, in the left side of each rendered input device +| right-spacing +: int +: no +: Space, in pixels, on the right side of each rendered input device +| spacing +: int +: no +: Short-hand for setting both _left-spacing_ and _right-spacing_ + +# EXAMPLES + +``` +bar: + left: + - sway-xkb: + identifiers: + - 1523:7:HID_05f3:0007 + - 7247:2:USB_USB_Keykoard + spacing: 5 + content: {string: {text: "{id}: {layout}"}} +``` + +# SEE ALSO + +*yambar-modules-xkb*(5), *yambar-modules*(5), *yambar-particles*(5), *yambar-tags*(5), *yambar-decorations*(5) + diff --git a/doc/yambar-modules-sway.5.scd b/doc/yambar-modules-sway.5.scd new file mode 100644 index 0000000..c440322 --- /dev/null +++ b/doc/yambar-modules-sway.5.scd @@ -0,0 +1,10 @@ +yambar-modules-sway(5) + +# DESCRIPTION + +Please use the i3 (*yambar-modules-i3*(5)) module, as it is fully compatible with Sway + +# SEE ALSO + +*yambar-modules*(5), *yambar-modules-i3*(5) + diff --git a/doc/yambar-modules-xkb.5.scd b/doc/yambar-modules-xkb.5.scd new file mode 100644 index 0000000..ef03097 --- /dev/null +++ b/doc/yambar-modules-xkb.5.scd @@ -0,0 +1,52 @@ +yambar-modules-xkb(5) + +# NAME +xkb - This module monitors the currently active XKB keyboard layout + +# DESCRIPTION + +This module monitors the currently active XKB keyboard layout and +lock-key states. + +Note: this module is X11 only. It does not work in Wayland. + +# TAGS + +[[ *Name* +:[ *Type* +:< *Description* +| name +: string +: Name of currently selected layout, long version (e.g. "English (US)") +| symbol +: string +: Name of currently selected layout, short version (e.g. "us") +| caps_lock +: bool +: True if *CapsLock* is enabled +| num_lock +: bool +: True if *NumLock* is enabled +| scroll_lock +: bool +: True if *ScrollLock* is enabled + +# CONFIGURATION + +No additional attributes supported, only the generic ones (see +*GENERIC CONFIGURATION* in *yambar-modules*(5)) + +# EXAMPLES + +``` +bar: + left: + - xkb: + content: + string: {text: "{symbol}"} +``` + +# SEE ALSO + +*yambar-modules-sway-xkb*(5), *yambar-modules*(5), *yambar-particles*(5), *yambar-tags*(5), *yambar-decorations*(5) + diff --git a/doc/yambar-modules-xwindow.5.scd b/doc/yambar-modules-xwindow.5.scd new file mode 100644 index 0000000..1aadbf7 --- /dev/null +++ b/doc/yambar-modules-xwindow.5.scd @@ -0,0 +1,45 @@ +yambar-modules-xwindow(5) + +# NAME +xwindow - This module provides the application name and window title + +# DESCRIPTION + +This module provides the application name and window title of the +currently focused window. + +Note: this module is X11 only. It does not work in Wayland. If you are +running Sway, take a look at the *i3* module and its _application_ and +_title_ tags. + +# TAGS + +[[ *Name* +:[ *Type* +:< *Description* +| application +: string +: Name of the application that owns the currently focused window +| title +: string +: The title of the currently focused window + +# CONFIGURATION + +No additional attributes supported, only the generic ones (see +*GENERIC CONFIGURATION* in *yambar-modules*(5)) + +# EXAMPLES + +``` +bar: + left: + - xwindow: + content: + string: {text: "{application}: {title}"} +``` + +# SEE ALSO + +*yambar-modules*(5), *yambar-particles*(5), *yambar-tags*(5), *yambar-decorations*(5) + diff --git a/doc/yambar-modules.5.scd b/doc/yambar-modules.5.scd index 947b57d..1ec4871 100644 --- a/doc/yambar-modules.5.scd +++ b/doc/yambar-modules.5.scd @@ -38,7 +38,7 @@ For example, to render _backlight_ as " 20%", you could use: ``` content: - string: - font: Font Awesome 5 Free:style=solid:pixelsize=14 + font: Font Awesome 6 Free:style=solid:pixelsize=14 text:  - string: font: Adobe Helvetica:pixelsize=12 @@ -68,20 +68,17 @@ in red. ``` content: map: - tag: carrier - values: - false: {empty: {}} - true: + conditions: + ~carrier: {empty: {}} + carrier: map: - tag: state default: {string: {text: , font: *awesome, foreground: ffffff66}} - values: - up: + conditions: + state == up: map: - tag: ipv4 default: {string: {text: , font: *awesome}} - values: - "": {string: {text: , font: *awesome, foreground: ffffff66}} + conditions: + ipv4 == "": {string: {text: , font: *awesome, foreground: ffffff66}} ``` ## Use yaml anchors @@ -94,7 +91,7 @@ In these cases, you can define an anchor point, either at top-level, or in a module's _anchors_ attribute: ``` -awesome: &awesome Font Awesome 5 Free:style=solid:pixelsize=14 +awesome: &awesome Font Awesome 6 Free:style=solid:pixelsize=14 ``` @@ -113,7 +110,7 @@ following attributes are supported by all modules: [[ *Name* :[ *Type* :[ *Req* -:[ *Description* +:< *Description* | content : particle : yes @@ -133,651 +130,59 @@ following attributes are supported by all modules: : Foreground (text) color of the content particle. This is an inherited attribute. -# ALSA +# BUILT-IN MODULES -Monitors an alsa soundcard for volume and mute/unmute changes. +Available modules have their own pages: -## TAGS +*yambar-modules-alsa*(5) -[[ *Name* -:[ *Type* -:[ *Description* -| volume -: range -: Volume level, with min and max as start and end range values -| muted -: bool -: True if muted, otherwise false +*yambar-modules-backlight*(5) +*yambar-modules-battery*(5) -## CONFIGURATION +*yambar-modules-clock*(5) -[[ *Name* -:[ *Type* -:[ *Req* -:[ *Description* -| card -: string -: yes -: The soundcard name. _Default_ might work. -| mixer -: string -: yes -: Mixer channel to monitor. _Master_ might work. +*yambar-modules-cpu*(5) -## EXAMPLES +*yambar-modules-disk-io*(5) -``` -bar: - left: - - alsa: - card: hw:PCH - mixer: Master - content: {string: {text: "{volume}"}} -``` +*yambar-modules-dwl*(5) -# BACKLIGHT +*yambar-modules-foreign-toplevel*(5) -This module reads monitor backlight status from -_/sys/class/backlight_, and uses *udev* to monitor for changes. +*yambar-modules-i3*(5) -## TAGS +*yambar-modules-label*(5) -[[ *Name* -:[ *Type* -:[ *Description* -| brightness -: range -: The current brightness level, in absolute value -| percent -: range -: The current brightness level, in percent +*yambar-modules-mem*(5) -## CONFIGURATION +*yambar-modules-mpd*(5) -[[ *Name* -:[ *Type* -:[ *Req* -:[ *Description* -| name -: string -: yes -: The backlight device's name (one of the names in */sys/class/backlight*) +*yambar-modules-network*(5) -## EXAMPLES +*yambar-modules-pipewire*(5) -``` -bar: - left: - - backlight: - name: intel_backlight - content: - string: {text: "backlight: {percent}%"} -``` +*yambar-modules-pulse*(5) -# BATTERY +*yambar-modules-removables*(5) -This module reads battery status from _/sys/class/power_supply_ and -uses *udev* to monitor for changes. +*yambar-modules-river*(5) -## TAGS +*yambar-modules-script*(5) -[[ *Name* -:[ *Type* -:[ *Description* -| name -: string -: Battery device name -| manufacturer -: string -: Name of the battery manufacturer -| model -: string -: Battery model name -| state -: string -: One of *full*, *charging*, *discharging* or *unknown* -| capacity -: range -: capacity left, in percent -| estimate -: string -: Estimated time left (to empty while discharging, or to full while - charging), formatted as HH:MM. +*yambar-modules-sway-xkb*(5) -## CONFIGURATION +*yambar-modules-sway*(5) -[[ *Name* -:[ *Type* -:[ *Req* -:[ *Description* -| name -: string -: yes -: Battery device name (one of the names in */sys/class/power_supply*) -| poll-interval -: int -: no -: How often, in seconds, to poll for capacity changes (default=*60*). Set to `0` to disable polling (*warning*: many batteries do not support asynchronous reporting). +*yambar-modules-niri-language*(5) -## EXAMPLES +*yambar-modules-niri-workspaces*(5) -``` -bar: - left: - - battery: - name: BAT0 - poll-interval: 30 - content: - string: {text: "BAT: {capacity}% {estimate}"} -``` +*yambar-modules-xkb*(5) -# CLOCK - -This module provides the current date and time. - -## TAGS - -[[ *Name* -:[ *Type* -:[ *Description* -| time -: string -: Current time, formatted using the _time-format_ attribute -| date -: string -: Current date, formatted using the _date-format_ attribute - -## CONFIGURATION - -[[ *Name* -:[ *Type* -:[ *Req* -:[ *Description* -| time-format -: string -: no -: *strftime* formatter for the _time_ tag (default=*%H:%M*) -| date-format -: string -: no -: *strftime* formatter for the _date_ date (default=*%x*) - -## EXAMPLES - -``` -bar: - left: - - clock: - time-format: "%H:%M %Z" - content: - string: {text: "{date} {time}"} -``` - -# I3 (and Sway) - -This module monitors i3 and sway workspaces. - -Unlike other modules where the _content_ attribute is just a single -*particle*, the i3 module's _content_ is an associative array mapping -i3/sway workspace names to a particle. - -You can add an empty workspace name, *""*, as a catch-all workspace -particle. The *i3* module will fallback to this entry if it cannot -find the workspace name in the _content_ map. - -It also recognizes the special name *current*, which always represents -the currently focused workspace. On Sway, this can be used together -with the _application_ and _title_ tags to replace the X11-only -*xwindow* module. - -## TAGS - -[[ *Name* -:[ *Type* -:[ *Description* -| name -: string -: The workspace name -| visible -: bool -: True if the workspace is currently visible (on any output) -| focused -: bool -: True if the workspace is currently focused -| urgent -: bool -: True if the workspace has the urgent flag set -| state -: string -: One of *urgent*, *focused*, *unfocused* or *invisible* (note: - *unfocused* is when it is visible, but neither focused nor urgent). -| application -: string -: Name of application currently focused on this workspace (Sway only - use the *xwindow* module in i3) -| title -: string -: This workspace's focused window's title - -## CONFIGURATION - -[[ *Name* -:[ *Type* -:[ *Req* -:[ *Description* -| content -: associative array -: yes -: Unlike other modules, _content_ is an associative array mapping - workspace names to particles. Use *""* to specify a default - fallback particle, or *current* for the currently active workspace. -| left-spacing -: int -: no -: Space, in pixels, on the left-side of each rendered workspace particle -| right-spacing -: int -: no -: Space, in pixels, on the right-side of each rendered workspace particle -| spacing -: int -: no -: Short-hand for setting both _left-spacing_ and _right-spacing_ - -## EXAMPLES - -This renders all workspace names, with an *\** indicating the -currently focused one. It also renders the currently focused -application name and window title. - -``` -bar: - left: - - i3: - content: - "": - map: - tag: state - default: {string: {text: "{name}"}} - values: - focused: {string: {text: "{name}*"}} - current: { string: {text: "{application}: {title}"}} -``` - -# LABEL - -This module renders the provided _content_ particle, but provides no -additional data. - -## TAGS - -None - -## CONFIGURATION - -No additional attributes supported, only the generic ones (see -*GENERIC CONFIGURATION*) - -## EXAMPLES - -``` -bar: - left: - - label: - content: {string: {text: hello world}} -``` - -# MPD - -This module provides MPD status such as currently playing -artist/album/song. - -## TAGS - -[[ *Name* -:[ *Type* -:[ *Description* -| state -: string -: One of *offline*, *stopped*, *paused* or *playing* -| repeat -: bool -: True if the *repeat* flag is set -| random -: bool -: True if the *random* flag is set -| consume -: bool -: True if the *consume* flag is set -| album -: string -: Currently playing album (also valid in *paused* state) -| artist -: string -: Artist of currently playing song (also valid in *paused* state) -| title -: string -: Title of currently playing song (also valid in *paused* state) -| 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. -| duration -: int -: Length of currently playing song, in milliseconds - -## CONFIGURATION - -[[ *Name* -:[ *Type* -:[ *Req* -:[ *Description* -| host -: string -: yes -: Hostname/IP/unix-socket to connect to -| port -: int -: no -: TCP port to connect to - -## EXAMPLES - -``` -bar: - left: - - mpd: - host: /run/mpd/socket - content: - string: {text: "{artist} - {album} - {title} ({end})"} -``` - -# NETWORK - -This module monitors network connection state; disconnected/connected -state and MAC/IP addresses. - -Note: while the module internally tracks all assigned IPv4/IPv6 -addresses, it currently exposes only a single IPv4 and a single IPv6 -address. - -## TAGS - -[[ *Name* -:[ *Type* -:[ *Description* -| name -: string -: Network interface name -| index -: int -: Network interface index -| carrier -: bool -: True if the interface has CARRIER. That is, if it is physically connected. -| state -: string -: One of *unknown*, *not present*, *down*, *lower layers down*, - *testing*, *dormant* or *up*. You are probably interrested in *down* and *up*. -| mac -: string -: MAC address -| ipv4 -: string -: IPv4 address assigned to the interface, or *""* if none -| ipv6 -: string -: IPv6 address assigned to the interface, or *""* if none - -## CONFIGURATION - -[[ *Name* -:[ *Type* -:[ *Req* -:[ *Description* -| name -: string -: Name of network interface to monitor - -## EXAMPLES - -``` -bar: - left: - - network: - name: wlp3s0 - content: - string: {text: "{name}: {state} ({ipv4})"} -``` - -# REMOVABLES - -This module detects removable drives (USB sticks, CD-ROMs) and -instantiates the provided _content_ particle for each detected drive. - -## TAGS - -[[ *Name* -:[ *Type* -:[ *Description* -| vendor -: string -: Name of the drive vendor -| model -: string -: Drive model name -| optical -: bool -: True if the drive is an optical drive (CD-ROM, DVD-ROM etc) -| device -: string -: Volume device name (typically */dev/sd?*) -| size -: range -: The volume's size, in bytes. The tag's maximum value is set to the - underlying block device's size -| label -: string -: The volume's label, or its size if it has no label -| mounted -: bool -: True if the volume is mounted -| mount_point -: string -: Path where the volume is mounted, or *""* if it is not mounted - -## CONFIGURATION - -[[ *Name* -:[ *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_ -| ignore -: list of strings -: no -: List of device paths that should be ignored (e.g. /dev/mmcblk0, or /dev/mmcblk0p1) - -## EXAMPLES - -``` -bar: - right: - - removables: - content: - map: - tag: mounted - values: - false: - string: - on-click: udisksctl mount -b {device} - text: "{label}" - true: - string: - on-click: udisksctl unmount -b {device} - text: "{label}" - deco: {underline: {size: 2, color: ffffffff}} -``` - -# SWAY-XKB - -This module uses *Sway* extenions to the I3 IPC API to monitor input -devices' active XKB layout. As such, it requires Sway to be running. - -*Note* that the _content_ configuration option is a *template*; -*sway-xkb* will instantiate a particle list, where each item is -instantiated from this template, and represents an input device. - -## TAGS - -[[ *Name* -:[ *Type* -:[ *Description* -| id -: string -: Input device indentifier -| layout -: string -: The input device's currently active XKB layout - -## CONFIGURATION - -[[ *Name* -:[ *Type* -:[ *Req* -:[ *Description* -| identifiers -: list of strings -: yes -: Identifiers of input devices to monitor. Use _swaymsg -t get_inputs_ to see available devices. -| content -: particle -: yes -: A particle template; each existing input device will be instantiated with this template. -| left-spacing -: int -: no -: Space, in pixels, in the left side of each rendered input device -| right-spacing -: int -: no -: Space, in pixels, on the right side of each rendered input device -| spacing -: int -: no -: Short-hand for setting both _left-spacing_ and _right-spacing_ - -## EXAMPLES - -``` -bar: - left: - - sway-xkb: - identifiers: - - 1523:7:HID_05f3:0007 - - 7247:2:USB_USB_Keykoard - spacing: 5 - content: {string: {text: "{id}: {layout}"}} -``` - -# XKB - -This module monitors the currently active XKB keyboard layout and -lock-key states. - -Note: this module is X11 only. It does not work in Wayland. - -## TAGS - -[[ *Name* -:[ *Type* -:[ *Description* -| name -: string -: Name of currently selected layout, long version (e.g. "English (US)") -| symbol -: string -: Name of currently selected layout, short version (e.g. "us") -| caps_lock -: bool -: True if *CapsLock* is enabled -| num_lock -: bool -: True if *NumLock* is enabled -| scroll_lock -: bool -: True if *ScrollLock* is enabled - -## CONFIGURATION - -No additional attributes supported, only the generic ones (see -*GENERIC CONFIGURATION*) - -## EXAMPLES - -``` -bar: - left: - - xkb: - content: - string: {text: "{symbol}"} -``` - -# XWINDOW - -This module provides the application name and window title of the -currently focused window. - -Note: this module is X11 only. It does not work in Wayland. If you are -running Sway, take a look at the *i3* module and its _application_ and -_title_ tags. - -## TAGS - -[[ *Name* -:[ *Type* -:[ *Description* -| application -: string -: Name of the application that owns the currently focused window -| title -: string -: The title of the currently focused window - -## CONFIGURATION - -No additional attributes supported, only the generic ones (see -*GENERIC CONFIGURATION*) - -## EXAMPLES - -``` -bar: - left: - - xwindow: - content: - string: {text: "{application}: {title}"} -``` +*yambar-modules-xwindow*(5) # SEE ALSO *yambar-particles*(5), *yambar-tags*(5), *yambar-decorations*(5) + diff --git a/doc/yambar-particles.5.scd b/doc/yambar-particles.5.scd index 6dbd6a1..325ef89 100644 --- a/doc/yambar-particles.5.scd +++ b/doc/yambar-particles.5.scd @@ -12,7 +12,7 @@ following attributes are supported by all particles: [[ *Name* :[ *Type* :[ *Req* -:[ *Description* +:< *Description* | left-margin : int : no @@ -31,20 +31,76 @@ following attributes are supported by all particles: : Font to use. Note that this is an inherited attribute; i.e. you can set it on e.g. a _list_ particle, and it will apply to all particles in the list. +| font-shaping +: enum +: no +: font-shaping; one of _full_ or _none_. When set to _full_ (the + default), strings will be "shaped" using HarfBuzz. Requires support + in fcft. | foreground : color : no : Foreground (text) color. Just like _font_, this is an inherited attribute. | on-click +: associative array/string +: no +: When set to a string, executes the string as a command when the + particle is left-clicked. Tags can be used. Note that the string is + *not* executed in a shell. Environment variables are not expanded. + *~/* is expanded, but only in the first argument. The same applies + to all attributes associated with it, below. +| on-click.left : string : no -: Command to execute when the particle is clicked. Tags can be - used. Note that the string is *not* executed in a shell. +: Command to execute when the particle is left-clicked. +| on-click.right +: string +: no +: Command to execute when the particle is right-clicked. +| on-click.middle +: string +: no +: Command to execute when the particle is middle-clicked. +| on-click.wheel-up +: string +: no +: Command to execute every time a 'wheel-up' event is triggered. +| on-click.wheel-down +: string +: no +: Command to execute every time a 'wheel-down' event is triggered. +| on-click.previous +: string +: no +: Command to execute when the particle is clicked with the 'previous' button. +| on-click.next +: string +: no +: Command to execute when the particle is clicked with the 'next' button. | deco : decoration : no : Decoration to apply to the particle. See *yambar-decorations*(5) +## EXAMPLES: + +*on-click* as a string (handles left click): +``` +content: + : + on-click: command args +``` + +*on-click* as an associative array (handles other buttons): +``` +content: + : + on-click: + left: command-1 + wheel-up: command-3 + wheel-down: command-4 +``` + # STRING This is the most basic particle. It takes a format string, consisting @@ -59,7 +115,7 @@ of free text mixed with tag specifiers. | text : string : yes -: Format string. Tags are spcified with _{tag_name}_. Some tag types +: Format string. Tags are specified with _{tag_name}_. Some tag types have suffixes that can be appended (e.g. _{tag_name:suffix}_). See *yambar-modules*(5)). | max @@ -67,9 +123,9 @@ of free text mixed with tag specifiers. : no : Sets the rendered string's maximum length. If the final string's length exceeds this, the rendered string will be truncated, and - "..." will be appended. Note that the trailing "..." are + "…" will be appended. Note that the trailing "…" is *included* in the maximum length. I.e. if you set _max_ to '5', you - will only get *2* characters from the string. + will only get *4* characters from the string. ## EXAMPLES @@ -82,7 +138,7 @@ content: # EMPTY This particle is a place-holder. While it does not render any tags, -margins and decortions are rendered. +margins and decorations are rendered. ## CONFIGURATION @@ -99,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_ @@ -110,7 +166,7 @@ particle itself. [[ *Name* :[ *Type* :[ *Req* -:[ *Description* +:< *Description* | items : list : yes @@ -158,51 +214,165 @@ 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 -particles. In addition to explicit tag values, you can also specify a +particles based on conditions. A condition takes either the form of: + +``` + +``` + +Or, for boolean tags: + +``` + +``` + +Where is the tag you would like to map, is one of: + +[- == +:- != +:- >= +:- > +:- <= +:- < + +and is the value you would like to compare it to. *If the +value contains any non-alphanumerical characters, you must +surround it with ' \" ' *: + +``` +"hello world" +"@#$%" +``` + +Negation is done with a preceding '~': + +``` +~ +~ +``` + +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: + +[- && +:- || + +in order to create more complex conditions: + +``` + && +``` + +You may surround with parenthesis for clarity or +specifying precedence: + +``` +() + && ( || ) +``` + +In addition to explicit tag values, you can also specify a default/fallback particle. +Note that conditions are evaluated in the order they appear. *If +multiple conditions are true, the first one will be used*. This means +that in a configuration such as: + +``` +tx-bitrate > 1000: +tx-bitrate > 1000000: +``` + +the second condition would never run, since whenever the second +condition is true, the first is also true. The correct way of doing +this would be to invert the order of the conditions: + +``` +tx-bitrate > 1000000: +tx-bitrate > 1000: +``` + + ## CONFIGURATION [[ *Name* :[ *Type* :[ *Req* -:[ *Description* -| tag -: string -: yes -: The tag (name of) which values should be mapped -| values +:< *Description* +| conditions : associative array : yes -: An associative array of tag values mapped to particles +: An associative array of conditions (see above) mapped to particles | default : particle : no -: Default particle to use, when tag's value does not match any of the - mapped values. +: Default particle to use, none of the conditions are true ## EXAMPLES ``` content: map: - tag: tag_name default: string: text: this is the default particle; the tag's value is now {tag_name} - values: - one_value: + conditions: + tag == one_value: string: text: tag's value is now one_value - another_value: + tag == another_value: string: text: tag's value is now another_value ``` +For a boolean tag: + +``` +content: + map: + conditions: + tag: + string: + text: tag is true + ~tag: + string: + text: tag is false +``` + # RAMP This particle uses a range tag to index into an array of @@ -215,7 +385,7 @@ indicator. [[ *Name* :[ *Type* :[ *Req* -:[ *Description* +:< *Description* | tag : string : yes @@ -224,8 +394,20 @@ indicator. : list : yes : List of particles. Note that the tag value is *not* used as-is; its - minumum and maximum values are used to map the tag's range to the + minimum and maximum values are used to map the tag's range to the particle list's range. +| min +: int +: no +: If present this will be used as a lower bound instead of the tags + minimum value. Tag values falling outside the defined range will + get clamped to min/max. +| max +: int +: no +: If present this will be used as an upper bound instead of the tags + maximum value. Tag values falling outside the defined range will + get clamped to min/max. ## EXAMPLES @@ -258,7 +440,7 @@ itself when needed. [[ *Name* :[ *Type* :[ *Req* -:[ *Description* +:< *Description* | tag : string : yes @@ -294,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 9b31712..b6b8b56 100644 --- a/doc/yambar-tags.5.scd +++ b/doc/yambar-tags.5.scd @@ -11,7 +11,7 @@ their information. Each module defines its own set of tags. The available tag *types* are: [[ *Type* -:[ *Description* +:< *Description* | string : Value is a string. Rendered as-is by the _string_ particle. | int @@ -25,14 +25,110 @@ The available tag *types* are: | range : Value is an integer, with a minimum and maximum value associated with it. By default, the _string_ particle renders the value. The - _:min_ or _:max_ suffixes by be added to instead render the mininum - or maximum value (_\"{tag_name:min}\"_). + *:min* or *:max* suffixes may be added to instead render the + minimum or maximum value (_\"{tag_name:min}\"_). | realtime : Value is an integer that changes in a predictable manner (in "realtime"). This allows the particle to update itself periodically. Only supported by the *yambar-particle-progress-bar*(5). Other particles can still render - the tag's value. And, the _string_ particle recognizes the _:unit_ + the tag's value. And, the _string_ particle recognizes the *:unit* suffix, which will be translated to a "s" for a tag with "seconds" resolution, or "ms" for one with "milliseconds" resolution. +# FORMATTING + +A tag may be followed by one or more formatters that alter the tags +rendition. + +Formatters are added by appending a ':' separated list of formatter +names: + + "{tag_name:max:hex}" + +In the table below, "kind" describes the type of action performed by +the formatter: + +- *format*: changes the representation of the tag's value +- *selector*: changes what to render + +In general, formatters of the same kind cannot be combined; if +multiple formatters of the same kind are specified, the last one will +be used. + +[[ *Formatter* +:[ *Kind* +:[ *Applies to* +:< *Description* +| [0][.] +: format +: Numeric tags (integer and floats) +: The width reserved to the field. The leading '0' is optional and + indicates zero padding, as opposed to space padding. The trailing + '.' is also optional +| . +: format +: Float tags +: How many decimals to print +| [0][.] +: format +: N: numeric tags, M: float tags +: Combined version of the two previous formatters +| hex +: format +: All tag types +: Renders a tag's value in hex +| oct +: format +: All tag types +: Renders a tag's value in octal +| % +: 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 +| kib, mib, gib +: format +: All tag types +: Same as *kb*, *mb* and *gb*, but divide by 1024^n instead of 1000^n. +| min +: selector +: Range tags +: Renders a range tag's minimum value +| max +: selector +: Range tags +: Renders a range tag's maximum value +| unit +: selector +: Realtime tags +: Renders a realtime tag's unit (e.g. "s", or "ms") + +# EXAMPLES + +- A numeric (float or int) tag with at least 3 digits, zero-padded if + necessary: + +``` +{tag:03} +``` + +- A float tag with 2 decimals: + +``` +{tag:.2} +``` + +- A "byte count" tag in gigabytes: + +``` +{tag:gib}GB +``` diff --git a/doc/yambar.1.scd b/doc/yambar.1.scd index f2526a2..2aaa46f 100644 --- a/doc/yambar.1.scd +++ b/doc/yambar.1.scd @@ -25,7 +25,11 @@ 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 + syslog. Default: _warning_. *-l*,*--log-colorize*=[{*never*,*always*,*auto*}] Enables or disables colorization of log output on stderr. diff --git a/doc/yambar.5.scd b/doc/yambar.5.scd index 6b4a5ff..38c521d 100644 --- a/doc/yambar.5.scd +++ b/doc/yambar.5.scd @@ -12,9 +12,10 @@ and reference them using anchors. Besides the normal yaml types, there are a couple of yambar specific types that are frequently used: -- *font*: this is a string in _fontconfig_ format. Example of valid values: - - Font Awesome 5 Brands - - Font Awesome 5 Free:style=solid +- *font*: this is a comma separated list of fonts in _fontconfig_ + format. Example of valid values: + - Font Awesome 6 Brands + - Font Awesome 6 Free:style=solid - Dina:pixelsize=10:slant=italic - Dina:pixelsize=10:weight=bold - *color*: an rgba hexstring; _RRGGBBAA_. Examples: @@ -22,12 +23,17 @@ 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* :[ *Type* :[ *Req* -:[ *Description* +:< *Description* | height : int : yes @@ -45,6 +51,11 @@ types that are frequently used: : no : Monitor to place the bar on. If not specified, the primary monitor will be used +| layer +: string +: no +: Layer to put bar on. One of _overlay_, _top_, _bottom_ or + _background_. Wayland only. Default: _bottom_. | left-spacing : int : no @@ -73,10 +84,26 @@ types that are frequently used: : associative array : no : Configures the border around the status bar +| border.left-width +: int +: no +: Width of the border on the left side, in pixels +| border.right-width +: int +: no +: Width of the border on the right side, in pixels +| border.top-width +: int +: no +: Width of the border on the top side, in pixels +| border.bottom-width +: int +: no +: Width of the border on the bottom side, in pixels | border.width : int : no -: Width, in pixels, of the border +: Short-hand for setting _border.left/right/top/bottom-width_ | border.color : color : no @@ -104,11 +131,27 @@ types that are frequently used: | font : font : no -: Default font to use in modules and particles +: Default font to use in modules and particles. May also be a comma + separated list of several fonts, in which case the first font is + the primary font, and the rest fallback fonts. These are yambar + custom fallback fonts that will be searched before the fontconfig + provided fallback list. +| font-shaping +: enum +: no +: Default setting for font-shaping, for use in particles. One of + _full_ or _none_. When set to _full_ (the default), strings will be + "shaped" using HarfBuzz. Requires support in fcft. | foreground : color : no : Default foreground (text) color to use +| trackpad-sensitivity +: int +: no +: How easy it is to trigger wheel-up and wheel-down on-click + handlers. Higher values means you need to drag your finger a longer + distance. The default is 30. | left : list : no @@ -135,8 +178,8 @@ bar: right: - clock: - content: - - string: {text: "{time}"} + content: + - string: {text: "{time}"} ``` # FILES diff --git a/examples/configurations/laptop.conf b/examples/configurations/laptop.conf new file mode 100644 index 0000000..1bdd16c --- /dev/null +++ b/examples/configurations/laptop.conf @@ -0,0 +1,289 @@ +# Typical laptop setup, with wifi, brightness, battery etc, for +# i3/Sway. + +# 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 reusing here and there +awesome: &awesome Font Awesome 6 Free:style=solid:pixelsize=14 +awesome_brands: &awesome_brands Font Awesome 6 Brands:pixelsize=16 + +std_underline: &std_underline {underline: { size: 2, color: ff0000ff}} + +# This is THE bar configuration +bar: + height: 26 + location: top + spacing: 5 + margin: 7 + + # Default font + font: Adobe Helvetica:pixelsize=12 + + foreground: ffffffff + background: 111111cc + + border: + width: 1 + color: 999999cc + margin: 5 + top-margin: 0 + + left: + - i3: + anchors: # Not used (directly) by f00bar; used here to avoid duplication + - string: &i3_common {margin: 5, on-click: "swaymsg --quiet workspace {name}"} + - string: &default {<<: *i3_common, text: "? {name}"} + - string: &main {<<: *i3_common, text: , font: *awesome} + - string: &surfing {<<: *i3_common, text: , font: *awesome_brands} + - string: &misc {<<: *i3_common, text: , font: *awesome} + - string: &mail {<<: *i3_common, text: , font: *awesome} + - string: &music {<<: *i3_common, text: , font: *awesome} + - focused: &focused + deco: {stack: [background: {color: ffa0a04c}, <<: *std_underline]} + - invisible: &invisible {foreground: ffffff55} + - urgent: &urgent + foreground: 000000ff + deco: {stack: [background: {color: bc2b3fff}, <<: *std_underline]} + - map: &i3_mode + default: + - string: + margin: 5 + text: "{mode}" + deco: {background: {color: cc421dff}} + - empty: {right-margin: 7} + conditions: + mode == default: {empty: {}} + content: + "": + map: + conditions: + state == focused: {string: {<<: [*default, *focused]}} + state == unfocused: {string: {<<: *default}} + state == invisible: {string: {<<: [*default, *invisible]}} + state == urgent: {string: {<<: [*default, *urgent]}} + main: + map: + conditions: + state == focused: {string: {<<: [*main, *focused]}} + state == unfocused: {string: {<<: *main}} + state == invisible: {string: {<<: [*main, *invisible]}} + state == urgent: {string: {<<: [*main, *urgent]}} + surfing: + map: + conditions: + state == focused: {string: {<<: [*surfing, *focused]}} + state == unfocused: {string: {<<: *surfing}} + state == invisible: {string: {<<: [*surfing, *invisible]}} + state == urgent: {string: {<<: [*surfing, *urgent]}} + misc: + map: + conditions: + state == focused: {string: {<<: [*misc, *focused]}} + state == unfocused: {string: {<<: *misc}} + state == invisible: {string: {<<: [*misc, *invisible]}} + state == urgent: {string: {<<: [*misc, *urgent]}} + + mail: + map: + conditions: + state == focused: {string: {<<: [*mail, *focused]}} + state == unfocused: {string: {<<: *mail}} + state == invisible: {string: {<<: [*mail, *invisible]}} + state == urgent: {string: {<<: [*mail, *urgent]}} + music: + map: + conditions: + state == focused: {string: {<<: [*music, *focused]}} + state == unfocused: {string: {<<: *music}} + state == invisible: {string: {<<: [*music, *invisible]}} + state == urgent: {string: {<<: [*music, *urgent]}} + + - foreign-toplevel: + content: + map: + conditions: + ~activated: {empty: {}} + activated: + - string: {text: "{app-id}", foreground: ffa0a0ff} + - string: {text: ": {title}"} + center: + - mpd: + host: /run/mpd/socket + anchors: + list: &artist_album_title + spacing: 0 + items: + - map: + conditions: + state == playing: {string: {text: "{artist}"}} + state == paused: {string: {text: "{artist}", foreground: ffffff66}} + - string: {text: " | ", foreground: ffffff66} + - map: + conditions: + state == playing: {string: {text: "{album}"}} + state == paused: {string: {text: "{album}", foreground: ffffff66}} + - string: {text: " | ", foreground: ffffff66} + - map: + conditions: + state == playing: {string: {text: "{title}", foreground: ffa0a0ff}} + state == paused: {string: {text: "{title}", foreground: ffffff66}} + + content: + map: + margin: 10 + conditions: + state == offline: {string: {text: offline, foreground: ff0000ff}} + state == stopped: {string: {text: stopped}} + state == paused: {list: *artist_album_title} + state == playing: {list: *artist_album_title} + + right: + - removables: + anchors: + drive: &drive { text: , font: *awesome} + optical: &optical {text: , font: *awesome} + spacing: 5 + content: + map: + conditions: + ~mounted: + map: + on-click: udisksctl mount -b {device} + conditions: + ~optical: [{string: *drive}, {string: {text: "{label}"}}] + optical: [{string: *optical}, {string: {text: "{label}"}}] + mounted: + map: + on-click: udisksctl unmount -b {device} + conditions: + ~optical: + - string: {<<: *drive, deco: *std_underline} + - string: {text: "{label}"} + optical: + - string: {<<: *optical, deco: *std_underline} + - string: {text: "{label}"} + - sway-xkb: + identifiers: [1:1:AT_Translated_Set_2_keyboard] + content: + - string: {text: , font: *awesome} + - string: {text: "{layout}"} + - network: + content: + map: + default: {empty: {}} + conditions: + name == enp1s0: + map: + conditions: + ~carrier: {empty: {}} + carrier: + map: + default: {string: {text: , font: *awesome, foreground: ffffff66}} + conditions: + state == up && ipv4 != "": {string: {text: , font: *awesome}} + - network: + poll-interval: 1000 + content: + map: + default: {empty: {}} + conditions: + name == wlp2s0: + map: + default: {string: {text: , font: *awesome, foreground: ffffff66}} + conditions: + state == down: {string: {text: , font: *awesome, foreground: ff0000ff}} + state == up: + map: + default: + - string: {text: , font: *awesome} + - string: {text: "{ssid} {dl-speed:mb}/{ul-speed:mb} Mb/s"} + + conditions: + ipv4 == "": + - string: {text: , font: *awesome, foreground: ffffff66} + - string: {text: "{ssid} {dl-speed:mb}/{ul-speed:mb} Mb/s", foreground: ffffff66} + - alsa: + card: hw:PCH + mixer: Master + content: + map: + conditions: + ~online: {string: {text: , font: *awesome, foreground: ff0000ff}} + online: + map: + on-click: /bin/sh -c "amixer -q sset Speaker unmute && amixer -q sset Headphone unmute && amixer -q sset Master toggle" + conditions: + muted: {string: {text: , font: *awesome, foreground: ffffff66}} + ~muted: + ramp: + tag: percent + items: + - string: {text: , font: *awesome} + - string: {text: , font: *awesome} + - string: {text: , font: *awesome} + - backlight: + name: intel_backlight + content: [ string: {text: , font: *awesome}, string: {text: "{percent}%"}] + - battery: + name: BAT0 + poll-interval: 30000 + anchors: + discharging: &discharging + list: + items: + - ramp: + tag: capacity + items: + - string: {text: , foreground: ff0000ff, font: *awesome} + - string: {text: , foreground: ffa600ff, font: *awesome} + - string: {text: , font: *awesome} + - string: {text: , font: *awesome} + - string: {text: , font: *awesome} + - string: {text: , font: *awesome} + - string: {text: , font: *awesome} + - string: {text: , font: *awesome} + - string: {text: , font: *awesome} + - string: {text: , foreground: 00ff00ff, font: *awesome} + - string: {text: "{capacity}% {estimate}"} + content: + map: + conditions: + state == unknown: + <<: *discharging + state == discharging: + <<: *discharging + state == charging: + - string: {text: , foreground: 00ff00ff, font: *awesome} + - string: {text: "{capacity}% {estimate}"} + state == full: + - string: {text: , foreground: 00ff00ff, font: *awesome} + - string: {text: "{capacity}% full"} + state == "not charging": + - ramp: + tag: capacity + items: + - string: {text:  , foreground: ff0000ff, font: *awesome} + - string: {text:  , foreground: ffa600ff, font: *awesome} + - string: {text:  , foreground: 00ff00ff, font: *awesome} + - string: {text:  , foreground: 00ff00ff, font: *awesome} + - string: {text:  , foreground: 00ff00ff, font: *awesome} + - string: {text:  , foreground: 00ff00ff, font: *awesome} + - string: {text:  , foreground: 00ff00ff, font: *awesome} + - string: {text:  , foreground: 00ff00ff, font: *awesome} + - string: {text:  , foreground: 00ff00ff, font: *awesome} + - string: {text:  , foreground: 00ff00ff, font: *awesome} + - string: {text: "{capacity}%"} + - clock: + time-format: "%H:%M %Z" + content: + - string: {text: , font: *awesome} + - string: {text: "{date}", right-margin: 5} + - string: {text: , font: *awesome} + - string: {text: "{time}"} + - label: + content: + string: + on-click: systemctl poweroff + text:  + font: *awesome diff --git a/examples/configurations/river-tags.conf b/examples/configurations/river-tags.conf new file mode 100644 index 0000000..28bc0b9 --- /dev/null +++ b/examples/configurations/river-tags.conf @@ -0,0 +1,58 @@ +hack: &hack Hack Nerd Font:pixelsize=13 +bg_default: &bg_default {stack: [{background: {color: 81A1C1ff}}, {underline: {size: 4, color: D8DEE9ff}}]} +bar: + height: 40 + location: top + font: JuliaMono:pixelsize=10 + spacing: 2 + margin: 0 + layer: bottom + foreground: eeeeeeff + background: 2E3440dd + + left: + - river: + anchors: + - base: &river_base + left-margin: 10 + right-margin: 13 + default: {string: {text: , font: *hack}} + conditions: + id == 1: {string: {text: ﳐ, font: *hack}} + id == 2: {string: {text: , font: *hack}} + id == 3: {string: {text: , font: *hack}} + id == 4: {string: {text: , font: *hack}} + id == 5: {string: {text: , font: *hack}} + id == 10: {string: {text: "scratchpad", font: *hack}} + id == 11: {string: {text: "work", font: *hack}} + + 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}} diff --git a/examples/laptop.conf b/examples/laptop.conf deleted file mode 100644 index d124e72..0000000 --- a/examples/laptop.conf +++ /dev/null @@ -1,273 +0,0 @@ -# Typical laptop setup, with wifi, brightness, battery etc, for -# i3/Sway. - -# 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 -awesome: &awesome Font Awesome 5 Free:style=solid:pixelsize=14 -awesome_brands: &awesome_brands Font Awesome 5 Brands:pixelsize=16 - -std_underline: &std_underline {underline: { size: 2, color: ff0000ff}} - -# This is THE bar configuration -bar: - height: 26 - location: top - spacing: 5 - margin: 7 - - # Default font - font: Adobe Helvetica:pixelsize=12 - - foreground: ffffffff - background: 111111cc - - border: - width: 1 - color: 999999cc - margin: 5 - top-margin: 0 - - left: - - i3: - anchors: # Not used (directly) by f00bar; used here to avoid duplication - - string: &i3_common {margin: 5, on-click: "swaymsg --quiet workspace {name}"} - - string: &default {<<: *i3_common, text: "? {name}"} - - string: &main {<<: *i3_common, text: , font: *awesome} - - string: &surfing {<<: *i3_common, text: , font: *awesome_brands} - - string: &misc {<<: *i3_common, text: , font: *awesome} - - string: &mail {<<: *i3_common, text: , font: *awesome} - - string: &music {<<: *i3_common, text: , font: *awesome} - - focused: &focused - deco: {stack: [background: {color: ffa0a04c}, <<: *std_underline]} - - invisible: &invisible {foreground: ffffff55} - - urgent: &urgent - foreground: 000000ff - deco: {stack: [background: {color: bc2b3fff}, <<: *std_underline]} - content: - "": - map: - tag: state - values: - focused: {string: {<<: [*default, *focused]}} - unfocused: {string: {<<: *default}} - invisible: {string: {<<: [*default, *invisible]}} - urgent: {string: {<<: [*default, *urgent]}} - main: - map: - tag: state - values: - focused: {string: {<<: [*main, *focused]}} - unfocused: {string: {<<: *main}} - invisible: {string: {<<: [*main, *invisible]}} - urgent: {string: {<<: [*main, *urgent]}} - surfing: - map: - tag: state - values: - focused: {string: {<<: [*surfing, *focused]}} - unfocused: {string: {<<: *surfing}} - invisible: {string: {<<: [*surfing, *invisible]}} - urgent: {string: {<<: [*surfing, *urgent]}} - misc: - map: - tag: state - values: - focused: {string: {<<: [*misc, *focused]}} - unfocused: {string: {<<: *misc}} - invisible: {string: {<<: [*misc, *invisible]}} - urgent: {string: {<<: [*misc, *urgent]}} - - mail: - map: - tag: state - values: - focused: {string: {<<: [*mail, *focused]}} - unfocused: {string: {<<: *mail}} - invisible: {string: {<<: [*mail, *invisible]}} - urgent: {string: {<<: [*mail, *urgent]}} - music: - map: - tag: state - values: - focused: {string: {<<: [*music, *focused]}} - unfocused: {string: {<<: *music}} - invisible: {string: {<<: [*music, *invisible]}} - urgent: {string: {<<: [*music, *urgent]}} - current: - map: - left-margin: 7 - tag: application - values: - "": {string: {text: "{title}"}} - default: - list: - spacing: 0 - items: - - string: {text: "{application}", max: 10, foreground: ffa0a0ff} - - string: {text: ": "} - - string: {text: "{title}", max: 35} - - center: - - mpd: - host: /run/mpd/socket - anchors: - list: &artist_album_title - spacing: 0 - items: - - map: - tag: state - values: - playing: {string: {text: "{artist}"}} - paused: {string: {text: "{artist}", foreground: ffffff66}} - - string: {text: " | ", foreground: ffffff66} - - map: - tag: state - values: - playing: {string: {text: "{album}"}} - paused: {string: {text: "{album}", foreground: ffffff66}} - - string: {text: " | ", foreground: ffffff66} - - map: - tag: state - values: - playing: {string: {text: "{title}", foreground: ffa0a0ff}} - paused: {string: {text: "{title}", foreground: ffffff66}} - - content: - map: - margin: 10 - tag: state - values: - offline: {string: {text: offline, foreground: ff0000ff}} - stopped: {string: {text: stopped}} - paused: {list: *artist_album_title} - playing: {list: *artist_album_title} - - right: - - removables: - anchors: - drive: &drive { text: , font: *awesome} - optical: &optical {text: , font: *awesome} - spacing: 5 - content: - map: - tag: mounted - values: - false: - map: - tag: optical - on-click: udisksctl mount -b {device} - values: - false: [{string: *drive}, {string: {text: "{label}"}}] - true: [{string: *optical}, {string: {text: "{label}"}}] - true: - map: - tag: optical - on-click: udisksctl unmount -b {device} - values: - false: - - string: {<<: *drive, deco: *std_underline} - - string: {text: "{label}"} - true: - - string: {<<: *optical, deco: *std_underline} - - string: {text: "{label}"} - - sway-xkb: - identifiers: [1:1:AT_Translated_Set_2_keyboard] - content: - - string: {text: , font: *awesome} - - string: {text: "{layout}"} - - network: - name: enp1s0 - content: - map: - tag: carrier - values: - false: {empty: {}} - true: - map: - tag: state - default: {string: {text: , font: *awesome, foreground: ffffff66}} - values: - up: - map: - tag: ipv4 - default: {string: {text: , font: *awesome}} - values: - "": {string: {text: , font: *awesome, foreground: ffffff66}} - - network: - name: wlp2s0 - content: - map: - tag: state - default: {string: {text: , font: *awesome, foreground: ffffff66}} - values: - down: {string: {text: , font: *awesome, foreground: ff0000ff}} - up: - map: - tag: ipv4 - default: {string: {text: , font: *awesome}} - values: - "": {string: {text: , font: *awesome, foreground: ffffff66}} - - alsa: - card: hw:PCH - mixer: Master - content: - map: - on-click: /bin/sh -c "amixer -q sset Speaker unmute && amixer -q sset Headphone unmute && amixer -q sset Master toggle" - tag: muted - values: - true: {string: {text: , font: *awesome, foreground: ffffff66}} - false: - ramp: - tag: volume - items: - - string: {text: , font: *awesome} - - string: {text: , font: *awesome} - - string: {text: , font: *awesome} - - string: {text: , font: *awesome} - - string: {text: , font: *awesome} - - backlight: - name: intel_backlight - content: [ string: {text: , font: *awesome}, string: {text: "{percent}%"}] - - battery: - name: BAT0 - poll-interval: 30 - content: - map: - tag: state - values: - discharging: - - ramp: - tag: capacity - items: - - string: {text: , foreground: ff0000ff, font: *awesome} - - string: {text: , foreground: ffa600ff, font: *awesome} - - string: {text: , font: *awesome} - - string: {text: , font: *awesome} - - string: {text: , font: *awesome} - - string: {text: , font: *awesome} - - string: {text: , font: *awesome} - - string: {text: , font: *awesome} - - string: {text: , font: *awesome} - - string: {text: , foreground: 00ff00ff, font: *awesome} - - string: {text: "{capacity}% {estimate}"} - charging: - - string: {text: , foreground: 00ff00ff, font: *awesome} - - string: {text: "{capacity}% {estimate}"} - full: - - string: {text: , foreground: 00ff00ff, font: *awesome} - - string: {text: "{capacity}% full"} - - clock: - time-format: "%H:%M %Z" - content: - - string: {text: , font: *awesome} - - string: {text: "{date}", right-margin: 5} - - string: {text: , font: *awesome} - - string: {text: "{time}"} - - label: - content: - string: - on-click: loginctl poweroff - text:  - font: *awesome 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/cpu.sh b/examples/scripts/cpu.sh new file mode 100755 index 0000000..66615c5 --- /dev/null +++ b/examples/scripts/cpu.sh @@ -0,0 +1,124 @@ +#!/bin/bash + +# cpu.sh - measures CPU usage at a configurable sample interval +# +# Usage: cpu.sh INTERVAL_IN_SECONDS +# +# This script will emit the following tags on stdout (N is the number +# of logical CPUs): +# +# Name Type +# -------------------- +# cpu range 0-100 +# cpu0 range 0-100 +# cpu1 range 0-100 +# ... +# cpuN-1 range 0-100 +# +# I.e. ‘cpu’ is the average (or aggregated) CPU usage, while cpuX is a +# specific CPU’s usage. +# +# Example configuration (update every second): +# +# - script: +# path: /path/to/cpu.sh +# args: [1] +# content: {string: {text: "{cpu}%"}} +# + +interval=${1} + +case ${interval} in + ''|*[!0-9]*) + echo "interval must be an integer" + exit 1 + ;; + *) + ;; +esac + +# Get number of CPUs, by reading /proc/stat +# The output looks like: +# +# cpu A B C D ... +# cpu0 A B C D ... +# cpu1 A B C D ... +# cpuN A B C D ... +# +# The first line is a summary line, accounting *all* CPUs +IFS=$'\n' readarray -t all_cpu_stats < <(grep -e "^cpu" /proc/stat) +cpu_count=$((${#all_cpu_stats[@]} - 1)) + +# Arrays of ‘previous’ idle and total stats, needed to calculate the +# difference between each sample. +prev_idle=() +prev_total=() +for i in $(seq ${cpu_count}); do + prev_idle+=(0) + prev_total+=(0) +done + +prev_average_idle=0 +prev_average_total=0 + +while true; do + IFS=$'\n' readarray -t all_cpu_stats < <(grep -e "^cpu" /proc/stat) + + usage=() # CPU usage in percent, 0 <= x <= 100 + + average_idle=0 # All CPUs idle time since boot + average_total=0 # All CPUs total time since boot + + for i in $(seq 0 $((cpu_count - 1))); do + # Split this CPUs stats into an array + stats=($(echo "${all_cpu_stats[$((i + 1))]}")) + + # man procfs(5) + user=${stats[1]} + nice=${stats[2]} + system=${stats[3]} + idle=${stats[4]} + iowait=${stats[5]} + irq=${stats[6]} + softirq=${stats[7]} + steal=${stats[8]} + guest=${stats[9]} + guestnice=${stats[10]} + + # Guest time already accounted for in user + user=$((user - guest)) + nice=$((nice - guestnice)) + + idle=$((idle + iowait)) + + total=$((user + nice + system + irq + softirq + idle + steal + guest + guestnice)) + + average_idle=$((average_idle + idle)) + average_total=$((average_total + total)) + + # Diff since last sample + diff_idle=$((idle - prev_idle[i])) + diff_total=$((total - prev_total[i])) + + usage[i]=$((100 * (diff_total - diff_idle) / diff_total)) + + prev_idle[i]=${idle} + prev_total[i]=${total} + done + + diff_average_idle=$((average_idle - prev_average_idle)) + diff_average_total=$((average_total - prev_average_total)) + + average_usage=$((100 * (diff_average_total - diff_average_idle) / diff_average_total)) + + prev_average_idle=${average_idle} + prev_average_total=${average_total} + + echo "cpu|range:0-100|${average_usage}" + for i in $(seq 0 $((cpu_count - 1))); do + echo "cpu${i}|range:0-100|${usage[i]}" + done + + echo "" + sleep "${interval}" +done diff --git a/examples/scripts/dwl-tags.sh b/examples/scripts/dwl-tags.sh new file mode 100755 index 0000000..4999548 --- /dev/null +++ b/examples/scripts/dwl-tags.sh @@ -0,0 +1,138 @@ +#!/usr/bin/env bash +# +# dwl-tags.sh - display dwl tags +# +# USAGE: dwl-tags.sh 1 +# +# REQUIREMENTS: +# - inotifywait ( 'inotify-tools' on arch ) +# - Launch dwl with `dwl > ~.cache/dwltags` or change $fname +# +# TAGS: +# Name Type Return +# ---------------------------------------------------- +# {tag_N} string dwl tags name +# {tag_N_occupied} bool dwl tags state occupied +# {tag_N_focused} bool dwl tags state focused +# {layout} string dwl layout +# {title} string client title +# +# Now the fun part +# +# Example configuration: +# +# - script: +# path: /absolute/path/to/dwl-tags.sh +# args: [1] +# anchors: +# - occupied: &occupied {foreground: 57bbf4ff} +# - focused: &focused {foreground: fc65b0ff} +# - default: &default {foreground: d2ccd6ff} +# content: +# - map: +# margin: 4 +# conditions: +# tag_0_occupied: +# map: +# conditions: +# tag_0_focused: {string: {text: "{tag_0}", <<: *focused}} +# ~tag_0_focused: {string: {text: "{tag_0}", <<: *occupied}} +# ~tag_0_occupied: +# map: +# conditions: +# tag_0_focused: {string: {text: "{tag_0}", <<: *focused}} +# ~tag_0_focused: {string: {text: "{tag_0}", <<: *default}} +# ... +# ... +# ... +# - map: +# margin: 4 +# conditions: +# tag_8_occupied: +# map: +# conditions: +# tag_8_focused: {string: {text: "{tag_8}", <<: *focused}} +# ~tag_8_focused: {string: {text: "{tag_8}", <<: *occupied}} +# ~tag_8_occupied: +# map: +# values: +# tag_8_focused: {string: {text: "{tag_8}", <<: *focused}} +# ~tag_8_focused: {string: {text: "{tag_8}", <<: *default}} +# - list: +# spacing: 3 +# items: +# - string: {text: "{layout}"} +# - string: {text: "{title}"} + + +# Variables +declare output title layout activetags selectedtags +declare -a tags name +readonly fname="$HOME"/.cache/dwltags + + +_cycle() { + tags=( "1" "2" "3" "4" "5" "6" "7" "8" "9" ) + + # Name of tag (optional) + # If there is no name, number are used + # + # Example: + # name=( "" "" "" "Media" ) + # -> return "" "" "" "Media" 5 6 7 8 9) + name=() + + for tag in "${!tags[@]}"; do + mask=$((1</dev/null; then + printf -- '%s\n' "${tag_name}_${tag}_focused|bool|true" + printf -- '%s\n' "title|string|${title}" + else + printf '%s\n' "${tag_name}_${tag}_focused|bool|false" + fi + + if (( "${activetags}" & mask )) 2>/dev/null; then + printf -- '%s\n' "${tag_name}_${tag}_occupied|bool|true" + else + printf -- '%s\n' "${tag_name}_${tag}_occupied|bool|false" + fi + done + + printf -- '%s\n' "layout|string|${layout}" + printf -- '%s\n' "" + +} + +# Call the function here so the tags are displayed at dwl launch +_cycle + +while true; do + + [[ ! -f "${fname}" ]] && printf -- '%s\n' \ + "You need to redirect dwl stdout to ~/.cache/dwltags" >&2 + + inotifywait -qq --event modify "${fname}" + + # Get info from the file + output="$(tail -n6 "${fname}")" + title="$(echo "${output}" | grep title | cut -d ' ' -f 3- )" + #selmon="$(echo "${output}" | grep 'selmon')" + layout="$(echo "${output}" | grep layout | cut -d ' ' -f 3- )" + + # Get the tag bit mask as a decimal + activetags="$(echo "${output}" | grep tags | awk '{print $3}')" + selectedtags="$(echo "${output}" | grep tags | awk '{print $4}')" + + _cycle + +done + +unset -v output title layout activetags selectedtags +unset -v tags name diff --git a/examples/scripts/pacman.sh b/examples/scripts/pacman.sh new file mode 100755 index 0000000..5026b5a --- /dev/null +++ b/examples/scripts/pacman.sh @@ -0,0 +1,78 @@ +#!/usr/bin/env bash +# +# pacman.sh - display number of packages update available +# by default check every hour +# +# USAGE: pacman.sh +# +# TAGS: +# Name Type Return +# ------------------------------------------- +# {pacman} int number of pacman packages +# {aur} int number of aur packages +# {pkg} int sum of both +# +# Examples configuration: +# - script: +# path: /absolute/path/to/pacman.sh +# args: [] +# content: { string: { text: "{pacman} + {aur} = {pkg}" } } +# +# To display a message when there is no update: +# - script: +# path: /absolute/path/to/pacman.sh +# args: [] +# content: +# map: +# default: { string: { text: "{pacman} + {aur} = {pkg}" } } +# conditions: +# pkg == 0: {string: {text: no updates}} + + +declare interval aur_helper pacman_num aur_num pkg_num + +# Error message in STDERR +_err() { + printf -- '%s\n' "[$(date +'%Y-%m-%d %H:%M:%S')]: $*" >&2 +} + +# Display tags before yambar fetch the updates number +printf -- '%s\n' "pacman|int|0" +printf -- '%s\n' "aur|int|0" +printf -- '%s\n' "pkg|int|0" +printf -- '%s\n' "" + + +while true; do + # Change interval + # NUMBER[SUFFIXE] + # Possible suffix: + # "s" seconds / "m" minutes / "h" hours / "d" days + interval="1h" + + # Change your aur manager + aur_helper="paru" + + # Get number of packages to update + pacman_num=$(checkupdates | wc -l) + + if ! hash "${aur_helper}" >/dev/null 2>&1; then + _err "aur helper not found, change it in the script" + exit 1 + else + aur_num=$("${aur_helper}" -Qmu | wc -l) + fi + + pkg_num=$(( pacman_num + aur_num )) + + printf -- '%s\n' "pacman|int|${pacman_num}" + printf -- '%s\n' "aur|int|${aur_num}" + printf -- '%s\n' "pkg|int|${pkg_num}" + printf -- '%s\n' "" + + sleep "${interval}" + +done + +unset -v interval aur_helper pacman_num aur_num pkg_num +unset -f _err diff --git a/external/river-status-unstable-v1.xml b/external/river-status-unstable-v1.xml index a4d6f4e..e9629dd 100644 --- a/external/river-status-unstable-v1.xml +++ b/external/river-status-unstable-v1.xml @@ -1,7 +1,7 @@ - Copyright 2020 Isaac Freund + Copyright 2020 The River Developers Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above @@ -16,7 +16,7 @@ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - + A global factory for objects that receive status information specific to river. It could be used to implement, for example, a status bar. @@ -47,7 +47,7 @@ - + This interface allows clients to receive information about the current windowing state of an output. @@ -75,12 +75,36 @@ + + + + Sent once on binding the interface and again whenever the set of + tags with at least one urgent view changes. + + + + + + + Sent once on binding the interface should a layout name exist and again + whenever the name changes. + + + + + + + Sent when the current layout name has been removed without a new one + being set, for example when the active layout generator disconnects. + + - + This interface allows clients to receive information about the current - focus of a seat. + focus of a seat. Note that (un)focused_output events will only be sent + if the client has bound the relevant wl_output globals. @@ -112,5 +136,13 @@ + + + + Sent once on binding the interface and again whenever a new mode + is entered (e.g. with riverctl enter-mode foobar). + + + diff --git a/external/wlr-foreign-toplevel-management-unstable-v1.xml b/external/wlr-foreign-toplevel-management-unstable-v1.xml new file mode 100644 index 0000000..1081337 --- /dev/null +++ b/external/wlr-foreign-toplevel-management-unstable-v1.xml @@ -0,0 +1,270 @@ + + + + Copyright © 2018 Ilia Bozhinov + + Permission to use, copy, modify, distribute, and sell this + software and its documentation for any purpose is hereby granted + without fee, provided that the above copyright notice appear in + all copies and that both that copyright notice and this permission + notice appear in supporting documentation, and that the name of + the copyright holders not be used in advertising or publicity + pertaining to distribution of the software without specific, + written prior permission. The copyright holders make no + representations about the suitability of this software for any + purpose. It is provided "as is" without express or implied + warranty. + + THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS + SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND + FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY + SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN + AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, + ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF + THIS SOFTWARE. + + + + + The purpose of this protocol is to enable the creation of taskbars + and docks by providing them with a list of opened applications and + letting them request certain actions on them, like maximizing, etc. + + After a client binds the zwlr_foreign_toplevel_manager_v1, each opened + toplevel window will be sent via the toplevel event + + + + + This event is emitted whenever a new toplevel window is created. It + is emitted for all toplevels, regardless of the app that has created + them. + + All initial details of the toplevel(title, app_id, states, etc.) will + be sent immediately after this event via the corresponding events in + zwlr_foreign_toplevel_handle_v1. + + + + + + + Indicates the client no longer wishes to receive events for new toplevels. + However the compositor may emit further toplevel_created events, until + the finished event is emitted. + + The client must not send any more requests after this one. + + + + + + This event indicates that the compositor is done sending events to the + zwlr_foreign_toplevel_manager_v1. The server will destroy the object + immediately after sending this request, so it will become invalid and + the client should free any resources associated with it. + + + + + + + A zwlr_foreign_toplevel_handle_v1 object represents an opened toplevel + window. Each app may have multiple opened toplevels. + + Each toplevel has a list of outputs it is visible on, conveyed to the + client with the output_enter and output_leave events. + + + + + This event is emitted whenever the title of the toplevel changes. + + + + + + + This event is emitted whenever the app-id of the toplevel changes. + + + + + + + This event is emitted whenever the toplevel becomes visible on + the given output. A toplevel may be visible on multiple outputs. + + + + + + + This event is emitted whenever the toplevel stops being visible on + the given output. It is guaranteed that an entered-output event + with the same output has been emitted before this event. + + + + + + + Requests that the toplevel be maximized. If the maximized state actually + changes, this will be indicated by the state event. + + + + + + Requests that the toplevel be unmaximized. If the maximized state actually + changes, this will be indicated by the state event. + + + + + + Requests that the toplevel be minimized. If the minimized state actually + changes, this will be indicated by the state event. + + + + + + Requests that the toplevel be unminimized. If the minimized state actually + changes, this will be indicated by the state event. + + + + + + Request that this toplevel be activated on the given seat. + There is no guarantee the toplevel will be actually activated. + + + + + + + The different states that a toplevel can have. These have the same meaning + as the states with the same names defined in xdg-toplevel + + + + + + + + + + + This event is emitted immediately after the zlw_foreign_toplevel_handle_v1 + is created and each time the toplevel state changes, either because of a + compositor action or because of a request in this protocol. + + + + + + + + This event is sent after all changes in the toplevel state have been + sent. + + This allows changes to the zwlr_foreign_toplevel_handle_v1 properties + to be seen as atomic, even if they happen via multiple events. + + + + + + Send a request to the toplevel to close itself. The compositor would + typically use a shell-specific method to carry out this request, for + example by sending the xdg_toplevel.close event. However, this gives + no guarantees the toplevel will actually be destroyed. If and when + this happens, the zwlr_foreign_toplevel_handle_v1.closed event will + be emitted. + + + + + + The rectangle of the surface specified in this request corresponds to + the place where the app using this protocol represents the given toplevel. + It can be used by the compositor as a hint for some operations, e.g + minimizing. The client is however not required to set this, in which + case the compositor is free to decide some default value. + + If the client specifies more than one rectangle, only the last one is + considered. + + The dimensions are given in surface-local coordinates. + Setting width=height=0 removes the already-set rectangle. + + + + + + + + + + + + + + + + This event means the toplevel has been destroyed. It is guaranteed there + won't be any more events for this zwlr_foreign_toplevel_handle_v1. The + toplevel itself becomes inert so any requests will be ignored except the + destroy request. + + + + + + Destroys the zwlr_foreign_toplevel_handle_v1 object. + + This request should be called either when the client does not want to + use the toplevel anymore or after the closed event to finalize the + destruction of the object. + + + + + + + + Requests that the toplevel be fullscreened on the given output. If the + fullscreen state and/or the outputs the toplevel is visible on actually + change, this will be indicated by the state and output_enter/leave + events. + + The output parameter is only a hint to the compositor. Also, if output + is NULL, the compositor should decide which output the toplevel will be + fullscreened on, if at all. + + + + + + + Requests that the toplevel be unfullscreened. If the fullscreen state + actually changes, this will be indicated by the state event. + + + + + + + + This event is emitted whenever the parent of the toplevel changes. + + No event is emitted when the parent handle is destroyed by the client. + + + + + diff --git a/external/wlr-layer-shell-unstable-v1.xml b/external/wlr-layer-shell-unstable-v1.xml index fa67001..d62fd51 100644 --- a/external/wlr-layer-shell-unstable-v1.xml +++ b/external/wlr-layer-shell-unstable-v1.xml @@ -25,7 +25,7 @@ THIS SOFTWARE. - + Clients can use this interface to assign the surface_layer role to wl_surfaces. Such surfaces are assigned to a "layer" of the output and @@ -47,6 +47,12 @@ or manipulate a buffer prior to the first layer_surface.configure call must also be treated as errors. + After creating a layer_surface object and setting it up, the client + must perform an initial commit without any buffer attached. + The compositor will reply with a layer_surface.configure event. + The client must acknowledge it and is then allowed to attach a buffer + to map the surface. + You may pass NULL for output to allow the compositor to decide which output to use. Generally this will be the one that the user most recently interacted with. @@ -94,7 +100,7 @@ - + An interface that may be implemented by a wl_surface, for surfaces that are designed to be rendered as a layer of a stacked desktop-like @@ -103,6 +109,14 @@ Layer surface state (layer, size, anchor, exclusive zone, margin, interactivity) is double-buffered, and will be applied at the time wl_surface.commit of the corresponding wl_surface is called. + + Attaching a null buffer to a layer surface unmaps it. + + Unmapping a layer_surface means that the surface cannot be shown by the + compositor until it is explicitly mapped again. The layer_surface + returns to the state it had right after layer_shell.get_layer_surface. + The client can re-map the surface by performing a commit without any + buffer attached, waiting for a configure event and handling it as usual. @@ -189,21 +203,85 @@ + + + Types of keyboard interaction possible for layer shell surfaces. The + rationale for this is twofold: (1) some applications are not interested + in keyboard events and not allowing them to be focused can improve the + desktop experience; (2) some applications will want to take exclusive + keyboard focus. + + + + + This value indicates that this surface is not interested in keyboard + events and the compositor should never assign it the keyboard focus. + + This is the default value, set for newly created layer shell surfaces. + + This is useful for e.g. desktop widgets that display information or + only have interaction with non-keyboard input devices. + + + + + Request exclusive keyboard focus if this surface is above the shell surface layer. + + For the top and overlay layers, the seat will always give + exclusive keyboard focus to the top-most layer which has keyboard + interactivity set to exclusive. If this layer contains multiple + surfaces with keyboard interactivity set to exclusive, the compositor + determines the one receiving keyboard events in an implementation- + defined manner. In this case, no guarantee is made when this surface + will receive keyboard focus (if ever). + + For the bottom and background layers, the compositor is allowed to use + normal focus semantics. + + This setting is mainly intended for applications that need to ensure + they receive all keyboard events, such as a lock screen or a password + prompt. + + + + + This requests the compositor to allow this surface to be focused and + unfocused by the user in an implementation-defined manner. The user + should be able to unfocus this surface even regardless of the layer + it is on. + + Typically, the compositor will want to use its normal mechanism to + manage keyboard focus between layer shell surfaces with this setting + and regular toplevels on the desktop layer (e.g. click to focus). + Nevertheless, it is possible for a compositor to require a special + interaction to focus or unfocus layer shell surfaces (e.g. requiring + a click even if focus follows the mouse normally, or providing a + keybinding to switch focus between layers). + + This setting is mainly intended for desktop shell components (e.g. + panels) that allow keyboard interaction. Using this option can allow + implementing a desktop shell that can be fully usable without the + mouse. + + + + - Set to 1 to request that the seat send keyboard events to this layer - surface. For layers below the shell surface layer, the seat will use - normal focus semantics. For layers above the shell surface layers, the - seat will always give exclusive keyboard focus to the top-most layer - which has keyboard interactivity set to true. + Set how keyboard events are delivered to this surface. By default, + layer shell surfaces do not receive keyboard events; this request can + be used to change this. + + This setting is inherited by child surfaces set by the get_popup + request. Layer surfaces receive pointer, touch, and tablet events normally. If you do not want to receive them, set the input region on your surface to an empty region. - Events is double-buffered, see wl_surface.commit. + Keyboard interactivity is double-buffered, see wl_surface.commit. - + @@ -288,6 +366,7 @@ + diff --git a/external/wlr-protocols b/external/wlr-protocols index 16a2888..d1598e8 160000 --- a/external/wlr-protocols +++ b/external/wlr-protocols @@ -1 +1 @@ -Subproject commit 16a28885bc92869d8e589e725e7bf018432c47e4 +Subproject commit d1598e82240d6e8ca57729495a94d4e11d222033 diff --git a/font-shaping.h b/font-shaping.h new file mode 100644 index 0000000..3ae3817 --- /dev/null +++ b/font-shaping.h @@ -0,0 +1,7 @@ +#pragma once + +enum font_shaping { + FONT_SHAPE_NONE, + FONT_SHAPE_GRAPHEMES, + FONT_SHAPE_FULL, +}; diff --git a/generate-version.sh b/generate-version.sh index d17cd40..8ac3b03 100755 --- a/generate-version.sh +++ b/generate-version.sh @@ -13,11 +13,18 @@ out_file=${3} if [ -d "${src_dir}/.git" ] && command -v git > /dev/null; then workdir=$(pwd) cd "${src_dir}" - git_version=$(git describe --always --tags) + + if git describe --tags > /dev/null 2>&1; then + git_version=$(git describe --always --tags) + else + # No tags available, happens in e.g. CI builds + git_version="${default_version}" + fi + git_branch=$(git rev-parse --abbrev-ref HEAD) cd "${workdir}" - new_version="${git_version} ($(env LC_TIME=C date "+%b %d %Y"), branch '${git_branch}')" + new_version="${git_version} ($(date "+%b %d %Y"), branch '${git_branch}')" else new_version="${default_version}" fi diff --git a/log.c b/log.c index c5d9093..ba4ebd9 100644 --- a/log.c +++ b/log.c @@ -1,41 +1,60 @@ #include "log.h" -#include -#include -#include -#include -#include -#include #include +#include +#include +#include +#include +#include +#include +#include +#include #include -#include +#define ALEN(v) (sizeof(v) / sizeof((v)[0])) +#define UNUSED __attribute__((unused)) static bool colorize = false; -static bool do_syslog = true; +static bool do_syslog = false; +static enum log_class log_level = LOG_CLASS_NONE; + +static const struct { + const char name[8]; + const char log_prefix[7]; + uint8_t color; + int syslog_equivalent; +} log_level_map[] = { + [LOG_CLASS_NONE] = {"none", "none", 5, -1}, + [LOG_CLASS_ERROR] = {"error", " err", 31, LOG_ERR}, + [LOG_CLASS_WARNING] = {"warning", "warn", 33, LOG_WARNING}, + [LOG_CLASS_INFO] = {"info", "info", 97, LOG_INFO}, + [LOG_CLASS_DEBUG] = {"debug", " dbg", 36, LOG_DEBUG}, +}; void -log_init(enum log_colorize _colorize, bool _do_syslog, - enum log_facility syslog_facility, enum log_class syslog_level) +log_init(enum log_colorize _colorize, bool _do_syslog, enum log_facility syslog_facility, enum log_class _log_level) { static const int facility_map[] = { [LOG_FACILITY_USER] = LOG_USER, [LOG_FACILITY_DAEMON] = LOG_DAEMON, }; - static const int level_map[] = { - [LOG_CLASS_ERROR] = LOG_ERR, - [LOG_CLASS_WARNING] = LOG_WARNING, - [LOG_CLASS_INFO] = LOG_INFO, - [LOG_CLASS_DEBUG] = LOG_DEBUG, - }; + /* 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 : isatty(STDERR_FILENO); + colorize = _colorize == LOG_COLORIZE_NEVER + ? false + : _colorize == LOG_COLORIZE_ALWAYS + ? true + : !no_color && isatty(STDERR_FILENO); do_syslog = _do_syslog; + log_level = _log_level; - if (do_syslog) { - openlog(NULL, /*LOG_PID*/0, facility_map[syslog_facility]); - setlogmask(LOG_UPTO(level_map[syslog_level])); + int slvl = log_level_map[_log_level].syslog_equivalent; + if (do_syslog && slvl != -1) { + openlog(NULL, /*LOG_PID*/ 0, facility_map[syslog_facility]); + setlogmask(LOG_UPTO(slvl)); } } @@ -47,120 +66,153 @@ log_deinit(void) } static void -_log(enum log_class log_class, const char *module, const char *file, int lineno, - const char *fmt, int sys_errno, va_list va) +_log(enum log_class log_class, const char *module, const char *file, int lineno, const char *fmt, int sys_errno, + va_list va) { - const char *class = "abcd"; - int class_clr = 0; - switch (log_class) { - case LOG_CLASS_ERROR: class = " err"; class_clr = 31; break; - case LOG_CLASS_WARNING: class = "warn"; class_clr = 33; break; - case LOG_CLASS_INFO: class = "info"; class_clr = 97; break; - case LOG_CLASS_DEBUG: class = " dbg"; class_clr = 36; break; - } + assert(log_class > LOG_CLASS_NONE); + assert(log_class < ALEN(log_level_map)); + + if (log_class > log_level) + return; + + const char *prefix = log_level_map[log_class].log_prefix; + unsigned int class_clr = log_level_map[log_class].color; char clr[16]; - snprintf(clr, sizeof(clr), "\e[%dm", class_clr); - fprintf(stderr, "%s%s%s: ", colorize ? clr : "", class, colorize ? "\e[0m" : ""); + snprintf(clr, sizeof(clr), "\033[%um", class_clr); + fprintf(stderr, "%s%s%s: ", colorize ? clr : "", prefix, colorize ? "\033[0m" : ""); if (colorize) - fprintf(stderr, "\e[2m"); + fputs("\033[2m", stderr); fprintf(stderr, "%s:%d: ", file, lineno); if (colorize) - fprintf(stderr, "\e[0m"); + fputs("\033[0m", stderr); vfprintf(stderr, fmt, va); if (sys_errno != 0) - fprintf(stderr, ": %s", strerror(sys_errno)); + fprintf(stderr, ": %s (%d)", strerror(sys_errno), sys_errno); - fprintf(stderr, "\n"); + fputc('\n', stderr); } static void -_sys_log(enum log_class log_class, const char *module, - const char *file __attribute__((unused)), - int lineno __attribute__((unused)), - const char *fmt, int sys_errno, va_list va) +_sys_log(enum log_class log_class, const char *module, const char UNUSED *file, int UNUSED lineno, const char *fmt, + int sys_errno, va_list va) { + assert(log_class > LOG_CLASS_NONE); + assert(log_class < ALEN(log_level_map)); + if (!do_syslog) return; + if (log_class > log_level) + return; + /* Map our log level to syslog's level */ - int level = -1; - switch (log_class) { - case LOG_CLASS_ERROR: level = LOG_ERR; break; - case LOG_CLASS_WARNING: level = LOG_WARNING; break; - case LOG_CLASS_INFO: level = LOG_INFO; break; - case LOG_CLASS_DEBUG: level = LOG_DEBUG; break; - } + int level = log_level_map[log_class].syslog_equivalent; - assert(level != -1); + char msg[4096]; + int n = vsnprintf(msg, sizeof(msg), fmt, va); + assert(n >= 0); - const char *sys_err = sys_errno != 0 ? strerror(sys_errno) : NULL; + if (sys_errno != 0 && (size_t)n < sizeof(msg)) + snprintf(msg + n, sizeof(msg) - n, ": %s", strerror(sys_errno)); - va_list va2; - va_copy(va2, va); - - /* Calculate required size of buffer holding the entire log message */ - int required_len = 0; - required_len += strlen(module) + 2; /* "%s: " */ - required_len += vsnprintf(NULL, 0, fmt, va2); va_end(va2); - - if (sys_errno != 0) - required_len += strlen(sys_err) + 2; /* ": %s" */ - - /* Format the msg */ - char *msg = malloc(required_len + 1); - int idx = 0; - - idx += snprintf(&msg[idx], required_len + 1 - idx, "%s: ", module); - idx += vsnprintf(&msg[idx], required_len + 1 - idx, fmt, va); - - if (sys_errno != 0) { - snprintf( - &msg[idx], required_len + 1 - idx, ": %s", strerror(sys_errno)); - } - - syslog(level, "%s", msg); - free(msg); + syslog(level, "%s: %s", module, msg); } void -log_msg(enum log_class log_class, const char *module, - const char *file, int lineno, const char *fmt, ...) +log_msg_va(enum log_class log_class, const char *module, const char *file, int lineno, const char *fmt, va_list va) { - va_list ap1, ap2; - va_start(ap1, fmt); - va_copy(ap2, ap1); - _log(log_class, module, file, lineno, fmt, 0, ap1); - _sys_log(log_class, module, file, lineno, fmt, 0, ap2); - va_end(ap1); - va_end(ap2); + va_list va2; + va_copy(va2, va); + _log(log_class, module, file, lineno, fmt, 0, va); + _sys_log(log_class, module, file, lineno, fmt, 0, va2); + va_end(va2); } -void log_errno(enum log_class log_class, const char *module, - const char *file, int lineno, - const char *fmt, ...) +void +log_msg(enum log_class log_class, const char *module, const char *file, int lineno, const char *fmt, ...) { - va_list ap1, ap2; - va_start(ap1, fmt); - va_copy(ap2, ap1); - _log(log_class, module, file, lineno, fmt, errno, ap1); - _sys_log(log_class, module, file, lineno, fmt, errno, ap2); - va_end(ap1); - va_end(ap2); + va_list va; + va_start(va, fmt); + log_msg_va(log_class, module, file, lineno, fmt, va); + va_end(va); } -void log_errno_provided(enum log_class log_class, const char *module, - const char *file, int lineno, int _errno, - const char *fmt, ...) +void +log_errno_va(enum log_class log_class, const char *module, const char *file, int lineno, const char *fmt, va_list va) { - va_list ap1, ap2; - va_start(ap1, fmt); - va_copy(ap2, ap1); - _log(log_class, module, file, lineno, fmt, _errno, ap1); - _sys_log(log_class, module, file, lineno, fmt, _errno, ap2); - va_end(ap1); - va_end(ap2); + log_errno_provided_va(log_class, module, file, lineno, errno, fmt, va); +} + +void +log_errno(enum log_class log_class, const char *module, const char *file, int lineno, const char *fmt, ...) +{ + va_list va; + va_start(va, fmt); + log_errno_va(log_class, module, file, lineno, fmt, va); + va_end(va); +} + +void +log_errno_provided_va(enum log_class log_class, const char *module, const char *file, int lineno, int errno_copy, + const char *fmt, va_list va) +{ + va_list va2; + va_copy(va2, va); + _log(log_class, module, file, lineno, fmt, errno_copy, va); + _sys_log(log_class, module, file, lineno, fmt, errno_copy, va2); + va_end(va2); +} + +void +log_errno_provided(enum log_class log_class, const char *module, const char *file, int lineno, int errno_copy, + const char *fmt, ...) +{ + va_list va; + va_start(va, fmt); + log_errno_provided_va(log_class, module, file, lineno, errno_copy, fmt, va); + va_end(va); +} + +static size_t +map_len(void) +{ + size_t len = ALEN(log_level_map); +#ifndef _DEBUG + /* Exclude "debug" entry for non-debug builds */ + len--; +#endif + return len; +} + +int +log_level_from_string(const char *str) +{ + if (str[0] == '\0') + return -1; + + for (int i = 0, n = map_len(); i < n; i++) + if (strcmp(str, log_level_map[i].name) == 0) + return i; + + return -1; +} + +const char * +log_level_string_hint(void) +{ + static char buf[64]; + if (buf[0] != '\0') + return buf; + + for (size_t i = 0, pos = 0, n = map_len(); i < n; i++) { + const char *entry = log_level_map[i].name; + const char *delim = (i + 1 < n) ? ", " : ""; + pos += snprintf(buf + pos, sizeof(buf) - pos, "'%s'%s", entry, delim); + } + + return buf; } diff --git a/log.h b/log.h index dfddd76..48f16fe 100644 --- a/log.h +++ b/log.h @@ -1,42 +1,43 @@ #pragma once +#include #include enum log_colorize { LOG_COLORIZE_NEVER, LOG_COLORIZE_ALWAYS, LOG_COLORIZE_AUTO }; enum log_facility { LOG_FACILITY_USER, LOG_FACILITY_DAEMON }; -enum log_class { LOG_CLASS_ERROR, LOG_CLASS_WARNING, LOG_CLASS_INFO, LOG_CLASS_DEBUG }; -void log_init(enum log_colorize colorize, bool do_syslog, - enum log_facility syslog_facility, enum log_class syslog_level); +enum log_class { LOG_CLASS_NONE, LOG_CLASS_ERROR, LOG_CLASS_WARNING, LOG_CLASS_INFO, LOG_CLASS_DEBUG }; + +void log_init(enum log_colorize colorize, bool do_syslog, enum log_facility syslog_facility, enum log_class log_level); void log_deinit(void); -void log_msg(enum log_class log_class, const char *module, - const char *file, int lineno, - const char *fmt, ...) __attribute__((format (printf, 5, 6))); +void log_msg(enum log_class log_class, const char *module, const char *file, int lineno, const char *fmt, ...) + __attribute__((format(printf, 5, 6))); -void log_errno(enum log_class log_class, const char *module, - const char *file, int lineno, - const char *fmt, ...) __attribute__((format (printf, 5, 6))); +void log_errno(enum log_class log_class, const char *module, const char *file, int lineno, const char *fmt, ...) + __attribute__((format(printf, 5, 6))); -void log_errno_provided( - enum log_class log_class, const char *module, - const char *file, int lineno, int _errno, - const char *fmt, ...) __attribute__((format (printf, 6, 7))); +void log_errno_provided(enum log_class log_class, const char *module, const char *file, int lineno, int _errno, + const char *fmt, ...) __attribute__((format(printf, 6, 7))); -#define LOG_ERR(fmt, ...) \ - log_msg(LOG_CLASS_ERROR, LOG_MODULE, __FILE__, __LINE__, fmt, ## __VA_ARGS__) -#define LOG_ERRNO(fmt, ...) \ - log_errno(LOG_CLASS_ERROR, LOG_MODULE, __FILE__, __LINE__, fmt, ## __VA_ARGS__) -#define LOG_ERRNO_P(fmt, _errno, ...) \ - log_errno_provided(LOG_CLASS_ERROR, LOG_MODULE, __FILE__, __LINE__, \ - _errno, fmt, ## __VA_ARGS__) -#define LOG_WARN(fmt, ...) \ - log_msg(LOG_CLASS_WARNING, LOG_MODULE, __FILE__, __LINE__, fmt, ## __VA_ARGS__) -#define LOG_INFO(fmt, ...) \ - log_msg(LOG_CLASS_INFO, LOG_MODULE, __FILE__, __LINE__, fmt, ## __VA_ARGS__) +void log_msg_va(enum log_class log_class, const char *module, const char *file, int lineno, const char *fmt, va_list va) + __attribute__((format(printf, 5, 0))); +void log_errno_va(enum log_class log_class, const char *module, const char *file, int lineno, const char *fmt, + va_list va) __attribute__((format(printf, 5, 0))); +void log_errno_provided_va(enum log_class log_class, const char *module, const char *file, int lineno, int _errno, + const char *fmt, va_list va) __attribute__((format(printf, 6, 0))); + +int log_level_from_string(const char *str); +const char *log_level_string_hint(void); + +#define LOG_ERR(...) log_msg(LOG_CLASS_ERROR, LOG_MODULE, __FILE__, __LINE__, __VA_ARGS__) +#define LOG_ERRNO(...) log_errno(LOG_CLASS_ERROR, LOG_MODULE, __FILE__, __LINE__, __VA_ARGS__) +#define LOG_ERRNO_P(_errno, ...) \ + log_errno_provided(LOG_CLASS_ERROR, LOG_MODULE, __FILE__, __LINE__, _errno, __VA_ARGS__) +#define LOG_WARN(...) log_msg(LOG_CLASS_WARNING, LOG_MODULE, __FILE__, __LINE__, __VA_ARGS__) +#define LOG_INFO(...) log_msg(LOG_CLASS_INFO, LOG_MODULE, __FILE__, __LINE__, __VA_ARGS__) #if defined(LOG_ENABLE_DBG) && LOG_ENABLE_DBG - #define LOG_DBG(fmt, ...) \ - log_msg(LOG_CLASS_DEBUG, LOG_MODULE, __FILE__, __LINE__, fmt, ## __VA_ARGS__) +#define LOG_DBG(...) log_msg(LOG_CLASS_DEBUG, LOG_MODULE, __FILE__, __LINE__, __VA_ARGS__) #else - #define LOG_DBG(fmt, ...) +#define LOG_DBG(...) #endif diff --git a/main.c b/main.c index d93b869..c355843 100644 --- a/main.c +++ b/main.c @@ -1,4 +1,6 @@ #include +#include +#include #include #include #include @@ -9,14 +11,12 @@ #include #include #include -#include -#include -#include -#include -#include #include #include +#include +#include +#include #include "bar/bar.h" #include "config.h" @@ -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; @@ -127,13 +127,14 @@ print_usage(const char *prog_name) printf("Usage: %s [OPTION]...\n", prog_name); printf("\n"); printf("Options:\n"); - printf(" -b,--backend={xcb,wayland,auto} backend to use (default: auto)\n" - " -c,--config=FILE alternative configuration file\n" - " -C,--validate verify configuration then quit\n" - " -p,--print-pid=FILE|FD print PID to file or FD\n" - " -l,--log-colorize=[never|always|auto] enable/disable colorization of log output on stderr\n" - " -s,--log-no-syslog disable syslog logging\n" - " -v,--version show the version number and quit\n"); + printf(" -b,--backend={xcb,wayland,auto} backend to use (default: auto)\n" + " -c,--config=FILE alternative configuration file\n" + " -C,--validate verify configuration then quit\n" + " -p,--print-pid=FILE|FD print PID to file or FD\n" + " -d,--log-level={info|warning|error|none} log level (warning)\n" + " -l,--log-colorize=[never|always|auto] enable/disable colorization of log output on stderr\n" + " -s,--log-no-syslog disable syslog logging\n" + " -v,--version show the version number and quit\n"); } static bool @@ -146,9 +147,8 @@ print_pid(const char *pid_file, bool *unlink_at_exit) int pid_fd = strtoul(pid_file, &end, 10); if (errno != 0 || *end != '\0') { - if ((pid_fd = open(pid_file, - O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC, - S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)) < 0) { + if ((pid_fd = open(pid_file, O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)) + < 0) { LOG_ERRNO("%s: failed to open", pid_file); return false; } else @@ -177,15 +177,16 @@ int main(int argc, char *const *argv) { static const struct option longopts[] = { - {"backend", required_argument, 0, 'b'}, - {"config", required_argument, 0, 'c'}, - {"validate", no_argument, 0, 'C'}, - {"print-pid", required_argument, 0, 'p'}, - {"log-colorize", optional_argument, 0, 'l'}, - {"log-no-syslog", no_argument, 0, 's'}, - {"version", no_argument, 0, 'v'}, - {"help", no_argument, 0, 'h'}, - {NULL, no_argument, 0, 0}, + {"backend", required_argument, 0, 'b'}, + {"config", required_argument, 0, 'c'}, + {"validate", no_argument, 0, 'C'}, + {"print-pid", required_argument, 0, 'p'}, + {"log-level", required_argument, 0, 'd'}, + {"log-colorize", optional_argument, 0, 'l'}, + {"log-no-syslog", no_argument, 0, 's'}, + {"version", no_argument, 0, 'v'}, + {"help", no_argument, 0, 'h'}, + {NULL, no_argument, 0, 0}, }; bool unlink_pid_file = false; @@ -195,11 +196,12 @@ main(int argc, char *const *argv) char *config_path = NULL; enum bar_backend backend = BAR_BACKEND_AUTO; + enum log_class log_level = LOG_CLASS_WARNING; enum log_colorize log_colorize = LOG_COLORIZE_AUTO; bool log_syslog = true; while (true) { - int c = getopt_long(argc, argv, ":b:c:Cp:l::svh", longopts, NULL); + int c = getopt_long(argc, argv, ":b:c:Cp:d:l::svh", longopts, NULL); if (c == -1) break; @@ -220,9 +222,8 @@ main(int argc, char *const *argv) if (stat(optarg, &st) == -1) { fprintf(stderr, "%s: invalid configuration file: %s\n", optarg, strerror(errno)); return EXIT_FAILURE; - } else if (!S_ISREG(st.st_mode)) { - fprintf(stderr, "%s: invalid configuration file: not a regular file\n", - optarg); + } else if (!S_ISREG(st.st_mode) && !S_ISFIFO(st.st_mode)) { + fprintf(stderr, "%s: invalid configuration file: neither a regular file nor a pipe or FIFO\n", optarg); return EXIT_FAILURE; } @@ -238,6 +239,16 @@ main(int argc, char *const *argv) pid_file = optarg; break; + case 'd': { + int lvl = log_level_from_string(optarg); + if (lvl < 0) { + fprintf(stderr, "-d,--log-level: %s: argument must be one of %s\n", optarg, log_level_string_hint()); + return EXIT_FAILURE; + } + log_level = lvl; + break; + } + case 'l': if (optarg == NULL || strcmp(optarg, "auto") == 0) log_colorize = LOG_COLORIZE_AUTO; @@ -273,7 +284,12 @@ main(int argc, char *const *argv) } } - log_init(log_colorize, log_syslog, LOG_FACILITY_DAEMON, LOG_CLASS_WARNING); + log_init(log_colorize, log_syslog, LOG_FACILITY_DAEMON, log_level); + + _Static_assert((int)LOG_CLASS_ERROR == (int)FCFT_LOG_CLASS_ERROR, "fcft log level enum offset"); + _Static_assert((int)LOG_COLORIZE_ALWAYS == (int)FCFT_LOG_COLORIZE_ALWAYS, "fcft colorize enum mismatch"); + fcft_init((enum fcft_log_colorize)log_colorize, log_syslog, (enum fcft_log_class)log_level); + atexit(&fcft_fini); const struct sigaction sa = {.sa_handler = &signal_handler}; sigaction(SIGINT, &sa, NULL); @@ -326,7 +342,7 @@ main(int argc, char *const *argv) thrd_t bar_thread; thrd_create(&bar_thread, (int (*)(void *))bar->run, bar); - /* Now unblock. We should be only thread receiving SIGINT */ + /* Now unblock. We should be only thread receiving SIGINT/SIGTERM */ pthread_sigmask(SIG_UNBLOCK, &signal_mask, NULL); if (pid_file != NULL) { @@ -336,19 +352,21 @@ main(int argc, char *const *argv) while (!aborted) { struct pollfd fds[] = {{.fd = abort_fd, .events = POLLIN}}; - int r __attribute__((unused)) = poll(fds, 1, -1); + int r __attribute__((unused)) = poll(fds, sizeof(fds) / sizeof(fds[0]), -1); - /* - * Either the bar aborted (triggering the abort_fd), or user - * killed us (triggering the signal handler which sets - * 'aborted') - */ - assert(aborted || r == 1); - break; + if (fds[0].revents & (POLLIN | POLLHUP)) { + /* + * Either the bar aborted (triggering the abort_fd), or user + * killed us (triggering the signal handler which sets + * 'aborted') + */ + assert(aborted || r == 1); + break; + } } if (aborted) - LOG_INFO("aborted: %s (%d)", strsignal(aborted), aborted); + LOG_INFO("aborted: %s (%ld)", strsignal(aborted), (long)aborted); done: /* Signal abort to other threads */ @@ -358,7 +376,7 @@ done: int res; int r = thrd_join(bar_thread, &res); if (r != 0) - LOG_ERRNO_P("failed to join bar thread", r); + LOG_ERRNO_P(r, "failed to join bar thread"); bar->destroy(bar); close(abort_fd); diff --git a/meson.build b/meson.build index db6a7c2..67d3096 100644 --- a/meson.build +++ b/meson.build @@ -1,20 +1,28 @@ project('yambar', 'c', - version: '1.5.0', + version: '1.11.0', license: 'MIT', - meson_version: '>=0.53.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', + args: ['-D_GNU_SOURCE=200809L'], + prefix: '#include ') + add_project_arguments('-DMEMFD_CREATE', language: 'c') +endif # Compute the relative path used by compiler invocations. source_root = meson.current_source_dir().split('/') -build_root = meson.build_root().split('/') +build_root = meson.global_build_root().split('/') relative_dir_parts = [] i = 0 in_prefix = true @@ -42,7 +50,10 @@ endif # Common dependencies dl = cc.find_library('dl') -threads = dependency('threads') +m = cc.find_library('m') +threads = [dependency('threads'), cc.find_library('stdthreads', required: false)] +libepoll = dependency('epoll-shim', required: false) +libinotify = dependency('libinotify', required: false) pixman = dependency('pixman-1') yaml = dependency('yaml-0.1') @@ -64,9 +75,10 @@ backend_wayland = wayland_client.found() and wayland_cursor.found() # "My" dependencies, fallback to subproject tllist = dependency('tllist', version: '>=1.0.1', fallback: 'tllist') -fcft = dependency('fcft', version: ['>=2.0.0', '<3.0.0'], fallback: 'fcft') +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'] : []) + @@ -83,30 +95,37 @@ if backend_x11 c_args: xcb_errors.found() ? '-DHAVE_XCB_ERRORS' : [], pic: plugs_as_libs) - xcb_stuff = declare_dependency(link_with: xcb_stuff_lib) + xcb_stuff = declare_dependency( + link_with: xcb_stuff_lib, + dependencies: [xcb_aux, xcb_cursor, xcb_event, xcb_ewmh, xcb_randr, + xcb_render, xcb_errors], + ) install_headers('xcb.h', subdir: 'yambar') endif subdir('completions') -subdir('doc') subdir('bar') subdir('decorations') subdir('particles') subdir('modules') +subdir('doc') +env = find_program('env', native: true) generate_version_sh = files('generate-version.sh') version = custom_target( 'generate_version', build_always_stale: true, output: 'version.h', - command: [generate_version_sh, meson.project_version(), '@SOURCE_DIR@', '@OUTPUT@']) + command: [env, 'LC_ALL=C', generate_version_sh, meson.project_version(), '@CURRENT_SOURCE_DIR@', '@OUTPUT@']) yambar = executable( 'yambar', + 'char32.c', 'char32.h', 'color.h', 'config-verify.c', 'config-verify.h', 'config.c', 'config.h', 'decoration.h', + 'font-shaping.h', 'log.c', 'log.h', 'main.c', 'module.c', 'module.h', @@ -115,7 +134,7 @@ yambar = executable( 'tag.c', 'tag.h', 'yml.c', 'yml.h', version, - dependencies: [bar, pixman, yaml, threads, dl, tllist, fcft] + + dependencies: [bar, libepoll, libinotify, pixman, yaml, threads, dl, tllist, fcft] + decorations + particles + modules, build_rpath: '$ORIGIN/modules:$ORIGIN/decorations:$ORIGIN/particles', export_dynamic: true, @@ -151,3 +170,34 @@ summary( }, bool_yn: true ) + +summary( + { + 'ALSA': plugin_alsa_enabled, + 'Backlight': plugin_backlight_enabled, + 'Battery': plugin_battery_enabled, + 'Clock': plugin_clock_enabled, + 'CPU monitoring': plugin_cpu_enabled, + 'Disk I/O monitoring': plugin_disk_io_enabled, + 'dwl (dwm for Wayland)': plugin_dwl_enabled, + '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, + 'Pipewire': plugin_pipewire_enabled, + 'PulseAudio': plugin_pulse_enabled, + 'Removables monitoring': plugin_removables_enabled, + '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, + }, + section: 'Optional modules', + bool_yn: true +) diff --git a/meson_options.txt b/meson_options.txt index f293c6f..23a8e11 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -5,3 +5,52 @@ option( option( 'core-plugins-as-shared-libraries', type: 'boolean', value: false, description: 'Compiles modules, particles and decorations as shared libraries, which are loaded on-demand') + +option('plugin-alsa', type: 'feature', value: 'auto', + description: 'ALSA support') +option('plugin-backlight', type: 'feature', value: 'auto', + description: 'Backlight support') +option('plugin-battery', type: 'feature', value: 'auto', + description: 'Battery support') +option('plugin-clock', type: 'feature', value: 'auto', + description: 'Clock support') +option('plugin-cpu', type: 'feature', value: 'auto', + description: 'CPU monitoring support') +option('plugin-disk-io', type: 'feature', value: 'auto', + description: 'Disk I/O support') +option('plugin-dwl', type: 'feature', value: 'auto', + description: 'dwl (dwm for wayland) support') +option('plugin-foreign-toplevel', type: 'feature', value: 'auto', + description: 'Foreign toplevel (window tracking for Wayland) support') +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', + description: 'Label support') +option('plugin-network', type: 'feature', value: 'auto', + description: 'Network monitoring support') +option('plugin-pipewire', type: 'feature', value: 'auto', + description: 'Pipewire support') +option('plugin-pulse', type: 'feature', value: 'auto', + description: 'PulseAudio support') +option('plugin-removables', type: 'feature', value: 'auto', + description: 'Removables (USB sticks, CD-ROM etc) monitoring support') +option('plugin-river', type: 'feature', value: 'auto', + description: 'River support') +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', + description: 'XWindow (window tracking for X11) support') diff --git a/module.c b/module.c index 1e80c32..d3bde3b 100644 --- a/module.c +++ b/module.c @@ -1,6 +1,6 @@ #include "module.h" -#include #include +#include #include struct module * diff --git a/module.h b/module.h index b757a04..3a83cdc 100644 --- a/module.h +++ b/module.h @@ -26,6 +26,8 @@ struct module { /* refresh_in() should schedule a module content refresh after the * specified number of milliseconds */ bool (*refresh_in)(struct module *mod, long milli_seconds); + + const char *(*description)(const struct module *mod); }; struct module *module_common_new(void); @@ -33,9 +35,9 @@ void module_default_destroy(struct module *mod); struct exposable *module_begin_expose(struct module *mod); /* List of attributes *all* modules implement */ -#define MODULE_COMMON_ATTRS \ - {"content", true, &conf_verify_particle}, \ - {"anchors", false, NULL}, \ - {"font", false, &conf_verify_font}, \ - {"foreground", false, &conf_verify_color}, \ - {NULL, false, NULL} +#define MODULE_COMMON_ATTRS \ + {"content", true, &conf_verify_particle}, {"anchors", false, NULL}, {"font", false, &conf_verify_font}, \ + {"foreground", false, &conf_verify_color}, \ + { \ + NULL, false, NULL \ + } diff --git a/modules/alsa.c b/modules/alsa.c index b274903..8d7cd25 100644 --- a/modules/alsa.c +++ b/modules/alsa.c @@ -1,5 +1,8 @@ +#include #include #include +#include +#include #include @@ -7,49 +10,149 @@ #define LOG_MODULE "alsa" #define LOG_ENABLE_DBG 0 -#include "../log.h" #include "../bar/bar.h" #include "../config-verify.h" #include "../config.h" +#include "../log.h" #include "../plugin.h" -struct private { - char *card; - char *mixer; - struct particle *label; +enum channel_type { CHANNEL_PLAYBACK, CHANNEL_CAPTURE }; - tll(snd_mixer_selem_channel_id_t) channels; +struct channel { + snd_mixer_selem_channel_id_t id; + enum channel_type type; + char *name; - long vol_min; - long vol_max; + bool use_db; long vol_cur; + long db_cur; bool muted; }; +struct private +{ + char *card; + char *mixer; + char *volume_name; + char *muted_name; + struct particle *label; + + tll(struct channel) channels; + + bool online; + + bool has_playback_volume; + long playback_vol_min; + long playback_vol_max; + + bool has_playback_db; + long playback_db_min; + long playback_db_max; + + bool has_capture_volume; + long capture_vol_min; + long capture_vol_max; + + long has_capture_db; + long capture_db_min; + long capture_db_max; + + const struct channel *volume_chan; + const struct channel *muted_chan; +}; + +static void +channel_free(struct channel *chan) +{ + free(chan->name); +} + static void destroy(struct module *mod) { struct private *m = mod->private; - tll_free(m->channels); + tll_foreach(m->channels, it) + { + channel_free(&it->item); + tll_remove(m->channels, it); + } m->label->destroy(m->label); free(m->card); free(m->mixer); + free(m->volume_name); + free(m->muted_name); free(m); module_default_destroy(mod); } +static const char * +description(const struct module *mod) +{ + static char desc[32]; + const struct private *m = mod->private; + snprintf(desc, sizeof(desc), "alsa(%s)", m->card); + return desc; +} + static struct exposable * content(struct module *mod) { struct private *m = mod->private; mtx_lock(&mod->lock); + + const struct channel *volume_chan = m->volume_chan; + const struct channel *muted_chan = m->muted_chan; + + bool muted = muted_chan != NULL ? muted_chan->muted : false; + long vol_min = 0, vol_max = 0, vol_cur = 0; + long db_min = 0, db_max = 0, db_cur = 0; + bool use_db = false; + + if (volume_chan != NULL) { + if (volume_chan->type == CHANNEL_PLAYBACK) { + db_min = m->playback_db_min; + db_max = m->playback_db_max; + vol_min = m->playback_vol_min; + vol_max = m->playback_vol_max; + } else { + db_min = m->capture_db_min; + db_max = m->capture_db_max; + vol_min = m->capture_vol_min; + vol_max = m->capture_vol_max; + } + vol_cur = volume_chan->vol_cur; + db_cur = volume_chan->db_cur; + use_db = volume_chan->use_db; + } + + int percent; + + if (use_db) { + bool use_linear = db_max - db_min <= 24 * 100; + if (use_linear) { + percent = db_min - db_max > 0 ? round(100. * (db_cur - db_min) / (db_max - db_min)) : 0; + } else { + double normalized = pow(10, (double)(db_cur - db_max) / 6000.); + if (db_min != SND_CTL_TLV_DB_GAIN_MUTE) { + double min_norm = pow(10, (double)(db_min - db_max) / 6000.); + normalized = (normalized - min_norm) / (1. - min_norm); + } + percent = round(100. * normalized); + } + } else { + percent = vol_max - vol_min > 0 ? round(100. * (vol_cur - vol_min) / (vol_max - vol_min)) : 0; + } + struct tag_set tags = { .tags = (struct tag *[]){ - tag_new_int_range(mod, "volume", m->vol_cur, m->vol_min, m->vol_max), - tag_new_bool(mod, "muted", m->muted), + tag_new_bool(mod, "online", m->online), + tag_new_int_range(mod, "volume", vol_cur, vol_min, vol_max), + tag_new_int_range(mod, "dB", db_cur, db_min, db_max), + tag_new_int_range(mod, "percent", percent, 0, 100), + tag_new_bool(mod, "muted", muted), }, - .count = 2, + .count = 5, }; mtx_unlock(&mod->lock); @@ -64,136 +167,137 @@ update_state(struct module *mod, snd_mixer_elem_t *elem) { struct private *m = mod->private; - int idx = 0; - - /* Get min/max volume levels */ - long min = 0, max = 0; - int r = snd_mixer_selem_get_playback_volume_range(elem, &min, &max); - - if (r < 0) { - LOG_DBG("%s,%s: failed to get volume min/max (mixer is digital?)", - m->card, m->mixer); - } - - /* Make sure min <= max */ - if (min > max) { - LOG_WARN( - "%s,%s: indicated minimum volume is greater than the maximum: " - "%ld > %ld", m->card, m->mixer, min, max); - min = max; - } - - long cur[tll_length(m->channels)]; - memset(cur, 0, sizeof(cur)); + mtx_lock(&mod->lock); /* If volume level can be changed (i.e. this isn't just a switch; - * e.g. a digital channel), get current level */ - if (max > 0) { - tll_foreach(m->channels, it) { - int r = snd_mixer_selem_get_playback_volume( - elem, it->item, &cur[idx]); + * e.g. a digital channel), get current channel levels */ + tll_foreach(m->channels, it) + { + struct channel *chan = &it->item; + + const bool has_volume = chan->type == CHANNEL_PLAYBACK ? m->has_playback_volume : m->has_capture_volume; + const bool has_db = chan->type == CHANNEL_PLAYBACK ? m->has_playback_db : m->has_capture_db; + + if (!has_volume && !has_db) + continue; + + if (has_db) { + chan->use_db = true; + + const long min = chan->type == CHANNEL_PLAYBACK ? m->playback_db_min : m->capture_db_min; + const long max = chan->type == CHANNEL_PLAYBACK ? m->playback_db_max : m->capture_db_max; + assert(min <= max); + + int r = chan->type == CHANNEL_PLAYBACK ? snd_mixer_selem_get_playback_dB(elem, chan->id, &chan->db_cur) + : snd_mixer_selem_get_capture_dB(elem, chan->id, &chan->db_cur); if (r < 0) { - LOG_WARN("%s,%s: %s: failed to get current volume", - m->card, m->mixer, - snd_mixer_selem_channel_name(it->item)); + LOG_ERR("%s,%s: %s: failed to get current dB", m->card, m->mixer, chan->name); } - LOG_DBG("%s,%s: %s: volume: %ld", m->card, m->mixer, - snd_mixer_selem_channel_name(it->item), cur[idx]); - idx++; - } - } + if (chan->db_cur < min) { + LOG_WARN("%s,%s: %s: current dB is less than the indicated minimum: " + "%ld < %ld", + m->card, m->mixer, chan->name, chan->db_cur, min); + chan->db_cur = min; + } - int unmuted[tll_length(m->channels)]; - memset(unmuted, 0, sizeof(unmuted)); + if (chan->db_cur > max) { + LOG_WARN("%s,%s: %s: current dB is greater than the indicated maximum: " + "%ld > %ld", + m->card, m->mixer, chan->name, chan->db_cur, max); + chan->db_cur = max; + } - /* Get muted state */ - idx = 0; - tll_foreach(m->channels, it) { - int r = snd_mixer_selem_get_playback_switch( - elem, it->item, &unmuted[idx]); + assert(chan->db_cur >= min); + assert(chan->db_cur <= max); + + LOG_DBG("%s,%s: %s: dB: %ld", m->card, m->mixer, chan->name, chan->db_cur); + } else + chan->use_db = false; + + const long min = chan->type == CHANNEL_PLAYBACK ? m->playback_vol_min : m->capture_vol_min; + const long max = chan->type == CHANNEL_PLAYBACK ? m->playback_vol_max : m->capture_vol_max; + assert(min <= max); + + int r = chan->type == CHANNEL_PLAYBACK ? snd_mixer_selem_get_playback_volume(elem, chan->id, &chan->vol_cur) + : snd_mixer_selem_get_capture_volume(elem, chan->id, &chan->vol_cur); if (r < 0) { - LOG_WARN("%s,%s: %s: failed to get muted state", - m->card, m->mixer, snd_mixer_selem_channel_name(it->item)); - unmuted[idx] = 1; + LOG_ERR("%s,%s: %s: failed to get current volume", m->card, m->mixer, chan->name); } - LOG_DBG("%s,%s: %s: muted: %d", m->card, m->mixer, - snd_mixer_selem_channel_name(it->item), !unmuted[idx]); - - idx++; - } - - /* Warn if volume level is inconsistent across the channels */ - for (size_t i = 1; i < tll_length(m->channels); i++) { - if (cur[i] != cur[i - 1]) { - LOG_WARN("%s,%s: channel volume mismatch, using value from %s", - m->card, m->mixer, - snd_mixer_selem_channel_name(tll_front(m->channels))); - break; + if (chan->vol_cur < min) { + LOG_WARN("%s,%s: %s: current volume is less than the indicated minimum: " + "%ld < %ld", + m->card, m->mixer, chan->name, chan->vol_cur, min); + chan->vol_cur = min; } - } - /* Warn if muted state is inconsistent across the channels */ - for (size_t i = 1; i < tll_length(m->channels); i++) { - if (unmuted[i] != unmuted[i - 1]) { - LOG_WARN("%s,%s: channel muted mismatch, using value from %s", - m->card, m->mixer, - snd_mixer_selem_channel_name(tll_front(m->channels))); - break; + if (chan->vol_cur > max) { + LOG_WARN("%s,%s: %s: current volume is greater than the indicated maximum: " + "%ld > %ld", + m->card, m->mixer, chan->name, chan->vol_cur, max); + chan->vol_cur = max; } + + assert(chan->vol_cur >= min); + assert(chan->vol_cur <= max); + + LOG_DBG("%s,%s: %s: volume: %ld", m->card, m->mixer, chan->name, chan->vol_cur); } - /* Make sure min <= cur <= max */ - if (cur[0] < min) { - LOG_WARN( - "%s,%s: current volume is less than the indicated minimum: " - "%ld < %ld", m->card, m->mixer, cur[0], min); - cur[0] = min; + /* Get channels’ muted state */ + tll_foreach(m->channels, it) + { + struct channel *chan = &it->item; + + int unmuted; + + int r = chan->type == CHANNEL_PLAYBACK ? snd_mixer_selem_get_playback_switch(elem, chan->id, &unmuted) + : snd_mixer_selem_get_capture_switch(elem, chan->id, &unmuted); + + if (r < 0) { + LOG_WARN("%s,%s: %s: failed to get muted state", m->card, m->mixer, chan->name); + unmuted = 1; + } + + chan->muted = !unmuted; + LOG_DBG("%s,%s: %s: muted: %d", m->card, m->mixer, chan->name, !unmuted); } - if (cur[0] > max) { - LOG_WARN( - "%s,%s: current volume is greater than the indicated maximum: " - "%ld > %ld", m->card, m->mixer, cur[0], max); - cur[0] = max; - } + m->online = true; - assert(cur[0] >= min); - assert(cur[0] <= max); - - LOG_DBG( - "muted=%d, cur=%ld, min=%ld, max=%ld", !unmuted[0], cur[0], min, max); - - mtx_lock(&mod->lock); - m->vol_min = min; - m->vol_max = max; - m->vol_cur = cur[0]; - m->muted = !unmuted[0]; mtx_unlock(&mod->lock); - mod->bar->refresh(mod->bar); } -static int -run(struct module *mod) +enum run_state { + RUN_ERROR, + RUN_FAILED_CONNECT, + RUN_DISCONNECTED, + RUN_DONE, +}; + +static enum run_state +run_while_online(struct module *mod) { struct private *m = mod->private; - int ret = 1; + enum run_state ret = RUN_ERROR; + + /* Make sure we aren’t still tracking channels from previous connects */ + tll_free(m->channels); snd_mixer_t *handle; if (snd_mixer_open(&handle, 0) != 0) { LOG_ERR("failed to open handle"); - return 1; + return ret; } - if (snd_mixer_attach(handle, m->card) != 0 || - snd_mixer_selem_register(handle, NULL, NULL) != 0 || - snd_mixer_load(handle) != 0) - { + if (snd_mixer_attach(handle, m->card) != 0 || snd_mixer_selem_register(handle, NULL, NULL) != 0 + || snd_mixer_load(handle) != 0) { LOG_ERR("failed to attach to card"); + ret = RUN_FAILED_CONNECT; goto err; } @@ -202,37 +306,142 @@ run(struct module *mod) snd_mixer_selem_id_set_index(sid, 0); snd_mixer_selem_id_set_name(sid, m->mixer); - snd_mixer_elem_t* elem = snd_mixer_find_selem(handle, sid); + snd_mixer_elem_t *elem = snd_mixer_find_selem(handle, sid); if (elem == NULL) { LOG_ERR("failed to find mixer"); goto err; } + /* Get playback volume range */ + m->has_playback_volume = snd_mixer_selem_has_playback_volume(elem) > 0; + if (m->has_playback_volume) { + if (snd_mixer_selem_get_playback_volume_range(elem, &m->playback_vol_min, &m->playback_vol_max) < 0) { + LOG_ERR("%s,%s: failed to get playback volume range", m->card, m->mixer); + assert(m->playback_vol_min == 0); + assert(m->playback_vol_max == 0); + } + + if (m->playback_vol_min > m->playback_vol_max) { + LOG_WARN("%s,%s: indicated minimum playback volume is greater than the " + "maximum: %ld > %ld", + m->card, m->mixer, m->playback_vol_min, m->playback_vol_max); + m->playback_vol_min = m->playback_vol_max; + } + } + + if (snd_mixer_selem_get_playback_dB_range(elem, &m->playback_db_min, &m->playback_db_max) < 0) { + LOG_WARN("%s,%s: failed to get playback dB range, " + "will use raw volume values instead", + m->card, m->mixer); + m->has_playback_db = false; + } else + m->has_playback_db = true; + + /* Get capture volume range */ + m->has_capture_volume = snd_mixer_selem_has_capture_volume(elem) > 0; + if (m->has_capture_volume) { + if (snd_mixer_selem_get_capture_volume_range(elem, &m->capture_vol_min, &m->capture_vol_max) < 0) { + LOG_ERR("%s,%s: failed to get capture volume range", m->card, m->mixer); + assert(m->capture_vol_min == 0); + assert(m->capture_vol_max == 0); + } + + if (m->capture_vol_min > m->capture_vol_max) { + LOG_WARN("%s,%s: indicated minimum capture volume is greater than the " + "maximum: %ld > %ld", + m->card, m->mixer, m->capture_vol_min, m->capture_vol_max); + m->capture_vol_min = m->capture_vol_max; + } + } + + if (snd_mixer_selem_get_capture_dB_range(elem, &m->capture_db_min, &m->capture_db_max) < 0) { + LOG_WARN("%s,%s: failed to get capture dB range, " + "will use raw volume values instead", + m->card, m->mixer); + m->has_capture_db = false; + } else + m->has_capture_db = true; + /* Get available channels */ for (size_t i = 0; i < SND_MIXER_SCHN_LAST; i++) { - if (snd_mixer_selem_has_playback_channel(elem, i)) { - tll_push_back(m->channels, i); + bool is_playback = snd_mixer_selem_has_playback_channel(elem, i) == 1; + bool is_capture = snd_mixer_selem_has_capture_channel(elem, i) == 1; + + if (is_playback || is_capture) { + struct channel chan = { + .id = i, + .type = is_playback ? CHANNEL_PLAYBACK : CHANNEL_CAPTURE, + .name = strdup(snd_mixer_selem_channel_name(i)), + }; + tll_push_back(m->channels, chan); } } + if (tll_length(m->channels) == 0) { + LOG_ERR("%s,%s: no channels", m->card, m->mixer); + goto err; + } + char channels_str[1024]; int channels_idx = 0; - tll_foreach(m->channels, it) { - channels_idx += snprintf( - &channels_str[channels_idx], sizeof(channels_str) - channels_idx, - channels_idx == 0 ? "%s" : ", %s", - snd_mixer_selem_channel_name(it->item)); + tll_foreach(m->channels, it) + { + const struct channel *chan = &it->item; + + channels_idx += snprintf(&channels_str[channels_idx], sizeof(channels_str) - channels_idx, + channels_idx == 0 ? "%s (%s)" : ", %s (%s)", chan->name, + chan->type == CHANNEL_PLAYBACK ? "🔊" : "🎤"); assert(channels_idx <= sizeof(channels_str)); } LOG_INFO("%s,%s: channels: %s", m->card, m->mixer, channels_str); + /* Verify volume/muted channel names are valid and exists */ + bool volume_channel_is_valid = m->volume_name == NULL; + bool muted_channel_is_valid = m->muted_name == NULL; + + tll_foreach(m->channels, it) + { + const struct channel *chan = &it->item; + if (m->volume_name != NULL && strcmp(chan->name, m->volume_name) == 0) { + m->volume_chan = chan; + volume_channel_is_valid = true; + } + if (m->muted_name != NULL && strcmp(chan->name, m->muted_name) == 0) { + m->muted_chan = chan; + muted_channel_is_valid = true; + } + } + + if (m->volume_name == NULL) + m->volume_chan = &tll_front(m->channels); + if (m->muted_name == NULL) + m->muted_chan = &tll_front(m->channels); + + if (!volume_channel_is_valid) { + assert(m->volume_name != NULL); + LOG_ERR("volume: invalid channel name: %s", m->volume_name); + goto err; + } + + if (!muted_channel_is_valid) { + assert(m->muted_name != NULL); + LOG_ERR("muted: invalid channel name: %s", m->muted_name); + goto err; + } + /* Initial state */ update_state(mod, elem); - LOG_INFO("%s,%s: volume min=%ld, max=%ld, current=%ld%s", - m->card, m->mixer, m->vol_min, m->vol_max, m->vol_cur, - m->muted ? ", muted" : ""); + LOG_INFO( + "%s,%s: %s range=%ld-%ld, current=%ld%s (sources: volume=%s, muted=%s)", m->card, m->mixer, + m->volume_chan->use_db ? "dB" : "volume", + (m->volume_chan->type == CHANNEL_PLAYBACK ? (m->volume_chan->use_db ? m->playback_db_min : m->playback_vol_min) + : (m->volume_chan->use_db ? m->capture_db_min : m->capture_vol_min)), + (m->volume_chan->type == CHANNEL_PLAYBACK ? (m->volume_chan->use_db ? m->playback_db_max : m->playback_vol_max) + : (m->volume_chan->use_db ? m->capture_db_max : m->capture_vol_max)), + m->volume_chan->use_db ? m->volume_chan->db_cur : m->volume_chan->vol_cur, + m->muted_chan->muted ? " (muted)" : "", m->volume_chan->name, m->muted_chan->name); mod->bar->refresh(mod->bar); @@ -245,42 +454,188 @@ run(struct module *mod) fds[0] = (struct pollfd){.fd = mod->abort_fd, .events = POLLIN}; snd_mixer_poll_descriptors(handle, &fds[1], fd_count); - poll(fds, fd_count + 1, -1); + int r = poll(fds, fd_count + 1, -1); + if (r < 0) { + if (errno == EINTR) + continue; - if (fds[0].revents & POLLIN) + LOG_ERRNO("failed to poll"); break; + } - if (fds[1].revents & POLLHUP) { - /* Don't know if this can happen */ - LOG_ERR("disconnected from alsa"); + if (fds[0].revents & POLLIN) { + ret = RUN_DONE; break; } + for (size_t i = 0; i < fd_count; i++) { + if (fds[1 + i].revents & (POLLHUP | POLLERR | POLLNVAL)) { + LOG_ERR("disconnected from alsa"); + + mtx_lock(&mod->lock); + m->online = false; + mtx_unlock(&mod->lock); + mod->bar->refresh(mod->bar); + + ret = RUN_DISCONNECTED; + goto err; + } + } + snd_mixer_handle_events(handle); update_state(mod, elem); } - ret = 0; - err: snd_mixer_close(handle); snd_config_update_free_global(); return ret; } +static int +run(struct module *mod) +{ + int ret = 1; + + int ifd = inotify_init1(IN_NONBLOCK | IN_CLOEXEC); + if (ifd < 0) { + LOG_ERRNO("failed to inotify"); + return 1; + } + + int wd = inotify_add_watch(ifd, "/dev/snd", IN_CREATE); + if (wd < 0) { + LOG_ERRNO("failed to create inotify watcher for /dev/snd"); + close(ifd); + return 1; + } + + while (true) { + enum run_state state = run_while_online(mod); + + switch (state) { + case RUN_DONE: + ret = 0; + goto out; + + case RUN_ERROR: + ret = 1; + goto out; + + case RUN_FAILED_CONNECT: + break; + + case RUN_DISCONNECTED: + /* + * We’ve been connected - drain the watcher + * + * We don’t want old, un-releated events (for other + * soundcards, for example) to trigger a storm of + * re-connect attempts. + */ + while (true) { + uint8_t buf[1024]; + ssize_t amount = read(ifd, buf, sizeof(buf)); + if (amount < 0) { + if (errno == EAGAIN) + break; + + LOG_ERRNO("failed to drain inotify watcher"); + ret = 1; + goto out; + } + + if (amount == 0) + break; + } + + break; + } + + bool have_create_event = false; + + while (!have_create_event) { + struct pollfd fds[] = {{.fd = mod->abort_fd, .events = POLLIN}, {.fd = ifd, .events = POLLIN}}; + int r = poll(fds, sizeof(fds) / sizeof(fds[0]), -1); + + if (r < 0) { + if (errno == EINTR) + continue; + + LOG_ERRNO("failed to poll"); + ret = 1; + goto out; + } + + if (fds[0].revents & (POLLIN | POLLHUP)) { + ret = 0; + goto out; + } + + if (fds[1].revents & POLLHUP) { + LOG_ERR("inotify socket closed"); + ret = 1; + goto out; + } + + assert(fds[1].revents & POLLIN); + + while (true) { + char buf[1024]; + ssize_t len = read(ifd, buf, sizeof(buf)); + + if (len < 0) { + if (errno == EAGAIN) + break; + + LOG_ERRNO("failed to read inotify events"); + ret = 1; + goto out; + } + + if (len == 0) + break; + + /* Consume inotify data */ + for (const char *ptr = buf; ptr < buf + len;) { + const struct inotify_event *e = (const struct inotify_event *)ptr; + + if (e->mask & IN_CREATE) { + LOG_DBG("inotify: CREATED: /dev/snd/%.*s", e->len, e->name); + have_create_event = true; + } + + ptr += sizeof(*e) + e->len; + } + } + } + } + +out: + if (wd >= 0) + inotify_rm_watch(ifd, wd); + if (ifd >= 0) + close(ifd); + return ret; +} + static struct module * -alsa_new(const char *card, const char *mixer, struct particle *label) +alsa_new(const char *card, const char *mixer, const char *volume_channel_name, const char *muted_channel_name, + struct particle *label) { struct private *priv = calloc(1, sizeof(*priv)); priv->label = label; priv->card = strdup(card); priv->mixer = strdup(mixer); + priv->volume_name = volume_channel_name != NULL ? strdup(volume_channel_name) : NULL; + priv->muted_name = muted_channel_name != NULL ? strdup(muted_channel_name) : NULL; struct module *mod = module_common_new(); mod->private = priv; mod->run = &run; mod->destroy = &destroy; mod->content = &content; + mod->description = &description; return mod; } @@ -289,12 +644,13 @@ from_conf(const struct yml_node *node, struct conf_inherit inherited) { const struct yml_node *card = yml_get_value(node, "card"); const struct yml_node *mixer = yml_get_value(node, "mixer"); + const struct yml_node *volume = yml_get_value(node, "volume"); + const struct yml_node *muted = yml_get_value(node, "muted"); const struct yml_node *content = yml_get_value(node, "content"); - return alsa_new( - yml_value_as_string(card), - yml_value_as_string(mixer), - conf_to_particle(content, inherited)); + return alsa_new(yml_value_as_string(card), yml_value_as_string(mixer), + volume != NULL ? yml_value_as_string(volume) : NULL, + muted != NULL ? yml_value_as_string(muted) : NULL, conf_to_particle(content, inherited)); } static bool @@ -303,6 +659,8 @@ verify_conf(keychain_t *chain, const struct yml_node *node) static const struct attr_info attrs[] = { {"card", true, &conf_verify_string}, {"mixer", true, &conf_verify_string}, + {"volume", false, &conf_verify_string}, + {"muted", false, &conf_verify_string}, MODULE_COMMON_ATTRS, }; diff --git a/modules/backlight.c b/modules/backlight.c index 1c65d38..1495c5c 100644 --- a/modules/backlight.c +++ b/modules/backlight.c @@ -1,23 +1,26 @@ +#include +#include +#include +#include #include #include #include -#include #include -#include -#include #include +#include #include #define LOG_MODULE "backlight" -#include "../log.h" #include "../bar/bar.h" -#include "../config.h" #include "../config-verify.h" +#include "../config.h" +#include "../log.h" #include "../plugin.h" -struct private { +struct private +{ struct particle *label; char *device; @@ -37,6 +40,12 @@ destroy(struct module *mod) module_default_destroy(mod); } +static const char * +description(const struct module *mod) +{ + return "backlight"; +} + static struct exposable * content(struct module *mod) { @@ -46,7 +55,7 @@ content(struct module *mod) const long current = m->current_brightness; const long max = m->max_brightness; - const long percent = max > 0 ? 100 * current / max : 0; + const long percent = max > 0 ? round(100. * current / max) : 0; struct tag_set tags = { .tags = (struct tag *[]){ @@ -103,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) { @@ -117,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); @@ -127,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) { @@ -137,8 +146,7 @@ initialize(struct private *m) m->current_brightness = readint_from_fd(current_fd); - LOG_INFO("%s: brightness: %ld (max: %ld)", m->device, m->current_brightness, - m->max_brightness); + LOG_INFO("%s: brightness: %ld (max: %ld)", m->device, m->current_brightness, m->max_brightness); return current_fd; } @@ -171,20 +179,31 @@ run(struct module *mod) bar->refresh(bar); + int ret = 1; while (true) { struct pollfd fds[] = { {.fd = mod->abort_fd, .events = POLLIN}, {.fd = udev_monitor_get_fd(mon), .events = POLLIN}, }; - poll(fds, 2, -1); + if (poll(fds, sizeof(fds) / sizeof(fds[0]), -1) < 0) { + if (errno == EINTR) + continue; - if (fds[0].revents & POLLIN) + LOG_ERRNO("failed to poll"); break; + } + + if (fds[0].revents & POLLIN) { + ret = 0; + break; + } struct udev_device *dev = udev_monitor_receive_device(mon); - const char *sysname = udev_device_get_sysname(dev); + if (dev == NULL) + continue; - bool is_us = strcmp(sysname, m->device) == 0; + const char *sysname = udev_device_get_sysname(dev); + bool is_us = sysname != NULL && strcmp(sysname, m->device) == 0; udev_device_unref(dev); if (!is_us) @@ -200,7 +219,7 @@ run(struct module *mod) udev_unref(udev); close(current_fd); - return 0; + return ret; } static struct module * @@ -215,6 +234,7 @@ backlight_new(const char *device, struct particle *label) mod->run = &run; mod->destroy = &destroy; mod->content = &content; + mod->description = &description; return mod; } @@ -224,8 +244,7 @@ from_conf(const struct yml_node *node, struct conf_inherit inherited) const struct yml_node *name = yml_get_value(node, "name"); const struct yml_node *c = yml_get_value(node, "content"); - return backlight_new( - yml_value_as_string(name), conf_to_particle(c, inherited)); + return backlight_new(yml_value_as_string(name), conf_to_particle(c, inherited)); } static bool diff --git a/modules/battery.c b/modules/battery.c index ce0d04c..34b98c8 100644 --- a/modules/battery.c +++ b/modules/battery.c @@ -1,31 +1,49 @@ +#include +#include +#include #include #include #include +#include #include -#include -#include #include -#include #include +#include #include +#include #define LOG_MODULE "battery" #define LOG_ENABLE_DBG 0 -#include "../log.h" #include "../bar/bar.h" -#include "../config.h" #include "../config-verify.h" +#include "../config.h" +#include "../log.h" #include "../plugin.h" -enum state { STATE_FULL, STATE_CHARGING, STATE_DISCHARGING }; +#define max(x, y) ((x) > (y) ? (x) : (y)) -struct private { +static const long min_poll_interval = 250; +static const long default_poll_interval = 60 * 1000; +static const long one_sec_in_ns = 1000000000; + +enum state { STATE_FULL, STATE_NOTCHARGING, STATE_CHARGING, STATE_DISCHARGING, STATE_UNKNOWN }; + +struct current_state { + long ema; + long current; + struct timespec time; +}; + +struct private +{ struct particle *label; - int poll_interval; + long poll_interval; + int battery_scale; + long smoothing_scale; char *battery; char *manufacturer; char *model; @@ -39,10 +57,64 @@ struct private { long energy; long power; long charge; - long current; + struct current_state ema_current; long time_to_empty; + long time_to_full; }; +static int64_t +difftimespec_ns(const struct timespec after, const struct timespec before) +{ + return ((int64_t)after.tv_sec - (int64_t)before.tv_sec) * (int64_t)one_sec_in_ns + + ((int64_t)after.tv_nsec - (int64_t)before.tv_nsec); +} + +// Linear Exponential Moving Average (unevenly spaced time series) +// http://www.eckner.com/papers/Algorithms%20for%20Unevenly%20Spaced%20Time%20Series.pdf +// Adapted from: https://github.com/andreas50/utsAlgorithms/blob/master/ema.c +static void +ema_linear(struct current_state *state, struct current_state curr, long tau) +{ + double w, w2, tmp; + + if (state->current == -1) { + *state = curr; + return; + } + + long time = difftimespec_ns(curr.time, state->time); + tmp = time / (double)tau; + w = exp(-tmp); + if (tmp > 1e-6) { + w2 = (1 - w) / tmp; + } else { + // Use taylor expansion for numerical stability + w2 = 1 - tmp / 2 + tmp * tmp / 6 - tmp * tmp * tmp / 24; + } + + double ema = state->ema * w + curr.current * (1 - w2) + state->current * (w2 - w); + + state->ema = ema; + state->current = curr.current; + state->time = curr.time; + + LOG_DBG("ema current: %ld", (long)ema); +} + +static void +timespec_sub(const struct timespec *a, const struct timespec *b, struct timespec *res) +{ + + res->tv_sec = a->tv_sec - b->tv_sec; + res->tv_nsec = a->tv_nsec - b->tv_nsec; + + /* tv_nsec may be negative */ + if (res->tv_nsec < 0) { + res->tv_sec--; + res->tv_nsec += one_sec_in_ns; + } +} + static void destroy(struct module *mod) { @@ -57,6 +129,15 @@ destroy(struct module *mod) module_default_destroy(mod); } +static const char * +description(const struct module *mod) +{ + static char desc[32]; + const struct private *m = mod->private; + snprintf(desc, sizeof(desc), "bat(%s)", m->battery); + return desc; +} + static struct exposable * content(struct module *mod) { @@ -64,22 +145,25 @@ content(struct module *mod) mtx_lock(&mod->lock); - assert(m->state == STATE_FULL || - m->state == STATE_CHARGING || - m->state == STATE_DISCHARGING); + assert(m->state == STATE_FULL || m->state == STATE_NOTCHARGING || m->state == STATE_CHARGING + || m->state == STATE_DISCHARGING || m->state == STATE_UNKNOWN); unsigned long hours; unsigned long minutes; - if (m->time_to_empty >= 0) { - hours = m->time_to_empty / 60; - minutes = m->time_to_empty % 60; - } else if (m->energy_full >= 0 && m->charge && m->power >= 0) { - unsigned long energy = m->state == STATE_CHARGING - ? m->energy_full - m->energy : m->energy; + if (m->time_to_empty > 0) { + minutes = m->time_to_empty / 60; + hours = minutes / 60; + minutes = minutes % 60; + } else if (m->time_to_full > 0) { + minutes = m->time_to_full / 60; + hours = minutes / 60; + minutes = minutes % 60; + } else if (m->energy_full >= 0 && m->charge && m->power >= 0) { + unsigned long energy = m->state == STATE_CHARGING ? m->energy_full - m->energy : m->energy; double hours_as_float; - if (m->state == STATE_FULL) + if (m->state == STATE_FULL || m->state == STATE_NOTCHARGING) hours_as_float = 0.0; else if (m->power > 0) hours_as_float = (double)energy / m->power; @@ -88,15 +172,14 @@ content(struct module *mod) hours = hours_as_float; minutes = (hours_as_float - (double)hours) * 60; - } else if (m->charge_full >= 0 && m->charge >= 0 && m->current >= 0) { - unsigned long charge = m->state == STATE_CHARGING - ? m->charge_full - m->charge : m->charge; + } else if (m->charge_full >= 0 && m->charge >= 0 && m->ema_current.current >= 0) { + unsigned long charge = m->state == STATE_CHARGING ? m->charge_full - m->charge : m->charge; double hours_as_float; - if (m->state == STATE_FULL) + if (m->state == STATE_FULL || m->state == STATE_NOTCHARGING) hours_as_float = 0.0; - else if (m->current > 0) - hours_as_float = (double)charge / m->current; + else if (m->ema_current.current > 0) + hours_as_float = (double)charge / m->ema_current.current; else hours_as_float = 99.0; @@ -117,6 +200,7 @@ content(struct module *mod) tag_new_string(mod, "model", m->model), tag_new_string(mod, "state", m->state == STATE_FULL ? "full" : + m->state == STATE_NOTCHARGING ? "not charging" : m->state == STATE_CHARGING ? "charging" : m->state == STATE_DISCHARGING ? "discharging" : "unknown"), @@ -135,20 +219,18 @@ content(struct module *mod) } static const char * -readline_from_fd(int fd) +readline_from_fd(int fd, size_t sz, char buf[static sz]) { - static char buf[4096]; - - ssize_t sz = read(fd, buf, sizeof(buf) - 1); + ssize_t bytes = read(fd, buf, sz - 1); lseek(fd, 0, SEEK_SET); - if (sz < 0) { + if (bytes < 0) { LOG_WARN("failed to read from FD=%d", fd); return NULL; } - buf[sz] = '\0'; - for (ssize_t i = sz - 1; i >= 0 && buf[i] == '\n'; sz--) + buf[bytes] = '\0'; + for (ssize_t i = bytes - 1; i >= 0 && buf[i] == '\n'; bytes--) buf[i] = '\0'; return buf; @@ -157,7 +239,8 @@ readline_from_fd(int fd) static long readint_from_fd(int fd) { - const char *s = readline_from_fd(fd); + char buf[512]; + const char *s = readline_from_fd(fd, sizeof(buf), buf); if (s == NULL) return 0; @@ -171,52 +254,51 @@ readint_from_fd(int fd) return ret; } -static int +static bool initialize(struct private *m) { - int pw_fd = open("/sys/class/power_supply", O_RDONLY); - if (pw_fd == -1) { + char line_buf[512]; + + int pw_fd = open("/sys/class/power_supply", O_RDONLY | O_CLOEXEC); + if (pw_fd < 0) { LOG_ERRNO("/sys/class/power_supply"); - return -1; + 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 == -1) { - LOG_ERRNO("%s", m->battery); - return -1; + if (base_dir_fd < 0) { + LOG_ERRNO("/sys/class/power_supply/%s", m->battery); + return false; } { - 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)); + LOG_WARN("/sys/class/power_supply/%s/manufacturer: %s", m->battery, strerror(errno)); m->manufacturer = NULL; } else { - m->manufacturer = strdup(readline_from_fd(fd)); + m->manufacturer = strdup(readline_from_fd(fd, sizeof(line_buf), line_buf)); close(fd); } } { - 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)); + LOG_WARN("/sys/class/power_supply/%s/model_name: %s", m->battery, strerror(errno)); m->model = NULL; } else { - m->model = strdup(readline_from_fd(fd)); + m->model = strdup(readline_from_fd(fd, sizeof(line_buf), line_buf)); close(fd); } } - if (faccessat(base_dir_fd, "energy_full_design", O_RDONLY, 0) == 0 && - faccessat(base_dir_fd, "energy_full", O_RDONLY, 0) == 0) - { + 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; @@ -227,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; @@ -240,83 +322,158 @@ initialize(struct private *m) m->energy_full = m->energy_full_design = -1; } - if (faccessat(base_dir_fd, "charge_full_design", O_RDONLY, 0) == 0 && - faccessat(base_dir_fd, "charge_full", O_RDONLY, 0) == 0) - { + 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; } - m->charge_full_design = readint_from_fd(fd); + m->charge_full_design = readint_from_fd(fd) / m->battery_scale; close(fd); } { - 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; } - m->charge_full = readint_from_fd(fd); + m->charge_full = readint_from_fd(fd) / m->battery_scale; close(fd); } } else { m->charge_full = m->charge_full_design = -1; } - return base_dir_fd; + close(base_dir_fd); + return true; err: close(base_dir_fd); - return -1; + return false; } -static void -update_status(struct module *mod, int capacity_fd, int energy_fd, int power_fd, - int charge_fd, int current_fd, int status_fd, int time_to_empty_fd) +static bool +update_status(struct module *mod) { struct private *m = mod->private; + 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 | O_CLOEXEC); + close(pw_fd); + + if (base_dir_fd < 0) { + LOG_ERRNO("/sys/class/power_supply/%s", m->battery); + return false; + } + + 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 | O_CLOEXEC); + if (capacity_fd < 0) { + LOG_ERRNO("/sys/class/power_supply/%s/capacity", m->battery); + close(status_fd); + close(base_dir_fd); + return false; + } + + 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; long power = power_fd >= 0 ? readint_from_fd(power_fd) : -1; long charge = charge_fd >= 0 ? readint_from_fd(charge_fd) : -1; long current = current_fd >= 0 ? readint_from_fd(current_fd) : -1; long time_to_empty = time_to_empty_fd >= 0 ? readint_from_fd(time_to_empty_fd) : -1; + long time_to_full = time_to_full_fd >= 0 ? readint_from_fd(time_to_full_fd) : -1; + + if (charge >= -1) { + charge /= m->battery_scale; + } + + char buf[512]; + const char *status = readline_from_fd(status_fd, sizeof(buf), buf); + + if (status_fd >= 0) + close(status_fd); + if (capacity_fd >= 0) + close(capacity_fd); + if (energy_fd >= 0) + close(energy_fd); + if (power_fd >= 0) + close(power_fd); + if (charge_fd >= 0) + close(charge_fd); + if (current_fd >= 0) + close(current_fd); + if (time_to_empty_fd >= 0) + close(time_to_empty_fd); + if (time_to_full_fd >= 0) + close(time_to_full_fd); + if (base_dir_fd >= 0) + close(base_dir_fd); - const char *status = readline_from_fd(status_fd); enum state state; - if (strcmp(status, "Full") == 0) + if (status == NULL) { + LOG_WARN("failed to read battery state"); + state = STATE_UNKNOWN; + } else if (strcmp(status, "Full") == 0) state = STATE_FULL; + else if (strcmp(status, "Not charging") == 0) + state = STATE_NOTCHARGING; else if (strcmp(status, "Charging") == 0) state = STATE_CHARGING; else if (strcmp(status, "Discharging") == 0) state = STATE_DISCHARGING; else if (strcmp(status, "Unknown") == 0) - state = STATE_DISCHARGING; + state = STATE_UNKNOWN; else { LOG_ERR("unrecognized battery state: %s", status); - state = STATE_DISCHARGING; + state = STATE_UNKNOWN; } LOG_DBG("capacity: %ld, energy: %ld, power: %ld, charge=%ld, current=%ld, " - "time-to-empty: %ld", capacity, energy, power, charge, current, - time_to_empty); + "time-to-empty: %ld, time-to-full: %ld", + capacity, energy, power, charge, current, time_to_empty, time_to_full); mtx_lock(&mod->lock); + if (m->state != state) { + m->ema_current = (struct current_state){-1, 0, (struct timespec){0, 0}}; + } m->state = state; m->capacity = capacity; m->energy = energy; m->power = power; m->charge = charge; - m->current = current; + if (current != -1) { + struct timespec t; + clock_gettime(CLOCK_MONOTONIC, &t); + ema_linear(&m->ema_current, (struct current_state){current, current, t}, m->smoothing_scale); + } m->time_to_empty = time_to_empty; + m->time_to_full = time_to_full; mtx_unlock(&mod->lock); + return true; } static int @@ -325,68 +482,106 @@ run(struct module *mod) const struct bar *bar = mod->bar; struct private *m = mod->private; - int base_dir_fd = initialize(m); - if (base_dir_fd == -1) + if (!initialize(m)) return -1; - LOG_INFO("%s: %s %s (at %.1f%% of original capacity)", - m->battery, m->manufacturer, m->model, - (m->energy_full > 0 - ? 100.0 * m->energy_full / m->energy_full_design - : m->charge_full > 0 - ? 100.0 * m->charge_full / m->charge_full_design - : 0.0)); + LOG_INFO("%s: %s %s (at %.1f%% of original capacity)", m->battery, m->manufacturer, m->model, + (m->energy_full > 0 ? 100.0 * m->energy_full / m->energy_full_design + : m->charge_full > 0 ? 100.0 * m->charge_full / m->charge_full_design + : 0.0)); int ret = 1; - int status_fd = openat(base_dir_fd, "status", O_RDONLY); - int capacity_fd = openat(base_dir_fd, "capacity", O_RDONLY); - 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); struct udev *udev = udev_new(); struct udev_monitor *mon = udev_monitor_new_from_netlink(udev, "udev"); - if (status_fd < 0 || capacity_fd < 0 || udev == NULL || mon == NULL) + if (udev == NULL || mon == NULL) goto out; udev_monitor_filter_add_match_subsystem_devtype(mon, "power_supply", NULL); udev_monitor_enable_receiving(mon); - update_status( - mod, capacity_fd, energy_fd, power_fd, - charge_fd, current_fd, status_fd, time_to_empty_fd); + if (!update_status(mod)) + goto out; + bar->refresh(bar); + int timeout_left_ms = m->poll_interval; + while (true) { struct pollfd fds[] = { {.fd = mod->abort_fd, .events = POLLIN}, {.fd = udev_monitor_get_fd(mon), .events = POLLIN}, }; - poll(fds, 2, m->poll_interval > 0 ? m->poll_interval * 1000 : -1); + + int timeout = m->poll_interval > 0 ? timeout_left_ms : -1; + + struct timespec time_before_poll; + if (clock_gettime(CLOCK_BOOTTIME, &time_before_poll) < 0) { + LOG_ERRNO("failed to get current time"); + break; + } + + const int poll_ret = poll(fds, sizeof(fds) / sizeof(fds[0]), timeout); + + if (poll_ret < 0) { + if (errno == EINTR) + continue; + + LOG_ERRNO("failed to poll"); + break; + } if (fds[0].revents & POLLIN) { ret = 0; break; } + bool udev_for_us = false; + if (fds[1].revents & POLLIN) { struct udev_device *dev = udev_monitor_receive_device(mon); - const char *sysname = udev_device_get_sysname(dev); + if (dev != NULL) { + const char *sysname = udev_device_get_sysname(dev); + udev_for_us = sysname != NULL && strcmp(sysname, m->battery) == 0; - bool is_us = sysname != NULL && strcmp(sysname, m->battery) == 0; - udev_device_unref(dev); + if (!udev_for_us) { + LOG_DBG("udev notification not for us (%s != %s)", m->battery, + sysname != sysname ? sysname : "NULL"); + } else + LOG_DBG("triggering update due to udev notification"); - if (!is_us) - continue; + udev_device_unref(dev); + } } - update_status( - mod, capacity_fd, energy_fd, power_fd, - charge_fd, current_fd, status_fd, time_to_empty_fd); - bar->refresh(bar); + if (udev_for_us || poll_ret == 0) { + if (update_status(mod)) + bar->refresh(bar); + } + + if (poll_ret == 0 || udev_for_us) { + LOG_DBG("resetting timeout-left to %ldms", m->poll_interval); + timeout_left_ms = m->poll_interval; + } else { + struct timespec time_after_poll; + if (clock_gettime(CLOCK_BOOTTIME, &time_after_poll) < 0) { + LOG_ERRNO("failed to get current time"); + break; + } + + struct timespec timeout_consumed; + timespec_sub(&time_after_poll, &time_before_poll, &timeout_consumed); + + const int timeout_consumed_ms = timeout_consumed.tv_sec * 1000 + timeout_consumed.tv_nsec / 1000000; + + LOG_DBG("timeout-left before: %dms, consumed: %dms, updated: %dms", timeout_left_ms, timeout_consumed_ms, + max(timeout_left_ms - timeout_consumed_ms, 0)); + + timeout_left_ms -= timeout_consumed_ms; + if (timeout_left_ms < 0) + timeout_left_ms = 0; + } } out: @@ -394,36 +589,28 @@ out: udev_monitor_unref(mon); if (udev != NULL) udev_unref(udev); - - if (power_fd != -1) - close(power_fd); - if (energy_fd != -1) - close(energy_fd); - if (capacity_fd != -1) - close(capacity_fd); - if (status_fd != -1) - close(status_fd); - if (time_to_empty_fd != -1) - close(time_to_empty_fd); - - close(base_dir_fd); return ret; } static struct module * -battery_new(const char *battery, struct particle *label, int poll_interval_secs) +battery_new(const char *battery, struct particle *label, long poll_interval_msecs, int battery_scale, + long smoothing_secs) { struct private *m = calloc(1, sizeof(*m)); m->label = label; - m->poll_interval = poll_interval_secs; + m->poll_interval = poll_interval_msecs; + m->battery_scale = battery_scale; + m->smoothing_scale = smoothing_secs * one_sec_in_ns; m->battery = strdup(battery); - m->state = STATE_DISCHARGING; + m->state = STATE_UNKNOWN; + m->ema_current = (struct current_state){-1, 0, (struct timespec){0, 0}}; struct module *mod = module_common_new(); mod->private = m; mod->run = &run; mod->destroy = &destroy; mod->content = &content; + mod->description = &description; return mod; } @@ -433,11 +620,29 @@ from_conf(const struct yml_node *node, struct conf_inherit inherited) const struct yml_node *c = yml_get_value(node, "content"); const struct yml_node *name = yml_get_value(node, "name"); const struct yml_node *poll_interval = yml_get_value(node, "poll-interval"); + const struct yml_node *battery_scale = yml_get_value(node, "battery-scale"); + const struct yml_node *smoothing_secs = yml_get_value(node, "smoothing-secs"); - return battery_new( - yml_value_as_string(name), - conf_to_particle(c, inherited), - poll_interval != NULL ? yml_value_as_int(poll_interval) : 60); + return battery_new(yml_value_as_string(name), conf_to_particle(c, inherited), + (poll_interval != NULL ? yml_value_as_int(poll_interval) : default_poll_interval), + (battery_scale != NULL ? yml_value_as_int(battery_scale) : 1), + (smoothing_secs != NULL ? yml_value_as_int(smoothing_secs) : 100)); +} + +static bool +conf_verify_poll_interval(keychain_t *chain, const struct yml_node *node) +{ + if (!conf_verify_unsigned(chain, node)) + return false; + + const long value = yml_value_as_int(node); + + if (value != 0 && value < min_poll_interval) { + LOG_ERR("%s: interval value cannot be less than %ldms", conf_err_prefix(chain, node), min_poll_interval); + return false; + } + + return true; } static bool @@ -445,7 +650,9 @@ verify_conf(keychain_t *chain, const struct yml_node *node) { static const struct attr_info attrs[] = { {"name", true, &conf_verify_string}, - {"poll-interval", false, &conf_verify_int}, + {"poll-interval", false, &conf_verify_poll_interval}, + {"battery-scale", false, &conf_verify_unsigned}, + {"smoothing-secs", false, &conf_verify_unsigned}, MODULE_COMMON_ATTRS, }; diff --git a/modules/clock.c b/modules/clock.c index 0599e9a..1758c0a 100644 --- a/modules/clock.c +++ b/modules/clock.c @@ -1,19 +1,30 @@ +#include +#include #include #include #include -#include #include +#include +#define LOG_MODULE "clock" +#define LOG_ENABLE_DBG 0 #include "../bar/bar.h" -#include "../config.h" #include "../config-verify.h" +#include "../config.h" +#include "../log.h" #include "../plugin.h" -struct private { +struct private +{ struct particle *label; + enum { + UPDATE_GRANULARITY_SECONDS, + UPDATE_GRANULARITY_MINUTES, + } update_granularity; char *date_format; char *time_format; + bool utc; }; static void @@ -27,12 +38,18 @@ destroy(struct module *mod) module_default_destroy(mod); } +static const char * +description(const struct module *mod) +{ + return "clock"; +} + static struct exposable * content(struct module *mod) { const struct private *m = mod->private; time_t t = time(NULL); - struct tm *tm = localtime(&t); + struct tm *tm = m->utc ? gmtime(&t) : localtime(&t); char date_str[1024]; strftime(date_str, sizeof(date_str), m->date_format, tm); @@ -41,8 +58,7 @@ content(struct module *mod) strftime(time_str, sizeof(time_str), m->time_format, tm); struct tag_set tags = { - .tags = (struct tag *[]){tag_new_string(mod, "time", time_str), - tag_new_string(mod, "date", date_str)}, + .tags = (struct tag *[]){tag_new_string(mod, "time", time_str), tag_new_string(mod, "date", date_str)}, .count = 2, }; @@ -55,43 +71,103 @@ content(struct module *mod) static int run(struct module *mod) { + const struct private *m = mod->private; const struct bar *bar = mod->bar; bar->refresh(bar); - while (true) { - time_t now = time(NULL); - time_t now_no_secs = now / 60 * 60; - assert(now_no_secs % 60 == 0); + int ret = 1; - time_t next_min = now_no_secs + 60; - time_t timeout = next_min - now; - assert(timeout >= 0 && timeout <= 60); + while (true) { + struct timespec _now; + clock_gettime(CLOCK_REALTIME, &_now); + + const struct timeval now = { + .tv_sec = _now.tv_sec, + .tv_usec = _now.tv_nsec / 1000, + }; + + int timeout_ms = 1000; + + switch (m->update_granularity) { + case UPDATE_GRANULARITY_SECONDS: { + const struct timeval next_second = {.tv_sec = now.tv_sec + 1, .tv_usec = 0}; + + struct timeval _timeout; + timersub(&next_second, &now, &_timeout); + + assert(_timeout.tv_sec == 0 || (_timeout.tv_sec == 1 && _timeout.tv_usec == 0)); + timeout_ms = _timeout.tv_usec / 1000; + break; + } + + case UPDATE_GRANULARITY_MINUTES: { + const struct timeval next_minute = { + .tv_sec = now.tv_sec / 60 * 60 + 60, + .tv_usec = 0, + }; + + struct timeval _timeout; + timersub(&next_minute, &now, &_timeout); + timeout_ms = _timeout.tv_sec * 1000 + _timeout.tv_usec / 1000; + } + } + + /* Add 1ms to account for rounding errors */ + timeout_ms++; + + LOG_DBG("now: %lds %ldµs -> timeout: %dms", now.tv_sec, now.tv_usec, timeout_ms); struct pollfd fds[] = {{.fd = mod->abort_fd, .events = POLLIN}}; - poll(fds, 1, timeout * 1000); + if (poll(fds, 1, timeout_ms) < 0) { + if (errno == EINTR) + continue; - if (fds[0].revents & POLLIN) + LOG_ERRNO("failed to poll"); break; + } + + if (fds[0].revents & POLLIN) { + ret = 0; + break; + } bar->refresh(bar); } - return 0; + return ret; } static struct module * -clock_new(struct particle *label, const char *date_format, const char *time_format) +clock_new(struct particle *label, const char *date_format, const char *time_format, bool utc) { struct private *m = calloc(1, sizeof(*m)); m->label = label; m->date_format = strdup(date_format); m->time_format = strdup(time_format); + m->utc = utc; + + static const char *const seconds_formatters[] = { + "%c", "%s", "%S", "%T", "%r", "%X", + }; + + m->update_granularity = UPDATE_GRANULARITY_MINUTES; + + for (size_t i = 0; i < sizeof(seconds_formatters) / sizeof(seconds_formatters[0]); i++) { + if (strstr(time_format, seconds_formatters[i]) != NULL) { + m->update_granularity = UPDATE_GRANULARITY_SECONDS; + break; + } + } + + LOG_DBG("using %s update granularity", + (m->update_granularity == UPDATE_GRANULARITY_MINUTES ? "minutes" : "seconds")); struct module *mod = module_common_new(); mod->private = m; mod->run = &run; mod->destroy = &destroy; mod->content = &content; + mod->description = &description; return mod; } @@ -101,11 +177,11 @@ from_conf(const struct yml_node *node, struct conf_inherit inherited) const struct yml_node *c = yml_get_value(node, "content"); const struct yml_node *date_format = yml_get_value(node, "date-format"); const struct yml_node *time_format = yml_get_value(node, "time-format"); + const struct yml_node *utc = yml_get_value(node, "utc"); - return clock_new( - conf_to_particle(c, inherited), - date_format != NULL ? yml_value_as_string(date_format) : "%x", - time_format != NULL ? yml_value_as_string(time_format) : "%H:%M"); + return clock_new(conf_to_particle(c, inherited), date_format != NULL ? yml_value_as_string(date_format) : "%x", + time_format != NULL ? yml_value_as_string(time_format) : "%H:%M", + utc != NULL ? yml_value_as_bool(utc) : false); } static bool @@ -114,6 +190,7 @@ verify_conf(keychain_t *chain, const struct yml_node *node) static const struct attr_info attrs[] = { {"date-format", false, &conf_verify_string}, {"time-format", false, &conf_verify_string}, + {"utc", false, &conf_verify_bool}, MODULE_COMMON_ATTRS, }; diff --git a/modules/cpu.c b/modules/cpu.c new file mode 100644 index 0000000..118361e --- /dev/null +++ b/modules/cpu.c @@ -0,0 +1,297 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define LOG_MODULE "cpu" +#define LOG_ENABLE_DBG 0 +#include "../log.h" + +#include "../bar/bar.h" +#include "../config-verify.h" +#include "../config.h" +#include "../particles/dynlist.h" +#include "../plugin.h" + +static const long min_poll_interval = 250; + +struct cpu_stats { + uint32_t *prev_cores_idle; + uint32_t *prev_cores_nidle; + + uint32_t *cur_cores_idle; + uint32_t *cur_cores_nidle; +}; + +struct private +{ + struct particle *template; + uint16_t interval; + size_t core_count; + struct cpu_stats cpu_stats; +}; + +static void +destroy(struct module *mod) +{ + struct private *m = mod->private; + + m->template->destroy(m->template); + free(m->cpu_stats.prev_cores_idle); + free(m->cpu_stats.prev_cores_nidle); + free(m->cpu_stats.cur_cores_idle); + free(m->cpu_stats.cur_cores_nidle); + free(m); + + module_default_destroy(mod); +} + +static const char * +description(const struct module *mod) +{ + return "cpu"; +} + +static uint32_t +get_cpu_nb_cores() +{ + int nb_cores = sysconf(_SC_NPROCESSORS_ONLN); + LOG_DBG("CPU count: %d", nb_cores); + + return nb_cores; +} + +static bool +parse_proc_stat_line(const char *line, uint32_t *user, uint32_t *nice, uint32_t *system, uint32_t *idle, + uint32_t *iowait, uint32_t *irq, uint32_t *softirq, uint32_t *steal, uint32_t *guest, + uint32_t *guestnice) +{ + int32_t core_id; + if (line[sizeof("cpu") - 1] == ' ') { + int read = sscanf(line, + "cpu %" SCNu32 " %" SCNu32 " %" SCNu32 " %" SCNu32 " %" SCNu32 " %" SCNu32 " %" SCNu32 + " %" SCNu32 " %" SCNu32 " %" SCNu32, + user, nice, system, idle, iowait, irq, softirq, steal, guest, guestnice); + return read == 10; + } else { + int read = sscanf(line, + "cpu%" SCNu32 " %" SCNu32 " %" SCNu32 " %" SCNu32 " %" SCNu32 " %" SCNu32 " %" SCNu32 + " %" SCNu32 " %" SCNu32 " %" SCNu32 " %" SCNu32, + &core_id, user, nice, system, idle, iowait, irq, softirq, steal, guest, guestnice); + return read == 11; + } +} + +static uint8_t +get_cpu_usage_percent(const struct cpu_stats *cpu_stats, int8_t core_idx) +{ + uint32_t prev_total = cpu_stats->prev_cores_idle[core_idx + 1] + cpu_stats->prev_cores_nidle[core_idx + 1]; + + uint32_t cur_total = cpu_stats->cur_cores_idle[core_idx + 1] + cpu_stats->cur_cores_nidle[core_idx + 1]; + + double totald = cur_total - prev_total; + double nidled = cpu_stats->cur_cores_nidle[core_idx + 1] - cpu_stats->prev_cores_nidle[core_idx + 1]; + + double percent = (nidled * 100) / (totald + 1); + return round(percent); +} + +static void +refresh_cpu_stats(struct cpu_stats *cpu_stats, size_t core_count) +{ + int32_t core = 0; + uint32_t user = 0; + uint32_t nice = 0; + uint32_t system = 0; + uint32_t idle = 0; + uint32_t iowait = 0; + uint32_t irq = 0; + uint32_t softirq = 0; + uint32_t steal = 0; + uint32_t guest = 0; + uint32_t guestnice = 0; + + FILE *fp = NULL; + char *line = NULL; + size_t len = 0; + ssize_t read; + + fp = fopen("/proc/stat", "re"); + if (NULL == fp) { + LOG_ERRNO("unable to open /proc/stat"); + return; + } + + while ((read = getline(&line, &len, fp)) != -1 && core <= core_count) { + if (strncmp(line, "cpu", sizeof("cpu") - 1) == 0) { + if (!parse_proc_stat_line(line, &user, &nice, &system, &idle, &iowait, &irq, &softirq, &steal, &guest, + &guestnice)) { + LOG_ERR("unable to parse /proc/stat line"); + goto exit; + } + + cpu_stats->prev_cores_idle[core] = cpu_stats->cur_cores_idle[core]; + cpu_stats->prev_cores_nidle[core] = cpu_stats->cur_cores_nidle[core]; + + cpu_stats->cur_cores_idle[core] = idle + iowait; + cpu_stats->cur_cores_nidle[core] = user + nice + system + irq + softirq + steal; + + core++; + } + } + +exit: + fclose(fp); + free(line); +} + +static struct exposable * +content(struct module *mod) +{ + const struct private *m = mod->private; + + mtx_lock(&mod->lock); + + const size_t list_count = m->core_count + 1; + struct exposable *parts[list_count]; + + { + uint8_t total_usage = get_cpu_usage_percent(&m->cpu_stats, -1); + + struct tag_set tags = { + .tags = (struct tag *[]){ + tag_new_int(mod, "id", -1), + tag_new_int_range(mod, "cpu", total_usage, 0, 100), + }, + .count = 2, + }; + + parts[0] = m->template->instantiate(m->template, &tags); + tag_set_destroy(&tags); + } + + for (size_t i = 0; i < m->core_count; i++) { + uint8_t core_usage = get_cpu_usage_percent(&m->cpu_stats, i); + + struct tag_set tags = { + .tags = (struct tag *[]){ + tag_new_int(mod, "id", i), + tag_new_int_range(mod, "cpu", core_usage, 0, 100), + }, + .count = 2, + }; + + parts[i + 1] = m->template->instantiate(m->template, &tags); + tag_set_destroy(&tags); + } + + mtx_unlock(&mod->lock); + return dynlist_exposable_new(parts, list_count, 0, 0); +} + +static int +run(struct module *mod) +{ + const struct bar *bar = mod->bar; + bar->refresh(bar); + + struct private *p = mod->private; + while (true) { + struct pollfd fds[] = {{.fd = mod->abort_fd, .events = POLLIN}}; + + int res = poll(fds, sizeof(fds) / sizeof(*fds), p->interval); + + if (res < 0) { + if (EINTR == errno) + continue; + LOG_ERRNO("unable to poll abort fd"); + return -1; + } + + if (fds[0].revents & POLLIN) + break; + + mtx_lock(&mod->lock); + refresh_cpu_stats(&p->cpu_stats, p->core_count); + mtx_unlock(&mod->lock); + bar->refresh(bar); + } + + return 0; +} + +static struct module * +cpu_new(uint16_t interval, struct particle *template) +{ + uint32_t nb_cores = get_cpu_nb_cores(); + + struct private *p = calloc(1, sizeof(*p)); + p->template = template; + p->interval = interval; + p->core_count = nb_cores; + + p->cpu_stats.prev_cores_nidle = calloc(nb_cores + 1, sizeof(*p->cpu_stats.prev_cores_nidle)); + p->cpu_stats.prev_cores_idle = calloc(nb_cores + 1, sizeof(*p->cpu_stats.prev_cores_idle)); + + p->cpu_stats.cur_cores_nidle = calloc(nb_cores + 1, sizeof(*p->cpu_stats.cur_cores_nidle)); + p->cpu_stats.cur_cores_idle = calloc(nb_cores + 1, sizeof(*p->cpu_stats.cur_cores_idle)); + + struct module *mod = module_common_new(); + mod->private = p; + mod->run = &run; + mod->destroy = &destroy; + mod->content = &content; + mod->description = &description; + return mod; +} + +static struct module * +from_conf(const struct yml_node *node, struct conf_inherit inherited) +{ + const struct yml_node *interval = yml_get_value(node, "poll-interval"); + const struct yml_node *c = yml_get_value(node, "content"); + + return cpu_new(interval == NULL ? min_poll_interval : yml_value_as_int(interval), conf_to_particle(c, inherited)); +} + +static bool +conf_verify_poll_interval(keychain_t *chain, const struct yml_node *node) +{ + if (!conf_verify_unsigned(chain, node)) + return false; + + if (yml_value_as_int(node) < min_poll_interval) { + LOG_ERR("%s: interval value cannot be less than %ldms", conf_err_prefix(chain, node), min_poll_interval); + return false; + } + + return true; +} + +static bool +verify_conf(keychain_t *chain, const struct yml_node *node) +{ + static const struct attr_info attrs[] = { + {"poll-interval", false, &conf_verify_poll_interval}, + MODULE_COMMON_ATTRS, + }; + + return conf_verify_dict(chain, node, attrs); +} + +const struct module_iface module_cpu_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_cpu_iface"))); +#endif 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 new file mode 100644 index 0000000..c33cbef --- /dev/null +++ b/modules/disk-io.c @@ -0,0 +1,350 @@ +#include +#include +#include +#include +#include +#include + +#include + +#define LOG_MODULE "disk-io" +#define LOG_ENABLE_DBG 0 +#include "../log.h" + +#include "../bar/bar.h" +#include "../config-verify.h" +#include "../config.h" +#include "../particles/dynlist.h" +#include "../plugin.h" + +static const long min_poll_interval = 250; + +struct device_stats { + char *name; + bool is_disk; + + uint64_t prev_sectors_read; + uint64_t cur_sectors_read; + + uint64_t prev_sectors_written; + uint64_t cur_sectors_written; + + uint32_t ios_in_progress; + + bool exists; +}; + +struct private +{ + struct particle *label; + uint16_t interval; + tll(struct device_stats *) devices; +}; + +static bool +is_disk(char const *name) +{ + DIR *dir = opendir("/sys/block"); + if (dir == NULL) { + LOG_ERRNO("failed to read /sys/block directory"); + return false; + } + + struct dirent *entry; + + bool found = false; + while ((entry = readdir(dir)) != NULL) { + if (strcmp(name, entry->d_name) == 0) { + found = true; + break; + } + } + + closedir(dir); + return found; +} + +static struct device_stats * +new_device_stats(char const *name) +{ + struct device_stats *dev = malloc(sizeof(*dev)); + dev->name = strdup(name); + dev->is_disk = is_disk(name); + return dev; +} + +static void +free_device_stats(struct device_stats *dev) +{ + free(dev->name); + free(dev); +} + +static void +destroy(struct module *mod) +{ + struct private *m = mod->private; + m->label->destroy(m->label); + tll_foreach(m->devices, it) { free_device_stats(it->item); } + tll_free(m->devices); + free(m); + module_default_destroy(mod); +} + +static const char * +description(const struct module *mod) +{ + return "disk-io"; +} + +static void +refresh_device_stats(struct private *m) +{ + FILE *fp = NULL; + char *line = NULL; + size_t len = 0; + ssize_t read; + + fp = fopen("/proc/diskstats", "re"); + if (NULL == fp) { + LOG_ERRNO("unable to open /proc/diskstats"); + return; + } + + /* + * Devices may be added or removed during the bar's lifetime, as external + * block devices are connected or disconnected from the machine. /proc/diskstats + * reports data only for the devices that are currently connected. + * + * This means that if we have a device that ISN'T in /proc/diskstats, it was + * disconnected, and we need to remove it from the list. + * + * On the other hand, if a device IS in /proc/diskstats, but not in our list, we + * must create a new device_stats struct and add it to the list. + * + * The 'exists' variable is what keep tracks of whether or not /proc/diskstats + * is still reporting the device (i.e., it is still connected). + */ + tll_foreach(m->devices, it) { it->item->exists = false; } + + while ((read = getline(&line, &len, fp)) != -1) { + /* + * For an explanation of the fields below, see + * https://www.kernel.org/doc/Documentation/ABI/testing/procfs-diskstats + */ + uint8_t major_number = 0; + uint8_t minor_number = 0; + char *device_name = NULL; + uint32_t completed_reads = 0; + uint32_t merged_reads = 0; + uint64_t sectors_read = 0; + uint32_t reading_time = 0; + uint32_t completed_writes = 0; + uint32_t merged_writes = 0; + uint64_t sectors_written = 0; + uint32_t writting_time = 0; + uint32_t ios_in_progress = 0; + uint32_t io_time = 0; + uint32_t io_weighted_time = 0; + uint32_t completed_discards = 0; + uint32_t merged_discards = 0; + uint32_t sectors_discarded = 0; + uint32_t discarding_time = 0; + uint32_t completed_flushes = 0; + uint32_t flushing_time = 0; + if (!sscanf(line, + " %" SCNu8 " %" SCNu8 " %ms %" SCNu32 " %" SCNu32 " %" SCNu64 " %" SCNu32 " %" SCNu32 " %" SCNu32 + " %" SCNu64 " %" SCNu32 " %" SCNu32 " %" SCNu32 " %" SCNu32 " %" SCNu32 " %" SCNu32 " %" SCNu32 + " %" SCNu32 " %" SCNu32 " %" SCNu32, + &major_number, &minor_number, &device_name, &completed_reads, &merged_reads, §ors_read, + &reading_time, &completed_writes, &merged_writes, §ors_written, &writting_time, + &ios_in_progress, &io_time, &io_weighted_time, &completed_discards, &merged_discards, + §ors_discarded, &discarding_time, &completed_flushes, &flushing_time)) { + LOG_ERR("unable to parse /proc/diskstats line"); + free(device_name); + goto exit; + } + + bool found = false; + tll_foreach(m->devices, it) + { + struct device_stats *dev = it->item; + if (strcmp(dev->name, device_name) == 0) { + dev->prev_sectors_read = dev->cur_sectors_read; + dev->prev_sectors_written = dev->cur_sectors_written; + dev->ios_in_progress = ios_in_progress; + dev->cur_sectors_read = sectors_read; + dev->cur_sectors_written = sectors_written; + dev->exists = true; + found = true; + break; + } + } + + if (!found) { + struct device_stats *new_dev = new_device_stats(device_name); + new_dev->ios_in_progress = ios_in_progress; + new_dev->prev_sectors_read = sectors_read; + new_dev->cur_sectors_read = sectors_read; + new_dev->prev_sectors_written = sectors_written; + new_dev->cur_sectors_written = sectors_written; + new_dev->exists = true; + tll_push_back(m->devices, new_dev); + } + + free(device_name); + } + + tll_foreach(m->devices, it) + { + if (!it->item->exists) { + free_device_stats(it->item); + tll_remove(m->devices, it); + } + } +exit: + fclose(fp); + free(line); +} + +static struct exposable * +content(struct module *mod) +{ + const struct private *p = mod->private; + uint64_t total_bytes_read = 0; + uint64_t total_bytes_written = 0; + uint32_t total_ios_in_progress = 0; + mtx_lock(&mod->lock); + struct exposable *tag_parts[p->devices.length + 1]; + int i = 0; + tll_foreach(p->devices, it) + { + struct device_stats *dev = it->item; + uint64_t bytes_read = (dev->cur_sectors_read - dev->prev_sectors_read) * 512; + uint64_t bytes_written = (dev->cur_sectors_written - dev->prev_sectors_written) * 512; + + if (dev->is_disk) { + total_bytes_read += bytes_read; + total_bytes_written += bytes_written; + total_ios_in_progress += dev->ios_in_progress; + } + + struct tag_set tags = { + .tags = (struct tag *[]) { + tag_new_string(mod, "device", dev->name), + tag_new_bool(mod, "is_disk", dev->is_disk), + tag_new_int(mod, "read_speed", (bytes_read * 1000) / p->interval), + tag_new_int(mod, "write_speed", (bytes_written * 1000) / p->interval), + tag_new_int(mod, "ios_in_progress", dev->ios_in_progress), + }, + .count = 5, + }; + tag_parts[i++] = p->label->instantiate(p->label, &tags); + tag_set_destroy(&tags); + } + struct tag_set tags = { + .tags = (struct tag *[]) { + tag_new_string(mod, "device", "Total"), + tag_new_bool(mod, "is_disk", true), + tag_new_int(mod, "read_speed", (total_bytes_read * 1000) / p->interval), + tag_new_int(mod, "write_speed", (total_bytes_written * 1000) / p->interval), + tag_new_int(mod, "ios_in_progress", total_ios_in_progress), + }, + .count = 5, + }; + tag_parts[i] = p->label->instantiate(p->label, &tags); + tag_set_destroy(&tags); + mtx_unlock(&mod->lock); + + return dynlist_exposable_new(tag_parts, p->devices.length + 1, 0, 0); +} + +static int +run(struct module *mod) +{ + const struct bar *bar = mod->bar; + bar->refresh(bar); + struct private *p = mod->private; + while (true) { + struct pollfd fds[] = {{.fd = mod->abort_fd, .events = POLLIN}}; + + int res = poll(fds, sizeof(fds) / sizeof(*fds), p->interval); + + if (res < 0) { + if (EINTR == errno) + continue; + LOG_ERRNO("unable to poll abort fd"); + return -1; + } + + if (fds[0].revents & POLLIN) + break; + + mtx_lock(&mod->lock); + refresh_device_stats(p); + mtx_unlock(&mod->lock); + bar->refresh(bar); + } + + return 0; +} + +static struct module * +disk_io_new(uint16_t interval, struct particle *label) +{ + struct private *p = calloc(1, sizeof(*p)); + p->label = label; + p->interval = interval; + + struct module *mod = module_common_new(); + mod->private = p; + mod->run = &run; + mod->destroy = &destroy; + mod->content = &content; + mod->description = &description; + return mod; +} + +static struct module * +from_conf(const struct yml_node *node, struct conf_inherit inherited) +{ + const struct yml_node *interval = yml_get_value(node, "poll-interval"); + const struct yml_node *c = yml_get_value(node, "content"); + + return disk_io_new(interval == NULL ? min_poll_interval : yml_value_as_int(interval), + conf_to_particle(c, inherited)); +} + +static bool +conf_verify_poll_interval(keychain_t *chain, const struct yml_node *node) +{ + if (!conf_verify_unsigned(chain, node)) + return false; + + if (yml_value_as_int(node) < min_poll_interval) { + LOG_ERR("%s: poll-interval value cannot be less than %ldms", conf_err_prefix(chain, node), min_poll_interval); + return false; + } + + return true; +} + +static bool +verify_conf(keychain_t *chain, const struct yml_node *node) +{ + static const struct attr_info attrs[] = { + {"poll-interval", false, &conf_verify_poll_interval}, + MODULE_COMMON_ATTRS, + }; + + return conf_verify_dict(chain, node, attrs); +} + +const struct module_iface module_disk_io_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_disk_io_iface"))); +#endif diff --git a/modules/dwl.c b/modules/dwl.c new file mode 100644 index 0000000..3b1bdcc --- /dev/null +++ b/modules/dwl.c @@ -0,0 +1,550 @@ +#include +#include +#include +#include +#include + +#define ARR_LEN(x) (sizeof((x)) / sizeof((x)[0])) + +#include "../config-verify.h" +#include "../config.h" +#include "../log.h" +#include "../module.h" +#include "../particles/dynlist.h" +#include "../plugin.h" + +#define LOG_MODULE "dwl" +#define LOG_ENABLE_DBG 0 + +struct dwl_tag { + int id; + char *name; + bool selected; + bool empty; + bool urgent; +}; + +struct private +{ + struct particle *label; + + char const *monitor; + + unsigned int number_of_tags; + char *dwl_info_filename; + + /* dwl data */ + char *title; + char *appid; + bool fullscreen; + bool floating; + bool selmon; + tll(struct dwl_tag *) tags; + char *layout; +}; + +enum LINE_MODE { + LINE_MODE_0, + LINE_MODE_TITLE, + LINE_MODE_APPID, + LINE_MODE_FULLSCREEN, + LINE_MODE_FLOATING, + LINE_MODE_SELMON, + LINE_MODE_TAGS, + LINE_MODE_LAYOUT, +}; + +static void +free_dwl_tag(struct dwl_tag *tag) +{ + free(tag->name); + free(tag); +} + +static void +destroy(struct module *module) +{ + struct private *private = module->private; + private->label->destroy(private->label); + + tll_free_and_free(private->tags, free_dwl_tag); + free(private->dwl_info_filename); + free(private->title); + free(private->layout); + free(private); + + module_default_destroy(module); +} + +static char const * +description(const struct module *module) +{ + return "dwl"; +} + +static struct exposable * +content(struct module *module) +{ + struct private const *private = module->private; + mtx_lock(&module->lock); + + size_t i = 0; + /* + 1 for `default` tag */ + struct exposable *exposable[tll_length(private->tags) + 1]; + tll_foreach(private->tags, it) + { + struct tag_set tags = { + .tags = (struct tag*[]){ + tag_new_string(module, "title", private->title), + tag_new_string(module, "appid", private->appid), + tag_new_bool(module, "fullscreen", private->fullscreen), + tag_new_bool(module, "floating", private->floating), + tag_new_bool(module, "selmon", private->selmon), + tag_new_string(module, "layout", private->layout), + tag_new_int(module, "id", it->item->id), + tag_new_string(module, "name", it->item->name), + tag_new_bool(module, "selected", it->item->selected), + tag_new_bool(module, "empty", it->item->empty), + tag_new_bool(module, "urgent", it->item->urgent), + }, + .count = 11, + }; + exposable[i++] = private->label->instantiate(private->label, &tags); + tag_set_destroy(&tags); + } + + /* default tag (used for title, layout, etc) */ + struct tag_set tags = { + .tags = (struct tag*[]){ + tag_new_string(module, "title", private->title), + tag_new_string(module, "appid", private->appid), + tag_new_bool(module, "fullscreen", private->fullscreen), + tag_new_bool(module, "floating", private->floating), + tag_new_bool(module, "selmon", private->selmon), + tag_new_string(module, "layout", private->layout), + tag_new_int(module, "id", 0), + tag_new_string(module, "name", "0"), + tag_new_bool(module, "selected", false), + tag_new_bool(module, "empty", true), + tag_new_bool(module, "urgent", false), + }, + .count = 11, + }; + exposable[i++] = private->label->instantiate(private->label, &tags); + tag_set_destroy(&tags); + + mtx_unlock(&module->lock); + return dynlist_exposable_new(exposable, i, 0, 0); +} + +static struct dwl_tag * +dwl_tag_from_id(struct private *private, uint32_t id) +{ + tll_foreach(private->tags, it) + { + if (it->item->id == id) + return it->item; + } + + assert(false); /* unreachable */ + return NULL; +} + +static void +process_line(char *line, struct module *module) +{ + struct private *private = module->private; + enum LINE_MODE line_mode = LINE_MODE_0; + + /* Remove \n */ + line[strcspn(line, "\n")] = '\0'; + + /* Split line by space */ + size_t index = 1; + char *save_pointer = NULL; + char *string = strtok_r(line, " ", &save_pointer); + while (string != NULL) { + /* dwl logs are formatted like this + * $1 -> monitor + * $2 -> action + * $3 -> arg1 + * $4 -> arg2 + * ... */ + + /* monitor */ + if (index == 1) { + /* Not our monitor */ + if (strcmp(string, private->monitor) != 0) + break; + } + /* action */ + else if (index == 2) { + if (strcmp(string, "title") == 0) { + line_mode = LINE_MODE_TITLE; + /* Update the title here, to avoid allocate and free memory on + * every iteration (the line is separated by spaces, then we + * join it again) a bit suboptimal, isn't it?) */ + free(private->title); + private->title = strdup(save_pointer); + break; + } else if (strcmp(string, "appid") == 0) { + line_mode = LINE_MODE_APPID; + /* Update the appid here, same as the title. */ + free(private->appid); + private->appid = strdup(save_pointer); + break; + } else if (strcmp(string, "fullscreen") == 0) + line_mode = LINE_MODE_FULLSCREEN; + else if (strcmp(string, "floating") == 0) + line_mode = LINE_MODE_FLOATING; + else if (strcmp(string, "selmon") == 0) + line_mode = LINE_MODE_SELMON; + else if (strcmp(string, "tags") == 0) + line_mode = LINE_MODE_TAGS; + else if (strcmp(string, "layout") == 0) + line_mode = LINE_MODE_LAYOUT; + else { + LOG_WARN("UNKNOWN action, please open an issue on https://codeberg.org/dnkl/yambar"); + return; + } + } + /* args */ + else { + if (line_mode == LINE_MODE_TAGS) { + static uint32_t occupied, selected, client_tags, urgent; + static uint32_t *target = NULL; + + /* dwl tags action log are formatted like this + * $3 -> occupied + * $4 -> tags + * $5 -> clientTags (not needed) + * $6 -> urgent */ + if (index == 3) + target = &occupied; + else if (index == 4) + target = &selected; + else if (index == 5) + target = &client_tags; + else if (index == 6) + target = &urgent; + + /* No need to check error IMHO */ + *target = strtoul(string, NULL, 10); + + /* Populate information */ + if (index == 6) { + for (size_t id = 1; id <= private->number_of_tags; ++id) { + uint32_t mask = 1 << (id - 1); + + struct dwl_tag *dwl_tag = dwl_tag_from_id(private, id); + dwl_tag->selected = mask & selected; + dwl_tag->empty = !(mask & occupied); + dwl_tag->urgent = mask & urgent; + } + } + } else + switch (line_mode) { + case LINE_MODE_TITLE: + case LINE_MODE_APPID: + assert(false); /* unreachable */ + break; + case LINE_MODE_FULLSCREEN: + private + ->fullscreen = (strcmp(string, "0") != 0); + break; + case LINE_MODE_FLOATING: + private + ->floating = (strcmp(string, "0") != 0); + break; + case LINE_MODE_SELMON: + private + ->selmon = (strcmp(string, "0") != 0); + break; + case LINE_MODE_LAYOUT: + free(private->layout); + private->layout = strdup(string); + break; + default:; + assert(false); /* unreachable */ + } + } + + string = strtok_r(NULL, " ", &save_pointer); + ++index; + } +} + +static int +file_read_content(FILE *file, struct module *module) +{ + static char buffer[1024]; + + errno = 0; + while (fgets(buffer, ARR_LEN(buffer), file) != NULL) + process_line(buffer, module); + + fseek(file, 0, SEEK_END); + + /* Check whether error has been */ + if (ferror(file) != 0) { + LOG_ERRNO("unable to read file's content."); + return 1; + } + + return 0; +} + +static void +file_seek_to_last_n_lines(FILE *file, int number_of_lines) +{ + if (number_of_lines == 0 || file == NULL) + return; + + fseek(file, 0, SEEK_END); + + long position = ftell(file); + while (position > 0) { + /* Cannot go less than position 0 */ + if (fseek(file, --position, SEEK_SET) == EINVAL) + break; + + if (fgetc(file) == '\n') + if (number_of_lines-- == 0) + break; + } +} + +static int +run_init(int *inotify_fd, int *inotify_wd, FILE **file, char *dwl_info_filename) +{ + *inotify_fd = inotify_init(); + if (*inotify_fd == -1) { + LOG_ERRNO("unable to create inotify fd."); + return -1; + } + + *inotify_wd = inotify_add_watch(*inotify_fd, dwl_info_filename, IN_MODIFY); + if (*inotify_wd == -1) { + close(*inotify_fd); + LOG_ERRNO("unable to add watch to inotify fd."); + return 1; + } + + *file = fopen(dwl_info_filename, "re"); + if (*file == NULL) { + inotify_rm_watch(*inotify_fd, *inotify_wd); + close(*inotify_fd); + LOG_ERRNO("unable to open file."); + return 1; + } + + return 0; +} + +static int +run_clean(int inotify_fd, int inotify_wd, FILE *file) +{ + if (inotify_fd != -1) { + if (inotify_wd != -1) + inotify_rm_watch(inotify_fd, inotify_wd); + close(inotify_fd); + } + + if (file != NULL) { + if (fclose(file) == EOF) { + LOG_ERRNO("unable to close file."); + return 1; + } + } + + return 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 */ + do { + private->monitor = module->bar->output_name(module->bar); + usleep(50); + } while (private->monitor == NULL); + + int inotify_fd = -1, inotify_wd = -1; + FILE *file = NULL; + if (run_init(&inotify_fd, &inotify_wd, &file, private->dwl_info_filename) != 0) + return 1; + + /* Dwl output is 6 lines per monitor, so let's assume that nobody has + * more than 5 monitors (6 * 5 = 30) */ + mtx_lock(&module->lock); + file_seek_to_last_n_lines(file, 30); + if (file_read_content(file, module) != 0) { + mtx_unlock(&module->lock); + return run_clean(inotify_fd, inotify_wd, file); + } + mtx_unlock(&module->lock); + + module->bar->refresh(module->bar); + + while (true) { + struct pollfd fds[] = { + (struct pollfd){.fd = module->abort_fd, .events = POLLIN}, + (struct pollfd){.fd = inotify_fd, .events = POLLIN}, + }; + + if (poll(fds, ARR_LEN(fds), -1) == -1) { + if (errno == EINTR) + continue; + + LOG_ERRNO("unable to poll."); + break; + } + + if (fds[0].revents & POLLIN) + break; + + /* fds[1] (inotify_fd) must be POLLIN otherwise issue happen'd */ + if (!(fds[1].revents & POLLIN)) { + LOG_ERR("expected POLLIN revent"); + break; + } + + /* Block until event */ + static char buffer[1024]; + ssize_t length = read(inotify_fd, buffer, ARR_LEN(buffer)); + + if (length == 0) + break; + + if (length == -1) { + if (errno == EAGAIN) + continue; + + LOG_ERRNO("unable to read %s", private->dwl_info_filename); + break; + } + + mtx_lock(&module->lock); + if (file_read_content(file, module) != 0) { + mtx_unlock(&module->lock); + break; + } + mtx_unlock(&module->lock); + + module->bar->refresh(module->bar); + } + + return run_clean(inotify_fd, inotify_wd, file); +} + +static struct module * +dwl_new(struct particle *label, int number_of_tags, struct yml_node const *name_of_tags, char const *dwl_info_filename) +{ + struct private *private = calloc(1, sizeof(struct private)); + private->label = label; + private->number_of_tags = number_of_tags; + private->dwl_info_filename = strdup(dwl_info_filename); + + struct yml_list_iter list = {0}; + if (name_of_tags) + list = yml_list_iter(name_of_tags); + + for (int i = 1; i <= number_of_tags; i++) { + struct dwl_tag *dwl_tag = calloc(1, sizeof(struct dwl_tag)); + dwl_tag->id = i; + if (list.node) { + dwl_tag->name = strdup(yml_value_as_string(list.node)); + yml_list_next(&list); + } else if (asprintf(&dwl_tag->name, "%d", i) < 0) { + LOG_ERRNO("asprintf"); + } + tll_push_back(private->tags, dwl_tag); + } + + 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"); + struct yml_node const *number_of_tags = yml_get_value(node, "number-of-tags"); + struct yml_node const *name_of_tags = yml_get_value(node, "name-of-tags"); + struct yml_node const *dwl_info_filename = yml_get_value(node, "dwl-info-filename"); + + return dwl_new(conf_to_particle(content, inherited), yml_value_as_int(number_of_tags), name_of_tags, + yml_value_as_string(dwl_info_filename)); +} + +static bool +verify_names(keychain_t *keychain, const struct yml_node *node) +{ + if (!yml_is_list(node)) { + LOG_ERR("%s: %s is not a list", conf_err_prefix(keychain, node), yml_value_as_string(node)); + return false; + } + return conf_verify_list(keychain, node, &conf_verify_string); +} + +static bool +verify_conf(keychain_t *keychain, struct yml_node const *node) +{ + + static struct attr_info const attrs[] = { + {"number-of-tags", true, &conf_verify_unsigned}, + {"name-of-tags", false, &verify_names}, + {"dwl-info-filename", true, &conf_verify_string}, + MODULE_COMMON_ATTRS, + }; + + if (!conf_verify_dict(keychain, node, attrs)) + return false; + + /* No need to check whether is `number_of_tags` is a int + * because `conf_verify_unsigned` already did it */ + struct yml_node const *ntags_key = yml_get_key(node, "number-of-tags"); + struct yml_node const *value = yml_get_value(node, "number-of-tags"); + int number_of_tags = yml_value_as_int(value); + if (number_of_tags == 0) { + LOG_ERR("%s: %s must not be 0", conf_err_prefix(keychain, ntags_key), yml_value_as_string(ntags_key)); + return false; + } + + struct yml_node const *key = yml_get_key(node, "name-of-tags"); + value = yml_get_value(node, "name-of-tags"); + if (value && yml_list_length(value) != number_of_tags) { + LOG_ERR("%s: %s must have the same number of elements that %s", conf_err_prefix(keychain, key), + yml_value_as_string(key), yml_value_as_string(ntags_key)); + return false; + } + + /* No need to check whether is `dwl_info_filename` is a string + * because `conf_verify_string` already did it */ + key = yml_get_key(node, "dwl-info-filename"); + value = yml_get_value(node, "dwl-info-filename"); + if (strlen(yml_value_as_string(value)) == 0) { + LOG_ERR("%s: %s must not be empty", conf_err_prefix(keychain, key), yml_value_as_string(key)); + return false; + } + + return true; +} + +struct module_iface const module_dwl_iface = { + .verify_conf = &verify_conf, + .from_conf = &from_conf, +}; + +#if defined(CORE_PLUGINS_AS_SHARED_LIBRARIES) +extern struct module_iface const iface __attribute__((weak, alias("module_dwl_iface"))); +#endif diff --git a/modules/foreign-toplevel.c b/modules/foreign-toplevel.c new file mode 100644 index 0000000..ccd6d5b --- /dev/null +++ b/modules/foreign-toplevel.c @@ -0,0 +1,666 @@ +#include +#include +#include +#include + +#include + +#include +#include + +#define LOG_MODULE "foreign-toplevel" +#define LOG_ENABLE_DBG 0 +#include "../log.h" +#include "../particles/dynlist.h" +#include "../plugin.h" + +#include "wlr-foreign-toplevel-management-unstable-v1.h" +#include "xdg-output-unstable-v1.h" + +#define min(x, y) ((x) < (y) ? (x) : (y)) + +static const int required_manager_interface_version = 2; + +struct output { + struct module *mod; + + uint32_t wl_name; + struct wl_output *wl_output; + struct zxdg_output_v1 *xdg_output; + + char *name; +}; + +struct toplevel { + struct module *mod; + struct zwlr_foreign_toplevel_handle_v1 *handle; + + char *app_id; + char *title; + + bool maximized; + bool minimized; + bool activated; + bool fullscreen; + + tll(const struct output *) outputs; +}; + +struct private +{ + struct particle *template; + uint32_t manager_wl_name; + struct zwlr_foreign_toplevel_manager_v1 *manager; + struct zxdg_output_manager_v1 *xdg_output_manager; + + bool all_monitors; + tll(struct toplevel) toplevels; + tll(struct output) outputs; +}; + +static void +output_free(struct output *output) +{ + free(output->name); + if (output->xdg_output != NULL) + zxdg_output_v1_destroy(output->xdg_output); + if (output->wl_output != NULL) + wl_output_release(output->wl_output); +} + +static void +toplevel_free(struct toplevel *top) +{ + if (top->handle != NULL) + zwlr_foreign_toplevel_handle_v1_destroy(top->handle); + + free(top->app_id); + free(top->title); + tll_free(top->outputs); +} + +static void +destroy(struct module *mod) +{ + struct private *m = mod->private; + m->template->destroy(m->template); + + assert(tll_length(m->toplevels) == 0); + assert(tll_length(m->outputs) == 0); + + free(m); + module_default_destroy(mod); +} + +static const char * +description(const struct module *mod) +{ + return "toplevel"; +} + +static struct exposable * +content(struct module *mod) +{ + const struct private *m = mod->private; + + mtx_lock(&mod->lock); + + const size_t toplevel_count = tll_length(m->toplevels); + size_t show_count = 0; + struct exposable *toplevels[toplevel_count]; + + const char *current_output = mod->bar->output_name(mod->bar); + + tll_foreach(m->toplevels, it) + { + const struct toplevel *top = &it->item; + + bool show = false; + + if (m->all_monitors) + show = true; + else if (current_output != NULL) { + tll_foreach(top->outputs, it2) + { + const struct output *output = it2->item; + if (output->name != NULL && strcmp(output->name, current_output) == 0) { + show = true; + break; + } + } + } + + if (!show) + continue; + + struct tag_set tags = { + .tags = (struct tag *[]){ + tag_new_string(mod, "app-id", it->item.app_id), + tag_new_string(mod, "title", it->item.title), + tag_new_bool(mod, "maximized", it->item.maximized), + tag_new_bool(mod, "minimized", it->item.minimized), + tag_new_bool(mod, "activated", it->item.activated), + tag_new_bool(mod, "fullscreen", it->item.fullscreen), + }, + .count = 6, + }; + + toplevels[show_count++] = m->template->instantiate(m->template, &tags); + tag_set_destroy(&tags); + } + + mtx_unlock(&mod->lock); + return dynlist_exposable_new(toplevels, show_count, 0, 0); +} + +static bool +verify_iface_version(const char *iface, uint32_t version, uint32_t wanted) +{ + if (version >= wanted) + return true; + + LOG_ERR("%s: need interface version %u, but compositor only implements %u", iface, wanted, version); + + return false; +} + +static void +xdg_output_handle_logical_position(void *data, struct zxdg_output_v1 *xdg_output, int32_t x, int32_t y) +{ +} + +static void +xdg_output_handle_logical_size(void *data, struct zxdg_output_v1 *xdg_output, int32_t width, int32_t height) +{ +} + +static void +xdg_output_handle_done(void *data, struct zxdg_output_v1 *xdg_output) +{ +} + +static void +xdg_output_handle_name(void *data, struct zxdg_output_v1 *xdg_output, const char *name) +{ + struct output *output = data; + struct module *mod = output->mod; + + mtx_lock(&mod->lock); + { + free(output->name); + output->name = name != NULL ? strdup(name) : NULL; + } + mtx_unlock(&mod->lock); +} + +static void +xdg_output_handle_description(void *data, struct zxdg_output_v1 *xdg_output, const char *description) +{ +} + +static struct zxdg_output_v1_listener xdg_output_listener = { + .logical_position = xdg_output_handle_logical_position, + .logical_size = xdg_output_handle_logical_size, + .done = xdg_output_handle_done, + .name = xdg_output_handle_name, + .description = xdg_output_handle_description, +}; + +static void +title(void *data, struct zwlr_foreign_toplevel_handle_v1 *handle, const char *title) +{ + struct toplevel *top = data; + + mtx_lock(&top->mod->lock); + { + free(top->title); + top->title = title != NULL ? strdup(title) : NULL; + } + mtx_unlock(&top->mod->lock); +} + +static void +app_id(void *data, struct zwlr_foreign_toplevel_handle_v1 *handle, const char *app_id) +{ + struct toplevel *top = data; + + mtx_lock(&top->mod->lock); + { + free(top->app_id); + top->app_id = app_id != NULL ? strdup(app_id) : NULL; + } + mtx_unlock(&top->mod->lock); +} + +static void +output_enter(void *data, struct zwlr_foreign_toplevel_handle_v1 *handle, struct wl_output *wl_output) +{ + struct toplevel *top = data; + struct module *mod = top->mod; + struct private *m = mod->private; + + mtx_lock(&mod->lock); + + const struct output *output = NULL; + tll_foreach(m->outputs, it) + { + if (it->item.wl_output == wl_output) { + output = &it->item; + break; + } + } + + if (output == NULL) { + LOG_ERR("output-enter event on untracked output"); + goto out; + } + + tll_foreach(top->outputs, it) + { + if (it->item == output) { + LOG_ERR("output-enter event on output we're already on"); + goto out; + } + } + + LOG_DBG("mapped: %s:%s on %s", top->app_id, top->title, output->name); + tll_push_back(top->outputs, output); + +out: + mtx_unlock(&mod->lock); +} + +static void +output_leave(void *data, struct zwlr_foreign_toplevel_handle_v1 *handle, struct wl_output *wl_output) +{ + struct toplevel *top = data; + struct module *mod = top->mod; + struct private *m = mod->private; + + mtx_lock(&mod->lock); + + const struct output *output = NULL; + tll_foreach(m->outputs, it) + { + if (it->item.wl_output == wl_output) { + output = &it->item; + break; + } + } + + if (output == NULL) { + LOG_ERR("output-leave event on untracked output"); + goto out; + } + + bool output_removed = false; + tll_foreach(top->outputs, it) + { + if (it->item == output) { + LOG_DBG("unmapped: %s:%s from %s", top->app_id, top->title, output->name); + tll_remove(top->outputs, it); + output_removed = true; + break; + } + } + + if (!output_removed) { + LOG_ERR("output-leave event on an output we're not on"); + goto out; + } + +out: + mtx_unlock(&mod->lock); +} + +static void +state(void *data, struct zwlr_foreign_toplevel_handle_v1 *handle, struct wl_array *states) +{ + struct toplevel *top = data; + + bool maximized = false; + bool minimized = false; + bool activated = false; + bool fullscreen = false; + + enum zwlr_foreign_toplevel_handle_v1_state *state; + wl_array_for_each(state, states) + { + switch (*state) { + case ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_STATE_MAXIMIZED: + maximized = true; + break; + case ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_STATE_MINIMIZED: + minimized = true; + break; + case ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_STATE_ACTIVATED: + activated = true; + break; + case ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_STATE_FULLSCREEN: + fullscreen = true; + break; + } + } + + mtx_lock(&top->mod->lock); + { + top->maximized = maximized; + top->minimized = minimized; + top->activated = activated; + top->fullscreen = fullscreen; + } + mtx_unlock(&top->mod->lock); +} + +static void +done(void *data, struct zwlr_foreign_toplevel_handle_v1 *handle) +{ + struct toplevel *top = data; + const struct bar *bar = top->mod->bar; + bar->refresh(bar); +} + +static void +closed(void *data, struct zwlr_foreign_toplevel_handle_v1 *handle) +{ + struct toplevel *top = data; + struct module *mod = top->mod; + struct private *m = mod->private; + + mtx_lock(&mod->lock); + tll_foreach(m->toplevels, it) + { + if (it->item.handle == handle) { + toplevel_free(top); + tll_remove(m->toplevels, it); + break; + } + } + mtx_unlock(&mod->lock); + + const struct bar *bar = mod->bar; + bar->refresh(bar); +} + +static void +parent(void *data, struct zwlr_foreign_toplevel_handle_v1 *handle, struct zwlr_foreign_toplevel_handle_v1 *parent) +{ +} + +static const struct zwlr_foreign_toplevel_handle_v1_listener toplevel_listener = { + .title = &title, + .app_id = &app_id, + .output_enter = &output_enter, + .output_leave = &output_leave, + .state = &state, + .done = &done, + .closed = &closed, + .parent = &parent, +}; + +static void +toplevel(void *data, struct zwlr_foreign_toplevel_manager_v1 *manager, struct zwlr_foreign_toplevel_handle_v1 *handle) +{ + struct module *mod = data; + struct private *m = mod->private; + + struct toplevel toplevel = { + .mod = mod, + .handle = handle, + }; + + mtx_lock(&mod->lock); + { + tll_push_back(m->toplevels, toplevel); + + zwlr_foreign_toplevel_handle_v1_add_listener(handle, &toplevel_listener, &tll_back(m->toplevels)); + } + mtx_unlock(&mod->lock); +} + +static void +finished(void *data, struct zwlr_foreign_toplevel_manager_v1 *manager) +{ + struct module *mod = data; + struct private *m = mod->private; + + assert(m->manager == manager); + zwlr_foreign_toplevel_manager_v1_destroy(m->manager); + m->manager = NULL; +} + +static const struct zwlr_foreign_toplevel_manager_v1_listener manager_listener = { + .toplevel = &toplevel, + .finished = &finished, +}; + +static void +output_xdg_output(struct output *output) +{ + struct private *m = output->mod->private; + + if (m->xdg_output_manager == NULL) + return; + if (output->xdg_output != NULL) + return; + + output->xdg_output = zxdg_output_manager_v1_get_xdg_output(m->xdg_output_manager, output->wl_output); + zxdg_output_v1_add_listener(output->xdg_output, &xdg_output_listener, output); +} + +static void +handle_global(void *data, struct wl_registry *registry, uint32_t name, const char *interface, uint32_t version) +{ + struct module *mod = data; + struct private *m = mod->private; + + if (strcmp(interface, zwlr_foreign_toplevel_manager_v1_interface.name) == 0) { + if (!verify_iface_version(interface, version, required_manager_interface_version)) + return; + + m->manager_wl_name = name; + } + + else if (strcmp(interface, wl_output_interface.name) == 0) { + const uint32_t required = 3; + if (!verify_iface_version(interface, version, required)) + return; + + struct output output = { + .mod = mod, + .wl_name = name, + .wl_output = wl_registry_bind(registry, name, &wl_output_interface, required), + }; + + mtx_lock(&mod->lock); + tll_push_back(m->outputs, output); + output_xdg_output(&tll_back(m->outputs)); + mtx_unlock(&mod->lock); + } + + else if (strcmp(interface, zxdg_output_manager_v1_interface.name) == 0) { + const uint32_t required = 2; + if (!verify_iface_version(interface, version, required)) + return; + + m->xdg_output_manager = wl_registry_bind(registry, name, &zxdg_output_manager_v1_interface, required); + + mtx_lock(&mod->lock); + tll_foreach(m->outputs, it) output_xdg_output(&it->item); + mtx_unlock(&mod->lock); + } +} + +static void +handle_global_remove(void *data, struct wl_registry *registry, uint32_t name) +{ + struct module *mod = data; + struct private *m = mod->private; + + mtx_lock(&mod->lock); + + tll_foreach(m->outputs, it) + { + const struct output *output = &it->item; + if (output->wl_name == name) { + + /* Loop all toplevels */ + tll_foreach(m->toplevels, it2) + { + + /* And remove this output from their list of tracked + * outputs */ + tll_foreach(it2->item.outputs, it3) + { + if (it3->item == output) { + tll_remove(it2->item.outputs, it3); + break; + } + } + } + + tll_remove(m->outputs, it); + goto out; + } + } + +out: + mtx_unlock(&mod->lock); +} + +static const struct wl_registry_listener registry_listener = { + .global = &handle_global, + .global_remove = &handle_global_remove, +}; + +static int +run(struct module *mod) +{ + struct private *m = mod->private; + int ret = -1; + + struct wl_display *display = NULL; + struct wl_registry *registry = NULL; + + if ((display = wl_display_connect(NULL)) == NULL) { + LOG_ERR("no Wayland compositor running"); + goto out; + } + + if ((registry = wl_display_get_registry(display)) == NULL + || wl_registry_add_listener(registry, ®istry_listener, mod) != 0) { + LOG_ERR("failed to get Wayland registry"); + goto out; + } + + wl_display_roundtrip(display); + + if (m->manager_wl_name == 0) { + LOG_ERR("compositor does not implement the foreign-toplevel-manager interface"); + goto out; + } + + m->manager = wl_registry_bind(registry, m->manager_wl_name, &zwlr_foreign_toplevel_manager_v1_interface, + required_manager_interface_version); + + zwlr_foreign_toplevel_manager_v1_add_listener(m->manager, &manager_listener, mod); + + while (true) { + wl_display_flush(display); + + struct pollfd fds[] = { + {.fd = mod->abort_fd, .events = POLLIN}, + {.fd = wl_display_get_fd(display), .events = POLLIN}, + }; + + int r = poll(fds, sizeof(fds) / sizeof(fds[0]), -1); + if (r < 0) { + if (errno == EINTR) + continue; + + LOG_ERRNO("failed to poll"); + break; + } + + if (fds[0].revents & (POLLIN | POLLHUP)) { + ret = 0; + break; + } + + if (fds[1].revents & POLLHUP) { + LOG_ERR("disconnected from the Wayland compositor"); + break; + } + + assert(fds[1].revents & POLLIN); + wl_display_dispatch(display); + } + +out: + tll_foreach(m->toplevels, it) + { + toplevel_free(&it->item); + tll_remove(m->toplevels, it); + } + + tll_foreach(m->outputs, it) + { + output_free(&it->item); + tll_remove(m->outputs, it); + } + + if (m->xdg_output_manager != NULL) + zxdg_output_manager_v1_destroy(m->xdg_output_manager); + if (m->manager != NULL) + zwlr_foreign_toplevel_manager_v1_destroy(m->manager); + if (registry != NULL) + wl_registry_destroy(registry); + if (display != NULL) + wl_display_disconnect(display); + return ret; +} + +static struct module * +ftop_new(struct particle *label, bool all_monitors) +{ + struct private *m = calloc(1, sizeof(*m)); + m->template = label; + m->all_monitors = all_monitors; + + struct module *mod = module_common_new(); + mod->private = m; + mod->run = &run; + mod->destroy = &destroy; + mod->content = &content; + mod->description = &description; + return mod; +} + +static struct module * +from_conf(const struct yml_node *node, struct conf_inherit inherited) +{ + const struct yml_node *c = yml_get_value(node, "content"); + const struct yml_node *all_monitors = yml_get_value(node, "all-monitors"); + + return ftop_new(conf_to_particle(c, inherited), all_monitors != NULL ? yml_value_as_bool(all_monitors) : false); +} + +static bool +verify_conf(keychain_t *chain, const struct yml_node *node) +{ + static const struct attr_info attrs[] = { + {"all-monitors", false, &conf_verify_bool}, + MODULE_COMMON_ATTRS, + }; + + return conf_verify_dict(chain, node, attrs); +} + +const struct module_iface module_foreign_toplevel_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_foreign_toplevel_iface"))); +#endif diff --git a/modules/i3-common.c b/modules/i3-common.c index 589bfb8..957a4d2 100644 --- a/modules/i3-common.c +++ b/modules/i3-common.c @@ -1,15 +1,15 @@ #include "i3-common.h" +#include #include #include #include -#include #include #if defined(ENABLE_X11) - #include - #include +#include +#include #endif #include @@ -19,7 +19,7 @@ #include "../log.h" #if defined(ENABLE_X11) - #include "../xcb.h" +#include "../xcb.h" #endif #include "i3-ipc.h" @@ -41,14 +41,11 @@ get_socket_address_x11(struct sockaddr_un *addr) xcb_atom_t atom = get_atom(conn, "I3_SOCKET_PATH"); assert(atom != XCB_ATOM_NONE); - xcb_get_property_cookie_t cookie - = xcb_get_property_unchecked( - conn, false, screen->root, atom, - XCB_GET_PROPERTY_TYPE_ANY, 0, sizeof(addr->sun_path)); + xcb_get_property_cookie_t cookie = xcb_get_property_unchecked(conn, false, screen->root, atom, + XCB_GET_PROPERTY_TYPE_ANY, 0, sizeof(addr->sun_path)); xcb_generic_error_t *err = NULL; - xcb_get_property_reply_t *reply = - xcb_get_property_reply(conn, cookie, &err); + xcb_get_property_reply_t *reply = xcb_get_property_reply(conn, cookie, &err); bool ret = false; if (err != NULL) { @@ -102,11 +99,7 @@ bool i3_send_pkg(int sock, int cmd, char *data) { const size_t size = data != NULL ? strlen(data) : 0; - const i3_ipc_header_t hdr = { - .magic = I3_IPC_MAGIC, - .size = size, - .type = cmd - }; + const i3_ipc_header_t hdr = {.magic = I3_IPC_MAGIC, .size = size, .type = cmd}; if (write(sock, &hdr, sizeof(hdr)) != (ssize_t)sizeof(hdr)) return false; @@ -120,8 +113,7 @@ i3_send_pkg(int sock, int cmd, char *data) } bool -i3_receive_loop(int abort_fd, int sock, - const struct i3_ipc_callbacks *cbs, void *data) +i3_receive_loop(int abort_fd, int sock, const struct i3_ipc_callbacks *cbs, void *data) { /* Initial reply typically requires a couple of KB. But we often * need more later. For example, switching workspaces can result @@ -133,10 +125,7 @@ i3_receive_loop(int abort_fd, int sock, bool err = false; while (!err) { - struct pollfd fds[] = { - {.fd = abort_fd, .events = POLLIN}, - {.fd = sock, .events = POLLIN} - }; + struct pollfd fds[] = {{.fd = abort_fd, .events = POLLIN}, {.fd = sock, .events = POLLIN}}; int res = poll(fds, 2, -1); if (res <= 0) { @@ -159,13 +148,11 @@ i3_receive_loop(int abort_fd, int sock, /* Grow receive buffer, if necessary */ if (buf_idx == reply_buf_size) { - LOG_DBG("growing reply buffer: %zu -> %zu", - reply_buf_size, reply_buf_size * 2); + LOG_DBG("growing reply buffer: %zu -> %zu", reply_buf_size, reply_buf_size * 2); char *new_buf = realloc(buf, reply_buf_size * 2); if (new_buf == NULL) { - LOG_ERR("failed to grow reply buffer from %zu to %zu bytes", - reply_buf_size, reply_buf_size * 2); + LOG_ERR("failed to grow reply buffer from %zu to %zu bytes", reply_buf_size, reply_buf_size * 2); err = true; break; } @@ -188,10 +175,8 @@ i3_receive_loop(int abort_fd, int sock, while (!err && buf_idx >= sizeof(i3_ipc_header_t)) { const i3_ipc_header_t *hdr = (const i3_ipc_header_t *)buf; if (strncmp(hdr->magic, I3_IPC_MAGIC, sizeof(hdr->magic)) != 0) { - LOG_ERR( - "i3 IPC header magic mismatch: expected \"%.*s\", got \"%.*s\"", - (int)sizeof(hdr->magic), I3_IPC_MAGIC, - (int)sizeof(hdr->magic), hdr->magic); + LOG_ERR("i3 IPC header magic mismatch: expected \"%.*s\", got \"%.*s\"", (int)sizeof(hdr->magic), + I3_IPC_MAGIC, (int)sizeof(hdr->magic), hdr->magic); err = true; break; @@ -210,10 +195,10 @@ i3_receive_loop(int abort_fd, int sock, char json_str[hdr->size + 1]; memcpy(json_str, &buf[sizeof(*hdr)], hdr->size); json_str[hdr->size] = '\0'; - //printf("raw: %s\n", json_str); + // printf("raw: %s\n", json_str); LOG_DBG("raw: %s\n", json_str); - //json_tokener *tokener = json_tokener_new(); + // json_tokener *tokener = json_tokener_new(); struct json_object *json = json_tokener_parse(json_str); if (json == NULL) { LOG_ERR("failed to parse json"); @@ -262,13 +247,13 @@ i3_receive_loop(int abort_fd, int sock, break; #endif /* Sway extensions */ - case 100: /* IPC_GET_INPUTS */ + case 100: /* IPC_GET_INPUTS */ pkt_handler = cbs->reply_inputs; break; - /* - * Events - */ + /* + * Events + */ case I3_IPC_EVENT_WORKSPACE: pkt_handler = cbs->event_workspace; @@ -295,7 +280,7 @@ i3_receive_loop(int abort_fd, int sock, pkt_handler = cbs->event_tick; break; - /* Sway extensions */ + /* Sway extensions */ #define SWAY_IPC_EVENT_INPUT ((1u << 31) | 21) case SWAY_IPC_EVENT_INPUT: pkt_handler = cbs->event_input; @@ -309,7 +294,7 @@ i3_receive_loop(int abort_fd, int sock, } if (pkt_handler != NULL) - err = !pkt_handler(hdr->type, json, data); + err = !pkt_handler(sock, hdr->type, json, data); else LOG_DBG("no handler for reply/event %d; ignoring", hdr->type); diff --git a/modules/i3-common.h b/modules/i3-common.h index e3d94d1..6ba6721 100644 --- a/modules/i3-common.h +++ b/modules/i3-common.h @@ -2,8 +2,8 @@ #include -#include #include +#include #include #include @@ -11,7 +11,7 @@ bool i3_get_socket_address(struct sockaddr_un *addr); bool i3_send_pkg(int sock, int cmd, char *data); -typedef bool (*i3_ipc_callback_t)(int type, const struct json_object *json, void *data); +typedef bool (*i3_ipc_callback_t)(int sock, int type, const struct json_object *json, void *data); struct i3_ipc_callbacks { void (*burst_done)(void *data); @@ -43,6 +43,4 @@ struct i3_ipc_callbacks { i3_ipc_callback_t event_input; }; -bool i3_receive_loop( - int abort_fd, int sock, - const struct i3_ipc_callbacks *callbacks, void *data); +bool i3_receive_loop(int abort_fd, int sock, const struct i3_ipc_callbacks *callbacks, void *data); diff --git a/modules/i3.c b/modules/i3.c index 0f998de..cbdafaf 100644 --- a/modules/i3.c +++ b/modules/i3.c @@ -1,23 +1,27 @@ +#include #include #include -#include -#include #include +#include -#include #include +#include + +#include #define LOG_MODULE "i3" #define LOG_ENABLE_DBG 0 -#include "../log.h" #include "../bar/bar.h" -#include "../config.h" #include "../config-verify.h" +#include "../config.h" +#include "../log.h" #include "../particles/dynlist.h" #include "../plugin.h" -#include "i3-ipc.h" #include "i3-common.h" +#include "i3-ipc.h" + +enum sort_mode { SORT_NONE, SORT_NATIVE, SORT_ASCENDING, SORT_DESCENDING }; struct ws_content { char *name; @@ -25,11 +29,16 @@ struct ws_content { }; struct workspace { + int id; char *name; + int name_as_int; /* -1 if name is not a decimal number */ + bool persistent; + char *output; bool visible; bool focused; bool urgent; + bool empty; struct { unsigned id; @@ -39,47 +48,98 @@ struct workspace { } window; }; -struct private { +struct private +{ int left_spacing; int right_spacing; bool dirty; + char *mode; + struct { struct ws_content *v; size_t count; } ws_content; - struct { - struct workspace *v; - size_t count; - } workspaces; + bool strip_workspace_numbers; + enum sort_mode sort_mode; + tll(struct workspace) workspaces; + + size_t persistent_count; + char **persistent_workspaces; }; +static int +workspace_name_as_int(const char *name) +{ + int name_as_int = 0; + + /* First check for N:name pattern (set $ws1 “1:foobar”) */ + const char *colon = strchr(name, ':'); + if (colon != NULL) { + for (const char *p = name; p < colon; p++) { + if (!(*p >= '0' && *p < '9')) + return -1; + + name_as_int *= 10; + name_as_int += *p - '0'; + } + + return name_as_int; + } + + /* Then, if the name is a number *only* (set $ws1 1) */ + for (const char *p = name; *p != '\0'; p++) { + if (!(*p >= '0' && *p <= '9')) + return -1; + + name_as_int *= 10; + name_as_int += *p - '0'; + } + + return name_as_int; +} + static bool workspace_from_json(const struct json_object *json, struct workspace *ws) { /* Always present */ - struct json_object *name, *output; - if (!json_object_object_get_ex(json, "name", &name) || - !json_object_object_get_ex(json, "output", &output)) - { - LOG_ERR("workspace reply/event without 'name' and/or 'output' property"); + struct json_object *id, *name, *output; + if (!json_object_object_get_ex(json, "id", &id) || !json_object_object_get_ex(json, "name", &name) + || !json_object_object_get_ex(json, "output", &output)) { + LOG_ERR("workspace reply/event without 'name' and/or 'output' " + "properties"); return false; } + /* Sway only */ + struct json_object *focus = NULL; + json_object_object_get_ex(json, "focus", &focus); + /* Optional */ struct json_object *visible = NULL, *focused = NULL, *urgent = NULL; json_object_object_get_ex(json, "visible", &visible); json_object_object_get_ex(json, "focused", &focused); json_object_object_get_ex(json, "urgent", &urgent); - *ws = (struct workspace) { - .name = strdup(json_object_get_string(name)), + const char *name_as_string = json_object_get_string(name); + + const size_t node_count = focus != NULL ? json_object_array_length(focus) : 0; + + const bool is_empty = node_count == 0; + int name_as_int = workspace_name_as_int(name_as_string); + + *ws = (struct workspace){ + .id = json_object_get_int(id), + .name = strdup(name_as_string), + .name_as_int = name_as_int, + .persistent = false, .output = strdup(json_object_get_string(output)), .visible = json_object_get_boolean(visible), .focused = json_object_get_boolean(focused), .urgent = json_object_get_boolean(urgent), + .empty = is_empty && json_object_get_boolean(focused), .window = {.title = NULL, .pid = -1}, }; @@ -87,70 +147,151 @@ workspace_from_json(const struct json_object *json, struct workspace *ws) } static void -workspace_free(struct workspace ws) +workspace_free_persistent(struct workspace *ws) { - free(ws.name); - free(ws.output); - free(ws.window.title); - free(ws.window.application); + free(ws->output); + ws->output = NULL; + free(ws->window.title); + ws->window.title = NULL; + free(ws->window.application); + ws->window.application = NULL; + ws->id = -1; } static void -workspaces_free(struct private *m) +workspace_free(struct workspace *ws) { - for (size_t i = 0; i < m->workspaces.count; i++) - workspace_free(m->workspaces.v[i]); + workspace_free_persistent(ws); + free(ws->name); + ws->name = NULL; +} - free(m->workspaces.v); - m->workspaces.v = NULL; - m->workspaces.count = 0; +static void +workspaces_free(struct private *m, bool free_persistent) +{ + tll_foreach(m->workspaces, it) + { + if (free_persistent || !it->item.persistent) { + workspace_free(&it->item); + tll_remove(m->workspaces, it); + } + } } static void workspace_add(struct private *m, struct workspace ws) { - size_t new_count = m->workspaces.count + 1; - struct workspace *new_v = realloc(m->workspaces.v, new_count * sizeof(new_v[0])); + switch (m->sort_mode) { + case SORT_NONE: + tll_push_back(m->workspaces, ws); + return; - m->workspaces.count = new_count; - m->workspaces.v = new_v; - m->workspaces.v[new_count - 1] = ws; + case SORT_NATIVE: + if (ws.name_as_int >= 0) { + tll_foreach(m->workspaces, it) + { + if (it->item.name_as_int < 0) + continue; + if (it->item.name_as_int > ws.name_as_int) { + tll_insert_before(m->workspaces, it, ws); + return; + } + } + }; + + tll_push_back(m->workspaces, ws); + return; + + case SORT_ASCENDING: + if (ws.name_as_int >= 0) { + tll_foreach(m->workspaces, it) + { + if (it->item.name_as_int < 0) + continue; + if (it->item.name_as_int > ws.name_as_int) { + tll_insert_before(m->workspaces, it, ws); + return; + } + } + } else { + tll_foreach(m->workspaces, it) + { + if (strcoll(it->item.name, ws.name) > 0 || it->item.name_as_int >= 0) { + tll_insert_before(m->workspaces, it, ws); + return; + } + } + } + tll_push_back(m->workspaces, ws); + return; + + case SORT_DESCENDING: + if (ws.name_as_int >= 0) { + tll_foreach(m->workspaces, it) + { + if (it->item.name_as_int < ws.name_as_int) { + tll_insert_before(m->workspaces, it, ws); + return; + } + } + } else { + tll_foreach(m->workspaces, it) + { + if (it->item.name_as_int >= 0) + continue; + if (strcoll(it->item.name, ws.name) < 0) { + tll_insert_before(m->workspaces, it, ws); + return; + } + } + } + tll_push_back(m->workspaces, ws); + return; + } } static void -workspace_del(struct private *m, const char *name) +workspace_del(struct private *m, int id) { - struct workspace *workspaces = m->workspaces.v; + tll_foreach(m->workspaces, it) + { + struct workspace *ws = &it->item; - for (size_t i = 0; i < m->workspaces.count; i++) { - const struct workspace *ws = &workspaces[i]; - - if (strcmp(ws->name, name) != 0) + if (ws->id != id) continue; - workspace_free(*ws); - - memmove( - &workspaces[i], - &workspaces[i + 1], - (m->workspaces.count - i - 1) * sizeof(workspaces[0])); - m->workspaces.count--; + workspace_free(ws); + tll_remove(m->workspaces, it); break; } } static struct workspace * -workspace_lookup(struct private *m, const char *name) +workspace_lookup(struct private *m, int id) { - for (size_t i = 0; i < m->workspaces.count; i++) - if (strcmp(m->workspaces.v[i].name, name) == 0) - return &m->workspaces.v[i]; + tll_foreach(m->workspaces, it) + { + struct workspace *ws = &it->item; + if (ws->id == id) + return ws; + } return NULL; } +static struct workspace * +workspace_lookup_by_name(struct private *m, const char *name) +{ + tll_foreach(m->workspaces, it) + { + struct workspace *ws = &it->item; + if (strcmp(ws->name, name) == 0) + return ws; + } + return NULL; +} static bool -handle_get_version_reply(int type, const struct json_object *json, void *_m) +handle_get_version_reply(int sock, int type, const struct json_object *json, void *_m) { struct json_object *version; if (!json_object_object_get_ex(json, "human_readable", &version)) { @@ -163,7 +304,7 @@ handle_get_version_reply(int type, const struct json_object *json, void *_m) } static bool -handle_subscribe_reply(int type, const struct json_object *json, void *_m) +handle_subscribe_reply(int sock, int type, const struct json_object *json, void *_m) { struct json_object *success; if (!json_object_object_get_ex(json, "success", &success)) { @@ -180,37 +321,88 @@ handle_subscribe_reply(int type, const struct json_object *json, void *_m) } static bool -handle_get_workspaces_reply(int type, const struct json_object *json, void *_mod) +workspace_update_or_add(struct private *m, const struct json_object *ws_json) +{ + struct json_object *_id; + if (!json_object_object_get_ex(ws_json, "id", &_id)) + return false; + + const int id = json_object_get_int(_id); + struct workspace *already_exists = workspace_lookup(m, id); + + if (already_exists == NULL) { + /* + * No workspace with this ID. + * + * Try looking it up again, but this time using the name. If + * we get a match, check if it’s an empty, persistent + * workspace, and if so, use it. + * + * This is necessary, since empty, persistent workspaces don’t + * exist in the i3/Sway server, and thus we don’t _have_ an + * ID. + */ + struct json_object *_name; + if (json_object_object_get_ex(ws_json, "name", &_name)) { + const char *name = json_object_get_string(_name); + if (name != NULL) { + struct workspace *maybe_persistent = workspace_lookup_by_name(m, name); + + if (maybe_persistent != NULL && maybe_persistent->persistent && maybe_persistent->id < 0) { + already_exists = maybe_persistent; + } + } + } + } + + if (already_exists != NULL) { + bool persistent = already_exists->persistent; + assert(persistent); + + workspace_free(already_exists); + if (!workspace_from_json(ws_json, already_exists)) + return false; + already_exists->persistent = persistent; + } else { + struct workspace ws; + if (!workspace_from_json(ws_json, &ws)) + return false; + + workspace_add(m, ws); + } + + return true; +} + +static bool +handle_get_workspaces_reply(int sock, int type, const struct json_object *json, void *_mod) { struct module *mod = _mod; struct private *m = mod->private; mtx_lock(&mod->lock); - workspaces_free(m); + workspaces_free(m, false); m->dirty = true; size_t count = json_object_array_length(json); - m->workspaces.count = count; - m->workspaces.v = calloc(count, sizeof(m->workspaces.v[0])); for (size_t i = 0; i < count; i++) { - if (!workspace_from_json( - json_object_array_get_idx(json, i), &m->workspaces.v[i])) { - workspaces_free(m); - mtx_unlock(&mod->lock); - return false; - } - - LOG_DBG("#%zu: %s", i, m->workspaces.v[i].name); + if (!workspace_update_or_add(m, json_object_array_get_idx(json, i))) + goto err; } mtx_unlock(&mod->lock); return true; + +err: + workspaces_free(m, false); + mtx_unlock(&mod->lock); + return false; } static bool -handle_workspace_event(int type, const struct json_object *json, void *_mod) +handle_workspace_event(int sock, int type, const struct json_object *json, void *_mod) { struct module *mod = _mod; struct private *m = mod->private; @@ -226,67 +418,59 @@ handle_workspace_event(int type, const struct json_object *json, void *_mod) bool is_init = strcmp(change_str, "init") == 0; bool is_empty = strcmp(change_str, "empty") == 0; bool is_focused = strcmp(change_str, "focus") == 0; + bool is_rename = strcmp(change_str, "rename") == 0; + bool is_move = strcmp(change_str, "move") == 0; bool is_urgent = strcmp(change_str, "urgent") == 0; bool is_reload = strcmp(change_str, "reload") == 0; - if (is_reload) { - LOG_WARN("unimplemented: 'reload' event"); - return true; - } - - struct json_object *current, *_current_name; - if (!json_object_object_get_ex(json, "current", ¤t) || - !json_object_object_get_ex(current, "name", &_current_name)) - { - LOG_ERR("workspace event without 'current' and/or 'name' properties"); + struct json_object *current, *_current_id; + if ((!json_object_object_get_ex(json, "current", ¤t) + || !json_object_object_get_ex(current, "id", &_current_id)) + && !is_reload) { + LOG_ERR("workspace event without 'current' and/or 'id' properties"); return false; } - const char *current_name = json_object_get_string(_current_name); + int current_id = json_object_get_int(_current_id); mtx_lock(&mod->lock); if (is_init) { - struct workspace *already_exists = workspace_lookup(m, current_name); - if (already_exists != NULL) { - LOG_WARN("workspace 'init' event for already existing workspace: %s", current_name); - workspace_free(*already_exists); - if (!workspace_from_json(current, already_exists)) - goto err; - } else { - struct workspace ws; - if (!workspace_from_json(current, &ws)) - goto err; - - workspace_add(m, ws); - } + if (!workspace_update_or_add(m, current)) + goto err; } else if (is_empty) { - assert(workspace_lookup(m, current_name) != NULL); - workspace_del(m, current_name); + struct workspace *ws = workspace_lookup(m, current_id); + assert(ws != NULL); + + if (!ws->persistent) + workspace_del(m, current_id); + else { + workspace_free_persistent(ws); + ws->empty = true; + } } else if (is_focused) { - struct json_object *old, *_old_name, *urgent; - if (!json_object_object_get_ex(json, "old", &old) || - !json_object_object_get_ex(old, "name", &_old_name) || - !json_object_object_get_ex(current, "urgent", &urgent)) - { + struct json_object *old, *_old_id, *urgent; + if (!json_object_object_get_ex(json, "old", &old) || !json_object_object_get_ex(old, "id", &_old_id) + || !json_object_object_get_ex(current, "urgent", &urgent)) { LOG_ERR("workspace 'focused' event without 'old', 'name' and/or 'urgent' property"); mtx_unlock(&mod->lock); return false; } - struct workspace *w = workspace_lookup(m, current_name); + struct workspace *w = workspace_lookup(m, current_id); assert(w != NULL); LOG_DBG("w: %s", w->name); /* Mark all workspaces on current's output invisible */ - for (size_t i = 0; i < m->workspaces.count; i++) { - struct workspace *ws = &m->workspaces.v[i]; - if (strcmp(ws->output, w->output) == 0) + tll_foreach(m->workspaces, it) + { + struct workspace *ws = &it->item; + if (ws->output != NULL && strcmp(ws->output, w->output) == 0) ws->visible = false; } @@ -295,12 +479,67 @@ handle_workspace_event(int type, const struct json_object *json, void *_mod) w->visible = true; /* Old workspace is no longer focused */ - const char *old_name = json_object_get_string(_old_name); - struct workspace *old_w = workspace_lookup(m, old_name); + int old_id = json_object_get_int(_old_id); + struct workspace *old_w = workspace_lookup(m, old_id); if (old_w != NULL) old_w->focused = false; } + else if (is_rename) { + struct workspace *w = workspace_lookup(m, current_id); + assert(w != NULL); + + struct json_object *_current_name; + if (!json_object_object_get_ex(current, "name", &_current_name)) { + LOG_ERR("workspace 'rename' event without 'name' property"); + mtx_unlock(&mod->lock); + return false; + } + + free(w->name); + w->name = strdup(json_object_get_string(_current_name)); + w->name_as_int = workspace_name_as_int(w->name); + + /* Re-add the workspace to ensure correct sorting */ + struct workspace ws = *w; + tll_foreach(m->workspaces, it) + { + if (it->item.id == current_id) { + tll_remove(m->workspaces, it); + break; + } + } + workspace_add(m, ws); + } + + else if (is_move) { + struct workspace *w = workspace_lookup(m, current_id); + + struct json_object *_current_output; + if (!json_object_object_get_ex(current, "output", &_current_output)) { + LOG_ERR("workspace 'move' event without 'output' property"); + mtx_unlock(&mod->lock); + return false; + } + const char *current_output_string = json_object_get_string(_current_output); + + /* Ignore fallback_output ("For when there's no connected outputs") */ + if (strcmp(current_output_string, "FALLBACK") != 0) { + + 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); + } + } + } + else if (is_urgent) { struct json_object *urgent; if (!json_object_object_get_ex(current, "urgent", &urgent)) { @@ -309,10 +548,20 @@ handle_workspace_event(int type, const struct json_object *json, void *_mod) return false; } - struct workspace *w = workspace_lookup(m, current_name); + struct workspace *w = workspace_lookup(m, current_id); w->urgent = json_object_get_boolean(urgent); } + else if (is_reload) { + /* Schedule full update to check if anything was changed + * during reload */ + i3_send_pkg(sock, I3_IPC_MESSAGE_TYPE_GET_WORKSPACES, NULL); + } + + else { + LOG_WARN("unimplemented workspace event '%s'", change_str); + } + m->dirty = true; mtx_unlock(&mod->lock); return true; @@ -323,7 +572,7 @@ err: } static bool -handle_window_event(int type, const struct json_object *json, void *_mod) +handle_window_event(int sock, int type, const struct json_object *json, void *_mod) { struct module *mod = _mod; struct private *m = mod->private; @@ -345,10 +594,11 @@ handle_window_event(int type, const struct json_object *json, void *_mod) mtx_lock(&mod->lock); struct workspace *ws = NULL; - size_t focused = 0; - for (size_t i = 0; i < m->workspaces.count; i++) { - if (m->workspaces.v[i].focused) { - ws = &m->workspaces.v[i]; + __attribute__((unused)) size_t focused = 0; + tll_foreach(m->workspaces, it) + { + if (it->item.focused) { + ws = &it->item; focused++; } } @@ -356,6 +606,20 @@ handle_window_event(int type, const struct json_object *json, void *_mod) assert(focused == 1); assert(ws != NULL); + struct json_object *container, *id, *name; + if (!json_object_object_get_ex(json, "container", &container) || !json_object_object_get_ex(container, "id", &id) + || !json_object_object_get_ex(container, "name", &name)) { + mtx_unlock(&mod->lock); + LOG_ERR("window event without 'container' with 'id' and 'name'"); + return false; + } + + if ((is_close || is_title) && ws->window.id != json_object_get_int(id)) { + /* Ignore close event and title changed event if it's not current window */ + mtx_unlock(&mod->lock); + return true; + } + if (is_close) { free(ws->window.title); free(ws->window.application); @@ -367,23 +631,6 @@ handle_window_event(int type, const struct json_object *json, void *_mod) m->dirty = true; mtx_unlock(&mod->lock); return true; - - } - - struct json_object *container, *id, *name; - if (!json_object_object_get_ex(json, "container", &container) || - !json_object_object_get_ex(container, "id", &id) || - !json_object_object_get_ex(container, "name", &name)) - { - mtx_unlock(&mod->lock); - LOG_ERR("window event without 'container' with 'id' and 'name'"); - return false; - } - - if (is_title && ws->window.id != json_object_get_int(id)) { - /* Ignore title changed event if it's not current window */ - mtx_unlock(&mod->lock); - return true; } free(ws->window.title); @@ -404,27 +651,24 @@ handle_window_event(int type, const struct json_object *json, void *_mod) struct json_object *app_id; struct json_object *pid; - if (json_object_object_get_ex(container, "app_id", &app_id) && - json_object_get_string(app_id) != NULL) - { + if (json_object_object_get_ex(container, "app_id", &app_id) && json_object_get_string(app_id) != NULL) { free(ws->window.application); ws->window.application = strdup(json_object_get_string(app_id)); LOG_DBG("application: \"%s\", via 'app_id'", ws->window.application); } /* If PID has changed, update application name from /proc//comm */ - else if (json_object_object_get_ex(container, "pid", &pid) && - ws->window.pid != json_object_get_int(pid)) - { + else if (json_object_object_get_ex(container, "pid", &pid) && ws->window.pid != json_object_get_int(pid)) { ws->window.pid = json_object_get_int(pid); 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); ws->window.application = NULL; + free(ws->window.application); + ws->window.application = NULL; ws->window.pid = -1; m->dirty = true; @@ -448,6 +692,30 @@ handle_window_event(int type, const struct json_object *json, void *_mod) return true; } +static bool +handle_mode_event(int sock, int type, const struct json_object *json, void *_mod) +{ + struct module *mod = _mod; + struct private *m = mod->private; + + struct json_object *change; + if (!json_object_object_get_ex(json, "change", &change)) { + LOG_ERR("mode event without 'change' property"); + return false; + } + + const char *current_mode = json_object_get_string(change); + + mtx_lock(&mod->lock); + { + free(m->mode); + m->mode = strdup(current_mode); + m->dirty = true; + } + mtx_unlock(&mod->lock); + return true; +} + static void burst_done(void *_mod) { @@ -467,7 +735,7 @@ run(struct module *mod) if (!i3_get_socket_address(&addr)) return 1; - int sock = socket(AF_UNIX, SOCK_STREAM, 0); + int sock = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0); if (sock == -1) { LOG_ERRNO("failed to create UNIX socket"); return 1; @@ -480,8 +748,29 @@ run(struct module *mod) return 1; } + struct private *m = mod->private; + for (size_t i = 0; i < m->persistent_count; i++) { + const char *name_as_string = m->persistent_workspaces[i]; + + int name_as_int = workspace_name_as_int(name_as_string); + if (m->strip_workspace_numbers) { + const char *colon = strchr(name_as_string, ':'); + if (colon != NULL) + name_as_string = colon++; + } + + struct workspace ws = { + .id = -1, + .name = strdup(name_as_string), + .name_as_int = name_as_int, + .persistent = true, + .empty = true, + }; + workspace_add(m, ws); + } + i3_send_pkg(sock, I3_IPC_MESSAGE_TYPE_GET_VERSION, NULL); - i3_send_pkg(sock, I3_IPC_MESSAGE_TYPE_SUBSCRIBE, "[\"workspace\", \"window\"]"); + i3_send_pkg(sock, I3_IPC_MESSAGE_TYPE_SUBSCRIBE, "[\"workspace\", \"window\", \"mode\"]"); i3_send_pkg(sock, I3_IPC_MESSAGE_TYPE_GET_WORKSPACES, NULL); static const struct i3_ipc_callbacks callbacks = { @@ -491,6 +780,7 @@ run(struct module *mod) .reply_workspaces = &handle_get_workspaces_reply, .event_workspace = &handle_workspace_event, .event_window = &handle_window_event, + .event_mode = &handle_mode_event, }; bool ret = i3_receive_loop(mod->abort_fd, sock, &callbacks, mod); @@ -510,8 +800,13 @@ destroy(struct module *mod) } free(m->ws_content.v); - workspaces_free(m); + workspaces_free(m, true); + for (size_t i = 0; i < m->persistent_count; i++) + free(m->persistent_workspaces[i]); + free(m->persistent_workspaces); + + free(m->mode); free(m); module_default_destroy(mod); } @@ -528,6 +823,12 @@ ws_content_for_name(struct private *m, const char *name) return NULL; } +static const char * +description(const struct module *mod) +{ + return "i3/sway"; +} + static struct exposable * content(struct module *mod) { @@ -536,39 +837,58 @@ content(struct module *mod) mtx_lock(&mod->lock); size_t particle_count = 0; - struct exposable *particles[m->workspaces.count + 1]; + struct exposable *particles[tll_length(m->workspaces) + 1]; struct exposable *current = NULL; - for (size_t i = 0; i < m->workspaces.count; i++) { - const struct workspace *ws = &m->workspaces.v[i]; + tll_foreach(m->workspaces, it) + { + struct workspace *ws = &it->item; const struct ws_content *template = NULL; /* Lookup content template for workspace. Fall back to default * template if this workspace doesn't have a specific * template */ + if (ws->name == NULL) { + LOG_ERR("%d %d", ws->name_as_int, ws->id); + } template = ws_content_for_name(m, ws->name); if (template == NULL) { LOG_DBG("no ws template for %s, using default template", ws->name); template = ws_content_for_name(m, ""); } - const char *state = - ws->urgent ? "urgent" : - ws->visible ? ws->focused ? "focused" : "unfocused" : - "invisible"; + const char *state = ws->urgent ? "urgent" : ws->visible ? ws->focused ? "focused" : "unfocused" : "invisible"; + + LOG_DBG("name=%s (name-as-int=%d): visible=%s, focused=%s, urgent=%s, empty=%s, state=%s, " + "application=%s, title=%s, mode=%s", + ws->name, ws->name_as_int, ws->visible ? "yes" : "no", ws->focused ? "yes" : "no", + ws->urgent ? "yes" : "no", ws->empty ? "yes" : "no", state, ws->window.application, ws->window.title, + m->mode); + + const char *name = ws->name; + + if (m->strip_workspace_numbers) { + const char *colon = strchr(name, ':'); + if (colon != NULL) + name = colon + 1; + } struct tag_set tags = { .tags = (struct tag *[]){ - tag_new_string(mod, "name", ws->name), + 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), + tag_new_bool(mod, "empty", ws->empty), tag_new_string(mod, "state", state), tag_new_string(mod, "application", ws->window.application), tag_new_string(mod, "title", ws->window.title), + + tag_new_string(mod, "mode", m->mode), }, - .count = 7, + .count = 10, }; if (ws->focused) { @@ -578,12 +898,9 @@ content(struct module *mod) } if (template == NULL) { - LOG_WARN( - "no ws template for %s, and no default template available", - ws->name); + LOG_WARN("no ws template for %s, and no default template available", ws->name); } else { - particles[particle_count++] = template->content->instantiate( - template->content, &tags); + particles[particle_count++] = template->content->instantiate(template->content, &tags); } tag_set_destroy(&tags); @@ -593,8 +910,7 @@ content(struct module *mod) particles[particle_count++] = current; mtx_unlock(&mod->lock); - return dynlist_exposable_new( - particles, particle_count, m->left_spacing, m->right_spacing); + return dynlist_exposable_new(particles, particle_count, m->left_spacing, m->right_spacing); } /* Maps workspace name to a content particle. */ @@ -604,11 +920,13 @@ struct i3_workspaces { }; static struct module * -i3_new(struct i3_workspaces workspaces[], size_t workspace_count, - int left_spacing, int right_spacing) +i3_new(struct i3_workspaces workspaces[], size_t workspace_count, int left_spacing, int right_spacing, + enum sort_mode sort_mode, size_t persistent_count, const char *persistent_workspaces[static persistent_count], + bool strip_workspace_numbers) { struct private *m = calloc(1, sizeof(*m)); + m->mode = strdup("default"); m->left_spacing = left_spacing; m->right_spacing = right_spacing; @@ -620,11 +938,21 @@ i3_new(struct i3_workspaces workspaces[], size_t workspace_count, m->ws_content.v[i].content = workspaces[i].content; } + m->strip_workspace_numbers = strip_workspace_numbers; + m->sort_mode = sort_mode; + + m->persistent_count = persistent_count; + m->persistent_workspaces = calloc(persistent_count, sizeof(m->persistent_workspaces[0])); + + for (size_t i = 0; i < persistent_count; i++) + m->persistent_workspaces[i] = strdup(persistent_workspaces[i]); + struct module *mod = module_common_new(); mod->private = m; mod->run = &run; mod->destroy = &destroy; mod->content = &content; + mod->description = &description; return mod; } @@ -635,44 +963,56 @@ from_conf(const struct yml_node *node, struct conf_inherit inherited) const struct yml_node *spacing = yml_get_value(node, "spacing"); const struct yml_node *left_spacing = yml_get_value(node, "left-spacing"); const struct yml_node *right_spacing = yml_get_value(node, "right-spacing"); + const struct yml_node *sort = yml_get_value(node, "sort"); + const struct yml_node *persistent = yml_get_value(node, "persistent"); + const struct yml_node *strip_workspace_number = yml_get_value(node, "strip-workspace-numbers"); - 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; + 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; + + const char *sort_value = sort != NULL ? yml_value_as_string(sort) : NULL; + enum sort_mode sort_mode = sort_value == NULL ? SORT_NONE + : strcmp(sort_value, "none") == 0 ? SORT_NONE + : strcmp(sort_value, "native") == 0 ? SORT_NATIVE + : strcmp(sort_value, "ascending") == 0 ? SORT_ASCENDING + : SORT_DESCENDING; + + const size_t persistent_count = persistent != NULL ? yml_list_length(persistent) : 0; + const char *persistent_workspaces[persistent_count]; + + if (persistent != NULL) { + size_t idx = 0; + for (struct yml_list_iter it = yml_list_iter(persistent); it.node != NULL; yml_list_next(&it), idx++) { + persistent_workspaces[idx] = yml_value_as_string(it.node); + } + } struct i3_workspaces workspaces[yml_dict_length(c)]; size_t idx = 0; - for (struct yml_dict_iter it = yml_dict_iter(c); - it.key != NULL; - yml_dict_next(&it), idx++) - { + for (struct yml_dict_iter it = yml_dict_iter(c); it.key != NULL; yml_dict_next(&it), idx++) { workspaces[idx].name = yml_value_as_string(it.key); workspaces[idx].content = conf_to_particle(it.value, inherited); } - return i3_new(workspaces, yml_dict_length(c), left, right); + return i3_new(workspaces, yml_dict_length(c), left, right, sort_mode, persistent_count, persistent_workspaces, + (strip_workspace_number != NULL ? yml_value_as_bool(strip_workspace_number) : false)); } static bool verify_content(keychain_t *chain, const struct yml_node *node) { if (!yml_is_dict(node)) { - LOG_ERR( - "%s: must be a dictionary of workspace-name: particle mappings", - conf_err_prefix(chain, node)); + LOG_ERR("%s: must be a dictionary of workspace-name: particle mappings", conf_err_prefix(chain, node)); return false; } - for (struct yml_dict_iter it = yml_dict_iter(node); - it.key != NULL; - yml_dict_next(&it)) - { + for (struct yml_dict_iter it = yml_dict_iter(node); it.key != NULL; yml_dict_next(&it)) { const char *key = yml_value_as_string(it.key); if (key == NULL) { - LOG_ERR("%s: key must be a string (a i3 workspace name)", - conf_err_prefix(chain, it.key)); + LOG_ERR("%s: key must be a string (a i3 workspace name)", conf_err_prefix(chain, it.key)); return false; } @@ -685,13 +1025,28 @@ verify_content(keychain_t *chain, const struct yml_node *node) return true; } +static bool +verify_sort(keychain_t *chain, const struct yml_node *node) +{ + return conf_verify_enum(chain, node, (const char *[]){"none", "native", "ascending", "descending"}, 4); +} + +static bool +verify_persistent(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[] = { - {"spacing", false, &conf_verify_int}, - {"left-spacing", false, &conf_verify_int}, - {"right-spacing", false, &conf_verify_int}, + {"spacing", false, &conf_verify_unsigned}, + {"left-spacing", false, &conf_verify_unsigned}, + {"right-spacing", false, &conf_verify_unsigned}, + {"sort", false, &verify_sort}, + {"persistent", false, &verify_persistent}, + {"strip-workspace-numbers", false, &conf_verify_bool}, {"content", true, &verify_content}, {"anchors", false, NULL}, {NULL, false, NULL}, @@ -706,5 +1061,5 @@ const struct module_iface module_i3_iface = { }; #if defined(CORE_PLUGINS_AS_SHARED_LIBRARIES) -extern const struct module_iface iface __attribute__((weak, alias("module_i3_iface"))) ; +extern const struct module_iface iface __attribute__((weak, alias("module_i3_iface"))); #endif diff --git a/modules/label.c b/modules/label.c index a29b6bd..5f1f158 100644 --- a/modules/label.c +++ b/modules/label.c @@ -1,16 +1,14 @@ -#include #include +#include #include -#include "../config.h" #include "../config-verify.h" +#include "../config.h" #include "../module.h" #include "../plugin.h" -struct private { - struct particle *label; -}; +struct private { struct particle *label; }; static void destroy(struct module *mod) @@ -21,6 +19,12 @@ destroy(struct module *mod) module_default_destroy(mod); } +static const char * +description(const struct module *mod) +{ + return "label"; +} + static struct exposable * content(struct module *mod) { @@ -45,6 +49,7 @@ label_new(struct particle *label) mod->run = &run; mod->destroy = &destroy; mod->content = &content; + mod->description = &description; return mod; } diff --git a/modules/mem.c b/modules/mem.c new file mode 100644 index 0000000..de4e133 --- /dev/null +++ b/modules/mem.c @@ -0,0 +1,200 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define LOG_MODULE "mem" +#define LOG_ENABLE_DBG 0 +#include "../bar/bar.h" +#include "../config-verify.h" +#include "../config.h" +#include "../log.h" +#include "../plugin.h" + +static const long min_poll_interval = 250; + +struct private +{ + struct particle *label; + uint16_t interval; + uint64_t mem_free; + uint64_t mem_total; +}; + +static void +destroy(struct module *mod) +{ + struct private *m = mod->private; + m->label->destroy(m->label); + free(m); + module_default_destroy(mod); +} + +static const char * +description(const struct module *mod) +{ + return "mem"; +} + +static bool +get_mem_stats(uint64_t *mem_free, uint64_t *mem_total) +{ + bool mem_total_found = false; + bool mem_free_found = false; + + FILE *fp = NULL; + char *line = NULL; + size_t len = 0; + ssize_t read = 0; + + fp = fopen("/proc/meminfo", "re"); + if (NULL == fp) { + LOG_ERRNO("unable to open /proc/meminfo"); + return false; + } + + while ((read = getline(&line, &len, fp)) != -1) { + if (strncmp(line, "MemTotal:", sizeof("MemTotal:") - 1) == 0) { + read = sscanf(line + sizeof("MemTotal:") - 1, "%" SCNu64, mem_total); + mem_total_found = (read == 1); + } + if (strncmp(line, "MemAvailable:", sizeof("MemAvailable:") - 1) == 0) { + read = sscanf(line + sizeof("MemAvailable:"), "%" SCNu64, mem_free); + mem_free_found = (read == 1); + } + } + free(line); + + fclose(fp); + + return mem_free_found && mem_total_found; +} + +static struct exposable * +content(struct module *mod) +{ + const struct private *p = mod->private; + + mtx_lock(&mod->lock); + + const uint64_t mem_free = p->mem_free; + const uint64_t mem_total = p->mem_total; + const uint64_t mem_used = mem_total - mem_free; + + double percent_used = ((double)mem_used * 100) / (mem_total + 1); + double percent_free = ((double)mem_free * 100) / (mem_total + 1); + + struct tag_set tags = { + .tags = (struct tag *[]){tag_new_int(mod, "free", mem_free * 1024), tag_new_int(mod, "used", mem_used * 1024), + tag_new_int(mod, "total", mem_total * 1024), + tag_new_int_range(mod, "percent_free", round(percent_free), 0, 100), + tag_new_int_range(mod, "percent_used", round(percent_used), 0, 100)}, + .count = 5, + }; + + struct exposable *exposable = p->label->instantiate(p->label, &tags); + tag_set_destroy(&tags); + mtx_unlock(&mod->lock); + return exposable; +} + +static int +run(struct module *mod) +{ + const struct bar *bar = mod->bar; + bar->refresh(bar); + struct private *p = mod->private; + while (true) { + struct pollfd fds[] = {{.fd = mod->abort_fd, .events = POLLIN}}; + + int res = poll(fds, 1, p->interval); + if (res < 0) { + if (EINTR == errno) { + continue; + } + + LOG_ERRNO("unable to poll abort fd"); + return -1; + } + + if (fds[0].revents & POLLIN) + break; + + mtx_lock(&mod->lock); + p->mem_free = 0; + p->mem_total = 0; + if (!get_mem_stats(&p->mem_free, &p->mem_total)) { + LOG_ERR("unable to retrieve the memory stats"); + } + mtx_unlock(&mod->lock); + bar->refresh(bar); + } + + return 0; +} + +static struct module * +mem_new(uint16_t interval, struct particle *label) +{ + struct private *p = calloc(1, sizeof(*p)); + p->label = label; + p->interval = interval; + + struct module *mod = module_common_new(); + mod->private = p; + mod->run = &run; + mod->destroy = &destroy; + mod->content = &content; + mod->description = &description; + return mod; +} + +static struct module * +from_conf(const struct yml_node *node, struct conf_inherit inherited) +{ + const struct yml_node *interval = yml_get_value(node, "poll-interval"); + const struct yml_node *c = yml_get_value(node, "content"); + + return mem_new(interval == NULL ? min_poll_interval : yml_value_as_int(interval), conf_to_particle(c, inherited)); +} + +static bool +conf_verify_poll_interval(keychain_t *chain, const struct yml_node *node) +{ + if (!conf_verify_unsigned(chain, node)) + return false; + + if (yml_value_as_int(node) < min_poll_interval) { + LOG_ERR("%s: interval value cannot be less than %ldms", conf_err_prefix(chain, node), min_poll_interval); + return false; + } + + return true; +} + +static bool +verify_conf(keychain_t *chain, const struct yml_node *node) +{ + static const struct attr_info attrs[] = { + {"poll-interval", false, &conf_verify_poll_interval}, + MODULE_COMMON_ATTRS, + }; + + return conf_verify_dict(chain, node, attrs); +} + +const struct module_iface module_mem_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_mem_iface"))); +#endif diff --git a/modules/meson.build b/modules/meson.build index a42828e..f6d53d8 100644 --- a/modules/meson.build +++ b/modules/meson.build @@ -2,53 +2,214 @@ module_sdk = declare_dependency(dependencies: [pixman, threads, tllist, fcft]) modules = [] -alsa = dependency('alsa') -udev = dependency('libudev') -json = dependency('json-c') -mpd = dependency('libmpdclient') -xcb_xkb = dependency('xcb-xkb', required: get_option('backend-x11')) +# Optional deps +alsa = dependency('alsa', required: get_option('plugin-alsa')) +plugin_alsa_enabled = alsa.found() + +udev_backlight = dependency('libudev', required: get_option('plugin-backlight')) +plugin_backlight_enabled = udev_backlight.found() + +udev_battery = dependency('libudev', required: get_option('plugin-battery')) +plugin_battery_enabled = udev_battery.found() + +plugin_clock_enabled = get_option('plugin-clock').allowed() +plugin_cpu_enabled = get_option('plugin-cpu').allowed() +plugin_disk_io_enabled = get_option('plugin-disk-io').allowed() +plugin_dwl_enabled = get_option('plugin-dwl').allowed() +plugin_foreign_toplevel_enabled = backend_wayland and get_option('plugin-foreign-toplevel').allowed() +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() + +plugin_label_enabled = get_option('plugin-label').allowed() +plugin_network_enabled = get_option('plugin-network').allowed() + +pipewire = dependency('libpipewire-0.3', required: get_option('plugin-pipewire')) +json_pipewire = dependency('json-c', required: get_option('plugin-pipewire')) +plugin_pipewire_enabled = pipewire.found() and json_pipewire.found() + +pulse = dependency('libpulse', required: get_option('plugin-pulse')) +plugin_pulse_enabled = pulse.found() + +udev_removables = dependency('libudev', required: get_option('plugin-removables')) +plugin_removables_enabled = udev_removables.found() + +plugin_river_enabled = backend_wayland and get_option('plugin-river').allowed() + +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() + +plugin_xwindow_enabled = backend_x11 and get_option('plugin-xwindow').allowed() # Module name -> (source-list, dep-list) -deps = { - 'alsa': [[], [alsa]], - 'backlight': [[], [udev]], - 'battery': [[], [udev]], - 'clock': [[], []], - 'i3': [['i3-common.c', 'i3-common.h'], [dynlist, json]], - 'label': [[], []], - 'mpd': [[], [mpd]], - 'network': [[], []], - 'removables': [[], [dynlist, udev]], - 'sway_xkb': [['i3-common.c', 'i3-common.h'], [dynlist, json]], -} +mod_data = {} -if backend_x11 - deps += { - 'xkb': [[], [xcb_stuff, xcb_xkb]], - 'xwindow': [[], [xcb_stuff]], - } +if plugin_alsa_enabled + mod_data += {'alsa': [[], [m, alsa]]} endif -if backend_wayland - deps += { - 'river': [[], []], - } +if plugin_backlight_enabled + mod_data += {'backlight': [[], [m, udev_backlight]]} endif -foreach mod, data : deps +if plugin_battery_enabled + mod_data += {'battery': [[], [udev_battery]]} +endif + +if plugin_clock_enabled + mod_data += {'clock': [[], []]} +endif + +if plugin_cpu_enabled + mod_data += {'cpu': [[], [m, dynlist]]} +endif + +if plugin_disk_io_enabled + mod_data += {'disk-io': [[], [dynlist]]} +endif + +if plugin_dwl_enabled + mod_data += {'dwl': [[], [dynlist]]} +endif + +if plugin_mem_enabled + mod_data += {'mem': [[], [m]]} +endif + +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 + +if plugin_label_enabled + mod_data += {'label': [[], []]} +endif + +if plugin_network_enabled + mod_data += {'network': [[], [dynlist]]} +endif + +if plugin_pipewire_enabled + mod_data += {'pipewire': [[], [m, pipewire, dynlist, json_pipewire]]} +endif + +if plugin_pulse_enabled + mod_data += {'pulse': [[], [m, pulse]]} +endif + +if plugin_removables_enabled + mod_data += {'removables': [[], [dynlist, udev_removables]]} +endif + +if plugin_script_enabled + mod_data += {'script': [[], []]} +endif + +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 + +if plugin_xwindow_enabled + mod_data += {'xwindow': [[], [xcb_stuff]]} +endif + +if plugin_river_enabled + river_proto_headers = [] + river_proto_src = [] + + foreach prot : ['../external/river-status-unstable-v1.xml'] + + river_proto_headers += custom_target( + prot.underscorify() + '-client-header', + output: '@BASENAME@.h', + input: prot, + command: [wscanner_prog, 'client-header', '@INPUT@', '@OUTPUT@']) + + river_proto_src += custom_target( + prot.underscorify() + '-private-code', + output: '@BASENAME@.c', + input: prot, + command: [wscanner_prog, 'private-code', '@INPUT@', '@OUTPUT@']) + endforeach + + mod_data += {'river': [[wl_proto_src + wl_proto_headers + river_proto_src + river_proto_headers], [dynlist, wayland_client]]} +endif + +if plugin_foreign_toplevel_enabled + ftop_proto_headers = [] + ftop_proto_src = [] + + foreach prot : ['../external/wlr-foreign-toplevel-management-unstable-v1.xml'] + + ftop_proto_headers += custom_target( + prot.underscorify() + '-client-header', + output: '@BASENAME@.h', + input: prot, + command: [wscanner_prog, 'client-header', '@INPUT@', '@OUTPUT@']) + + ftop_proto_src += custom_target( + prot.underscorify() + '-private-code', + output: '@BASENAME@.c', + input: prot, + command: [wscanner_prog, 'private-code', '@INPUT@', '@OUTPUT@']) + endforeach + + mod_data += {'foreign-toplevel': [[wl_proto_src + wl_proto_headers + ftop_proto_headers + ftop_proto_src], [m, dynlist, wayland_client]]} +endif + +foreach mod, data : mod_data sources = data[0] - dep = data[1] + deps = data[1] if plugs_as_libs shared_module(mod, '@0@.c'.format(mod), sources, - dependencies: [module_sdk] + dep, + dependencies: [module_sdk] + deps, name_prefix: 'module_', install: true, install_dir: join_paths(get_option('libdir'), 'yambar')) else modules += [declare_dependency( sources: ['@0@.c'.format(mod)] + sources, - dependencies: [module_sdk] + dep, - compile_args: '-DHAVE_PLUGIN_@0@'.format(mod))] + dependencies: [module_sdk] + deps, + compile_args: '-DHAVE_PLUGIN_@0@'.format(mod.underscorify()))] endif endforeach diff --git a/modules/mpd.c b/modules/mpd.c index 743dc2e..e70e41f 100644 --- a/modules/mpd.c +++ b/modules/mpd.c @@ -1,53 +1,50 @@ -#include -#include -#include -#include -#include -#include -#include #include #include +#include +#include +#include +#include +#include +#include +#include -#include #include +#include -#include -#include -#include #include #include +#include +#include +#include #include #define LOG_MODULE "mpd" #define LOG_ENABLE_DBG 0 -#include "../log.h" #include "../bar/bar.h" -#include "../config.h" #include "../config-verify.h" +#include "../config.h" +#include "../log.h" #include "../plugin.h" -enum state { - STATE_OFFLINE = 1000, - STATE_STOP = MPD_STATE_STOP, - STATE_PAUSE = MPD_STATE_PAUSE, - STATE_PLAY = MPD_STATE_PLAY, -}; - -struct private { +struct private +{ char *host; uint16_t port; struct particle *label; struct mpd_connection *conn; - enum state state; + enum mpd_state state; bool repeat; bool random; bool consume; + bool single; + int volume; char *album; char *artist; char *title; + char *file; struct { uint64_t value; @@ -65,11 +62,9 @@ destroy(struct module *mod) struct private *m = mod->private; if (m->refresh_thread_id != 0) { assert(m->refresh_abort_fd != -1); - if (write(m->refresh_abort_fd, &(uint64_t){1}, sizeof(uint64_t)) - != sizeof(uint64_t)) - { + if (write(m->refresh_abort_fd, &(uint64_t){1}, sizeof(uint64_t)) != sizeof(uint64_t)) { LOG_ERRNO("failed to signal abort to refresher thread"); - } else{ + } else { int res; thrd_join(m->refresh_thread_id, &res); } @@ -81,6 +76,7 @@ destroy(struct module *mod) free(m->album); free(m->artist); free(m->title); + free(m->file); assert(m->conn == NULL); m->label->destroy(m->label); @@ -89,6 +85,12 @@ destroy(struct module *mod) module_default_destroy(mod); } +static const char * +description(const struct module *mod) +{ + return "mpd"; +} + static uint64_t timespec_diff_milli_seconds(const struct timespec *a, const struct timespec *b) { @@ -127,15 +129,14 @@ content(struct module *mod) /* Calculate what 'elapsed' is now */ uint64_t elapsed = m->elapsed.value; - if (m->state == STATE_PLAY) { + if (m->state == MPD_STATE_PLAY) { elapsed += timespec_diff_milli_seconds(&now, &m->elapsed.when); if (elapsed > m->duration) { - LOG_DBG( - "dynamic update of elapsed overflowed: " - "elapsed=%"PRIu64", duration=%"PRIu64, elapsed, m->duration); + LOG_DBG("dynamic update of elapsed overflowed: " + "elapsed=%" PRIu64 ", duration=%" PRIu64, + elapsed, m->duration); elapsed = m->duration; } - } unsigned elapsed_secs = elapsed / 1000; @@ -148,16 +149,27 @@ content(struct module *mod) /* State as string */ const char *state_str = NULL; - switch (m->state) { - case STATE_OFFLINE: state_str = "offline"; break; - case STATE_STOP: state_str = "stopped"; break; - case STATE_PAUSE: state_str = "paused"; break; - case STATE_PLAY: state_str = "playing"; break; + if (m->conn == NULL) + state_str = "offline"; + else { + switch (m->state) { + case MPD_STATE_UNKNOWN: + state_str = "unknown"; + break; + case MPD_STATE_STOP: + state_str = "stopped"; + break; + case MPD_STATE_PAUSE: + state_str = "paused"; + break; + case MPD_STATE_PLAY: + state_str = "playing"; + break; + } } /* Tell particle to real-time track? */ - enum tag_realtime_unit realtime = m->state == STATE_PLAY - ? TAG_REALTIME_MSECS : TAG_REALTIME_NONE; + enum tag_realtime_unit realtime = m->state == MPD_STATE_PLAY ? TAG_REALTIME_MSECS : TAG_REALTIME_NONE; struct tag_set tags = { .tags = (struct tag *[]){ @@ -165,16 +177,19 @@ 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), tag_new_string(mod, "title", m->title), + tag_new_string(mod, "file", m->file), tag_new_string(mod, "pos", pos), tag_new_string(mod, "end", end), tag_new_int(mod, "duration", m->duration), tag_new_int_realtime( mod, "elapsed", elapsed, 0, m->duration, realtime), }, - .count = 11, + .count = 14, }; mtx_unlock(&mod->lock); @@ -218,7 +233,7 @@ wait_for_socket_create(const struct module *mod) struct stat st; if (stat(m->host, &st) == 0 && S_ISSOCK(st.st_mode)) { - int s = socket(AF_UNIX, SOCK_STREAM, 0); + int s = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0); struct sockaddr_un addr = {.sun_family = AF_UNIX}; strncpy(addr.sun_path, m->host, sizeof(addr.sun_path) - 1); @@ -229,8 +244,7 @@ wait_for_socket_create(const struct module *mod) LOG_DBG("%s: already exists, and is connectable", m->host); have_mpd_socket = true; } else { - LOG_DBG("%s: already exists, but isn't connectable: %s", - m->host, strerror(errno)); + LOG_DBG("%s: already exists, but isn't connectable: %s", m->host, strerror(errno)); } close(s); @@ -241,12 +255,15 @@ wait_for_socket_create(const struct module *mod) bool ret = false; while (!have_mpd_socket) { - struct pollfd fds[] = { - {.fd = mod->abort_fd, .events = POLLIN}, - {.fd = fd, .events = POLLIN} - }; + struct pollfd fds[] = {{.fd = mod->abort_fd, .events = POLLIN}, {.fd = fd, .events = POLLIN}}; - poll(fds, 2, -1); + if (poll(fds, sizeof(fds) / sizeof(fds[0]), -1) < 0) { + if (errno == EINTR) + continue; + + LOG_ERRNO("failed to poll"); + break; + } if (fds[0].revents & POLLIN) { ret = true; @@ -258,7 +275,7 @@ wait_for_socket_create(const struct module *mod) char buf[1024]; ssize_t len = read(fd, buf, sizeof(buf)); - for (const char *ptr = buf; ptr < buf + len; ) { + for (const char *ptr = buf; ptr < buf + len;) { const struct inotify_event *e = (const struct inotify_event *)ptr; LOG_DBG("inotify: CREATED: %s/%.*s", directory, e->len, e->name); @@ -268,7 +285,7 @@ wait_for_socket_create(const struct module *mod) break; } - ptr += sizeof(*e) + e->len; + ptr += sizeof(*e) + e->len; } } @@ -291,8 +308,7 @@ connect_to_mpd(const struct module *mod) enum mpd_error merr = mpd_connection_get_error(conn); if (merr != MPD_ERROR_SUCCESS) { - LOG_WARN("failed to connect to MPD: %s", - mpd_connection_get_error_message(conn)); + LOG_WARN("failed to connect to MPD: %s", mpd_connection_get_error_message(conn)); mpd_connection_free(conn); return NULL; } @@ -310,8 +326,7 @@ update_status(struct module *mod) struct mpd_status *status = mpd_run_status(m->conn); if (status == NULL) { - LOG_ERR("failed to get status: %s", - mpd_connection_get_error_message(m->conn)); + LOG_ERR("failed to get status: %s", mpd_connection_get_error_message(m->conn)); return false; } @@ -323,6 +338,8 @@ 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); m->elapsed.when = now; @@ -332,30 +349,37 @@ update_status(struct module *mod) struct mpd_song *song = mpd_run_current_song(m->conn); if (song == NULL && mpd_connection_get_error(m->conn) != MPD_ERROR_SUCCESS) { - LOG_ERR("failed to get current song: %s", - mpd_connection_get_error_message(m->conn)); + LOG_ERR("failed to get current song: %s", mpd_connection_get_error_message(m->conn)); return false; } if (song == NULL) { mtx_lock(&mod->lock); - free(m->album); m->album = NULL; - free(m->artist); m->artist = NULL; - free(m->title); m->title = NULL; + free(m->album); + m->album = NULL; + free(m->artist); + m->artist = NULL; + free(m->title); + m->title = NULL; + free(m->file); + m->file = NULL; mtx_unlock(&mod->lock); } else { const char *album = mpd_song_get_tag(song, MPD_TAG_ALBUM, 0); const char *artist = mpd_song_get_tag(song, MPD_TAG_ARTIST, 0); const char *title = mpd_song_get_tag(song, MPD_TAG_TITLE, 0); + const char *file = mpd_song_get_uri(song); mtx_lock(&mod->lock); free(m->album); free(m->artist); free(m->title); + free(m->file); m->album = album != NULL ? strdup(album) : NULL; m->artist = artist != NULL ? strdup(artist) : NULL; m->title = title != NULL ? strdup(title) : NULL; + m->file = file != NULL ? strdup(file) : NULL; mtx_unlock(&mod->lock); mpd_song_free(song); @@ -371,8 +395,9 @@ run(struct module *mod) struct private *m = mod->private; bool aborted = false; + int ret = 0; - while (!aborted) { + while (!aborted && ret == 0) { if (m->conn != NULL) { mpd_connection_free(m->conn); @@ -381,16 +406,21 @@ run(struct module *mod) /* Reset state */ mtx_lock(&mod->lock); - free(m->album); m->album = NULL; - free(m->artist); m->artist = NULL; - free(m->title); m->title = NULL; - m->state = STATE_OFFLINE; + free(m->album); + m->album = NULL; + free(m->artist); + m->artist = NULL; + free(m->title); + m->title = NULL; + free(m->file); + m->file = NULL; + m->state = MPD_STATE_UNKNOWN; m->elapsed.value = m->duration = 0; m->elapsed.when.tv_sec = m->elapsed.when.tv_nsec = 0; mtx_unlock(&mod->lock); /* Keep trying to connect, until we succeed */ - while (!aborted) { + while (!aborted && ret == 0) { if (m->port == 0) { /* Use inotify to watch for socket creation */ aborted = wait_for_socket_create(mod); @@ -408,16 +438,33 @@ run(struct module *mod) * host), wait for a while until we try to re-connect * again. */ - struct pollfd fds[] = {{.fd = mod->abort_fd, .events = POLLIN}}; - int res = poll(fds, 1, 10 * 1000); + while (!aborted) { + struct pollfd fds[] = {{.fd = mod->abort_fd, .events = POLLIN}}; + int res = poll(fds, sizeof(fds) / sizeof(fds[0]), 2 * 1000); + + if (res < 0) { + if (errno == EINTR) + continue; + + LOG_ERRNO("failed to poll"); + ret = 1; + break; + } + + if (res == 0) { + ret = 0; + break; + } + + else if (res == 1) { + assert(fds[0].revents & POLLIN); + aborted = true; + } - if (res == 1) { - assert(fds[0].revents & POLLIN); - aborted = true; } } - if (aborted) + if (aborted || ret != 0) break; /* Initial state (after establishing a connection) */ @@ -435,12 +482,18 @@ run(struct module *mod) }; if (!mpd_send_idle(m->conn)) { - LOG_ERR("failed to send IDLE command: %s", - mpd_connection_get_error_message(m->conn)); + LOG_ERR("failed to send IDLE command: %s", mpd_connection_get_error_message(m->conn)); break; } - poll(fds, 2, -1); + if (poll(fds, sizeof(fds) / sizeof(fds[0]), -1) < 0) { + if (errno == EINTR) + continue; + + LOG_ERRNO("failed to poll"); + ret = 1; + break; + } if (fds[0].revents & POLLIN) { aborted = true; @@ -453,8 +506,7 @@ run(struct module *mod) } if (fds[1].revents & POLLIN) { - enum mpd_idle idle __attribute__ ((unused)) = - mpd_recv_idle(m->conn, true); + enum mpd_idle idle __attribute__((unused)) = mpd_recv_idle(m->conn, true); LOG_DBG("IDLE mask: %d", idle); @@ -471,7 +523,7 @@ run(struct module *mod) m->conn = NULL; } - return 0; + return aborted ? 0 : ret; } struct refresh_context { @@ -526,9 +578,7 @@ refresh_in(struct module *mod, long milli_seconds) /* 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)) - { + 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; } @@ -568,7 +618,7 @@ refresh_in(struct module *mod, long milli_seconds) } /* Detach - we don't want to have to thrd_join() it */ - //thrd_detach(tid); + // thrd_detach(tid); return r == 0; } @@ -579,7 +629,7 @@ mpd_new(const char *host, uint16_t port, struct particle *label) priv->host = strdup(host); priv->port = port; priv->label = label; - priv->state = STATE_OFFLINE; + priv->state = MPD_STATE_UNKNOWN; priv->refresh_abort_fd = -1; struct module *mod = module_common_new(); @@ -588,6 +638,7 @@ mpd_new(const char *host, uint16_t port, struct particle *label) mod->destroy = &destroy; mod->content = &content; mod->refresh_in = &refresh_in; + mod->description = &description; return mod; } @@ -598,10 +649,8 @@ from_conf(const struct yml_node *node, struct conf_inherit inherited) const struct yml_node *port = yml_get_value(node, "port"); const struct yml_node *c = yml_get_value(node, "content"); - return mpd_new( - yml_value_as_string(host), - port != NULL ? yml_value_as_int(port) : 0, - conf_to_particle(c, inherited)); + return mpd_new(yml_value_as_string(host), port != NULL ? yml_value_as_int(port) : 0, + conf_to_particle(c, inherited)); } static bool @@ -609,7 +658,7 @@ verify_conf(keychain_t *chain, const struct yml_node *node) { static const struct attr_info attrs[] = { {"host", true, &conf_verify_string}, - {"port", false, &conf_verify_int}, + {"port", false, &conf_verify_unsigned}, MODULE_COMMON_ATTRS, }; 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 51839bf..46a3148 100644 --- a/modules/network.c +++ b/modules/network.c @@ -1,29 +1,48 @@ +#include +#include +#include #include #include -#include #include -#include +#include #include -#include #include +#include +#include + +#include +#include #include +#include #include +#include #include +#include #include #include #define LOG_MODULE "network" -#define LOG_ENABLE_DBG 0 -#include "../log.h" +#define LOG_ENABLE_DBG 1 #include "../bar/bar.h" -#include "../config.h" #include "../config-verify.h" +#include "../config.h" +#include "../log.h" #include "../module.h" +#include "../particles/dynlist.h" #include "../plugin.h" +#define max(x, y) ((x) > (y) ? (x) : (y)) + +static const long min_poll_interval = 250; + +struct rt_stats_msg { + struct rtmsg rth; + struct rtnl_link_stats64 stats; +}; + struct af_addr { int family; union { @@ -32,40 +51,94 @@ struct af_addr { } addr; }; -struct private { - char *iface; - struct particle *label; +struct iface { + char *name; + char *type; /* ARPHRD_NNN */ + char *kind; /* IFLA_LINKINFO::IFLA_INFO_KIND */ - int nl_sock; + uint32_t get_stats_seq_nr; - bool get_addresses; - - int ifindex; + int index; uint8_t mac[6]; bool carrier; - uint8_t state; /* IFLA_OPERSTATE */ + uint8_t state; /* IFLA_OPERSTATE */ /* IPv4 and IPv6 addresses */ tll(struct af_addr) addrs; + + /* WiFi extensions */ + char *ssid; + int signal_strength_dbm; + uint32_t rx_bitrate; + uint32_t tx_bitrate; + + double ul_speed; + uint64_t ul_bits; + + double dl_speed; + uint64_t dl_bits; }; +struct private +{ + struct particle *label; + int poll_interval; + + int left_spacing; + int right_spacing; + + bool get_addresses; + + int genl_sock; + int rt_sock; + int urandom_fd; + + struct { + uint16_t family_id; + uint32_t get_interface_seq_nr; + uint32_t get_scan_seq_nr; + } nl80211; + + tll(struct iface) ifaces; +}; + +static void +free_iface(struct iface iface) +{ + tll_free(iface.addrs); + free(iface.ssid); + free(iface.kind); + free(iface.type); + free(iface.name); +} + static void destroy(struct module *mod) { struct private *m = mod->private; - assert(m->nl_sock == -1); + assert(m->rt_sock == -1); m->label->destroy(m->label); - tll_free(m->addrs); + if (m->urandom_fd >= 0) + close(m->urandom_fd); + + tll_foreach(m->ifaces, it) { + free_iface(it->item); + tll_remove(m->ifaces, it); + } - free(m->iface); free(m); - module_default_destroy(mod); } +static const char * +description(const struct module *mod) +{ + return "network"; +} + static struct exposable * content(struct module *mod) { @@ -73,52 +146,97 @@ content(struct module *mod) mtx_lock(&mod->lock); - const char *state = NULL; - switch (m->state) { - case IF_OPER_UNKNOWN: state = "unknown"; break; - case IF_OPER_NOTPRESENT: state = "not present"; break; - case IF_OPER_DOWN: state = "down"; break; - case IF_OPER_LOWERLAYERDOWN: state = "lower layers down"; break; - case IF_OPER_TESTING: state = "testing"; break; - case IF_OPER_DORMANT: state = "dormant"; break; - case IF_OPER_UP: state = "up"; break; - default: state = "unknown"; break; + struct exposable *exposables[max(tll_length(m->ifaces), 1)]; + size_t idx = 0; + + tll_foreach(m->ifaces, it) + { + struct iface *iface = &it->item; + + const char *state = NULL; + switch (iface->state) { + case IF_OPER_UNKNOWN: + state = "unknown"; + break; + case IF_OPER_NOTPRESENT: + state = "not present"; + break; + case IF_OPER_DOWN: + state = "down"; + break; + case IF_OPER_LOWERLAYERDOWN: + state = "lower layers down"; + break; + case IF_OPER_TESTING: + state = "testing"; + break; + case IF_OPER_DORMANT: + state = "dormant"; + break; + case IF_OPER_UP: + state = "up"; + break; + default: + state = "unknown"; + break; + } + + char mac_str[6 * 2 + 5 + 1]; + char ipv4_str[INET_ADDRSTRLEN] = {0}; + char ipv6_str[INET6_ADDRSTRLEN] = {0}; + + snprintf(mac_str, sizeof(mac_str), "%02x:%02x:%02x:%02x:%02x:%02x", iface->mac[0], iface->mac[1], iface->mac[2], + iface->mac[3], iface->mac[4], iface->mac[5]); + + /* TODO: this exposes the *last* added address of each kind. Can + * we expose all in some way? */ + tll_foreach(iface->addrs, it) + { + if (it->item.family == AF_INET) + inet_ntop(AF_INET, &it->item.addr.ipv4, ipv4_str, sizeof(ipv4_str)); + else if (it->item.family == AF_INET6) + if (!IN6_IS_ADDR_LINKLOCAL(&it->item.addr.ipv6)) + inet_ntop(AF_INET6, &it->item.addr.ipv6, ipv6_str, sizeof(ipv6_str)); + } + + int quality = 0; + if (iface->signal_strength_dbm != 0) { + if (iface->signal_strength_dbm <= -100) + quality = 0; + else if (iface->signal_strength_dbm >= -50) + quality = 100; + else + quality = 2 * (iface->signal_strength_dbm + 100); + } + + 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), + tag_new_string(mod, "mac", mac_str), + tag_new_string(mod, "ipv4", ipv4_str), + tag_new_string(mod, "ipv6", ipv6_str), + tag_new_string(mod, "ssid", iface->ssid), + tag_new_int(mod, "signal", iface->signal_strength_dbm), + tag_new_int_range(mod, "quality", quality, 0, 100), + tag_new_int(mod, "rx-bitrate", iface->rx_bitrate), + tag_new_int(mod, "tx-bitrate", iface->tx_bitrate), + tag_new_float(mod, "dl-speed", iface->dl_speed), + tag_new_float(mod, "ul-speed", iface->ul_speed), + }, + .count = 16, + }; + exposables[idx++] = m->label->instantiate(m->label, &tags); + tag_set_destroy(&tags); } - char mac_str[6 * 2 + 5 + 1]; - char ipv4_str[INET_ADDRSTRLEN] = {0}; - char ipv6_str[INET6_ADDRSTRLEN] = {0}; - - snprintf(mac_str, sizeof(mac_str), "%02x:%02x:%02x:%02x:%02x:%02x", - m->mac[0], m->mac[1], m->mac[2], m->mac[3], m->mac[4], m->mac[5]); - - /* TODO: this exposes the *last* added address of each kind. Can - * we expose all in some way? */ - tll_foreach(m->addrs, it) { - if (it->item.family == AF_INET) - inet_ntop(AF_INET, &it->item.addr.ipv4, ipv4_str, sizeof(ipv4_str)); - else if (it->item.family == AF_INET6) - inet_ntop(AF_INET6, &it->item.addr.ipv6, ipv6_str, sizeof(ipv6_str)); - } - - struct tag_set tags = { - .tags = (struct tag *[]){ - tag_new_string(mod, "name", m->iface), - tag_new_int(mod, "index", m->ifindex), - tag_new_bool(mod, "carrier", m->carrier), - tag_new_string(mod, "state", state), - tag_new_string(mod, "mac", mac_str), - tag_new_string(mod, "ipv4", ipv4_str), - tag_new_string(mod, "ipv6", ipv6_str), - }, - .count = 7, - }; - mtx_unlock(&mod->lock); - struct exposable *exposable = m->label->instantiate(m->label, &tags); - tag_set_destroy(&tags); - return exposable; + return dynlist_exposable_new(exposables, idx, m->left_spacing, m->right_spacing); } /* Returns a value suitable for nl_pid/nlmsg_pid */ @@ -130,9 +248,9 @@ nl_pid_value(void) /* Connect and bind to netlink socket. Returns socket fd, or -1 on error */ static int -netlink_connect(void) +netlink_connect_rt(void) { - int sock = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE); + int sock = socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_ROUTE); if (sock == -1) { LOG_ERRNO("failed to create netlink socket"); return -1; @@ -144,7 +262,30 @@ netlink_connect(void) .nl_groups = RTMGRP_LINK | RTMGRP_IPV4_IFADDR | RTMGRP_IPV6_IFADDR, }; - if (bind(sock, (const struct sockaddr *)&addr, sizeof(addr)) == -1) { + if (bind(sock, (const struct sockaddr *)&addr, sizeof(addr)) < 0) { + LOG_ERRNO("failed to bind netlink RT socket"); + close(sock); + return -1; + } + + return sock; +} + +static int +netlink_connect_genl(void) +{ + int sock = socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_GENERIC); + if (sock == -1) { + LOG_ERRNO("failed to create netlink socket"); + return -1; + } + + const struct sockaddr_nl addr = { + .nl_family = AF_NETLINK, .nl_pid = nl_pid_value(), + /* no multicast notifications by default, will be added later */ + }; + + if (bind(sock, (const struct sockaddr *)&addr, sizeof(addr)) < 0) { LOG_ERRNO("failed to bind netlink socket"); close(sock); return -1; @@ -154,11 +295,20 @@ netlink_connect(void) } static bool -send_rt_request(int nl_sock, int request) +send_nlmsg(int sock, const void *nlmsg, size_t len) +{ + int r = sendto(sock, nlmsg, len, 0, (struct sockaddr *)&(struct sockaddr_nl){.nl_family = AF_NETLINK}, + sizeof(struct sockaddr_nl)); + + return r == len; +} + +static bool +send_rt_request(struct private *m, int request) { struct { struct nlmsghdr hdr; - struct rtgenmsg rt; + struct rtgenmsg rt __attribute__((aligned(NLMSG_ALIGNTO))); } req = { .hdr = { .nlmsg_len = NLMSG_LENGTH(sizeof(req.rt)), @@ -173,101 +323,408 @@ send_rt_request(int nl_sock, int request) }, }; - int r = sendto( - nl_sock, &req, req.hdr.nlmsg_len, 0, - (struct sockaddr *)&(struct sockaddr_nl){.nl_family = AF_NETLINK}, - sizeof(struct sockaddr_nl)); - - if (r == -1) { - LOG_ERRNO("failed to send netlink request"); + if (!send_nlmsg(m->rt_sock, &req, req.hdr.nlmsg_len)) { + LOG_ERRNO("failed to send netlink RT request (%d)", request); return false; } return true; } + static bool -find_my_ifindex(struct module *mod, const struct ifinfomsg *msg, size_t len) +send_rt_getstats_request(struct private *m, struct iface *iface) { - struct private *m = mod->private; - - for (const struct rtattr *attr = IFLA_RTA(msg); - RTA_OK(attr, len); - attr = RTA_NEXT(attr, len)) - { - switch (attr->rta_type) { - case IFLA_IFNAME: - if (strcmp((const char *)RTA_DATA(attr), m->iface) == 0) { - LOG_INFO("%s: ifindex=%d", m->iface, msg->ifi_index); - - mtx_lock(&mod->lock); - m->ifindex = msg->ifi_index; - mtx_unlock(&mod->lock); - return true; - } - - return false; - } + if (iface->get_stats_seq_nr > 0) { + LOG_DBG("%s: RT get-stats request already in progress", iface->name); + return true; } - return false; + LOG_DBG("%s: sending RT get-stats request", iface->name); + + uint32_t seq; + if (read(m->urandom_fd, &seq, sizeof(seq)) != sizeof(seq)) { + LOG_ERRNO("failed to read from /dev/urandom"); + return false; + } + + struct { + struct nlmsghdr hdr; + struct if_stats_msg rt; + } req = { + .hdr = { + .nlmsg_len = NLMSG_LENGTH(sizeof(req.rt)), + .nlmsg_type = RTM_GETSTATS, + .nlmsg_flags = NLM_F_REQUEST, + .nlmsg_seq = seq, + .nlmsg_pid = nl_pid_value(), + }, + + .rt = { + .ifindex = iface->index, + .filter_mask = IFLA_STATS_LINK_64, + .family = AF_UNSPEC, + }, + }; + + if (!send_nlmsg(m->rt_sock, &req, req.hdr.nlmsg_len)) { + LOG_ERRNO("%s: failed to send netlink RT getstats request (%d)", iface->name, RTM_GETSTATS); + return false; + } + iface->get_stats_seq_nr = seq; + return true; +} + +static bool +send_ctrl_get_family_request(struct private *m) +{ + const struct { + struct nlmsghdr hdr; + struct { + struct genlmsghdr genl; + struct { + struct nlattr hdr; + char data[8] __attribute__((aligned(NLA_ALIGNTO))); + } family_name_attr __attribute__((aligned(NLA_ALIGNTO))); + } msg __attribute__((aligned(NLMSG_ALIGNTO))); + } req = { + .hdr = { + .nlmsg_len = NLMSG_LENGTH(sizeof(req.msg)), + .nlmsg_type = GENL_ID_CTRL, + .nlmsg_flags = NLM_F_REQUEST, + .nlmsg_seq = 1, + .nlmsg_pid = nl_pid_value(), + }, + + .msg = { + .genl = { + .cmd = CTRL_CMD_GETFAMILY, + .version = 1, + }, + + .family_name_attr = { + .hdr = { + .nla_type = CTRL_ATTR_FAMILY_NAME, + .nla_len = sizeof(req.msg.family_name_attr), + }, + + .data = NL80211_GENL_NAME, + }, + }, + }; + + _Static_assert(sizeof(req.msg.family_name_attr) == NLA_HDRLEN + NLA_ALIGN(sizeof(req.msg.family_name_attr.data)), + ""); + + if (!send_nlmsg(m->genl_sock, &req, req.hdr.nlmsg_len)) { + LOG_ERRNO("failed to send netlink ctrl-get-family request"); + return false; + } + + return true; +} + +static bool +send_nl80211_request(struct private *m, uint8_t cmd, uint32_t seq) +{ + if (m->nl80211.family_id == (uint16_t)-1) + return false; + + const struct { + struct nlmsghdr hdr; + struct { + struct genlmsghdr genl; + } msg __attribute__((aligned(NLMSG_ALIGNTO))); + } req = { + .hdr = { + .nlmsg_len = NLMSG_LENGTH(sizeof(req.msg)), + .nlmsg_type = m->nl80211.family_id, + .nlmsg_flags = NLM_F_REQUEST | NLM_F_DUMP, + .nlmsg_seq = seq, + .nlmsg_pid = nl_pid_value(), + }, + + .msg = { + .genl = { + .cmd = cmd, + .version = 1, + }, + }, + }; + + if (!send_nlmsg(m->genl_sock, &req, req.hdr.nlmsg_len)) { + LOG_ERRNO("failed to send netlink nl80211 get-inteface request"); + return false; + } + + return true; +} + +static bool +send_nl80211_get_interface(struct private *m) +{ + if (m->nl80211.get_interface_seq_nr > 0) { + LOG_DBG("nl80211 get-interface request already in progress"); + return true; + } + + uint32_t seq; + if (read(m->urandom_fd, &seq, sizeof(seq)) != sizeof(seq)) { + LOG_ERRNO("failed to read from /dev/urandom"); + return false; + } + + LOG_DBG("sending nl80211 get-interface request %d", seq); + + if (!send_nl80211_request(m, NL80211_CMD_GET_INTERFACE, seq)) + return false; + + m->nl80211.get_interface_seq_nr = seq; + return true; +} + +static bool +send_nl80211_get_station(struct private *m, struct iface *iface) +{ + LOG_DBG("sending nl80211 get-station request"); + + if (m->nl80211.family_id == (uint16_t)-1) + return false; + + const struct { + struct nlmsghdr hdr; + struct { + struct genlmsghdr genl; + struct { + struct nlattr attr; + int index __attribute__((aligned(NLA_ALIGNTO))); + } ifindex __attribute__((aligned(NLA_ALIGNTO))); + } msg __attribute__((aligned(NLMSG_ALIGNTO))); + } req = { + .hdr = { + .nlmsg_len = NLMSG_LENGTH(sizeof(req.msg)), + .nlmsg_type = m->nl80211.family_id, + .nlmsg_flags = NLM_F_REQUEST | NLM_F_DUMP, + .nlmsg_seq = 1, + .nlmsg_pid = nl_pid_value(), + }, + + .msg = { + .genl = { + .cmd = NL80211_CMD_GET_STATION, + .version = 1, + }, + + .ifindex = { + .attr = { + .nla_type = NL80211_ATTR_IFINDEX, + .nla_len = sizeof(req.msg.ifindex), + }, + + .index = iface->index, + }, + }, + }; + + if (!send_nlmsg(m->genl_sock, &req, req.hdr.nlmsg_len)) { + LOG_ERRNO("failed to send netlink nl80211 get-inteface request"); + return false; + } + + return true; +} + +static bool +send_nl80211_get_scan(struct private *m) +{ + if (m->nl80211.get_scan_seq_nr > 0) { + LOG_ERR("nl80211 get-scan request already in progress"); + return true; + } + + uint32_t seq; + if (read(m->urandom_fd, &seq, sizeof(seq)) != sizeof(seq)) { + LOG_ERRNO("failed to read from /dev/urandom"); + return false; + } + + LOG_DBG("sending nl80211 get-scan request %d", seq); + + if (!send_nl80211_request(m, NL80211_CMD_GET_SCAN, seq)) + return false; + + m->nl80211.get_scan_seq_nr = seq; + 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) +handle_link(struct module *mod, uint16_t type, const struct ifinfomsg *msg, size_t len) { assert(type == RTM_NEWLINK || type == RTM_DELLINK); struct private *m = mod->private; - if (m->ifindex == -1) { - /* We don't know our own ifindex yet. Let's see if we can find - * it in the message */ - if (!find_my_ifindex(mod, msg, len)) { - /* Nope, message wasn't for us (IFLA_IFNAME mismatch) */ - return; + if (type == RTM_DELLINK) { + tll_foreach(m->ifaces, it) + { + if (msg->ifi_index != it->item.index) + continue; + mtx_lock(&mod->lock); + tll_remove_and_free(m->ifaces, it, free_iface); + mtx_unlock(&mod->lock); + break; } - } - assert(m->ifindex >= 0); - - if (msg->ifi_index != m->ifindex) { - /* Not for us */ + mod->bar->refresh(mod->bar); return; } - bool update_bar = false; - - for (const struct rtattr *attr = IFLA_RTA(msg); - RTA_OK(attr, len); - attr = RTA_NEXT(attr, len)) + struct iface *iface = NULL; + tll_foreach(m->ifaces, it) { + if (msg->ifi_index != it->item.index) + continue; + iface = &it->item; + break; + } + + 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(), + })); + mtx_unlock(&mod->lock); + iface = &tll_back(m->ifaces); + } + + for (const struct rtattr *attr = IFLA_RTA(msg); RTA_OK(attr, len); attr = RTA_NEXT(attr, len)) { switch (attr->rta_type) { + case IFLA_IFNAME: + mtx_lock(&mod->lock); + iface->name = strdup((const char *)RTA_DATA(attr)); + 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 (m->state == operstate) + if (iface->state == operstate) break; - LOG_DBG("%s: IFLA_OPERSTATE: %hhu -> %hhu", m->iface, m->state, operstate); + LOG_DBG("%s: IFLA_OPERSTATE: %hhu -> %hhu", iface->name, iface->state, operstate); mtx_lock(&mod->lock); - m->state = operstate; + iface->state = operstate; mtx_unlock(&mod->lock); - update_bar = true; break; } case IFLA_CARRIER: { uint8_t carrier = *(const uint8_t *)RTA_DATA(attr); - if (m->carrier == carrier) + if (iface->carrier == carrier) break; - LOG_DBG("%s: IFLA_CARRIER: %hhu -> %hhu", m->iface, m->carrier, carrier); + LOG_DBG("%s: IFLA_CARRIER: %hhu -> %hhu", iface->name, iface->carrier, carrier); mtx_lock(&mod->lock); - m->carrier = carrier; + iface->carrier = carrier; mtx_unlock(&mod->lock); - update_bar = true; break; } @@ -276,47 +733,62 @@ handle_link(struct module *mod, uint16_t type, break; const uint8_t *mac = RTA_DATA(attr); - if (memcmp(m->mac, mac, sizeof(m->mac)) == 0) + if (memcmp(iface->mac, mac, sizeof(iface->mac)) == 0) break; LOG_DBG("%s: IFLA_ADDRESS: %02x:%02x:%02x:%02x:%02x:%02x", - m->iface, - mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); + iface->name, mac[0], mac[1], mac[2], mac[3], + mac[4], mac[5]); mtx_lock(&mod->lock); - memcpy(m->mac, mac, sizeof(m->mac)); + memcpy(iface->mac, mac, sizeof(iface->mac)); mtx_unlock(&mod->lock); - update_bar = true; + break; + } + + case IFLA_LINKINFO: { + foreach_nlattr_nested( + mod, iface, RTA_DATA(attr), RTA_PAYLOAD(attr), + &parse_linkinfo, NULL); break; } } } - if (update_bar) - mod->bar->refresh(mod->bar); + assert(iface->name != NULL); + + /* Reset address initialization */ + m->get_addresses = true; + + send_nl80211_get_interface(m); + mod->bar->refresh(mod->bar); } static void -handle_address(struct module *mod, uint16_t type, - const struct ifaddrmsg *msg, size_t len) +handle_address(struct module *mod, uint16_t type, const struct ifaddrmsg *msg, size_t len) { assert(type == RTM_NEWADDR || type == RTM_DELADDR); struct private *m = mod->private; - assert(m->ifindex >= 0); + bool update_bar = false; - if (msg->ifa_index != m->ifindex) { - /* Not for us */ + struct iface *iface = NULL; + + tll_foreach(m->ifaces, it) + { + if (msg->ifa_index != it->item.index) + continue; + iface = &it->item; + break; + } + + if (iface == NULL) { + LOG_ERR("failed to find network interface with index %d. Probably a yambar bug", msg->ifa_index); return; } - bool update_bar = false; - - for (const struct rtattr *attr = IFA_RTA(msg); - RTA_OK(attr, len); - attr = RTA_NEXT(attr, len)) - { + for (const struct rtattr *attr = IFA_RTA(msg); RTA_OK(attr, len); attr = RTA_NEXT(attr, len)) { switch (attr->rta_type) { case IFA_ADDRESS: { const void *raw_addr = RTA_DATA(attr); @@ -326,21 +798,21 @@ handle_address(struct module *mod, uint16_t type, char s[INET6_ADDRSTRLEN]; inet_ntop(msg->ifa_family, raw_addr, s, sizeof(s)); #endif - LOG_DBG("%s: IFA_ADDRESS (%s): %s", m->iface, - type == RTM_NEWADDR ? "add" : "del", s); + LOG_DBG("%s: IFA_ADDRESS (%s): %s", iface->name, type == RTM_NEWADDR ? "add" : "del", s); mtx_lock(&mod->lock); if (type == RTM_DELADDR) { /* Find address in our list and remove it */ - tll_foreach(m->addrs, it) { + tll_foreach(iface->addrs, it) + { if (it->item.family != msg->ifa_family) continue; if (memcmp(&it->item.addr, raw_addr, addr_len) != 0) continue; - tll_remove(m->addrs, it); + tll_remove(iface->addrs, it); update_bar = true; break; } @@ -348,7 +820,7 @@ handle_address(struct module *mod, uint16_t type, /* Append address to our list */ struct af_addr a = {.family = msg->ifa_family}; memcpy(&a.addr, raw_addr, addr_len); - tll_push_back(m->addrs, a); + tll_push_back(iface->addrs, a); update_bar = true; } @@ -362,6 +834,368 @@ handle_address(struct module *mod, uint16_t type, mod->bar->refresh(mod->bar); } +struct mcast_group { + uint32_t id; + const char *name; +}; + +static bool +parse_mcast_group(struct module *mod, struct iface *iface, uint16_t type, bool nested, const void *payload, size_t len, + void *_ctx) +{ + struct mcast_group *ctx = _ctx; + + switch (type) { + case CTRL_ATTR_MCAST_GRP_ID: { + ctx->id = *(uint32_t *)payload; + break; + } + + case CTRL_ATTR_MCAST_GRP_NAME: { + ctx->name = (const char *)payload; + break; + } + + default: + LOG_WARN("unrecognized GENL MCAST GRP attribute: " + "type=%hu, nested=%d, len=%zu", + type, nested, len); + break; + } + + return true; +} + +static bool +parse_mcast_groups(struct module *mod, struct iface *iface, uint16_t type, bool nested, const void *payload, size_t len, + void *_ctx) +{ + struct private *m = mod->private; + + struct mcast_group group = {0}; + foreach_nlattr_nested(mod, NULL, payload, len, &parse_mcast_group, &group); + + LOG_DBG("MCAST: %s -> %u", group.name, group.id); + + if (strcmp(group.name, NL80211_MULTICAST_GROUP_MLME) == 0) { + /* + * Join the nl80211 MLME multicast group - for + * CONNECT/DISCONNECT events. + */ + + int r = setsockopt(m->genl_sock, SOL_NETLINK, NETLINK_ADD_MEMBERSHIP, &group.id, sizeof(int)); + + if (r < 0) + LOG_ERRNO("failed to joint the nl80211 MLME mcast group"); + } + + return true; +} + +static bool +handle_genl_ctrl(struct module *mod, struct iface *iface, uint16_t type, bool nested, const void *payload, size_t len, + void *_ctx) +{ + struct private *m = mod->private; + + switch (type) { + case CTRL_ATTR_FAMILY_ID: { + m->nl80211.family_id = *(const uint16_t *)payload; + send_nl80211_get_interface(m); + break; + } + + case CTRL_ATTR_FAMILY_NAME: + // LOG_INFO("NAME: %.*s (%zu bytes)", (int)len, (const char *)payload, len); + break; + + case CTRL_ATTR_MCAST_GROUPS: + foreach_nlattr_nested(mod, NULL, payload, len, &parse_mcast_groups, NULL); + break; + + default: + LOG_DBG("unrecognized GENL CTRL attribute: " + "type=%hu, nested=%d, len=%zu", + type, nested, len); + break; + } + + return true; +} + +static bool +find_nl80211_iface(struct module *mod, struct iface *_iface, uint16_t type, bool nested, const void *payload, + size_t len, void *ctx) +{ + struct private *m = mod->private; + struct iface **iface = ctx; + + switch (type) { + case NL80211_ATTR_IFINDEX: + if (*iface != NULL) + if (*(uint32_t *)payload == (*iface)->index) + return false; + tll_foreach(m->ifaces, it) + { + if (*(uint32_t *)payload != it->item.index) + continue; + *iface = &it->item; + return false; + } + LOG_ERR("could not find interface with index %d", *(uint32_t *)payload); + break; + } + + return true; +} + +static bool +handle_nl80211_new_interface(struct module *mod, struct iface *iface, uint16_t type, bool nested, const void *payload, + size_t len, void *_ctx) +{ + switch (type) { + case NL80211_ATTR_IFINDEX: + assert(*(uint32_t *)payload == iface->index); + break; + + case NL80211_ATTR_SSID: { + const char *ssid = payload; + + if (iface->ssid == NULL || strncmp(iface->ssid, ssid, len) != 0) + LOG_INFO("%s: SSID: %.*s", iface->name, (int)len, ssid); + + mtx_lock(&mod->lock); + free(iface->ssid); + iface->ssid = strndup(ssid, len); + mtx_unlock(&mod->lock); + + mod->bar->refresh(mod->bar); + break; + } + + default: + LOG_DBG("%s: unrecognized nl80211 attribute: " + "type=%hu, nested=%d, len=%zu", + iface->name, type, nested, len); + break; + } + + return true; +} + +struct rate_info_ctx { + unsigned bitrate; +}; + +static bool +handle_nl80211_rate_info(struct module *mod, struct iface *iface, uint16_t type, bool nested, const void *payload, + size_t len, void *_ctx) +{ + struct rate_info_ctx *ctx = _ctx; + + switch (type) { + case NL80211_RATE_INFO_BITRATE32: { + uint32_t bitrate_100kbit = *(uint32_t *)payload; + ctx->bitrate = bitrate_100kbit * 100 * 1000; + break; + } + + case NL80211_RATE_INFO_BITRATE: + if (ctx->bitrate == 0) { + uint16_t bitrate_100kbit = *(uint16_t *)payload; + ctx->bitrate = bitrate_100kbit * 100 * 1000; + } else { + /* Prefer the BITRATE32 attribute */ + } + break; + + default: + LOG_DBG("%s: unrecognized nl80211 rate info attribute: " + "type=%hu, nested=%d, len=%zu", + iface->name, type, nested, len); + break; + } + + return true; +} + +struct station_info_ctx { + bool update_bar; +}; + +static bool +handle_nl80211_station_info(struct module *mod, struct iface *iface, uint16_t type, bool nested, const void *payload, + size_t len, void *_ctx) +{ + struct station_info_ctx *ctx = _ctx; + + switch (type) { + case NL80211_STA_INFO_SIGNAL: + LOG_DBG("signal strength (last): %hhd dBm", *(uint8_t *)payload); + break; + + case NL80211_STA_INFO_SIGNAL_AVG: + LOG_DBG("signal strength (average): %hhd dBm", *(uint8_t *)payload); + mtx_lock(&mod->lock); + iface->signal_strength_dbm = *(int8_t *)payload; + mtx_unlock(&mod->lock); + ctx->update_bar = true; + break; + + case NL80211_STA_INFO_TX_BITRATE: { + struct rate_info_ctx rctx = {0}; + foreach_nlattr_nested(mod, iface, payload, len, &handle_nl80211_rate_info, &rctx); + + LOG_DBG("TX bitrate: %.1f Mbit/s", rctx.bitrate / 1000. / 1000.); + mtx_lock(&mod->lock); + iface->tx_bitrate = rctx.bitrate; + mtx_unlock(&mod->lock); + ctx->update_bar = true; + break; + } + + case NL80211_STA_INFO_RX_BITRATE: { + struct rate_info_ctx rctx = {0}; + foreach_nlattr_nested(mod, iface, payload, len, &handle_nl80211_rate_info, &rctx); + + LOG_DBG("RX bitrate: %.1f Mbit/s", rctx.bitrate / 1000. / 1000.); + mtx_lock(&mod->lock); + iface->rx_bitrate = rctx.bitrate; + mtx_unlock(&mod->lock); + ctx->update_bar = true; + break; + } + + default: + LOG_DBG("%s: unrecognized nl80211 station info attribute: " + "type=%hu, nested=%d, len=%zu", + iface->name, type, nested, len); + break; + } + + return true; +} + +static bool +handle_nl80211_new_station(struct module *mod, struct iface *iface, uint16_t type, bool nested, const void *payload, + size_t len, void *_ctx) +{ + switch (type) { + case NL80211_ATTR_IFINDEX: + break; + + case NL80211_ATTR_STA_INFO: { + struct station_info_ctx ctx = {0}; + foreach_nlattr_nested(mod, iface, payload, len, &handle_nl80211_station_info, &ctx); + + if (ctx.update_bar) + mod->bar->refresh(mod->bar); + break; + } + + default: + LOG_DBG("%s: unrecognized nl80211 attribute: " + "type=%hu, nested=%d, len=%zu", + iface->name, type, nested, len); + break; + } + + return true; +} + +static bool +handle_ies(struct module *mod, struct iface *iface, const void *_ies, size_t len) +{ + const uint8_t *ies = _ies; + + while (len >= 2 && len - 2 >= ies[1]) { + switch (ies[0]) { + case 0: { /* SSID */ + const char *ssid = (const char *)&ies[2]; + const size_t ssid_len = ies[1]; + + if (iface->ssid == NULL || strncmp(iface->ssid, ssid, ssid_len) != 0) + LOG_INFO("%s: SSID: %.*s", iface->name, (int)ssid_len, ssid); + + mtx_lock(&mod->lock); + free(iface->ssid); + iface->ssid = strndup(ssid, ssid_len); + mtx_unlock(&mod->lock); + + mod->bar->refresh(mod->bar); + break; + } + } + len -= ies[1] + 2; + ies += ies[1] + 2; + } + + return true; +} + +struct scan_results_context { + bool associated; + + const void *ies; + size_t ies_size; +}; + +static bool +handle_nl80211_bss(struct module *mod, struct iface *iface, uint16_t type, bool nested, const void *payload, size_t len, + void *_ctx) +{ + struct scan_results_context *ctx = _ctx; + + switch (type) { + case NL80211_BSS_STATUS: { + const uint32_t status = *(uint32_t *)payload; + + if (status == NL80211_BSS_STATUS_ASSOCIATED) { + ctx->associated = true; + + if (ctx->ies != NULL) { + /* Deferred handling of BSS_INFORMATION_ELEMENTS */ + return handle_ies(mod, iface, ctx->ies, ctx->ies_size); + } + } + break; + } + + case NL80211_BSS_INFORMATION_ELEMENTS: + if (ctx->associated) + return handle_ies(mod, iface, payload, len); + else { + /* + * We’re either not associated, or, we haven’t seen the + * BSS_STATUS attribute yet. + * + * Save a pointer to the IES payload, so that we can + * process it later, if we see a + * BSS_STATUS == BSS_STATUS_ASSOCIATED. + */ + ctx->ies = payload; + ctx->ies_size = len; + } + } + + return true; +} + +static bool +handle_nl80211_scan_results(struct module *mod, struct iface *iface, uint16_t type, bool nested, const void *payload, + size_t len, void *_ctx) +{ + struct scan_results_context ctx = {0}; + + switch (type) { + case NL80211_ATTR_BSS: + foreach_nlattr_nested(mod, iface, payload, len, &handle_nl80211_bss, &ctx); + break; + } + + return true; +} + /* * Reads at least one (possibly more) message. * @@ -400,24 +1234,57 @@ netlink_receive_messages(int sock, void **reply, size_t *len) return true; } +static void +handle_stats(struct module *mod, uint32_t seq, struct rt_stats_msg *msg) +{ + struct private *m = mod->private; + + struct iface *iface = NULL; + + tll_foreach(m->ifaces, it) + { + if (seq != it->item.get_stats_seq_nr) + continue; + iface = &it->item; + /* Current request is now considered complete */ + iface->get_stats_seq_nr = 0; + break; + } + + if (iface == NULL) { + LOG_ERR("Couldn't find iface"); + return; + } + + uint64_t ul_bits = msg->stats.tx_bytes * 8; + uint64_t dl_bits = msg->stats.rx_bytes * 8; + + const double poll_interval_secs = (double)m->poll_interval / 1000.; + + if (iface->ul_bits != 0) + iface->ul_speed = (double)(ul_bits - iface->ul_bits) / poll_interval_secs; + if (iface->dl_bits != 0) + iface->dl_speed = (double)(dl_bits - iface->dl_bits) / poll_interval_secs; + + iface->ul_bits = ul_bits; + iface->dl_bits = dl_bits; +} + static bool -parse_reply(struct module *mod, const struct nlmsghdr *hdr, size_t len) +parse_rt_reply(struct module *mod, const struct nlmsghdr *hdr, size_t len) { struct private *m = mod->private; /* Process response */ for (; NLMSG_OK(hdr, len); hdr = NLMSG_NEXT(hdr, len)) { + switch (hdr->nlmsg_type) { case NLMSG_DONE: - if (m->ifindex == -1) { - LOG_ERR("%s: failed to find interface", m->iface); - return false; - } /* Request initial list of IPv4/6 addresses */ - if (m->get_addresses && m->ifindex != -1) { + if (m->get_addresses) { m->get_addresses = false; - send_rt_request(m->nl_sock, RTM_GETADDR); + send_rt_request(m, RTM_GETADDR); } break; @@ -438,17 +1305,146 @@ parse_reply(struct module *mod, const struct nlmsghdr *hdr, size_t len) handle_address(mod, hdr->nlmsg_type, msg, msg_len); break; } + case RTM_NEWSTATS: { + struct rt_stats_msg *msg = NLMSG_DATA(hdr); + handle_stats(mod, hdr->nlmsg_seq, msg); + break; + } - case NLMSG_ERROR:{ + case NLMSG_ERROR: { const struct nlmsgerr *err = NLMSG_DATA(hdr); - LOG_ERRNO_P("netlink", err->error); + LOG_ERRNO_P(-err->error, "netlink RT reply"); return false; } default: - LOG_WARN( - "unrecognized netlink message type: 0x%x", hdr->nlmsg_type); - break; + LOG_WARN("unrecognized netlink message type: 0x%x", hdr->nlmsg_type); + return false; + } + } + + return true; +} + +static bool +parse_genl_reply(struct module *mod, const struct nlmsghdr *hdr, size_t len) +{ + struct private *m = mod->private; + struct iface *iface = NULL; + + for (; NLMSG_OK(hdr, len); hdr = NLMSG_NEXT(hdr, len)) { + if (hdr->nlmsg_type == GENL_ID_CTRL) { + assert(hdr->nlmsg_seq == 1); + const struct genlmsghdr *genl = NLMSG_DATA(hdr); + const size_t msg_size = NLMSG_PAYLOAD(hdr, 0); + foreach_nlattr(mod, NULL, genl, msg_size, &handle_genl_ctrl, NULL); + continue; + } + + if (hdr->nlmsg_seq == m->nl80211.get_interface_seq_nr) { + /* Current request is now considered complete */ + m->nl80211.get_interface_seq_nr = 0; + + /* Can’t issue both get-station and get-scan at the + * same time. So, always run a get-scan when a + * get-station is complete */ + send_nl80211_get_scan(m); + } + + if (hdr->nlmsg_type == NLMSG_DONE) { + if (hdr->nlmsg_seq == m->nl80211.get_scan_seq_nr) { + /* Current request is now considered complete */ + m->nl80211.get_scan_seq_nr = 0; + + tll_foreach(m->ifaces, it) send_nl80211_get_station(m, &it->item); + } + } + + else if (hdr->nlmsg_type == m->nl80211.family_id) { + const struct genlmsghdr *genl = NLMSG_DATA(hdr); + const size_t msg_size = NLMSG_PAYLOAD(hdr, 0); + + switch (genl->cmd) { + case NL80211_CMD_NEW_INTERFACE: + if (foreach_nlattr(mod, NULL, genl, msg_size, &find_nl80211_iface, &iface)) + 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; + + case NL80211_CMD_CONNECT: + /* + * Update SSID + * + * Unfortunately, the SSID doesn’t appear to be + * included in *any* of the notifications sent when + * associating, authenticating and connecting to a + * station. + * + * Thus, we need to explicitly request an update. + */ + LOG_DBG("connected, requesting interface information"); + send_nl80211_get_interface(m); + break; + + case NL80211_CMD_DISCONNECT: + if (foreach_nlattr(mod, NULL, genl, msg_size, &find_nl80211_iface, &iface)) + continue; + + LOG_DBG("%s: disconnected, resetting SSID etc", iface->name); + + mtx_lock(&mod->lock); + free(iface->ssid); + iface->ssid = NULL; + iface->signal_strength_dbm = 0; + iface->rx_bitrate = iface->tx_bitrate = 0; + mtx_unlock(&mod->lock); + break; + + case NL80211_CMD_NEW_STATION: + if (foreach_nlattr(mod, NULL, genl, msg_size, &find_nl80211_iface, &iface)) + continue; + + LOG_DBG("%s: got station information", iface->name); + foreach_nlattr(mod, iface, genl, msg_size, &handle_nl80211_new_station, NULL); + + LOG_DBG("%s: signal: %d dBm, RX=%u Mbit/s, TX=%u Mbit/s", iface->name, iface->signal_strength_dbm, + iface->rx_bitrate / 1000 / 1000, iface->tx_bitrate / 1000 / 1000); + break; + + case NL80211_CMD_NEW_SCAN_RESULTS: + if (foreach_nlattr(mod, NULL, genl, msg_size, &find_nl80211_iface, &iface)) + continue; + + LOG_DBG("%s: got scan results", iface->name); + foreach_nlattr(mod, iface, genl, msg_size, &handle_nl80211_scan_results, NULL); + break; + + default: + LOG_DBG("unrecognized nl80211 command: %hhu", genl->cmd); + break; + } + } + + else if (hdr->nlmsg_type == NLMSG_ERROR) { + const struct nlmsgerr *err = NLMSG_DATA(hdr); + int nl_errno = -err->error; + + if (nl_errno == ENODEV) + ; /* iface is not an nl80211 device */ + else if (nl_errno == ENOENT) + ; /* iface down? */ + else { + LOG_ERRNO_P(nl_errno, "nl80211 reply (seq-nr: %u)", hdr->nlmsg_seq); + } + } + + else { + LOG_WARN("unrecognized netlink message type: 0x%x", hdr->nlmsg_type); + return false; } } @@ -458,92 +1454,195 @@ parse_reply(struct module *mod, const struct nlmsghdr *hdr, size_t len) static int run(struct module *mod) { + int ret = 1; struct private *m = mod->private; - m->nl_sock = netlink_connect(); - if (m->nl_sock == -1) - return 1; + int timer_fd = -1; + if (m->poll_interval > 0) { + timer_fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC); + if (timer_fd < 0) { + LOG_ERRNO("failed to create poll timer FD"); + goto out; + } - if (!send_rt_request(m->nl_sock, RTM_GETLINK)) { - close(m->nl_sock); - m->nl_sock = -1; - return 1; + const long secs = m->poll_interval / 1000; + const long msecs = m->poll_interval % 1000; + + struct itimerspec poll_time = { + .it_value = {.tv_sec = secs, .tv_nsec = msecs * 1000000}, + .it_interval = {.tv_sec = secs, .tv_nsec = msecs * 1000000}, + }; + + if (timerfd_settime(timer_fd, 0, &poll_time, NULL) < 0) { + LOG_ERRNO("failed to arm poll timer"); + goto out; + } + } + + m->rt_sock = netlink_connect_rt(); + m->genl_sock = netlink_connect_genl(); + + if (m->rt_sock < 0 || m->genl_sock < 0) + goto out; + + if (!send_rt_request(m, RTM_GETLINK) || !send_ctrl_get_family_request(m)) { + goto out; } /* Main loop */ while (true) { struct pollfd fds[] = { {.fd = mod->abort_fd, .events = POLLIN}, - {.fd = m->nl_sock, .events = POLLIN} + {.fd = m->rt_sock, .events = POLLIN}, + {.fd = m->genl_sock, .events = POLLIN}, + {.fd = timer_fd, .events = POLLIN}, }; - poll(fds, 2, -1); + poll(fds, 3 + (timer_fd >= 0 ? 1 : 0), -1); - if (fds[0].revents & POLLIN) + if (fds[0].revents & (POLLIN | POLLHUP)) break; - if (fds[1].revents & POLLHUP) { + if ((fds[1].revents & POLLHUP) || (fds[2].revents & POLLHUP)) { LOG_ERR("disconnected from netlink socket"); break; } - assert(fds[1].revents & POLLIN); - - /* Read one (or more) messages */ - void *reply; - size_t len; - if (!netlink_receive_messages(m->nl_sock, &reply, &len)) - break; - - /* Parse (and act upon) the received message(s) */ - if (!parse_reply(mod, (const struct nlmsghdr *)reply, len)) { - free(reply); + if (fds[3].revents & POLLHUP) { + LOG_ERR("disconnected from timer FD"); break; } - free(reply); + if (fds[1].revents & POLLIN) { + /* Read one (or more) messages */ + void *reply; + size_t len; + if (!netlink_receive_messages(m->rt_sock, &reply, &len)) + break; + + /* Parse (and act upon) the received message(s) */ + if (!parse_rt_reply(mod, (const struct nlmsghdr *)reply, len)) { + free(reply); + break; + } + + free(reply); + } + + if (fds[2].revents & POLLIN) { + /* Read one (or more) messages */ + void *reply; + size_t len; + if (!netlink_receive_messages(m->genl_sock, &reply, &len)) + break; + + if (!parse_genl_reply(mod, (const struct nlmsghdr *)reply, len)) { + free(reply); + break; + } + + free(reply); + } + + if (fds[3].revents & POLLIN) { + uint64_t count; + ssize_t amount = read(timer_fd, &count, sizeof(count)); + if (amount < 0) { + LOG_ERRNO("failed to read from timer FD"); + break; + } + + tll_foreach(m->ifaces, it) + { + send_nl80211_get_station(m, &it->item); + send_rt_getstats_request(m, &it->item); + }; + } } - close(m->nl_sock); - m->nl_sock = -1; - return 0; + ret = 0; + +out: + if (m->rt_sock >= 0) + close(m->rt_sock); + if (m->genl_sock >= 0) + close(m->genl_sock); + if (timer_fd >= 0) + close(timer_fd); + m->rt_sock = m->genl_sock = -1; + return ret; } static struct module * -network_new(const char *iface, struct particle *label) +network_new(struct particle *label, int poll_interval, int left_spacing, int right_spacing) { - struct private *priv = calloc(1, sizeof(*priv)); - priv->iface = strdup(iface); - priv->label = label; + int urandom_fd = open("/dev/urandom", O_RDONLY | O_CLOEXEC); + if (urandom_fd < 0) { + LOG_ERRNO("failed to open /dev/urandom"); + return NULL; + } - priv->nl_sock = -1; - priv->get_addresses = true; - priv->ifindex = -1; - priv->state = IF_OPER_DOWN; + struct private *priv = calloc(1, sizeof(*priv)); + priv->label = label; + priv->poll_interval = poll_interval; + priv->left_spacing = left_spacing; + priv->right_spacing = right_spacing; + + priv->genl_sock = -1; + priv->rt_sock = -1; + priv->urandom_fd = urandom_fd; + priv->get_addresses = false; + priv->nl80211.family_id = -1; struct module *mod = module_common_new(); mod->private = priv; mod->run = &run; mod->destroy = &destroy; mod->content = &content; + mod->description = &description; return mod; } static struct module * from_conf(const struct yml_node *node, struct conf_inherit inherited) { - const struct yml_node *name = yml_get_value(node, "name"); const struct yml_node *content = yml_get_value(node, "content"); + const struct yml_node *poll = yml_get_value(node, "poll-interval"); + const struct yml_node *spacing = yml_get_value(node, "spacing"); + const struct yml_node *left_spacing = yml_get_value(node, "left-spacing"); + const struct yml_node *right_spacing = yml_get_value(node, "right-spacing"); - return network_new( - yml_value_as_string(name), conf_to_particle(content, inherited)); + 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 network_new(conf_to_particle(content, inherited), poll != NULL ? yml_value_as_int(poll) : 0, left, right); +} + +static bool +conf_verify_poll_interval(keychain_t *chain, const struct yml_node *node) +{ + if (!conf_verify_unsigned(chain, node)) + return false; + + int interval = yml_value_as_int(node); + if (interval > 0 && interval < min_poll_interval) { + LOG_ERR("%s: interval value cannot be less than %ldms", conf_err_prefix(chain, node), min_poll_interval); + return false; + } + + return true; } static bool verify_conf(keychain_t *chain, const struct yml_node *node) { static const struct attr_info attrs[] = { - {"name", true, &conf_verify_string}, + {"spacing", false, &conf_verify_unsigned}, + {"left-spacing", false, &conf_verify_unsigned}, + {"right-spacing", false, &conf_verify_unsigned}, + {"poll-interval", false, &conf_verify_poll_interval}, MODULE_COMMON_ATTRS, }; 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 new file mode 100644 index 0000000..1ff3642 --- /dev/null +++ b/modules/pipewire.c @@ -0,0 +1,1025 @@ +#include "spa/utils/list.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define LOG_MODULE "pipewire" +#define LOG_ENABLE_DBG 0 +#include "../config-verify.h" +#include "../config.h" +#include "../log.h" +#include "../module.h" +#include "../particle.h" +#include "../particles/dynlist.h" +#include "../plugin.h" +#include "../yml.h" + +#define ARRAY_LENGTH(x) (sizeof((x)) / sizeof((x)[0])) +/* clang-format off */ +#define X_FREE_SET(t, v) do { free((t)); (t) = (v); } while (0) +/* clang-format on */ +#define X_STRDUP(s) ((s) != NULL ? strdup((s)) : NULL) + +struct output_informations { + /* internal */ + uint32_t device_id; + uint32_t card_profile_device_id; + + /* information */ + bool muted; + uint16_t linear_volume; /* classic volume */ + uint16_t cubic_volume; /* volume a la pulseaudio */ + char *name; + char *description; + char *form_factor; /* headset, headphone, speaker, ..., can be null */ + char *bus; /* alsa, bluetooth, etc */ + char *icon; +}; +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; + struct output_informations source_informations; +}; + +/* This struct is needed because when param event occur, the function + * `node_events_param` will receive the corresponding event about the node + * but there's no simple way of knowing from which node the event come from */ +struct node_data { + struct data *data; + /* otherwise is_source */ + bool is_sink; +}; + +/* struct data */ +struct node; +struct data { + /* yambar module */ + struct module *module; + + char *target_sink; + char *target_source; + + struct node *binded_sink; + struct node *binded_source; + + struct node_data node_data_sink; + struct node_data node_data_source; + + /* proxies */ + void *metadata; + void *node_sink; + void *node_source; + + /* main struct */ + struct pw_main_loop *loop; + struct pw_context *context; + struct pw_core *core; + struct pw_registry *registry; + + /* listener */ + struct spa_hook registry_listener; + struct spa_hook core_listener; + struct spa_hook metadata_listener; + struct spa_hook node_sink_listener; + struct spa_hook node_source_listener; + + /* list */ + struct spa_list node_list; + struct spa_list device_list; + + int sync; +}; + +/* struct Route */ +struct route { + struct device *device; + + struct spa_list link; + + enum spa_direction direction; /* direction */ + int profile_device_id; /* device */ + char *form_factor; /* info.type */ + char *icon_name; /* info.icon-name */ +}; + +static void +route_free(struct route *route) +{ + free(route->form_factor); + free(route->icon_name); + spa_list_remove(&route->link); + free(route); +} + +/* struct Device */ +struct device { + struct data *data; + + struct spa_list link; + uint32_t id; + struct spa_list routes; + + void *proxy; + struct spa_hook listener; +}; + +static void +device_free(struct device *device, struct data *data) +{ + struct route *route = NULL; + spa_list_consume(route, &device->routes, link) route_free(route); + + spa_hook_remove(&device->listener); + pw_proxy_destroy((struct pw_proxy *)device->proxy); + + spa_list_remove(&device->link); + free(device); +} + +static struct route * +route_find_or_create(struct device *device, uint32_t profile_device_id) +{ + struct route *route = NULL; + spa_list_for_each(route, &device->routes, link) + { + if (route->profile_device_id == profile_device_id) + return route; + } + + /* route not found, let's create it */ + route = calloc(1, sizeof(struct route)); + assert(route != NULL); + route->device = device; + route->profile_device_id = profile_device_id; + spa_list_append(&device->routes, &route->link); + return route; +} + +struct node { + struct spa_list link; + uint32_t id; + char *name; +}; + +/* struct node */ +static struct route * +node_find_route(struct data *data, bool is_sink) +{ + struct private *private = data->module->private; + struct output_informations *output_informations = NULL; + + if (is_sink) { + if (data->node_sink == NULL) + return NULL; + output_informations = &private->sink_informations; + } else { + if (data->node_source == NULL) + return NULL; + output_informations = &private->source_informations; + } + + struct device *device = NULL; + spa_list_for_each(device, &data->device_list, link) + { + if (device->id != output_informations->device_id) + continue; + + struct route *route = NULL; + spa_list_for_each(route, &device->routes, link) + { + if (route->profile_device_id == output_informations->card_profile_device_id) + return route; + } + } + + return NULL; +} + +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) + return; + + spa_hook_remove(target_listener); + pw_proxy_destroy(*target_proxy); + + *target_node = NULL; + *target_proxy = NULL; + + output_informations_destroy(output_informations); + *output_informations = output_informations_null; +} + +static void +node_free(struct node *node, struct data *data) +{ + if (data->binded_sink == node) + node_unhook_binded_node(data, true); + else if (data->binded_source == node) + node_unhook_binded_node(data, false); + + spa_list_remove(&node->link); + free(node->name); + free(node); +} + +/* Device events */ +static void +device_events_info(void *userdata, const struct pw_device_info *info) +{ + struct device *device = userdata; + + /* We only want the "Route" param, which is in Params */ + if (!(info->change_mask & PW_DEVICE_CHANGE_MASK_PARAMS)) + return; + + for (size_t i = 0; i < info->n_params; ++i) { + if (info->params[i].id == SPA_PARAM_Route) { + pw_device_enum_params(device->proxy, 0, info->params[i].id, 0, -1, NULL); + break; + } + } +} + +static void +device_events_param(void *userdata, int seq, uint32_t id, uint32_t index, uint32_t next, const struct spa_pod *param) +{ + /* We should only receive ParamRoute */ + assert(spa_pod_is_object_type(param, SPA_TYPE_OBJECT_ParamRoute)); + + struct route data = {0}; + struct spa_pod_prop const *prop = NULL; + + /* device must be present otherwise I can't do anything with the data */ + prop = spa_pod_find_prop(param, NULL, SPA_PARAM_ROUTE_device); + if (prop == NULL) + return; + spa_pod_get_int(&prop->value, &data.profile_device_id); + + /* same for direction, required too */ + prop = spa_pod_find_prop(param, NULL, SPA_PARAM_ROUTE_direction); + if (prop == NULL) + return; + char const *direction = NULL; + spa_pod_get_string(&prop->value, &direction); + if (spa_streq(direction, "Output")) + data.direction = SPA_DIRECTION_OUTPUT; + else + data.direction = SPA_DIRECTION_INPUT; + + /* same for info, it's required */ + prop = spa_pod_find_prop(param, NULL, SPA_PARAM_ROUTE_info); + if (prop == NULL) + return; + + struct spa_pod *iter = NULL; + char const *header = NULL; + SPA_POD_STRUCT_FOREACH(&prop->value, iter) + { + /* no previous header */ + if (header == NULL) { + /* headers are always string */ + if (spa_pod_is_string(iter)) + spa_pod_get_string(iter, &header); + /* otherwise it's the first iteration (number of elements in the struct) */ + continue; + } + + /* Values needed: + * - (string) device.icon_name [icon_name] + * - (string) port.type [form_factor] */ + if (spa_pod_is_string(iter)) { + if (spa_streq(header, "device.icon_name")) + spa_pod_get_string(iter, (char const **)&data.icon_name); + else if (spa_streq(header, "port.type")) { + spa_pod_get_string(iter, (char const **)&data.form_factor); + } + } + + header = NULL; + } + + struct device *device = userdata; + + struct route *route = route_find_or_create(device, data.profile_device_id); + X_FREE_SET(route->form_factor, X_STRDUP(data.form_factor)); + X_FREE_SET(route->icon_name, X_STRDUP(data.icon_name)); + route->direction = data.direction; + + /* set missing information if possible */ + struct private *private = device->data->module->private; + struct node *binded_node = NULL; + struct output_informations *output_informations = NULL; + + if (route->direction == SPA_DIRECTION_INPUT) { + binded_node = private->data->binded_source; + output_informations = &private->source_informations; + } else { + binded_node = private->data->binded_sink; + output_informations = &private->sink_informations; + } + + /* Node not binded */ + if (binded_node == NULL) + return; + + /* Node's device is the same as route's device */ + if (output_informations->device_id != route->device->id) + return; + + /* Route is not the Node's device route */ + if (output_informations->card_profile_device_id != route->profile_device_id) + return; + + /* 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)); + + device->data->module->bar->refresh(device->data->module->bar); +} + +static struct pw_device_events const device_events = { + PW_VERSION_DEVICE_EVENTS, + .info = device_events_info, + .param = device_events_param, +}; + +/* Node events */ +static void +node_events_info(void *userdata, struct pw_node_info const *info) +{ + struct node_data *node_data = userdata; + struct data *data = node_data->data; + struct private *private = data->module->private; + + if (info->change_mask & PW_NODE_CHANGE_MASK_PARAMS) { + /* We only need the Props param, so let's try to find it */ + 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 param will then be handled + * in node_events_param */ + pw_node_enum_params(target_node, 0, info->params[i].id, 0, -1, NULL); + break; + } + } + } + + if (info->change_mask & PW_NODE_CHANGE_MASK_PROPS) { + struct output_informations *output_informations + = (node_data->is_sink ? &private->sink_informations : &private->source_informations); + struct spa_dict_item const *item = NULL; + + item = spa_dict_lookup_item(info->props, "node.name"); + X_FREE_SET(output_informations->name, item != NULL ? X_STRDUP(item->value) : NULL); + + item = spa_dict_lookup_item(info->props, "node.description"); + 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"); + if (item != NULL) { + 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 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) + X_FREE_SET(output_informations->icon, X_STRDUP(route->icon_name)); + else { + item = spa_dict_lookup_item(info->props, "device.icon-name"); + X_FREE_SET(output_informations->icon, item != NULL ? X_STRDUP(item->value) : NULL); + } + /* form_factor */ + if (route != NULL && route->form_factor != NULL) + X_FREE_SET(output_informations->form_factor, X_STRDUP(route->form_factor)); + else { + item = spa_dict_lookup_item(info->props, "device.form-factor"); + X_FREE_SET(output_informations->form_factor, item != NULL ? X_STRDUP(item->value) : NULL); + } + + item = spa_dict_lookup_item(info->props, "device.bus"); + X_FREE_SET(output_informations->bus, item != NULL ? X_STRDUP(item->value) : NULL); + + data->module->bar->refresh(data->module->bar); + } +} + +static void +node_events_param(void *userdata, __attribute__((unused)) int seq, __attribute__((unused)) uint32_t id, + __attribute__((unused)) uint32_t index, __attribute__((unused)) uint32_t next, + const struct spa_pod *param) +{ + struct node_data *node_data = userdata; + struct data *data = node_data->data; + struct private *private = data->module->private; + + struct output_informations *output_informations + = (node_data->is_sink ? &private->sink_informations : &private->source_informations); + struct spa_pod_prop const *prop = NULL; + + prop = spa_pod_find_prop(param, NULL, SPA_PROP_mute); + if (prop != NULL) { + bool value = false; + spa_pod_get_bool(&prop->value, &value); + output_informations->muted = value; + } + + prop = spa_pod_find_prop(param, NULL, SPA_PROP_channelVolumes); + if (prop != NULL) { + uint32_t n_values = 0; + float *values = spa_pod_get_array(&prop->value, &n_values); + float total = 0.0f; + + /* Array can be empty some times, assume that values have not changed */ + if (n_values != 0) { + for (uint32_t i = 0; i < n_values; ++i) + total += values[i]; + + float base_volume = total / n_values; + output_informations->linear_volume = roundf(base_volume * 100); + output_informations->cubic_volume = roundf(cbrtf(base_volume) * 100); + } + } + + data->module->bar->refresh(data->module->bar); +} + +static struct pw_node_events const node_events = { + PW_VERSION_NODE_EVENTS, + .info = node_events_info, + .param = node_events_param, +}; + +/* Metadata events */ +static int +metadata_property(void *userdata, __attribute__((unused)) uint32_t id, char const *key, + __attribute__((unused)) char const *type, char const *value) +{ + struct data *data = userdata; + bool is_sink = false; // true for source mode + char **target_name = NULL; + + /* We only want default.audio.sink or default.audio.source */ + if (spa_streq(key, "default.audio.sink")) { + is_sink = true; + target_name = &data->target_sink; + } else if (spa_streq(key, "default.audio.source")) { + is_sink = false; /* just to be explicit */ + target_name = &data->target_source; + } else + return 0; + + /* Value is NULL when the profile is set to `off`. */ + if (value == NULL) { + node_unhook_binded_node(data, is_sink); + free(*target_name); + *target_name = NULL; + data->module->bar->refresh(data->module->bar); + return 0; + } + + struct json_object *json = json_tokener_parse(value); + struct json_object_iterator json_it = json_object_iter_begin(json); + struct json_object_iterator json_it_end = json_object_iter_end(json); + + while (!json_object_iter_equal(&json_it, &json_it_end)) { + char const *key = json_object_iter_peek_name(&json_it); + if (!spa_streq(key, "name")) { + json_object_iter_next(&json_it); + continue; + } + + /* Found name */ + struct json_object *value = json_object_iter_peek_value(&json_it); + assert(json_object_is_type(value, json_type_string)); + + char const *name = json_object_get_string(value); + /* `auto_null` is the same as `value == NULL` see lines above. */ + if (spa_streq(name, "auto_null")) { + node_unhook_binded_node(data, is_sink); + free(*target_name); + *target_name = NULL; + data->module->bar->refresh(data->module->bar); + break; + } + + /* target_name is the same */ + if (spa_streq(name, *target_name)) + break; + + /* Unhook the binded_node */ + node_unhook_binded_node(data, is_sink); + + /* Update the target */ + free(*target_name); + *target_name = strdup(name); + + /* Sync the core, core_events_done will then try to bind the good node */ + data->sync = pw_core_sync(data->core, PW_ID_CORE, data->sync); + break; + } + + json_object_put(json); + + return 0; +} + +static struct pw_metadata_events const metadata_events = { + PW_VERSION_METADATA_EVENTS, + .property = metadata_property, +}; + +/* Registry events */ +static void +registry_event_global(void *userdata, uint32_t id, __attribute__((unused)) uint32_t permissions, char const *type, + __attribute__((unused)) uint32_t version, struct spa_dict const *props) +{ + struct data *data = userdata; + + /* New device */ + if (spa_streq(type, PW_TYPE_INTERFACE_Device)) { + struct device *device = calloc(1, sizeof(struct device)); + assert(device != NULL); + device->data = data; + device->id = id; + spa_list_init(&device->routes); + device->proxy = pw_registry_bind(data->registry, id, type, PW_VERSION_DEVICE, 0); + assert(device->proxy != NULL); + pw_device_add_listener(device->proxy, &device->listener, &device_events, device); + + spa_list_append(&data->device_list, &device->link); + } + /* New node */ + else if (spa_streq(type, PW_TYPE_INTERFACE_Node)) { + /* Fill a new node */ + struct node *node = calloc(1, sizeof(struct node)); + assert(node != NULL); + node->id = id; + node->name = strdup(spa_dict_lookup(props, PW_KEY_NODE_NAME)); + + /* Store it */ + spa_list_append(&data->node_list, &node->link); + } + /* New metadata */ + else if (spa_streq(type, PW_TYPE_INTERFACE_Metadata)) { + /* A metadata has already been bind */ + if (data->metadata != NULL) + return; + + /* Target only metadata which has "default" key */ + char const *name = spa_dict_lookup(props, PW_KEY_METADATA_NAME); + if (name == NULL || !spa_streq(name, "default")) + return; + + /* Bind metadata */ + data->metadata = pw_registry_bind(data->registry, id, type, PW_VERSION_METADATA, 0); + assert(data->metadata != NULL); + pw_metadata_add_listener(data->metadata, &data->metadata_listener, &metadata_events, data); + } + + /* `core_events_done` will then try to bind the good node */ + data->sync = pw_core_sync(data->core, PW_ID_CORE, data->sync); +} + +static void +registry_event_global_remove(void *userdata, uint32_t id) +{ + struct data *data = userdata; + + /* Try to find a node with the same `id` */ + struct node *node = NULL, *node_temp = NULL; + spa_list_for_each_safe(node, node_temp, &data->node_list, link) + { + if (node->id == id) { + node_free(node, data); + return; + } + } + + /* No node with this `id` maybe it's a device */ + struct device *device = NULL, *device_temp = NULL; + spa_list_for_each_safe(device, device_temp, &data->device_list, link) + { + if (device->id == id) { + device_free(device, data); + return; + } + } +} + +static struct pw_registry_events const registry_events = { + PW_VERSION_REGISTRY_EVENTS, + .global = registry_event_global, + .global_remove = registry_event_global_remove, +}; + +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 deactivated */ + if (target_name == NULL) + return; + + struct data *data = node_data->data; + + struct node *node = NULL; + spa_list_for_each(node, &data->node_list, link) + { + if (!spa_streq(target_name, node->name)) + continue; + + /* Found good node */ + + *target_node = node; + *target_proxy = pw_registry_bind(data->registry, node->id, PW_TYPE_INTERFACE_Node, PW_VERSION_NODE, 0); + assert(*target_proxy != NULL); + pw_node_add_listener(*target_proxy, target_listener, &node_events, node_data); + break; + } +} + +/* Core events */ +static void +core_events_done(void *userdata, uint32_t id, int seq) +{ + struct data *data = userdata; + + if (id != PW_ID_CORE) + return; + + /* Not our seq */ + if (data->sync != seq) + return; + + /* Sync ended, try to bind the node which has the targeted sink or the targeted source */ + + /* Node sink not binded and target_sink is set */ + if (data->binded_sink == NULL && data->target_sink != NULL) + try_to_bind_node(&data->node_data_sink, data->target_sink, &data->binded_sink, &data->node_sink, + &data->node_sink_listener); + + /* Node source not binded and target_source is set */ + if (data->binded_source == NULL && data->target_source != NULL) + try_to_bind_node(&data->node_data_source, data->target_source, &data->binded_source, &data->node_source, + &data->node_source_listener); +} + +static void +core_events_error(void *userdata, uint32_t id, int seq, int res, char const *message) +{ + pw_log_error("error id:%u seq:%d res:%d (%s): %s", id, seq, res, spa_strerror(res), message); + + if (id == PW_ID_CORE && res == -EPIPE) { + struct data *data = userdata; + pw_main_loop_quit(data->loop); + } +} + +static struct pw_core_events const core_events = { + PW_VERSION_CORE_EVENTS, + .done = core_events_done, + .error = core_events_error, +}; + +/* init, deinit */ +static struct data * +pipewire_init(struct module *module) +{ + pw_init(NULL, NULL); + + /* Data */ + struct data *data = calloc(1, sizeof(struct data)); + assert(data != NULL); + + spa_list_init(&data->node_list); + spa_list_init(&data->device_list); + + /* Main loop */ + data->loop = pw_main_loop_new(NULL); + if (data->loop == NULL) { + LOG_ERR("failed to instantiate main loop"); + goto err; + } + + /* Context */ + data->context = pw_context_new(pw_main_loop_get_loop(data->loop), NULL, 0); + if (data->context == NULL) { + LOG_ERR("failed to instantiate pipewire context"); + goto err; + } + + /* Core */ + data->core = pw_context_connect(data->context, NULL, 0); + if (data->core == NULL) { + LOG_ERR("failed to connect to pipewire"); + goto err; + } + pw_core_add_listener(data->core, &data->core_listener, &core_events, data); + + /* Registry */ + data->registry = pw_core_get_registry(data->core, PW_VERSION_REGISTRY, 0); + if (data->registry == NULL) { + LOG_ERR("failed to get core registry"); + goto err; + } + pw_registry_add_listener(data->registry, &data->registry_listener, ®istry_events, data); + + /* Sync */ + data->sync = pw_core_sync(data->core, PW_ID_CORE, data->sync); + + data->module = module; + + /* node_events_param_data */ + data->node_data_sink.data = data; + data->node_data_sink.is_sink = true; + data->node_data_source.data = data; + data->node_data_source.is_sink = false; + + return data; + +err: + if (data->registry != NULL) + pw_proxy_destroy((struct pw_proxy *)data->registry); + if (data->core != NULL) + pw_core_disconnect(data->core); + if (data->context != NULL) + pw_context_destroy(data->context); + if (data->loop != NULL) + pw_main_loop_destroy(data->loop); + free(data); + return NULL; +} + +static void +pipewire_deinit(struct data *data) +{ + if (data == NULL) + return; + + struct node *node = NULL; + spa_list_consume(node, &data->node_list, link) node_free(node, data); + + struct device *device = NULL; + spa_list_consume(device, &data->device_list, link) device_free(device, data); + + if (data->metadata) + pw_proxy_destroy((struct pw_proxy *)data->metadata); + spa_hook_remove(&data->registry_listener); + pw_proxy_destroy((struct pw_proxy *)data->registry); + spa_hook_remove(&data->core_listener); + spa_hook_remove(&data->metadata_listener); + pw_core_disconnect(data->core); + pw_context_destroy(data->context); + pw_main_loop_destroy(data->loop); + free(data->target_sink); + free(data->target_source); + pw_deinit(); +} + +static void +destroy(struct module *module) +{ + struct private *private = module->private; + + pipewire_deinit(private->data); + private->label->destroy(private->label); + + output_informations_destroy(&private->sink_informations); + output_informations_destroy(&private->source_informations); + + free(private); + module_default_destroy(module); +} + +static char const * +description(const struct module *module) +{ + return "pipewire"; +} + +static struct exposable * +content(struct module *module) +{ + struct private *private = module->private; + + if (private->data == NULL) + return dynlist_exposable_new(NULL, 0, 0, 0); + + mtx_lock(&module->lock); + + struct exposable *exposables[2]; + size_t exposables_length = ARRAY_LENGTH(exposables); + + struct output_informations const *output_informations = NULL; + + /* sink */ + output_informations + = (private->data->target_sink == NULL ? &output_informations_null : &private->sink_informations); + + struct tag_set sink_tag_set = (struct tag_set){ + .tags = (struct tag *[]){ + tag_new_string(module, "type", "sink"), + tag_new_string(module, "name", output_informations->name), + tag_new_string(module, "description", output_informations->description), + tag_new_string(module, "icon", output_informations->icon), + tag_new_string(module, "form_factor", output_informations->form_factor), + tag_new_string(module, "bus", output_informations->bus), + tag_new_bool(module, "muted", output_informations->muted), + tag_new_int_range(module, "linear_volume", output_informations->linear_volume, 0, 100), + tag_new_int_range(module, "cubic_volume", output_informations->cubic_volume, 0, 100), + }, + .count = 9, + }; + + /* source */ + output_informations + = (private->data->target_source == NULL ? &output_informations_null : &private->source_informations); + + struct tag_set source_tag_set = (struct tag_set){ + .tags = (struct tag *[]){ + tag_new_string(module, "type", "source"), + tag_new_string(module, "name", output_informations->name), + tag_new_string(module, "description", output_informations->description), + tag_new_string(module, "icon", output_informations->icon), + tag_new_string(module, "form_factor", output_informations->form_factor), + tag_new_string(module, "bus", output_informations->bus), + tag_new_bool(module, "muted", output_informations->muted), + tag_new_int_range(module, "linear_volume", output_informations->linear_volume, 0, 100), + tag_new_int_range(module, "cubic_volume", output_informations->cubic_volume, 0, 100), + }, + .count = 9, + }; + + exposables[0] = private->label->instantiate(private->label, &sink_tag_set); + exposables[1] = private->label->instantiate(private->label, &source_tag_set); + + tag_set_destroy(&sink_tag_set); + tag_set_destroy(&source_tag_set); + + mtx_unlock(&module->lock); + + return dynlist_exposable_new(exposables, exposables_length, private->left_spacing, private->right_spacing); +} + +static int +run(struct module *module) +{ + struct private *private = module->private; + if (private->data == NULL) + return 1; + + struct pw_loop *pw_loop = pw_main_loop_get_loop(private->data->loop); + struct pollfd pollfds[] = { + /* abort_fd */ + (struct pollfd){.fd = module->abort_fd, .events = POLLIN}, + /* pipewire */ + (struct pollfd){.fd = pw_loop_get_fd(pw_loop), .events = POLLIN}, + }; + + while (true) { + if (poll(pollfds, ARRAY_LENGTH(pollfds), -1) == -1) { + if (errno == EINTR) + continue; + + LOG_ERRNO("Unable to poll: %s", strerror(errno)); + break; + } + + /* abort_fd */ + if (pollfds[0].revents & POLLIN) + break; + + /* pipewire */ + if (!(pollfds[1].revents & POLLIN)) + /* issue happened */ + break; + + int result = pw_loop_iterate(pw_loop, 0); + if (result < 0) { + LOG_ERRNO("Unable to iterate pipewire loop: %s", spa_strerror(result)); + break; + } + } + + return 0; +} + +static struct module * +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; + module->run = &run; + module->destroy = &destroy; + module->content = &content; + module->description = &description; + + private->data = pipewire_init(module); + + 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"); + 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); +} + +struct module_iface const module_pipewire_iface = { + .from_conf = &from_conf, + .verify_conf = &verify_conf, +}; + +#if defined(CORE_PLUGINS_AS_SHARED_LIBRARIES) +extern struct module_iface const iface __attribute__((weak, alias("module_pipewire_iface"))); +#endif diff --git a/modules/pulse.c b/modules/pulse.c new file mode 100644 index 0000000..f6c7f69 --- /dev/null +++ b/modules/pulse.c @@ -0,0 +1,523 @@ +#include +#include +#include +#include +#include + +#include +#include + +#include + +#define LOG_MODULE "pulse" +#define LOG_ENABLE_DBG 0 +#include "../bar/bar.h" +#include "../config-verify.h" +#include "../config.h" +#include "../log.h" +#include "../plugin.h" + +struct private +{ + char *sink_name; + char *source_name; + struct particle *label; + + bool online; + + bool sink_online; + pa_cvolume sink_volume; + bool sink_muted; + char *sink_port; + uint32_t sink_index; + + bool source_online; + pa_cvolume source_volume; + bool source_muted; + char *source_port; + uint32_t source_index; + + int refresh_timer_fd; + bool refresh_scheduled; + + pa_mainloop *mainloop; + pa_context *context; +}; + +static void +destroy(struct module *mod) +{ + struct private *priv = mod->private; + priv->label->destroy(priv->label); + free(priv->sink_name); + free(priv->source_name); + free(priv->sink_port); + free(priv->source_port); + free(priv); + module_default_destroy(mod); +} + +static const char * +description(const struct module *mod) +{ + return "pulse"; +} + +static struct exposable * +content(struct module *mod) +{ + struct private *priv = mod->private; + + mtx_lock(&mod->lock); + + pa_volume_t sink_volume_max = pa_cvolume_max(&priv->sink_volume); + pa_volume_t source_volume_max = pa_cvolume_max(&priv->source_volume); + int sink_percent = round(100.0 * sink_volume_max / PA_VOLUME_NORM); + int source_percent = round(100.0 * source_volume_max / PA_VOLUME_NORM); + + struct tag_set tags = { + .tags = (struct tag *[]){ + tag_new_bool(mod, "online", priv->online), + + tag_new_bool(mod, "sink_online", priv->sink_online), + tag_new_int_range(mod, "sink_percent", sink_percent, 0, 100), + tag_new_bool(mod, "sink_muted", priv->sink_muted), + tag_new_string(mod, "sink_port", priv->sink_port), + + tag_new_bool(mod, "source_online", priv->source_online), + tag_new_int_range(mod, "source_percent", source_percent, 0, 100), + tag_new_bool(mod, "source_muted", priv->source_muted), + tag_new_string(mod, "source_port", priv->source_port), + }, + .count = 9, + }; + + mtx_unlock(&mod->lock); + + struct exposable *exposable = priv->label->instantiate(priv->label, &tags); + + tag_set_destroy(&tags); + return exposable; +} + +static const char * +context_error(pa_context *c) +{ + return pa_strerror(pa_context_errno(c)); +} + +static void +abort_event_cb(pa_mainloop_api *api, pa_io_event *event, int fd, pa_io_event_flags_t flags, void *userdata) +{ + struct module *mod = userdata; + struct private *priv = mod->private; + + pa_context_disconnect(priv->context); +} + +static void +refresh_timer_cb(pa_mainloop_api *api, pa_io_event *event, int fd, pa_io_event_flags_t flags, void *userdata) +{ + struct module *mod = userdata; + struct private *priv = mod->private; + + // Drain the refresh timer. + uint64_t n; + if (read(priv->refresh_timer_fd, &n, sizeof n) < 0) + LOG_ERRNO("failed to read from timerfd"); + + // Clear the refresh flag. + priv->refresh_scheduled = false; + + // Refresh the bar. + mod->bar->refresh(mod->bar); +} + +// Refresh the bar after a small delay. Without the delay, the bar +// would be refreshed multiple times per event (e.g., a volume change), +// and sometimes the active port would be reported incorrectly for a +// brief moment. (This behavior was observed with PipeWire 0.3.61.) +static void +schedule_refresh(struct module *mod) +{ + struct private *priv = mod->private; + + // Do nothing if a refresh has already been scheduled. + if (priv->refresh_scheduled) + return; + + // Start the refresh timer. + struct itimerspec t = { + .it_interval = {.tv_sec = 0, .tv_nsec = 0}, + .it_value = {.tv_sec = 0, .tv_nsec = 50000000}, + }; + timerfd_settime(priv->refresh_timer_fd, 0, &t, NULL); + + // Set the refresh flag. + priv->refresh_scheduled = true; +} + +static void +set_server_online(struct module *mod) +{ + struct private *priv = mod->private; + + mtx_lock(&mod->lock); + priv->online = true; + mtx_unlock(&mod->lock); + + schedule_refresh(mod); +} + +static void +set_server_offline(struct module *mod) +{ + struct private *priv = mod->private; + + mtx_lock(&mod->lock); + priv->online = false; + priv->sink_online = false; + priv->source_online = false; + mtx_unlock(&mod->lock); + + schedule_refresh(mod); +} + +static void +set_sink_info(struct module *mod, const pa_sink_info *sink_info) +{ + struct private *priv = mod->private; + + mtx_lock(&mod->lock); + + free(priv->sink_port); + + priv->sink_online = true; + priv->sink_index = sink_info->index; + priv->sink_volume = sink_info->volume; + priv->sink_muted = sink_info->mute; + priv->sink_port = sink_info->active_port != NULL ? strdup(sink_info->active_port->description) : NULL; + + mtx_unlock(&mod->lock); + + schedule_refresh(mod); +} + +static void +set_sink_offline(struct module *mod) +{ + struct private *priv = mod->private; + + mtx_lock(&mod->lock); + priv->sink_online = false; + mtx_unlock(&mod->lock); + + schedule_refresh(mod); +} + +static void +set_source_info(struct module *mod, const pa_source_info *source_info) +{ + struct private *priv = mod->private; + + mtx_lock(&mod->lock); + + free(priv->source_port); + + priv->source_online = true; + priv->source_index = source_info->index; + priv->source_volume = source_info->volume; + priv->source_muted = source_info->mute; + priv->source_port = source_info->active_port != NULL ? strdup(source_info->active_port->description) : NULL; + + mtx_unlock(&mod->lock); + + schedule_refresh(mod); +} + +static void +set_source_offline(struct module *mod) +{ + struct private *priv = mod->private; + + mtx_lock(&mod->lock); + priv->source_online = false; + mtx_unlock(&mod->lock); + + schedule_refresh(mod); +} + +static void +sink_info_cb(pa_context *c, const pa_sink_info *i, int eol, void *userdata) +{ + struct module *mod = userdata; + + if (eol < 0) { + LOG_ERR("failed to get sink info: %s", context_error(c)); + set_sink_offline(mod); + } else if (eol == 0) { + set_sink_info(mod, i); + } +} + +static void +source_info_cb(pa_context *c, const pa_source_info *i, int eol, void *userdata) +{ + struct module *mod = userdata; + + if (eol < 0) { + LOG_ERR("failed to get source info: %s", context_error(c)); + set_source_offline(mod); + } else if (eol == 0) { + set_source_info(mod, i); + } +} + +static void +server_info_cb(pa_context *c, const pa_server_info *i, void *userdata) +{ + LOG_INFO("%s, version %s", i->server_name, i->server_version); +} + +static void +get_sink_info_by_name(pa_context *c, const char *name, void *userdata) +{ + pa_operation *o = pa_context_get_sink_info_by_name(c, name, sink_info_cb, userdata); + pa_operation_unref(o); +} + +static void +get_source_info_by_name(pa_context *c, const char *name, void *userdata) +{ + pa_operation *o = pa_context_get_source_info_by_name(c, name, source_info_cb, userdata); + pa_operation_unref(o); +} + +static void +get_sink_info_by_index(pa_context *c, uint32_t index, void *userdata) +{ + pa_operation *o = pa_context_get_sink_info_by_index(c, index, sink_info_cb, userdata); + pa_operation_unref(o); +} + +static void +get_source_info_by_index(pa_context *c, uint32_t index, void *userdata) +{ + pa_operation *o = pa_context_get_source_info_by_index(c, index, source_info_cb, userdata); + pa_operation_unref(o); +} + +static void +get_server_info(pa_context *c, void *userdata) +{ + pa_operation *o = pa_context_get_server_info(c, server_info_cb, userdata); + pa_operation_unref(o); +} + +static void +subscribe(pa_context *c, void *userdata) +{ + pa_subscription_mask_t mask = PA_SUBSCRIPTION_MASK_SERVER | PA_SUBSCRIPTION_MASK_SINK | PA_SUBSCRIPTION_MASK_SOURCE; + pa_operation *o = pa_context_subscribe(c, mask, NULL, userdata); + pa_operation_unref(o); +} + +static pa_context *connect_to_server(struct module *mod); + +static void +context_state_change_cb(pa_context *c, void *userdata) +{ + struct module *mod = userdata; + struct private *priv = mod->private; + + pa_context_state_t state = pa_context_get_state(c); + switch (state) { + case PA_CONTEXT_UNCONNECTED: + case PA_CONTEXT_CONNECTING: + case PA_CONTEXT_AUTHORIZING: + case PA_CONTEXT_SETTING_NAME: + break; + + case PA_CONTEXT_READY: + set_server_online(mod); + subscribe(c, mod); + get_server_info(c, mod); + get_sink_info_by_name(c, priv->sink_name, mod); + get_source_info_by_name(c, priv->source_name, mod); + break; + + case PA_CONTEXT_FAILED: + LOG_WARN("connection lost"); + set_server_offline(mod); + pa_context_unref(priv->context); + priv->context = connect_to_server(mod); + break; + + case PA_CONTEXT_TERMINATED: + LOG_DBG("connection terminated"); + set_server_offline(mod); + pa_mainloop_quit(priv->mainloop, 0); + break; + } +} + +static void +subscription_event_cb(pa_context *c, pa_subscription_event_type_t event_type, uint32_t index, void *userdata) +{ + struct module *mod = userdata; + struct private *priv = mod->private; + + int facility = event_type & PA_SUBSCRIPTION_EVENT_FACILITY_MASK; + int type = event_type & PA_SUBSCRIPTION_EVENT_TYPE_MASK; + + switch (facility) { + case PA_SUBSCRIPTION_EVENT_SERVER: + get_sink_info_by_name(c, priv->sink_name, mod); + get_source_info_by_name(c, priv->source_name, mod); + break; + + case PA_SUBSCRIPTION_EVENT_SINK: + if (index == priv->sink_index) { + if (type == PA_SUBSCRIPTION_EVENT_CHANGE) + get_sink_info_by_index(c, index, mod); + else if (type == PA_SUBSCRIPTION_EVENT_REMOVE) + set_sink_offline(mod); + } + break; + + case PA_SUBSCRIPTION_EVENT_SOURCE: + if (index == priv->source_index) { + if (type == PA_SUBSCRIPTION_EVENT_CHANGE) + get_source_info_by_index(c, index, mod); + else if (type == PA_SUBSCRIPTION_EVENT_REMOVE) + set_source_offline(mod); + } + break; + } +} + +static pa_context * +connect_to_server(struct module *mod) +{ + struct private *priv = mod->private; + + // Create connection context. + pa_mainloop_api *api = pa_mainloop_get_api(priv->mainloop); + pa_context *c = pa_context_new(api, "yambar"); + if (c == NULL) { + LOG_ERR("failed to create PulseAudio connection context"); + return NULL; + } + + // Register callback functions. + pa_context_set_state_callback(c, context_state_change_cb, mod); + pa_context_set_subscribe_callback(c, subscription_event_cb, mod); + + // Connect to server. + pa_context_flags_t flags = PA_CONTEXT_NOFAIL | PA_CONTEXT_NOAUTOSPAWN; + if (pa_context_connect(c, NULL, flags, NULL) < 0) { + LOG_ERR("failed to connect to PulseAudio server: %s", context_error(c)); + pa_context_unref(c); + return NULL; + } + + return c; +} + +static int +run(struct module *mod) +{ + struct private *priv = mod->private; + int ret = -1; + + // Create main loop. + priv->mainloop = pa_mainloop_new(); + if (priv->mainloop == NULL) { + LOG_ERR("failed to create PulseAudio main loop"); + return -1; + } + + // Create refresh timer. + 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); + return -1; + } + + // Connect to server. + priv->context = connect_to_server(mod); + if (priv->context == NULL) { + pa_mainloop_free(priv->mainloop); + close(priv->refresh_timer_fd); + return -1; + } + + // Poll refresh timer and abort event. + pa_mainloop_api *api = pa_mainloop_get_api(priv->mainloop); + api->io_new(api, priv->refresh_timer_fd, PA_IO_EVENT_INPUT, refresh_timer_cb, mod); + api->io_new(api, mod->abort_fd, PA_IO_EVENT_INPUT | PA_IO_EVENT_HANGUP, abort_event_cb, mod); + + // Run main loop. + if (pa_mainloop_run(priv->mainloop, &ret) < 0) { + LOG_ERR("PulseAudio main loop error"); + ret = -1; + } + + // Clean up. + pa_context_unref(priv->context); + pa_mainloop_free(priv->mainloop); + close(priv->refresh_timer_fd); + + return ret; +} + +static struct module * +pulse_new(const char *sink_name, const char *source_name, struct particle *label) +{ + struct private *priv = calloc(1, sizeof *priv); + priv->label = label; + priv->sink_name = strdup(sink_name); + priv->source_name = strdup(source_name); + + struct module *mod = module_common_new(); + mod->private = priv; + mod->run = &run; + mod->destroy = &destroy; + mod->content = &content; + mod->description = &description; + return mod; +} + +static struct module * +from_conf(const struct yml_node *node, struct conf_inherit inherited) +{ + const struct yml_node *sink = yml_get_value(node, "sink"); + const struct yml_node *source = yml_get_value(node, "source"); + const struct yml_node *content = yml_get_value(node, "content"); + + return pulse_new(sink != NULL ? yml_value_as_string(sink) : "@DEFAULT_SINK@", + source != NULL ? yml_value_as_string(source) : "@DEFAULT_SOURCE@", + conf_to_particle(content, inherited)); +} + +static bool +verify_conf(keychain_t *chain, const struct yml_node *node) +{ + static const struct attr_info attrs[] = { + {"sink", false, &conf_verify_string}, + {"source", false, &conf_verify_string}, + MODULE_COMMON_ATTRS, + }; + + return conf_verify_dict(chain, node, attrs); +} + +const struct module_iface module_pulse_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_pulse_iface"))); +#endif diff --git a/modules/removables.c b/modules/removables.c index 498824b..df4ade4 100644 --- a/modules/removables.c +++ b/modules/removables.c @@ -1,14 +1,15 @@ -#include -#include -#include +#include +#include #include +#include +#include +#include #include #include -#include +#include #include #include -#include #include @@ -16,13 +17,15 @@ #define LOG_MODULE "removables" #define LOG_ENABLE_DBG 0 -#include "../log.h" #include "../bar/bar.h" -#include "../config.h" #include "../config-verify.h" +#include "../config.h" +#include "../log.h" #include "../particles/dynlist.h" #include "../plugin.h" +#define max(x, y) ((x) > (y) ? (x) : (y)) + typedef tll(char *) mount_point_list_t; struct partition { @@ -33,8 +36,8 @@ struct partition { char *label; uint64_t size; + bool audio_cd; - /*tll(char *) mount_points;*/ mount_point_list_t mount_points; }; @@ -51,7 +54,8 @@ struct block_device { tll(struct partition) partitions; }; -struct private { +struct private +{ struct particle *label; int left_spacing; int right_spacing; @@ -72,8 +76,7 @@ free_partition(struct partition *p) static void free_device(struct block_device *b) { - tll_foreach(b->partitions, it) - free_partition(&it->item); + tll_foreach(b->partitions, it) free_partition(&it->item); tll_free(b->partitions); free(b->sys_path); @@ -88,8 +91,7 @@ destroy(struct module *mod) struct private *m = mod->private; m->label->destroy(m->label); - tll_foreach(m->devices, it) - free_device(&it->item); + tll_foreach(m->devices, it) free_device(&it->item); tll_free(m->devices); tll_free_and_free(m->ignore, free); @@ -97,6 +99,12 @@ destroy(struct module *mod) module_default_destroy(mod); } +static const char * +description(const struct module *mod) +{ + return "removables"; +} + static struct exposable * content(struct module *mod) { @@ -104,24 +112,23 @@ content(struct module *mod) tll(const struct partition *) partitions = tll_init(); - tll_foreach(m->devices, dev) { - tll_foreach(dev->item.partitions, part) { - tll_push_back(partitions, &part->item); - } + tll_foreach(m->devices, dev) + { + tll_foreach(dev->item.partitions, part) { tll_push_back(partitions, &part->item); } } - struct exposable *exposables[tll_length(partitions)]; + struct exposable *exposables[max(tll_length(partitions), 1)]; size_t idx = 0; - tll_foreach(partitions, it) { + tll_foreach(partitions, it) + { const struct partition *p = it->item; char dummy_label[16]; const char *label = p->label; if (label == NULL) { - snprintf(dummy_label, sizeof(dummy_label), - "%.1f GB", (double)p->size / 1024 / 1024 / 1024 * 512); + snprintf(dummy_label, sizeof(dummy_label), "%.1f GB", (double)p->size / 1024 / 1024 / 1024 * 512); label = dummy_label; } @@ -133,13 +140,14 @@ content(struct module *mod) tag_new_string(mod, "vendor", p->block->vendor), tag_new_string(mod, "model", p->block->model), tag_new_bool(mod, "optical", p->block->optical), + tag_new_bool(mod, "audio", p->audio_cd), tag_new_string(mod, "device", p->dev_path), tag_new_int_range(mod, "size", p->size, 0, p->block->size), tag_new_string(mod, "label", label), tag_new_bool(mod, "mounted", is_mounted), tag_new_string(mod, "mount_point", mount_point), }, - .count = 8, + .count = 9, }; exposables[idx++] = m->label->instantiate(m->label, &tags); @@ -147,24 +155,24 @@ content(struct module *mod) } tll_free(partitions); - return dynlist_exposable_new( - exposables, idx, m->left_spacing, m->right_spacing); + return dynlist_exposable_new(exposables, idx, m->left_spacing, m->right_spacing); } static void find_mount_points(const char *dev_path, mount_point_list_t *mount_points) { - FILE *f = fopen("/proc/self/mountinfo", "r"); - assert(f != NULL); + FILE *f = fopen("/proc/self/mountinfo", "re"); + + if (f == NULL) { + LOG_ERRNO("failed to open /proc/self/mountinfo"); + return; + } char line[4096]; - while (fgets(line, sizeof(line), f) != NULL) { char *dev = NULL, *path = NULL; - if (sscanf(line, "%*u %*u %*u:%*u %*s %ms %*[^-] - %*s %ms %*s", - &path, &dev) != 2) - { + if (sscanf(line, "%*u %*u %*u:%*u %*s %ms %*[^-] - %*s %ms %*s", &path, &dev) != 2) { LOG_ERR("failed to parse /proc/self/mountinfo: %s", line); free(dev); free(path); @@ -191,9 +199,11 @@ update_mount_points(struct partition *partition) /* Remove mount points that no longer exists (i.e. old mount * points that aren't in the new list) */ - tll_foreach(partition->mount_points, old) { + tll_foreach(partition->mount_points, old) + { bool gone = true; - tll_foreach(new_mounts, new) { + tll_foreach(new_mounts, new) + { if (strcmp(new->item, old->item) == 0) { /* Remove from new list, as it's already in the * partitions list */ @@ -212,7 +222,8 @@ update_mount_points(struct partition *partition) /* Add new mount points (i.e. mount points in the new list, that * aren't in the old list) */ - tll_foreach(new_mounts, new) { + tll_foreach(new_mounts, new) + { LOG_DBG("%s: mounted on %s", partition->dev_path, new->item); tll_push_back(partition->mount_points, new->item); @@ -226,14 +237,13 @@ update_mount_points(struct partition *partition) } static struct partition * -add_partition(struct module *mod, struct block_device *block, - struct udev_device *dev) +add_partition(struct module *mod, struct block_device *block, struct udev_device *dev) { struct private *m = mod->private; const char *_size = udev_device_get_sysattr_value(dev, "size"); uint64_t size = 0; if (_size != NULL) - sscanf(_size, "%"SCNu64, &size); + sscanf(_size, "%" SCNu64, &size); #if 0 struct udev_list_entry *e = NULL; @@ -244,7 +254,8 @@ add_partition(struct module *mod, struct block_device *block, const char *devname = udev_device_get_property_value(dev, "DEVNAME"); if (devname != NULL) { - tll_foreach(m->ignore, it) { + tll_foreach(m->ignore, it) + { if (strcmp(it->item, devname) == 0) { LOG_DBG("ignoring %s because it is on the ignore list", devname); return NULL; @@ -256,20 +267,70 @@ add_partition(struct module *mod, struct block_device *block, if (label == NULL) label = udev_device_get_property_value(dev, "ID_LABEL"); - LOG_INFO("partition: add: %s: label=%s, size=%"PRIu64, - udev_device_get_devnode(dev), label, size); + LOG_INFO("partition: add: %s: label=%s, size=%" PRIu64, udev_device_get_devnode(dev), label, size); mtx_lock(&mod->lock); - tll_push_back( - block->partitions, - ((struct partition){ - .block = block, - .sys_path = strdup(udev_device_get_devpath(dev)), - .dev_path = strdup(udev_device_get_devnode(dev)), - .label = label != NULL ? strdup(label) : NULL, - .size = size, - .mount_points = tll_init()})); + tll_push_back(block->partitions, ((struct partition){.block = block, + .sys_path = strdup(udev_device_get_devpath(dev)), + .dev_path = strdup(udev_device_get_devnode(dev)), + .label = label != NULL ? strdup(label) : NULL, + .size = size, + .audio_cd = false, + .mount_points = tll_init()})); + + struct partition *p = &tll_back(block->partitions); + update_mount_points(p); + mtx_unlock(&mod->lock); + + return p; +} + +static struct partition * +add_audio_cd(struct module *mod, struct block_device *block, struct udev_device *dev) +{ + struct private *m = mod->private; + const char *_size = udev_device_get_sysattr_value(dev, "size"); + uint64_t size = 0; + if (_size != NULL) + sscanf(_size, "%" SCNu64, &size); + +#if 0 + struct udev_list_entry *e = NULL; + udev_list_entry_foreach(e, udev_device_get_properties_list_entry(dev)) { + LOG_DBG("%s -> %s", udev_list_entry_get_name(e), udev_list_entry_get_value(e)); + } +#endif + + const char *devname = udev_device_get_property_value(dev, "DEVNAME"); + if (devname != NULL) { + tll_foreach(m->ignore, it) + { + if (strcmp(it->item, devname) == 0) { + LOG_DBG("ignoring %s because it is on the ignore list", devname); + return NULL; + } + } + } + + const char *_track_count = udev_device_get_property_value(dev, "ID_CDROM_MEDIA_TRACK_COUNT_AUDIO"); + unsigned long track_count = strtoul(_track_count, NULL, 10); + + char label[64]; + snprintf(label, sizeof(label), "Audio CD - %lu tracks", track_count); + + LOG_INFO("audio CD: add: %s: tracks=%lu, label=%s, size=%" PRIu64, udev_device_get_devnode(dev), track_count, label, + size); + + mtx_lock(&mod->lock); + + tll_push_back(block->partitions, ((struct partition){.block = block, + .sys_path = strdup(udev_device_get_devpath(dev)), + .dev_path = strdup(udev_device_get_devnode(dev)), + .label = label != NULL ? strdup(label) : NULL, + .size = size, + .audio_cd = true, + .mount_points = tll_init()})); struct partition *p = &tll_back(block->partitions); update_mount_points(p); @@ -279,15 +340,15 @@ add_partition(struct module *mod, struct block_device *block, } static bool -del_partition(struct module *mod, struct block_device *block, - struct udev_device *dev) +del_partition(struct module *mod, struct block_device *block, struct udev_device *dev) { const char *sys_path = udev_device_get_devpath(dev); mtx_lock(&mod->lock); - tll_foreach(block->partitions, it) { + tll_foreach(block->partitions, it) + { if (strcmp(it->item.sys_path, sys_path) == 0) { - LOG_INFO("partition: del: %s", it->item.dev_path); + LOG_INFO("%s: del: %s", it->item.audio_cd ? "audio CD" : "partition", it->item.dev_path); free_partition(&it->item); tll_remove(block->partitions, it); @@ -316,7 +377,8 @@ add_device(struct module *mod, struct udev_device *dev) const char *devname = udev_device_get_property_value(dev, "DEVNAME"); if (devname != NULL) { - tll_foreach(m->ignore, it) { + tll_foreach(m->ignore, it) + { if (strcmp(it->item, devname) == 0) { LOG_DBG("ignoring %s because it is on the ignore list", devname); return NULL; @@ -327,11 +389,12 @@ add_device(struct module *mod, struct udev_device *dev) const char *_size = udev_device_get_sysattr_value(dev, "size"); uint64_t size = 0; if (_size != NULL) - sscanf(_size, "%"SCNu64, &size); + sscanf(_size, "%" SCNu64, &size); #if 1 struct udev_list_entry *e = NULL; - udev_list_entry_foreach(e, udev_device_get_properties_list_entry(dev)) { + udev_list_entry_foreach(e, udev_device_get_properties_list_entry(dev)) + { LOG_DBG("%s -> %s", udev_list_entry_get_name(e), udev_list_entry_get_value(e)); } #endif @@ -342,31 +405,38 @@ add_device(struct module *mod, struct udev_device *dev) const char *_optical = udev_device_get_property_value(dev, "ID_CDROM"); bool optical = _optical != NULL && strcmp(_optical, "1") == 0; - const char *_fs_usage = udev_device_get_property_value(dev, "ID_FS_USAGE"); - bool media = _fs_usage != NULL && strcmp(_fs_usage, "filesystem") == 0; + const char *_media = udev_device_get_property_value(dev, "ID_CDROM_MEDIA"); + bool media = _media != NULL && strcmp(_media, "1") == 0; - LOG_DBG("device: add: %s: vendor=%s, model=%s, optical=%d, size=%"PRIu64, - udev_device_get_devnode(dev), vendor, model, optical, size); + const char *_fs_usage = udev_device_get_property_value(dev, "ID_FS_USAGE"); + bool have_fs = _fs_usage != NULL && strcmp(_fs_usage, "filesystem") == 0; + + const char *_audio_track_count = udev_device_get_property_value(dev, "ID_CDROM_MEDIA_TRACK_COUNT_AUDIO"); + unsigned long audio_track_count = _audio_track_count != NULL ? strtoul(_audio_track_count, NULL, 10) : 0; + + LOG_DBG("device: add: %s: vendor=%s, model=%s, optical=%d, size=%" PRIu64, udev_device_get_devnode(dev), vendor, + model, optical, size); mtx_lock(&mod->lock); - tll_push_back( - m->devices, - ((struct block_device){ - .sys_path = strdup(udev_device_get_devpath(dev)), - .dev_path = strdup(udev_device_get_devnode(dev)), - .size = size, - .vendor = vendor != NULL ? strdup(vendor) : NULL, - .model = model != NULL ? strdup(model) : NULL, - .optical = optical, - .media = media, - .partitions = tll_init()})); + tll_push_back(m->devices, ((struct block_device){.sys_path = strdup(udev_device_get_devpath(dev)), + .dev_path = strdup(udev_device_get_devnode(dev)), + .size = size, + .vendor = vendor != NULL ? strdup(vendor) : NULL, + .model = model != NULL ? strdup(model) : NULL, + .optical = optical, + .media = media, + .partitions = tll_init()})); mtx_unlock(&mod->lock); struct block_device *block = &tll_back(m->devices); - if (optical && media) - add_partition(mod, block, dev); + if (optical) { + if (have_fs) + add_partition(mod, block, dev); + else if (audio_track_count > 0) + add_audio_cd(mod, block, dev); + } return &tll_back(m->devices); } @@ -378,7 +448,8 @@ del_device(struct module *mod, struct udev_device *dev) const char *sys_path = udev_device_get_devpath(dev); mtx_lock(&mod->lock); - tll_foreach(m->devices, it) { + tll_foreach(m->devices, it) + { if (strcmp(it->item.sys_path, sys_path) == 0) { LOG_DBG("device: del: %s", it->item.dev_path); @@ -400,31 +471,51 @@ change_device(struct module *mod, struct udev_device *dev) const char *sys_path = udev_device_get_devpath(dev); mtx_lock(&mod->lock); - tll_foreach(m->devices, it) { + struct block_device *block = NULL; + + tll_foreach(m->devices, it) + { if (strcmp(it->item.sys_path, sys_path) == 0) { - LOG_DBG("device: change: %s", it->item.dev_path); - - if (it->item.optical) { - const char *_media = udev_device_get_property_value(dev, "ID_FS_USAGE"); - bool media = _media != NULL && strcmp(_media, "filesystem") == 0; - bool media_change = media != it->item.media; - - it->item.media = media; - mtx_unlock(&mod->lock); - - if (media_change) { - LOG_INFO("device: change: %s: media %s", - it->item.dev_path, media ? "inserted" : "removed"); - - if (media) - return add_partition(mod, &it->item, dev) != NULL; - else - return del_partition(mod, &it->item, dev); - } - } + block = &it->item; + break; } } + if (block == NULL) + goto out; + + LOG_DBG("device: change: %s", block->dev_path); + + if (!block->optical) + goto out; + + const char *_media = udev_device_get_property_value(dev, "ID_CDROM_MEDIA"); + bool media = _media != NULL && strcmp(_media, "1") == 0; + + const char *_fs_usage = udev_device_get_property_value(dev, "ID_FS_USAGE"); + bool have_fs = _fs_usage != NULL && strcmp(_fs_usage, "filesystem") == 0; + + const char *_audio_track_count = udev_device_get_property_value(dev, "ID_CDROM_MEDIA_TRACK_COUNT_AUDIO"); + unsigned long audio_track_count = _audio_track_count != NULL ? strtoul(_audio_track_count, NULL, 10) : 0; + + bool media_change = media != block->media; + + block->media = media; + mtx_unlock(&mod->lock); + + if (media_change) { + LOG_INFO("device: change: %s: media %s", block->dev_path, media ? "inserted" : "removed"); + + if (media) { + if (have_fs) + return add_partition(mod, block, dev) != NULL; + else if (audio_track_count > 0) + return add_audio_cd(mod, block, dev) != NULL; + } else + return del_partition(mod, block, dev); + } + +out: mtx_unlock(&mod->lock); return false; } @@ -459,7 +550,8 @@ handle_udev_event(struct module *mod, struct udev_device *dev) struct udev_device *parent = udev_device_get_parent(dev); const char *parent_sys_path = udev_device_get_devpath(parent); - tll_foreach(m->devices, it) { + tll_foreach(m->devices, it) + { if (strcmp(it->item.sys_path, parent_sys_path) != 0) continue; @@ -468,8 +560,7 @@ handle_udev_event(struct module *mod, struct udev_device *dev) else if (del) return del_partition(mod, &it->item, dev); else { - LOG_ERR("unimplemented: 'change' event on partition: %s", - udev_device_get_devpath(dev)); + LOG_ERR("unimplemented: 'change' event on partition: %s", udev_device_get_devpath(dev)); return false; } break; @@ -496,15 +587,15 @@ run(struct module *mod) udev_enumerate_add_match_subsystem(dev_enum, "block"); /* TODO: verify how an optical presents itself */ - //udev_enumerate_add_match_sysattr(dev_enum, "removable", "1"); + // udev_enumerate_add_match_sysattr(dev_enum, "removable", "1"); udev_enumerate_add_match_property(dev_enum, "DEVTYPE", "disk"); udev_enumerate_scan_devices(dev_enum); /* Loop list, and for each device, enumerate its partitions */ struct udev_list_entry *entry = NULL; - udev_list_entry_foreach(entry, udev_enumerate_get_list_entry(dev_enum)) { - struct udev_device *dev = udev_device_new_from_syspath( - udev, udev_list_entry_get_name(entry)); + udev_list_entry_foreach(entry, udev_enumerate_get_list_entry(dev_enum)) + { + struct udev_device *dev = udev_device_new_from_syspath(udev, udev_list_entry_get_name(entry)); struct block_device *block = add_device(mod, dev); if (block == NULL) { @@ -521,9 +612,9 @@ run(struct module *mod) udev_enumerate_scan_devices(part_enum); struct udev_list_entry *sub_entry = NULL; - udev_list_entry_foreach(sub_entry, udev_enumerate_get_list_entry(part_enum)) { - struct udev_device *partition = udev_device_new_from_syspath( - udev, udev_list_entry_get_name(sub_entry)); + udev_list_entry_foreach(sub_entry, udev_enumerate_get_list_entry(part_enum)) + { + struct udev_device *partition = udev_device_new_from_syspath(udev, udev_list_entry_get_name(sub_entry)); add_partition(mod, block, partition); udev_device_unref(partition); } @@ -537,7 +628,9 @@ run(struct module *mod) /* To be able to poll() mountinfo for changes, to detect * mount/unmount operations */ - int mount_info_fd = open("/proc/self/mountinfo", O_RDONLY); + int mount_info_fd = open("/proc/self/mountinfo", O_RDONLY | O_CLOEXEC); + + int ret = 1; while (true) { struct pollfd fds[] = { @@ -545,16 +638,26 @@ run(struct module *mod) {.fd = udev_monitor_get_fd(dev_mon), .events = POLLIN}, {.fd = mount_info_fd, .events = POLLPRI}, }; - poll(fds, 3, -1); + if (poll(fds, sizeof(fds) / sizeof(fds[0]), -1) < 0) { + if (errno == EINTR) + continue; - if (fds[0].revents & POLLIN) + LOG_ERRNO("failed to poll"); break; + } + + if (fds[0].revents & POLLIN) { + ret = 0; + break; + } bool update = false; if (fds[2].revents & POLLPRI) { - tll_foreach(m->devices, dev) { - tll_foreach(dev->item.partitions, part) { + tll_foreach(m->devices, dev) + { + tll_foreach(dev->item.partitions, part) + { if (update_mount_points(&part->item)) update = true; } @@ -563,6 +666,9 @@ run(struct module *mod) if (fds[1].revents & POLLIN) { struct udev_device *dev = udev_monitor_receive_device(dev_mon); + if (dev == NULL) + continue; + if (handle_udev_event(mod, dev)) update = true; udev_device_unref(dev); @@ -576,12 +682,12 @@ run(struct module *mod) udev_monitor_unref(dev_mon); udev_unref(udev); - return 0; + return ret; } static struct module * -removables_new(struct particle *label, int left_spacing, int right_spacing, - size_t ignore_count, const char *ignore[static ignore_count]) +removables_new(struct particle *label, int left_spacing, int right_spacing, size_t ignore_count, + const char *ignore[static ignore_count]) { struct private *priv = calloc(1, sizeof(*priv)); priv->label = label; @@ -596,6 +702,7 @@ removables_new(struct particle *label, int left_spacing, int right_spacing, mod->run = &run; mod->destroy = &destroy; mod->content = &content; + mod->description = &description; return mod; } @@ -608,26 +715,22 @@ from_conf(const struct yml_node *node, struct conf_inherit inherited) const struct yml_node *right_spacing = yml_get_value(node, "right-spacing"); const struct yml_node *ignore_list = yml_get_value(node, "ignore"); - 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; + 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; size_t ignore_count = ignore_list != NULL ? yml_list_length(ignore_list) : 0; - const char *ignore[ignore_count]; + const char *ignore[max(ignore_count, 1)]; if (ignore_list != NULL) { size_t i = 0; - for (struct yml_list_iter iter = yml_list_iter(ignore_list); - iter.node != NULL; - yml_list_next(&iter), i++) - { + for (struct yml_list_iter iter = yml_list_iter(ignore_list); iter.node != NULL; yml_list_next(&iter), i++) { ignore[i] = yml_value_as_string(iter.node); } } - return removables_new( - conf_to_particle(content, inherited), left, right, ignore_count, ignore); + return removables_new(conf_to_particle(content, inherited), left, right, ignore_count, ignore); } static bool @@ -640,9 +743,9 @@ static bool verify_conf(keychain_t *chain, const struct yml_node *node) { static const struct attr_info attrs[] = { - {"spacing", false, &conf_verify_int}, - {"left-spacing", false, &conf_verify_int}, - {"right-spacing", false, &conf_verify_int}, + {"spacing", false, &conf_verify_unsigned}, + {"left-spacing", false, &conf_verify_unsigned}, + {"right-spacing", false, &conf_verify_unsigned}, {"ignore", false, &verify_ignore}, MODULE_COMMON_ATTRS, }; diff --git a/modules/river.c b/modules/river.c index bb6a03b..ec25f9f 100644 --- a/modules/river.c +++ b/modules/river.c @@ -1,21 +1,23 @@ +#include +#include +#include #include #include -#include -#include -#include -#include #include +#include #define LOG_MODULE "river" -#define LOG_ENABLE_DBG 1 +#define LOG_ENABLE_DBG 0 #include "../log.h" -#include "../plugin.h" #include "../particles/dynlist.h" +#include "../plugin.h" #include "river-status-unstable-v1.h" #include "xdg-output-unstable-v1.h" +#define min(x, y) ((x) < (y) ? (x) : (y)) + struct private; struct output { @@ -29,6 +31,10 @@ struct output { /* Tags */ uint32_t occupied; uint32_t focused; + uint32_t urgent; + + /* Layout */ + char *layout; }; struct seat { @@ -38,18 +44,21 @@ struct seat { uint32_t wl_name; char *name; + char *mode; char *title; struct output *output; }; -struct private { +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; struct particle *title; + bool all_monitors; - bool is_starting_up; tll(struct output) outputs; tll(struct seat) seats; }; @@ -65,24 +74,46 @@ destroy(struct module *mod) module_default_destroy(mod); } +static const char * +description(const struct module *mod) +{ + return "river"; +} + static struct exposable * content(struct module *mod) { const struct private *m = mod->private; + const char *output_bar_is_on = mod->bar->output_name(mod->bar); + 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; uint32_t seat_focused = 0; - uint32_t occupied = 0; - tll_foreach(m->outputs, it) { + tll_foreach(m->outputs, it) + { const struct output *output = &it->item; + if (!m->all_monitors && output_bar_is_on != NULL && output->name != NULL + && strcmp(output->name, output_bar_is_on) != 0) { + continue; + } + output_focused |= output->focused; + urgent |= output->urgent; occupied |= output->occupied; - tll_foreach(m->seats, it2) { + tll_foreach(m->seats, it2) + { const struct seat *seat = &it2->item; if (seat->output == output) { seat_focused |= output->focused; @@ -95,27 +126,31 @@ content(struct module *mod) for (unsigned i = 0; i < 32; i++) { /* It's visible if any output has it focused */ - bool visible = output_focused & (1u << i); + bool is_visible = output_focused & (1u << i); /* It's focused if any output that has seat focus has it focused */ - bool focused = seat_focused & (1u << i); + bool is_focused = seat_focused & (1u << i); - const char *state = visible ? focused ? "focused" : "unfocused" : "invisible"; + bool is_urgent = urgent & (1u << i); + bool is_occupied = occupied & (1u << i); + + const char *state = is_urgent ? "urgent" : is_visible ? is_focused ? "focused" : "unfocused" : "invisible"; #if 0 LOG_DBG("tag: #%u, visible=%d, focused=%d, occupied=%d, state=%s", - i, visible, focused, occupied & (1u << i), state); + i, is_visible, is_focused, is_occupied & (1u << i), state); #endif struct tag_set tags = { .tags = (struct tag *[]){ tag_new_int(mod, "id", i + 1), - tag_new_bool(mod, "visible", visible), - tag_new_bool(mod, "focused", focused), - tag_new_bool(mod, "occupied", occupied & (1u << i)), + tag_new_bool(mod, "urgent", is_urgent), + tag_new_bool(mod, "visible", is_visible), + tag_new_bool(mod, "focused", is_focused), + tag_new_bool(mod, "occupied", is_occupied), tag_new_string(mod, "state", state), }, - .count = 5, + .count = 6, }; tag_parts[i] = m->template->instantiate(m->template, &tags); @@ -124,22 +159,26 @@ content(struct module *mod) if (m->title != NULL) { size_t i = 32; - tll_foreach(m->seats, it) { + tll_foreach(m->seats, it) + { const struct seat *seat = &it->item; + const char *layout = seat->output != NULL && seat->output->layout != NULL ? seat->output->layout : ""; struct tag_set tags = { .tags = (struct tag *[]){ tag_new_string(mod, "seat", seat->name), tag_new_string(mod, "title", seat->title), + tag_new_string(mod, "mode", seat->mode), + tag_new_string(mod, "layout", layout), }, - .count = 2, + .count = 4, }; tag_parts[i++] = m->title->instantiate(m->title, &tags); tag_set_destroy(&tags); } } - + mtx_unlock(&m->mod->lock); return dynlist_exposable_new(tag_parts, 32 + seat_count, 0, 0); } @@ -150,21 +189,27 @@ verify_iface_version(const char *iface, uint32_t version, uint32_t wanted) if (version >= wanted) return true; - LOG_ERR("%s: need interface version %u, but compositor only implements %u", - iface, wanted, version); + LOG_ERR("%s: need interface version %u, but compositor only implements %u", iface, wanted, version); return false; } static void output_destroy(struct output *output) { + tll_foreach(output->m->seats, it) + { + struct seat *seat = &it->item; + if (seat->output == output) + seat->output = NULL; + } free(output->name); + free(output->layout); if (output->status != NULL) zriver_output_status_v1_destroy(output->status); if (output->xdg_output != NULL) zxdg_output_v1_destroy(output->xdg_output); if (output->wl_output != NULL) - wl_output_destroy(output->wl_output); + wl_output_release(output->wl_output); } static void @@ -172,6 +217,7 @@ seat_destroy(struct seat *seat) { free(seat->title); free(seat->name); + free(seat->mode); if (seat->status != NULL) zriver_seat_status_v1_destroy(seat->status); if (seat->wl_seat != NULL) @@ -179,25 +225,24 @@ seat_destroy(struct seat *seat) } static void -focused_tags(void *data, struct zriver_output_status_v1 *zriver_output_status_v1, - uint32_t tags) +focused_tags(void *data, struct zriver_output_status_v1 *zriver_output_status_v1, uint32_t tags) { struct output *output = data; - struct module *mod = output->m->mod; + + if (output->focused == tags) + return; LOG_DBG("output: %s: focused tags: 0x%08x", output->name, tags); + struct module *mod = output->m->mod; mtx_lock(&mod->lock); - { - output->focused = tags; - } + output->focused = tags; mtx_unlock(&mod->lock); mod->bar->refresh(mod->bar); } static void -view_tags(void *data, struct zriver_output_status_v1 *zriver_output_status_v1, - struct wl_array *tags) +view_tags(void *data, struct zriver_output_status_v1 *zriver_output_status_v1, struct wl_array *tags) { struct output *output = data; struct module *mod = output->m->mod; @@ -209,31 +254,81 @@ view_tags(void *data, struct zriver_output_status_v1 *zriver_output_status_v1, /* Each entry in the list is a view, and the value is the tags * associated with that view */ uint32_t *set; - wl_array_for_each(set, tags) { - output->occupied |= *set; - } - + wl_array_for_each(set, tags) { output->occupied |= *set; } + LOG_DBG("output: %s: occupied tags: 0x%0x", output->name, output->occupied); } mtx_unlock(&mod->lock); mod->bar->refresh(mod->bar); } +static void +urgent_tags(void *data, struct zriver_output_status_v1 *zriver_output_status_v1, uint32_t tags) +{ + struct output *output = data; + struct module *mod = output->m->mod; + + mtx_lock(&mod->lock); + { + output->urgent = tags; + } + mtx_unlock(&mod->lock); + mod->bar->refresh(mod->bar); +} + +#if defined(ZRIVER_OUTPUT_STATUS_V1_LAYOUT_NAME_SINCE_VERSION) +static void +layout_name(void *data, struct zriver_output_status_v1 *zriver_output_status_v1, const char *name) +{ + struct output *output = data; + struct module *mod = output->m->mod; + + mtx_lock(&mod->lock); + { + free(output->layout); + output->layout = name != NULL ? strdup(name) : NULL; + } + mtx_unlock(&mod->lock); + mod->bar->refresh(mod->bar); +} +#endif + +#if defined(ZRIVER_OUTPUT_STATUS_V1_LAYOUT_NAME_CLEAR_SINCE_VERSION) +static void +layout_name_clear(void *data, struct zriver_output_status_v1 *zriver_output_status_v1) +{ + struct output *output = data; + struct module *mod = output->m->mod; + + mtx_lock(&mod->lock); + { + free(output->layout); + output->layout = NULL; + } + mtx_unlock(&mod->lock); + mod->bar->refresh(mod->bar); +} +#endif + static const struct zriver_output_status_v1_listener river_status_output_listener = { .focused_tags = &focused_tags, .view_tags = &view_tags, + .urgent_tags = &urgent_tags, +#if defined(ZRIVER_OUTPUT_STATUS_V1_LAYOUT_NAME_SINCE_VERSION) + .layout_name = &layout_name, +#endif +#if defined(ZRIVER_OUTPUT_STATUS_V1_LAYOUT_NAME_CLEAR_SINCE_VERSION) + .layout_name_clear = &layout_name_clear, +#endif }; static void -xdg_output_handle_logical_position(void *data, - struct zxdg_output_v1 *xdg_output, - int32_t x, int32_t y) +xdg_output_handle_logical_position(void *data, struct zxdg_output_v1 *xdg_output, int32_t x, int32_t y) { } static void -xdg_output_handle_logical_size(void *data, struct zxdg_output_v1 *xdg_output, - int32_t width, int32_t height) +xdg_output_handle_logical_size(void *data, struct zxdg_output_v1 *xdg_output, int32_t width, int32_t height) { } @@ -243,8 +338,7 @@ xdg_output_handle_done(void *data, struct zxdg_output_v1 *xdg_output) } static void -xdg_output_handle_name(void *data, struct zxdg_output_v1 *xdg_output, - const char *name) +xdg_output_handle_name(void *data, struct zxdg_output_v1 *xdg_output, const char *name) { struct output *output = data; struct module *mod = output->m->mod; @@ -259,8 +353,7 @@ xdg_output_handle_name(void *data, struct zxdg_output_v1 *xdg_output, } static void -xdg_output_handle_description(void *data, struct zxdg_output_v1 *xdg_output, - const char *description) +xdg_output_handle_description(void *data, struct zxdg_output_v1 *xdg_output, const char *description) { } @@ -273,66 +366,67 @@ static struct zxdg_output_v1_listener xdg_output_listener = { }; static void -instantiate_output(struct output *output) +update_output(struct output *output) { - if (output->m->is_starting_up) - return; - assert(output->wl_output != NULL); - if (output->m->status_manager != NULL && output->status == NULL) { - output->status = zriver_status_manager_v1_get_river_output_status( - output->m->status_manager, output->wl_output); + if (output->m->status_manager != NULL) { + /* + * Bind river output status, if we have already bound the status manager + */ if (output->status != NULL) { - zriver_output_status_v1_add_listener( - output->status, &river_status_output_listener, output); + zriver_output_status_v1_destroy(output->status); + output->status = NULL; + } + + output->status = zriver_status_manager_v1_get_river_output_status(output->m->status_manager, output->wl_output); + + if (output->status != NULL) { + zriver_output_status_v1_add_listener(output->status, &river_status_output_listener, output); } } if (output->m->xdg_output_manager != NULL && output->xdg_output == NULL) { - output->xdg_output = zxdg_output_manager_v1_get_xdg_output( - output->m->xdg_output_manager, output->wl_output); + output->xdg_output = zxdg_output_manager_v1_get_xdg_output(output->m->xdg_output_manager, output->wl_output); if (output->xdg_output != NULL) { - zxdg_output_v1_add_listener( - output->xdg_output, &xdg_output_listener, output); + zxdg_output_v1_add_listener(output->xdg_output, &xdg_output_listener, output); } } } static void -focused_output(void *data, struct zriver_seat_status_v1 *zriver_seat_status_v1, - struct wl_output *wl_output) +focused_output(void *data, struct zriver_seat_status_v1 *zriver_seat_status_v1, struct wl_output *wl_output) { struct seat *seat = data; struct private *m = seat->m; struct module *mod = m->mod; - mtx_lock(&mod->lock); + struct output *output = NULL; + tll_foreach(m->outputs, it) { - struct output *output = NULL; - tll_foreach(m->outputs, it) { - if (it->item.wl_output == wl_output) { - output = &it->item; - break; - } + if (it->item.wl_output == wl_output) { + output = &it->item; + break; } + } - LOG_DBG("seat: %s: focused output: %s", seat->name, output != NULL ? output->name : ""); + LOG_DBG("seat: %s: focused output: %s", seat->name, output != NULL ? output->name : ""); - if (output == NULL) - LOG_WARN("seat: %s: couldn't find output we are mapped on", seat->name); + if (output == NULL) + LOG_WARN("seat: %s: couldn't find output we are mapped on", seat->name); + if (seat->output != output) { + mtx_lock(&mod->lock); seat->output = output; + mtx_unlock(&mod->lock); + mod->bar->refresh(mod->bar); } - mtx_unlock(&mod->lock); - mod->bar->refresh(mod->bar); } static void -unfocused_output(void *data, struct zriver_seat_status_v1 *zriver_seat_status_v1, - struct wl_output *wl_output) +unfocused_output(void *data, struct zriver_seat_status_v1 *zriver_seat_status_v1, struct wl_output *wl_output) { struct seat *seat = data; struct private *m = seat->m; @@ -340,9 +434,9 @@ unfocused_output(void *data, struct zriver_seat_status_v1 *zriver_seat_status_v1 mtx_lock(&mod->lock); { - struct output *output = NULL; - tll_foreach(m->outputs, it) { + tll_foreach(m->outputs, it) + { if (it->item.wl_output == wl_output) { output = &it->item; break; @@ -360,32 +454,65 @@ unfocused_output(void *data, struct zriver_seat_status_v1 *zriver_seat_status_v1 } static void -focused_view(void *data, struct zriver_seat_status_v1 *zriver_seat_status_v1, - const char *title) +focused_view(void *data, struct zriver_seat_status_v1 *zriver_seat_status_v1, const char *title) { struct seat *seat = data; struct module *mod = seat->m->mod; + if (seat->title == NULL && title == NULL) + return; + + if (seat->title != NULL && title != NULL && strcmp(seat->title, title) == 0) + return; + LOG_DBG("seat: %s: focused view: %s", seat->name, title); + const char *output_bar_is_on = mod->bar->output_name(mod->bar); + + if (seat->m->all_monitors + || (output_bar_is_on != NULL && seat->output != NULL && seat->output->name != NULL + && strcmp(output_bar_is_on, seat->output->name) == 0)) { + mtx_lock(&mod->lock); + { + free(seat->title); + seat->title = title != NULL ? strdup(title) : NULL; + } + mtx_unlock(&mod->lock); + mod->bar->refresh(mod->bar); + } +} + +#if defined(ZRIVER_SEAT_STATUS_V1_MODE_SINCE_VERSION) +static void +mode(void *data, struct zriver_seat_status_v1 *zriver_seat_status_v1, const char *name) +{ + struct seat *seat = data; + struct module *mod = seat->m->mod; + mtx_lock(&mod->lock); { - free(seat->title); - seat->title = title != NULL ? strdup(title) : NULL; + free(seat->mode); + seat->mode = strdup(name); + mtx_unlock(&mod->lock); } - mtx_unlock(&mod->lock); mod->bar->refresh(mod->bar); + + LOG_DBG("seat: %s, current mode: %s", seat->name, seat->mode); } +#endif + static const struct zriver_seat_status_v1_listener river_seat_status_listener = { .focused_output = &focused_output, .unfocused_output = &unfocused_output, .focused_view = &focused_view, +#if defined(ZRIVER_SEAT_STATUS_V1_MODE_SINCE_VERSION) + .mode = &mode, +#endif }; static void -seat_handle_capabilities(void *data, struct wl_seat *wl_seat, - enum wl_seat_capability caps) +seat_handle_capabilities(void *data, struct wl_seat *wl_seat, enum wl_seat_capability caps) { } @@ -410,46 +537,51 @@ static const struct wl_seat_listener seat_listener = { }; static void -instantiate_seat(struct seat *seat) +update_seat(struct seat *seat) { assert(seat->wl_seat != NULL); - if (seat->m->is_starting_up) - return; - if (seat->m->status_manager == NULL) return; - seat->status = zriver_status_manager_v1_get_river_seat_status( - seat->m->status_manager, seat->wl_seat); + if (seat->status != NULL) { + zriver_seat_status_v1_destroy(seat->status); + seat->status = NULL; + } + + seat->status = zriver_status_manager_v1_get_river_seat_status(seat->m->status_manager, seat->wl_seat); if (seat->status == NULL) return; - zriver_seat_status_v1_add_listener( - seat->status, &river_seat_status_listener, seat); + zriver_seat_status_v1_add_listener(seat->status, &river_seat_status_listener, seat); } static void -handle_global(void *data, struct wl_registry *registry, - uint32_t name, const char *interface, uint32_t version) +handle_global(void *data, struct wl_registry *registry, uint32_t name, const char *interface, uint32_t version) { struct private *m = data; if (strcmp(interface, wl_output_interface.name) == 0) { - const uint32_t required = 1; + const uint32_t required = 3; if (!verify_iface_version(interface, version, required)) return; - struct wl_output *wl_output = wl_registry_bind( - registry, name, &wl_output_interface, required); + struct wl_output *wl_output = wl_registry_bind(registry, name, &wl_output_interface, required); if (wl_output == NULL) return; + struct output output = { + .m = m, + .wl_output = wl_output, + .wl_name = name, + }; + mtx_lock(&m->mod->lock); - tll_push_back(m->outputs, ((struct output){.m = m, .wl_output = wl_output, .wl_name = name})); - instantiate_output(&tll_back(m->outputs)); + tll_push_back(m->outputs, output); + update_output(&tll_back(m->outputs)); + tll_foreach(m->seats, it) update_seat(&it->item); mtx_unlock(&m->mod->lock); } @@ -458,12 +590,10 @@ handle_global(void *data, struct wl_registry *registry, if (!verify_iface_version(interface, version, required)) return; - m->xdg_output_manager = wl_registry_bind( - registry, name, &zxdg_output_manager_v1_interface, required); + m->xdg_output_manager = wl_registry_bind(registry, name, &zxdg_output_manager_v1_interface, required); mtx_lock(&m->mod->lock); - tll_foreach(m->outputs, it) - instantiate_output(&it->item); + tll_foreach(m->outputs, it) update_output(&it->item); mtx_unlock(&m->mod->lock); } @@ -472,8 +602,7 @@ handle_global(void *data, struct wl_registry *registry, if (!verify_iface_version(interface, version, required)) return; - struct wl_seat *wl_seat = wl_registry_bind( - registry, name, &wl_seat_interface, required); + struct wl_seat *wl_seat = wl_registry_bind(registry, name, &wl_seat_interface, required); if (wl_seat == NULL) return; @@ -483,23 +612,20 @@ handle_global(void *data, struct wl_registry *registry, struct seat *seat = &tll_back(m->seats); wl_seat_add_listener(wl_seat, &seat_listener, seat); - instantiate_seat(seat); + update_seat(seat); mtx_unlock(&m->mod->lock); } else if (strcmp(interface, zriver_status_manager_v1_interface.name) == 0) { - const uint32_t required = 1; + const uint32_t required = 2; if (!verify_iface_version(interface, version, required)) return; - m->status_manager = wl_registry_bind( - registry, name, &zriver_status_manager_v1_interface, required); + m->status_manager = wl_registry_bind(registry, name, &zriver_status_manager_v1_interface, min(version, 4)); mtx_lock(&m->mod->lock); - tll_foreach(m->outputs, it) - instantiate_output(&it->item); - tll_foreach(m->seats, it) - instantiate_seat(&it->item); + tll_foreach(m->outputs, it) update_output(&it->item); + tll_foreach(m->seats, it) update_seat(&it->item); mtx_unlock(&m->mod->lock); } } @@ -510,7 +636,8 @@ handle_global_remove(void *data, struct wl_registry *registry, uint32_t name) struct private *m = data; mtx_lock(&m->mod->lock); - tll_foreach(m->outputs, it) { + tll_foreach(m->outputs, it) + { if (it->item.wl_name == name) { output_destroy(&it->item); tll_remove(m->outputs, it); @@ -519,7 +646,8 @@ handle_global_remove(void *data, struct wl_registry *registry, uint32_t name) } } - tll_foreach(m->seats, it) { + tll_foreach(m->seats, it) + { if (it->item.wl_name == name) { seat_destroy(&it->item); tll_remove(m->seats, it); @@ -550,9 +678,8 @@ run(struct module *mod) goto out; } - if ((registry = wl_display_get_registry(display)) == NULL || - wl_registry_add_listener(registry, ®istry_listener, m) != 0) - { + if ((registry = wl_display_get_registry(display)) == NULL + || wl_registry_add_listener(registry, ®istry_listener, m) != 0) { LOG_ERR("failed to get Wayland registry"); goto out; } @@ -564,13 +691,9 @@ run(struct module *mod) goto out; } - wl_display_roundtrip(display); - m->is_starting_up = false; + m->is_running = true; - tll_foreach(m->outputs, it) - instantiate_output(&it->item); - tll_foreach(m->seats, it) - instantiate_seat(&it->item); + wl_display_roundtrip(display); while (true) { wl_display_flush(display); @@ -603,11 +726,9 @@ run(struct module *mod) ret = 0; out: - tll_foreach(m->seats, it) - seat_destroy(&it->item); + tll_foreach(m->seats, it) seat_destroy(&it->item); tll_free(m->seats); - tll_foreach(m->outputs, it) - output_destroy(&it->item); + tll_foreach(m->outputs, it) output_destroy(&it->item); tll_free(m->outputs); if (m->xdg_output_manager != NULL) @@ -622,18 +743,19 @@ out: } static struct module * -river_new(struct particle *template, struct particle *title) +river_new(struct particle *template, struct particle *title, bool all_monitors) { struct private *m = calloc(1, sizeof(*m)); m->template = template; m->title = title; - m->is_starting_up = true; + m->all_monitors = all_monitors; struct module *mod = module_common_new(); mod->private = m; mod->run = &run; mod->destroy = &destroy; mod->content = &content; + mod->description = &description; m->mod = mod; return mod; } @@ -643,9 +765,10 @@ from_conf(const struct yml_node *node, struct conf_inherit inherited) { const struct yml_node *c = yml_get_value(node, "content"); const struct yml_node *title = yml_get_value(node, "title"); - return river_new( - conf_to_particle(c, inherited), - title != NULL ? conf_to_particle(title, inherited) : NULL); + const struct yml_node *all_monitors = yml_get_value(node, "all-monitors"); + + return river_new(conf_to_particle(c, inherited), title != NULL ? conf_to_particle(title, inherited) : NULL, + all_monitors != NULL ? yml_value_as_bool(all_monitors) : false); } static bool @@ -653,6 +776,7 @@ verify_conf(keychain_t *chain, const struct yml_node *node) { static const struct attr_info attrs[] = { {"title", false, &conf_verify_particle}, + {"all-monitors", false, &conf_verify_bool}, MODULE_COMMON_ATTRS, }; diff --git a/modules/script.c b/modules/script.c new file mode 100644 index 0000000..9f9b40a --- /dev/null +++ b/modules/script.c @@ -0,0 +1,735 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include + +#define LOG_MODULE "script" +#define LOG_ENABLE_DBG 0 +#include "../config-verify.h" +#include "../config.h" +#include "../log.h" +#include "../module.h" +#include "../plugin.h" + +static const long min_poll_interval = 250; + +struct private +{ + char *path; + size_t argc; + char **argv; + int poll_interval; + bool aborted; + + struct particle *content; + + struct tag_set tags; + + struct { + char *data; + size_t sz; + size_t idx; + } recv_buf; +}; + +static void +destroy(struct module *mod) +{ + struct private *m = mod->private; + m->content->destroy(m->content); + + struct tag **tag_array = m->tags.tags; + tag_set_destroy(&m->tags); + free(tag_array); + + for (size_t i = 0; i < m->argc; i++) + free(m->argv[i]); + free(m->argv); + free(m->recv_buf.data); + free(m->path); + free(m); + module_default_destroy(mod); +} + +static const char * +description(const struct module *mod) +{ + static char desc[32]; + const struct private *m = mod->private; + + char *path = strdup(m->path); + snprintf(desc, sizeof(desc), "script(%s)", basename(path)); + + free(path); + return desc; +} + +static struct exposable * +content(struct module *mod) +{ + const struct private *m = mod->private; + + mtx_lock(&mod->lock); + struct exposable *e = m->content->instantiate(m->content, &m->tags); + mtx_unlock(&mod->lock); + + return e; +} + +static struct tag * +process_line(struct module *mod, const char *line, size_t len) +{ + char *name = NULL; + char *value = NULL; + + const char *_name = line; + + const char *type = memchr(line, '|', len); + if (type == NULL) + goto bad_tag; + + size_t name_len = type - _name; + type++; + + const char *_value = memchr(type, '|', len - name_len - 1); + if (_value == NULL) + goto bad_tag; + + size_t type_len = _value - type; + _value++; + + size_t value_len = line + len - _value; + + LOG_DBG("%.*s: name=\"%.*s\", type=\"%.*s\", value=\"%.*s\"", (int)len, line, (int)name_len, _name, (int)type_len, + type, (int)value_len, _value); + + name = malloc(name_len + 1); + memcpy(name, _name, name_len); + name[name_len] = '\0'; + + value = malloc(value_len + 1); + memcpy(value, _value, value_len); + value[value_len] = '\0'; + + struct tag *tag = NULL; + + if (type_len == 6 && memcmp(type, "string", 6) == 0) + tag = tag_new_string(mod, name, value); + + else if (type_len == 3 && memcmp(type, "int", 3) == 0) { + errno = 0; + char *end; + long v = strtol(value, &end, 0); + + if (errno != 0 || *end != '\0') { + LOG_ERR("tag value is not an integer: %s", value); + goto bad_tag; + } + tag = tag_new_int(mod, name, v); + } + + else if (type_len == 4 && memcmp(type, "bool", 4) == 0) { + bool v; + if (strcmp(value, "true") == 0) + v = true; + else if (strcmp(value, "false") == 0) + v = false; + else { + LOG_ERR("tag value is not a boolean: %s", value); + goto bad_tag; + } + tag = tag_new_bool(mod, name, v); + } + + else if (type_len == 5 && memcmp(type, "float", 5) == 0) { + errno = 0; + char *end; + double v = strtod(value, &end); + + if (errno != 0 || *end != '\0') { + LOG_ERR("tag value is not a float: %s", value); + goto bad_tag; + } + + tag = tag_new_float(mod, name, v); + } + + else if ((type_len > 6 && memcmp(type, "range:", 6) == 0) || (type_len > 9 && memcmp(type, "realtime:", 9) == 0)) { + const char *_start = type + 6; + const char *split = memchr(_start, '-', type_len - 6); + + if (split == NULL || split == _start || (split + 1) - type >= type_len) { + LOG_ERR("tag range delimiter ('-') not found in type: %.*s", (int)type_len, type); + goto bad_tag; + } + + const char *_end = split + 1; + + size_t start_len = split - _start; + size_t end_len = type + type_len - _end; + + long start = 0; + for (size_t i = 0; i < start_len; i++) { + if (!(_start[i] >= '0' && _start[i] <= '9')) { + LOG_ERR("tag range start is not an integer: %.*s", (int)start_len, _start); + goto bad_tag; + } + + start *= 10; + start += _start[i] - '0'; + } + + long end = 0; + for (size_t i = 0; i < end_len; i++) { + if (!(_end[i] >= '0' && _end[i] <= '9')) { + LOG_ERR("tag range end is not an integer: %.*s", (int)end_len, _end); + goto bad_tag; + } + + end *= 10; + end += _end[i] - '0'; + } + + if (type_len > 9 && memcmp(type, "realtime:", 9) == 0) { + LOG_ERR("unimplemented: realtime tag"); + goto bad_tag; + } + + errno = 0; + char *vend; + long v = strtol(value, &vend, 0); + if (errno != 0 || *vend != '\0') { + LOG_ERR("tag value is not an integer: %s", value); + goto bad_tag; + } + + if (v < start || v > end) { + LOG_ERR("tag value is outside range: %ld <= %ld <= %ld", start, v, end); + goto bad_tag; + } + + tag = tag_new_int_range(mod, name, v, start, end); + } + + else { + goto bad_tag; + } + + free(name); + free(value); + return tag; + +bad_tag: + LOG_ERR("invalid tag: %.*s", (int)len, line); + free(name); + free(value); + return NULL; +} + +static void +process_transaction(struct module *mod, size_t size) +{ + struct private *m = mod->private; + mtx_lock(&mod->lock); + + size_t left = size; + const char *line = m->recv_buf.data; + + size_t line_count = 0; + { + const char *p = line; + while ((p = memchr(p, '\n', size - (p - line))) != NULL) { + p++; + line_count++; + } + } + + struct tag **old_tag_array = m->tags.tags; + tag_set_destroy(&m->tags); + free(old_tag_array); + + m->tags.tags = calloc(line_count, sizeof(m->tags.tags[0])); + m->tags.count = line_count; + + size_t idx = 0; + + while (left > 0) { + char *line_end = memchr(line, '\n', left); + assert(line_end != NULL); + + size_t line_len = line_end - line; + + struct tag *tag = process_line(mod, line, line_len); + if (tag != NULL) + m->tags.tags[idx++] = tag; + + left -= line_len + 1; + line += line_len + 1; + } + + m->tags.count = idx; + + mtx_unlock(&mod->lock); + mod->bar->refresh(mod->bar); +} + +static bool +data_received(struct module *mod, const char *data, size_t len) +{ + struct private *m = mod->private; + + while (len > m->recv_buf.sz - m->recv_buf.idx) { + size_t new_sz = m->recv_buf.sz == 0 ? 1024 : m->recv_buf.sz * 2; + char *new_buf = realloc(m->recv_buf.data, new_sz); + + if (new_buf == NULL) + return false; + + m->recv_buf.data = new_buf; + m->recv_buf.sz = new_sz; + } + + assert(m->recv_buf.sz >= m->recv_buf.idx); + assert(m->recv_buf.sz - m->recv_buf.idx >= len); + + memcpy(&m->recv_buf.data[m->recv_buf.idx], data, len); + m->recv_buf.idx += len; + + while (true) { + const char *eot = memmem(m->recv_buf.data, m->recv_buf.idx, "\n\n", 2); + if (eot == NULL) { + /* End of transaction not yet available */ + return true; + } + + const size_t transaction_size = eot - m->recv_buf.data + 1; + process_transaction(mod, transaction_size); + + assert(m->recv_buf.idx >= transaction_size + 1); + memmove(m->recv_buf.data, &m->recv_buf.data[transaction_size + 1], m->recv_buf.idx - (transaction_size + 1)); + m->recv_buf.idx -= transaction_size + 1; + } + + return true; +} + +static int +run_loop(struct module *mod, pid_t pid, int comm_fd) +{ + int ret = 1; + + while (true) { + struct pollfd fds[] = { + {.fd = mod->abort_fd, .events = POLLIN}, + {.fd = comm_fd, .events = POLLIN}, + }; + + int r = poll(fds, sizeof(fds) / sizeof(fds[0]), -1); + if (r < 0) { + if (errno == EINTR) + continue; + LOG_ERRNO("failed to poll"); + break; + } + + if (fds[1].revents & POLLIN) { + char data[4096]; + ssize_t amount = read(comm_fd, data, sizeof(data)); + if (amount < 0) { + LOG_ERRNO("failed to read from script"); + break; + } + + LOG_DBG("recv: \"%.*s\"", (int)amount, data); + + data_received(mod, data, amount); + } + + if (fds[0].revents & (POLLHUP | POLLIN)) { + /* Aborted */ + struct private *m = mod->private; + m->aborted = true; + ret = 0; + break; + } + + if (fds[1].revents & POLLHUP) { + /* Child's stdout closed */ + LOG_DBG("script pipe closed (script terminated?)"); + ret = 0; + break; + } + } + + return ret; +} + +static int +execute_script(struct module *mod) +{ + struct private *m = mod->private; + + /* Pipe to detect exec() failures */ + int exec_pipe[2]; + if (pipe2(exec_pipe, O_CLOEXEC) < 0) { + LOG_ERRNO("failed to create pipe"); + return -1; + } + + /* Stdout redirection pipe */ + int comm_pipe[2]; + if (pipe2(comm_pipe, O_CLOEXEC) < 0) { + LOG_ERRNO("failed to create stdin/stdout redirection pipe"); + close(exec_pipe[0]); + close(exec_pipe[1]); + return -1; + } + + int pid = fork(); + if (pid < 0) { + LOG_ERRNO("failed to fork"); + close(comm_pipe[0]); + close(comm_pipe[1]); + close(exec_pipe[0]); + close(exec_pipe[1]); + return -1; + } + + if (pid == 0) { + /* Child */ + + /* Construct argv for execvp() */ + char *argv[1 + m->argc + 1]; + argv[0] = m->path; + for (size_t i = 0; i < m->argc; i++) + argv[i + 1] = m->argv[i]; + argv[1 + m->argc] = NULL; + + /* Restore signal handlers and signal mask */ + sigset_t mask; + sigemptyset(&mask); + + const struct sigaction sa = {.sa_handler = SIG_DFL}; + if (sigaction(SIGINT, &sa, NULL) < 0 || sigaction(SIGTERM, &sa, NULL) < 0 || sigaction(SIGCHLD, &sa, NULL) < 0 + || sigprocmask(SIG_SETMASK, &mask, NULL) < 0) { + goto fail; + } + + /* New process group, so that we can use killpg() */ + setpgid(0, 0); + + /* Close pipe read ends */ + close(exec_pipe[0]); + close(comm_pipe[0]); + + /* Re-direct stdin/stdout */ + int dev_null = open("/dev/null", O_RDONLY | O_CLOEXEC); + if (dev_null < 0) + goto fail; + + if (dup2(dev_null, STDIN_FILENO) < 0 || dup2(comm_pipe[1], STDOUT_FILENO) < 0) { + goto fail; + } + + /* We're done with the redirection pipe */ + close(comm_pipe[1]); + comm_pipe[1] = -1; + + execvp(m->path, argv); + + fail: + (void)!write(exec_pipe[1], &errno, sizeof(errno)); + close(exec_pipe[1]); + if (comm_pipe[1] >= 0) + close(comm_pipe[1]); + _exit(errno); + } + + /* Close pipe write ends */ + close(exec_pipe[1]); + close(comm_pipe[1]); + + int _errno; + static_assert(sizeof(_errno) == sizeof(errno), "errno size mismatch"); + + /* Wait for errno from child, or FD being closed in execvp() */ + int r = read(exec_pipe[0], &_errno, sizeof(_errno)); + close(exec_pipe[0]); + + if (r < 0) { + LOG_ERRNO("failed to read from pipe"); + close(comm_pipe[0]); + return -1; + } + + if (r > 0) { + LOG_ERRNO_P(_errno, "%s: failed to start", m->path); + close(comm_pipe[0]); + waitpid(pid, NULL, 0); + return -1; + } + + /* Pipe was closed. I.e. execvp() succeeded */ + assert(r == 0); + LOG_DBG("script running under PID=%u", pid); + + int ret = run_loop(mod, pid, comm_pipe[0]); + close(comm_pipe[0]); + + if (waitpid(pid, NULL, WNOHANG) == 0) { + static const struct { + int signo; + int timeout; + const char *name; + } sig_info[] = { + {SIGINT, 2, "SIGINT"}, + {SIGTERM, 5, "SIGTERM"}, + {SIGKILL, 0, "SIGKILL"}, + }; + + for (size_t i = 0; i < sizeof(sig_info) / sizeof(sig_info[0]); i++) { + struct timeval start; + gettimeofday(&start, NULL); + + const int signo = sig_info[i].signo; + const int timeout = sig_info[i].timeout; + const char *const name __attribute__((unused)) = sig_info[i].name; + + LOG_DBG("sending %s to PID=%u (timeout=%ds)", name, pid, timeout); + killpg(pid, signo); + + /* + * Child is unlikely to terminate *immediately*. Wait a + * *short* period of time before checking waitpid() the + * first time + */ + usleep(10000); + + pid_t waited_pid; + while ((waited_pid = waitpid(pid, NULL, timeout > 0 ? WNOHANG : 0)) == 0) { + struct timeval now; + gettimeofday(&now, NULL); + + struct timeval elapsed; + timersub(&now, &start, &elapsed); + + if (elapsed.tv_sec >= timeout) + break; + + /* Don't spinning */ + thrd_yield(); + usleep(100000); /* 100ms */ + } + + if (waited_pid == pid) { + /* Child finally dead */ + break; + } + } + } else + LOG_DBG("PID=%u already terminated", pid); + + return ret; +} + +static int +run(struct module *mod) +{ + struct private *m = mod->private; + + int ret = 1; + bool keep_going = true; + + while (keep_going && !m->aborted) { + ret = execute_script(mod); + + if (ret != 0) + break; + if (m->aborted) + break; + if (m->poll_interval <= 0) + break; + + struct timeval now; + if (gettimeofday(&now, NULL) < 0) { + LOG_ERRNO("failed to get current time"); + break; + } + + struct timeval poll_interval = { + .tv_sec = m->poll_interval / 1000, + .tv_usec = (m->poll_interval % 1000) * 1000, + }; + + struct timeval timeout; + timeradd(&now, &poll_interval, &timeout); + + while (true) { + struct pollfd fds[] = {{.fd = mod->abort_fd, .events = POLLIN}}; + + struct timeval now; + if (gettimeofday(&now, NULL) < 0) { + LOG_ERRNO("failed to get current time"); + keep_going = false; + break; + } + + if (!timercmp(&now, &timeout, <)) { + /* We’ve reached the timeout, it’s time to execute the script again */ + break; + } + + struct timeval time_left; + timersub(&timeout, &now, &time_left); + + int r = poll(fds, 1, time_left.tv_sec * 1000 + time_left.tv_usec / 1000); + if (r < 0) { + if (errno == EINTR) + continue; + LOG_ERRNO("failed to poll"); + keep_going = false; + break; + } + + if (r > 0) { + m->aborted = true; + break; + } + } + } + + return ret; +} + +static struct module * +script_new(char *path, size_t argc, const char *const argv[static argc], int poll_interval, struct particle *_content) +{ + struct private *m = calloc(1, sizeof(*m)); + m->path = path; + m->content = _content; + m->argc = argc; + m->argv = malloc(argc * sizeof(m->argv[0])); + for (size_t i = 0; i < argc; i++) + m->argv[i] = strdup(argv[i]); + m->poll_interval = poll_interval; + + struct module *mod = module_common_new(); + mod->private = m; + mod->run = &run; + mod->destroy = &destroy; + mod->content = &content; + mod->description = &description; + return mod; +} + +static struct module * +from_conf(const struct yml_node *node, struct conf_inherit inherited) +{ + const struct yml_node *path_node = yml_get_value(node, "path"); + const struct yml_node *args = yml_get_value(node, "args"); + const struct yml_node *c = yml_get_value(node, "content"); + const struct yml_node *poll_interval = yml_get_value(node, "poll-interval"); + + size_t argc = args != NULL ? yml_list_length(args) : 0; + const char *argv[argc]; + + if (args != NULL) { + size_t i = 0; + for (struct yml_list_iter iter = yml_list_iter(args); iter.node != NULL; yml_list_next(&iter), i++) { + argv[i] = yml_value_as_string(iter.node); + } + } + + const char *yml_path = yml_value_as_string(path_node); + char *path = NULL; + + if (yml_path[0] == '~' && yml_path[1] == '/') { + const char *home_dir = getenv("HOME"); + + if (home_dir == NULL) { + LOG_ERRNO("failed to expand '~"); + return NULL; + } + + if (asprintf(&path, "%s/%s", home_dir, yml_path + 2) < 0) { + LOG_ERRNO("failed to expand '~"); + return NULL; + } + } else + path = strdup(yml_path); + + return script_new(path, argc, argv, poll_interval != NULL ? yml_value_as_int(poll_interval) : 0, + conf_to_particle(c, inherited)); +} + +static bool +conf_verify_path(keychain_t *chain, const struct yml_node *node) +{ + if (!conf_verify_string(chain, node)) + return false; + + const char *path = yml_value_as_string(node); + + const bool is_tilde = path[0] == '~' && path[1] == '/'; + const bool is_absolute = path[0] == '/'; + + if (!is_tilde && !is_absolute) { + LOG_ERR("%s: path must either be absolute, or begin with '~/'", conf_err_prefix(chain, node)); + return false; + } + + return true; +} + +static bool +conf_verify_args(keychain_t *chain, const struct yml_node *node) +{ + return conf_verify_list(chain, node, &conf_verify_string); +} + +static bool +conf_verify_poll_interval(keychain_t *chain, const struct yml_node *node) +{ + if (!conf_verify_unsigned(chain, node)) + return false; + + if (yml_value_as_int(node) < min_poll_interval) { + LOG_ERR("%s: interval value cannot be less than %ldms", conf_err_prefix(chain, node), min_poll_interval); + return false; + } + + return true; +} + +static bool +verify_conf(keychain_t *chain, const struct yml_node *node) +{ + static const struct attr_info attrs[] = { + {"path", true, &conf_verify_path}, + {"args", false, &conf_verify_args}, + {"poll-interval", false, &conf_verify_poll_interval}, + MODULE_COMMON_ATTRS, + }; + + return conf_verify_dict(chain, node, attrs); +} + +const struct module_iface module_script_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_script_iface"))); +#endif diff --git a/modules/sway_xkb.c b/modules/sway-xkb.c similarity index 71% rename from modules/sway_xkb.c rename to modules/sway-xkb.c index 114a361..1507241 100644 --- a/modules/sway_xkb.c +++ b/modules/sway-xkb.c @@ -3,15 +3,17 @@ #define LOG_MODULE "sway-xkb" #define LOG_ENABLE_DBG 0 -#include "../log.h" #include "../bar/bar.h" #include "../config-verify.h" #include "../config.h" +#include "../log.h" #include "../particles/dynlist.h" #include "../plugin.h" -#include "i3-ipc.h" #include "i3-common.h" +#include "i3-ipc.h" + +#define max(x, y) ((x) > (y) ? (x) : (y)) struct input { bool exists; @@ -19,7 +21,8 @@ struct input { char *layout; }; -struct private { +struct private +{ struct particle *template; int left_spacing; int right_spacing; @@ -52,6 +55,12 @@ destroy(struct module *mod) module_default_destroy(mod); } +static const char * +description(const struct module *mod) +{ + return "sway-xkb"; +} + static struct exposable * content(struct module *mod) { @@ -59,7 +68,9 @@ content(struct module *mod) mtx_lock(&mod->lock); - struct exposable *particles[m->num_existing_inputs]; + assert(m->num_existing_inputs <= m->num_inputs); + struct exposable *particles[max(m->num_existing_inputs, 1)]; + for (size_t i = 0, j = 0; i < m->num_inputs; i++) { const struct input *input = &m->inputs[i]; @@ -79,12 +90,11 @@ content(struct module *mod) } mtx_unlock(&mod->lock); - return dynlist_exposable_new( - particles, m->num_existing_inputs, m->left_spacing, m->right_spacing); + return dynlist_exposable_new(particles, m->num_existing_inputs, m->left_spacing, m->right_spacing); } static bool -handle_input_reply(int type, const struct json_object *json, void *_mod) +handle_input_reply(int sock, int type, const struct json_object *json, void *_mod) { struct module *mod = _mod; struct private *m = mod->private; @@ -99,12 +109,23 @@ handle_input_reply(int type, const struct json_object *json, void *_mod) return false; const char *id = json_object_get_string(identifier); + + struct json_object *type; + if (!json_object_object_get_ex(obj, "type", &type)) + return false; + if (strcmp(json_object_get_string(type), "keyboard") != 0) { + LOG_DBG("ignoring non-keyboard input '%s'", id); + continue; + } + struct input *input = NULL; for (size_t i = 0; i < m->num_inputs; i++) { struct input *maybe_input = &m->inputs[i]; - if (strcmp(maybe_input->identifier, id) == 0) { + if (strcmp(maybe_input->identifier, id) == 0 && !maybe_input->exists) { input = maybe_input; + LOG_DBG("adding: %s", id); + mtx_lock(&mod->lock); input->exists = true; m->num_existing_inputs++; @@ -120,8 +141,7 @@ handle_input_reply(int type, const struct json_object *json, void *_mod) /* Get current/active layout */ struct json_object *layout; - if (!json_object_object_get_ex( - obj, "xkb_active_layout_name", &layout)) + if (!json_object_object_get_ex(obj, "xkb_active_layout_name", &layout)) return false; const char *new_layout_str = json_object_get_string(layout); @@ -140,7 +160,7 @@ handle_input_reply(int type, const struct json_object *json, void *_mod) } static bool -handle_input_event(int type, const struct json_object *json, void *_mod) +handle_input_event(int sock, int type, const struct json_object *json, void *_mod) { struct module *mod = _mod; struct private *m = mod->private; @@ -166,6 +186,15 @@ handle_input_event(int type, const struct json_object *json, void *_mod) return false; const char *id = json_object_get_string(identifier); + + struct json_object *input_type; + if (!json_object_object_get_ex(obj, "type", &input_type)) + return false; + if (strcmp(json_object_get_string(input_type), "keyboard") != 0) { + LOG_DBG("ignoring non-keyboard input '%s'", id); + return true; + } + struct input *input = NULL; for (size_t i = 0; i < m->num_inputs; i++) { struct input *maybe_input = &m->inputs[i]; @@ -180,27 +209,36 @@ handle_input_event(int type, const struct json_object *json, void *_mod) return true; } - if (is_removed || is_added) { - mtx_lock(&mod->lock); - assert((is_removed && input->exists) || - (is_added && !input->exists)); + if (is_removed) { + if (input->exists) { + LOG_DBG("removing: %s", id); - input->exists = !input->exists; - m->num_existing_inputs += is_added ? 1 : -1; - m->dirty = true; + mtx_lock(&mod->lock); + input->exists = false; + m->num_existing_inputs--; + m->dirty = true; + mtx_unlock(&mod->lock); + } + return true; + } - mtx_unlock(&mod->lock); + if (is_added) { + if (!input->exists) { + LOG_DBG("adding: %s", id); - if (is_removed) - return true; + mtx_lock(&mod->lock); + input->exists = true; + m->num_existing_inputs++; + m->dirty = true; + mtx_unlock(&mod->lock); + } - /* let is_added fall through, to update layout */ + /* “fallthrough”, to query current/active layout */ } /* Get current/active layout */ struct json_object *layout; - if (!json_object_object_get_ex( - obj, "xkb_active_layout_name", &layout)) + if (!json_object_object_get_ex(obj, "xkb_active_layout_name", &layout)) return false; const char *new_layout_str = json_object_get_string(layout); @@ -240,7 +278,7 @@ run(struct module *mod) if (!i3_get_socket_address(&addr)) return 1; - int sock = socket(AF_UNIX, SOCK_STREAM, 0); + int sock = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0); if (sock == -1) { LOG_ERRNO("failed to create UNIX socket"); return 1; @@ -268,8 +306,8 @@ run(struct module *mod) } static struct module * -sway_xkb_new(struct particle *template, const char *identifiers[], - size_t num_identifiers, int left_spacing, int right_spacing) +sway_xkb_new(struct particle *template, const char *identifiers[], size_t num_identifiers, int left_spacing, + int right_spacing) { struct private *m = calloc(1, sizeof(*m)); m->template = template; @@ -289,6 +327,7 @@ sway_xkb_new(struct particle *template, const char *identifiers[], mod->run = &run; mod->destroy = &destroy; mod->content = &content; + mod->description = &description; return mod; } @@ -301,40 +340,32 @@ from_conf(const struct yml_node *node, struct conf_inherit inherited) const struct yml_node *left_spacing = yml_get_value(node, "left-spacing"); const struct yml_node *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; + 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; const struct yml_node *ids = yml_get_value(node, "identifiers"); const size_t num_ids = yml_list_length(ids); const char *identifiers[num_ids]; size_t i = 0; - for (struct yml_list_iter it = yml_list_iter(ids); - it.node != NULL; - yml_list_next(&it), i++) - { + for (struct yml_list_iter it = yml_list_iter(ids); it.node != NULL; yml_list_next(&it), i++) { identifiers[i] = yml_value_as_string(it.node); } - return sway_xkb_new( - conf_to_particle(c, inherited), identifiers, num_ids, left, right); + return sway_xkb_new(conf_to_particle(c, inherited), identifiers, num_ids, left, right); } static bool verify_identifiers(keychain_t *chain, const struct yml_node *node) { if (!yml_is_list(node)) { - LOG_ERR("%s: identifiers must be a list of strings", - conf_err_prefix(chain, node)); + LOG_ERR("%s: identifiers must be a list of strings", conf_err_prefix(chain, node)); return false; } - for (struct yml_list_iter it = yml_list_iter(node); - it.node != NULL; - yml_list_next(&it)) - { + for (struct yml_list_iter it = yml_list_iter(node); it.node != NULL; yml_list_next(&it)) { if (!conf_verify_string(chain, it.node)) return false; } @@ -346,9 +377,9 @@ static bool verify_conf(keychain_t *chain, const struct yml_node *node) { static const struct attr_info attrs[] = { - {"spacing", false, &conf_verify_int}, - {"left-spacing", false, &conf_verify_int}, - {"right-spacing", false, &conf_verify_int}, + {"spacing", false, &conf_verify_unsigned}, + {"left-spacing", false, &conf_verify_unsigned}, + {"right-spacing", false, &conf_verify_unsigned}, {"identifiers", true, &verify_identifiers}, MODULE_COMMON_ATTRS, }; @@ -362,5 +393,5 @@ const struct module_iface module_sway_xkb_iface = { }; #if defined(CORE_PLUGINS_AS_SHARED_LIBRARIES) -extern const struct module_iface iface __attribute__((weak, alias("module_sway_xkb_iface"))) ; +extern const struct module_iface iface __attribute__((weak, alias("module_sway_xkb_iface"))); #endif diff --git a/modules/xkb.c b/modules/xkb.c index 16cc864..e8e3c91 100644 --- a/modules/xkb.c +++ b/modules/xkb.c @@ -1,7 +1,7 @@ -#include -#include #include #include +#include +#include #include @@ -10,10 +10,10 @@ #define LOG_MODULE "xkb" #define LOG_ENABLE_DBG 0 -#include "../log.h" #include "../bar/bar.h" -#include "../config.h" #include "../config-verify.h" +#include "../config.h" +#include "../log.h" #include "../plugin.h" #include "../xcb.h" @@ -32,7 +32,8 @@ struct indicators { char **names; }; -struct private { +struct private +{ struct particle *label; struct indicators indicators; struct layouts layouts; @@ -72,6 +73,12 @@ destroy(struct module *mod) module_default_destroy(mod); } +static const char * +description(const struct module *mod) +{ + return "xkb"; +} + static struct exposable * content(struct module *mod) { @@ -111,10 +118,8 @@ xkb_enable(xcb_connection_t *conn) { xcb_generic_error_t *err; - xcb_xkb_use_extension_cookie_t cookie = xcb_xkb_use_extension( - conn, XCB_XKB_MAJOR_VERSION, XCB_XKB_MINOR_VERSION); - xcb_xkb_use_extension_reply_t *reply = xcb_xkb_use_extension_reply( - conn, cookie, &err); + xcb_xkb_use_extension_cookie_t cookie = xcb_xkb_use_extension(conn, XCB_XKB_MAJOR_VERSION, XCB_XKB_MINOR_VERSION); + xcb_xkb_use_extension_reply_t *reply = xcb_xkb_use_extension_reply(conn, cookie, &err); if (err != NULL) { LOG_ERR("failed to query for XKB extension: %s", xcb_error(err)); @@ -136,8 +141,7 @@ xkb_enable(xcb_connection_t *conn) static int get_xkb_event_base(xcb_connection_t *conn) { - const struct xcb_query_extension_reply_t *reply = xcb_get_extension_data( - conn, &xcb_xkb_id); + const struct xcb_query_extension_reply_t *reply = xcb_get_extension_data(conn, &xcb_xkb_id); if (reply == NULL) { LOG_ERR("failed to get XKB extension data"); @@ -153,19 +157,14 @@ get_xkb_event_base(xcb_connection_t *conn) } static bool -get_layouts_and_indicators(xcb_connection_t *conn, struct layouts *layouts, - struct indicators *indicators) +get_layouts_and_indicators(xcb_connection_t *conn, struct layouts *layouts, struct indicators *indicators) { xcb_generic_error_t *err; - xcb_xkb_get_names_cookie_t cookie = xcb_xkb_get_names( - conn, - XCB_XKB_ID_USE_CORE_KBD, - XCB_XKB_NAME_DETAIL_GROUP_NAMES | - XCB_XKB_NAME_DETAIL_SYMBOLS | - XCB_XKB_NAME_DETAIL_INDICATOR_NAMES); + xcb_xkb_get_names_cookie_t cookie = xcb_xkb_get_names(conn, XCB_XKB_ID_USE_CORE_KBD, + XCB_XKB_NAME_DETAIL_GROUP_NAMES | XCB_XKB_NAME_DETAIL_SYMBOLS + | XCB_XKB_NAME_DETAIL_INDICATOR_NAMES); - xcb_xkb_get_names_reply_t *reply = xcb_xkb_get_names_reply( - conn, cookie, &err); + xcb_xkb_get_names_reply_t *reply = xcb_xkb_get_names_reply(conn, cookie, &err); if (err != NULL) { LOG_ERR("failed to get layouts and indicators: %s", xcb_error(err)); @@ -175,22 +174,18 @@ get_layouts_and_indicators(xcb_connection_t *conn, struct layouts *layouts, xcb_xkb_get_names_value_list_t vlist; void *buf = xcb_xkb_get_names_value_list(reply); - xcb_xkb_get_names_value_list_unpack( - buf, reply->nTypes, reply->indicators, reply->virtualMods, - reply->groupNames, reply->nKeys, reply->nKeyAliases, - reply->nRadioGroups, reply->which, &vlist); + xcb_xkb_get_names_value_list_unpack(buf, reply->nTypes, reply->indicators, reply->virtualMods, reply->groupNames, + reply->nKeys, reply->nKeyAliases, reply->nRadioGroups, reply->which, &vlist); /* Number of groups (aka layouts) */ layouts->count = xcb_xkb_get_names_value_list_groups_length(reply, &vlist); layouts->layouts = calloc(layouts->count, sizeof(layouts->layouts[0])); /* Number of indicators */ - indicators->count = xcb_xkb_get_names_value_list_indicator_names_length( - reply, &vlist); + indicators->count = xcb_xkb_get_names_value_list_indicator_names_length(reply, &vlist); indicators->names = calloc(indicators->count, sizeof(indicators->names[0])); - xcb_get_atom_name_cookie_t symbols_name_cookie = xcb_get_atom_name( - conn, vlist.symbolsName); + xcb_get_atom_name_cookie_t symbols_name_cookie = xcb_get_atom_name(conn, vlist.symbolsName); xcb_get_atom_name_cookie_t group_name_cookies[layouts->count]; for (size_t i = 0; i < layouts->count; i++) @@ -203,17 +198,14 @@ get_layouts_and_indicators(xcb_connection_t *conn, struct layouts *layouts, char *symbols = NULL; /* Get layout short names (e.g. "us") */ - xcb_get_atom_name_reply_t *atom_name = xcb_get_atom_name_reply( - conn, symbols_name_cookie, &err); + xcb_get_atom_name_reply_t *atom_name = xcb_get_atom_name_reply(conn, symbols_name_cookie, &err); if (err != NULL) { LOG_ERR("failed to get 'symbols' atom name: %s", xcb_error(err)); free(err); goto err; } - symbols = strndup( - xcb_get_atom_name_name(atom_name), - xcb_get_atom_name_name_length(atom_name)); + symbols = strndup(xcb_get_atom_name_name(atom_name), xcb_get_atom_name_name_length(atom_name)); LOG_DBG("symbols: %s", symbols); free(atom_name); @@ -226,9 +218,7 @@ get_layouts_and_indicators(xcb_connection_t *conn, struct layouts *layouts, goto err; } - layouts->layouts[i].name = strndup( - xcb_get_atom_name_name(atom_name), - xcb_get_atom_name_name_length(atom_name)); + layouts->layouts[i].name = strndup(xcb_get_atom_name_name(atom_name), xcb_get_atom_name_name_length(atom_name)); LOG_DBG("layout #%zd: long name: %s", i, layouts->layouts[i].name); free(atom_name); @@ -243,9 +233,7 @@ get_layouts_and_indicators(xcb_connection_t *conn, struct layouts *layouts, goto err; } - indicators->names[i] = strndup( - xcb_get_atom_name_name(atom_name), - xcb_get_atom_name_name_length(atom_name)); + indicators->names[i] = strndup(xcb_get_atom_name_name(atom_name), xcb_get_atom_name_name_length(atom_name)); LOG_DBG("indicator #%zd: %s", i, indicators->names[i]); free(atom_name); @@ -253,8 +241,7 @@ get_layouts_and_indicators(xcb_connection_t *conn, struct layouts *layouts, /* e.g. pc+us+inet(evdev)+group(..) */ size_t layout_idx = 0; - for (char *tok_ctx = NULL, *tok = strtok_r(symbols, "+", &tok_ctx); - tok != NULL; + for (char *tok_ctx = NULL, *tok = strtok_r(symbols, "+", &tok_ctx); tok != NULL; tok = strtok_r(NULL, "+", &tok_ctx)) { char *fname = strtok(tok, "()"); @@ -273,8 +260,7 @@ get_layouts_and_indicators(xcb_connection_t *conn, struct layouts *layouts, continue; if (layout_idx >= layouts->count) { - LOG_ERR("layout vs group name count mismatch: %zd > %zd", - layout_idx + 1, layouts->count); + LOG_ERR("layout vs group name count mismatch: %zd > %zd", layout_idx + 1, layouts->count); goto err; } @@ -284,8 +270,7 @@ get_layouts_and_indicators(xcb_connection_t *conn, struct layouts *layouts, } if (layout_idx != layouts->count) { - LOG_ERR("layout vs group name count mismatch: %zd != %zd", - layout_idx, layouts->count); + LOG_ERR("layout vs group name count mismatch: %zd != %zd", layout_idx, layouts->count); goto err; } @@ -306,10 +291,8 @@ get_current_layout(xcb_connection_t *conn) { xcb_generic_error_t *err; - xcb_xkb_get_state_cookie_t cookie = xcb_xkb_get_state( - conn, XCB_XKB_ID_USE_CORE_KBD); - xcb_xkb_get_state_reply_t *reply = xcb_xkb_get_state_reply( - conn, cookie, &err); + xcb_xkb_get_state_cookie_t cookie = xcb_xkb_get_state(conn, XCB_XKB_ID_USE_CORE_KBD); + xcb_xkb_get_state_reply_t *reply = xcb_xkb_get_state_reply(conn, cookie, &err); if (err != NULL) { LOG_ERR("failed to get XKB state: %s", xcb_error(err)); @@ -326,10 +309,8 @@ static uint32_t get_indicator_state(xcb_connection_t *conn) { xcb_generic_error_t *err; - xcb_xkb_get_indicator_state_cookie_t cookie = xcb_xkb_get_indicator_state( - conn, XCB_XKB_ID_USE_CORE_KBD); - xcb_xkb_get_indicator_state_reply_t *reply = xcb_xkb_get_indicator_state_reply( - conn, cookie, &err); + xcb_xkb_get_indicator_state_cookie_t cookie = xcb_xkb_get_indicator_state(conn, XCB_XKB_ID_USE_CORE_KBD); + xcb_xkb_get_indicator_state_reply_t *reply = xcb_xkb_get_indicator_state_reply(conn, cookie, &err); if (err != NULL) { LOG_ERR("failed to get indicator state: %s", xcb_error(err)); @@ -347,23 +328,14 @@ get_indicator_state(xcb_connection_t *conn) static bool register_for_events(xcb_connection_t *conn) { - xcb_void_cookie_t cookie = xcb_xkb_select_events_checked( - conn, - XCB_XKB_ID_USE_CORE_KBD, - ( - XCB_XKB_EVENT_TYPE_NEW_KEYBOARD_NOTIFY | - XCB_XKB_EVENT_TYPE_STATE_NOTIFY | - XCB_XKB_EVENT_TYPE_MAP_NOTIFY | - XCB_XKB_EVENT_TYPE_INDICATOR_STATE_NOTIFY - ), - 0, - ( - XCB_XKB_EVENT_TYPE_NEW_KEYBOARD_NOTIFY | - XCB_XKB_EVENT_TYPE_STATE_NOTIFY | - XCB_XKB_EVENT_TYPE_MAP_NOTIFY | - XCB_XKB_EVENT_TYPE_INDICATOR_STATE_NOTIFY - ), - 0, 0, NULL); + xcb_void_cookie_t cookie + = xcb_xkb_select_events_checked(conn, XCB_XKB_ID_USE_CORE_KBD, + (XCB_XKB_EVENT_TYPE_NEW_KEYBOARD_NOTIFY | XCB_XKB_EVENT_TYPE_STATE_NOTIFY + | XCB_XKB_EVENT_TYPE_MAP_NOTIFY | XCB_XKB_EVENT_TYPE_INDICATOR_STATE_NOTIFY), + 0, + (XCB_XKB_EVENT_TYPE_NEW_KEYBOARD_NOTIFY | XCB_XKB_EVENT_TYPE_STATE_NOTIFY + | XCB_XKB_EVENT_TYPE_MAP_NOTIFY | XCB_XKB_EVENT_TYPE_INDICATOR_STATE_NOTIFY), + 0, 0, NULL); xcb_generic_error_t *err = xcb_request_check(conn, cookie); if (err != NULL) { @@ -387,13 +359,17 @@ event_loop(struct module *mod, xcb_connection_t *conn, int xkb_event_base) assert(xcb_fd >= 0); while (!has_error) { - struct pollfd pfds[] = { - {.fd = mod->abort_fd, .events = POLLIN }, - {.fd = xcb_fd, .events = POLLIN | POLLHUP } - }; + struct pollfd pfds[] = {{.fd = mod->abort_fd, .events = POLLIN}, {.fd = xcb_fd, .events = POLLIN | POLLHUP}}; /* Use poll() since xcb_wait_for_events() doesn't return on signals */ - poll(pfds, sizeof(pfds) / sizeof(pfds[0]), -1); + if (poll(pfds, sizeof(pfds) / sizeof(pfds[0]), -1) < 0) { + if (errno == EINTR) + continue; + + LOG_ERRNO("failed to poll"); + break; + } + if (pfds[0].revents & POLLIN) { ret = true; break; @@ -412,9 +388,7 @@ event_loop(struct module *mod, xcb_connection_t *conn, int xkb_event_base) * not for long though... */ - for (xcb_generic_event_t *_evt = xcb_wait_for_event(conn); - _evt != NULL; - _evt = xcb_poll_for_event(conn)) { + for (xcb_generic_event_t *_evt = xcb_wait_for_event(conn); _evt != NULL; _evt = xcb_poll_for_event(conn)) { if (_evt->response_type != xkb_event_base) { LOG_WARN("non-XKB event ignored: %d", _evt->response_type); @@ -422,7 +396,7 @@ event_loop(struct module *mod, xcb_connection_t *conn, int xkb_event_base) continue; } - switch(_evt->pad0) { + switch (_evt->pad0) { default: LOG_WARN("unimplemented XKB event: %d", _evt->pad0); break; @@ -450,7 +424,7 @@ event_loop(struct module *mod, xcb_connection_t *conn, int xkb_event_base) mtx_unlock(&mod->lock); bar->refresh(bar); } else { - /* Can happen while transitioning to a new map */ + /* Can happen while transitioning to a new map */ free_layouts(layouts); free_indicators(indicators); } @@ -459,8 +433,7 @@ event_loop(struct module *mod, xcb_connection_t *conn, int xkb_event_base) } case XCB_XKB_STATE_NOTIFY: { - const xcb_xkb_state_notify_event_t *evt = - (const xcb_xkb_state_notify_event_t *)_evt; + const xcb_xkb_state_notify_event_t *evt = (const xcb_xkb_state_notify_event_t *)_evt; if (evt->changed & XCB_XKB_STATE_PART_GROUP_STATE) { mtx_lock(&mod->lock); @@ -477,8 +450,8 @@ event_loop(struct module *mod, xcb_connection_t *conn, int xkb_event_base) break; case XCB_XKB_INDICATOR_STATE_NOTIFY: { - const xcb_xkb_indicator_state_notify_event_t *evt = - (const xcb_xkb_indicator_state_notify_event_t *)_evt; + const xcb_xkb_indicator_state_notify_event_t *evt + = (const xcb_xkb_indicator_state_notify_event_t *)_evt; #if 0 size_t idx = __builtin_ctz(evt->stateChanged); @@ -495,8 +468,7 @@ event_loop(struct module *mod, xcb_connection_t *conn, int xkb_event_base) continue; bool enabled = (evt->state >> i) & 1; - LOG_DBG("%s: %s", m->indicators.names[i], - enabled ? "enabled" : "disabled"); + LOG_DBG("%s: %s", m->indicators.names[i], enabled ? "enabled" : "disabled"); const char *name = m->indicators.names[i]; bool is_caps = strcasecmp(name, "caps lock") == 0; @@ -596,18 +568,12 @@ talk_to_xkb(struct module *mod, xcb_connection_t *conn) size_t idx = 0; for (size_t i = 0; i < layouts.count; i++) { - idx += snprintf(&buf[idx], sizeof(buf) - idx, "%s%s (%s)%s", - i == m->current ? "*" : "", - layouts.layouts[i].name, - layouts.layouts[i].symbol, - i + 1 < layouts.count ? ", " : ""); + idx += snprintf(&buf[idx], sizeof(buf) - idx, "%s%s (%s)%s", i == m->current ? "*" : "", + layouts.layouts[i].name, layouts.layouts[i].symbol, i + 1 < layouts.count ? ", " : ""); } - LOG_INFO("layouts: %s, caps-lock:%s, num-lock:%s, scroll-lock:%s", - buf, - caps_lock ? "on" : "off", - num_lock ? "on" : "off", - scroll_lock ? "on" : "off"); + LOG_INFO("layouts: %s, caps-lock:%s, num-lock:%s, scroll-lock:%s", buf, caps_lock ? "on" : "off", + num_lock ? "on" : "off", scroll_lock ? "on" : "off"); } mtx_lock(&mod->lock); @@ -650,6 +616,7 @@ xkb_new(struct particle *label) mod->run = &run; mod->destroy = &destroy; mod->content = &content; + mod->description = &description; return mod; } diff --git a/modules/xwindow.c b/modules/xwindow.c index ad856a4..c730128 100644 --- a/modules/xwindow.c +++ b/modules/xwindow.c @@ -1,27 +1,30 @@ +#include +#include +#include #include #include #include -#include -#include #include +#include -#include #include #include +#include #include #include #include #define LOG_MODULE "xwindow" -#include "../log.h" #include "../bar/bar.h" -#include "../config.h" #include "../config-verify.h" +#include "../config.h" +#include "../log.h" #include "../plugin.h" #include "../xcb.h" -struct private { +struct private +{ /* Accessed from bar thread only */ struct particle *label; @@ -36,27 +39,29 @@ struct private { xcb_window_t active_win; }; +static const char * +description(const struct module *mod) +{ + return "xwindow"; +} + static void update_active_window(struct private *m) { if (m->active_win != 0) { - xcb_void_cookie_t c = xcb_change_window_attributes_checked( - m->conn, m->active_win, XCB_CW_EVENT_MASK, - (const uint32_t []){XCB_EVENT_MASK_NO_EVENT}); + xcb_void_cookie_t c = xcb_change_window_attributes_checked(m->conn, m->active_win, XCB_CW_EVENT_MASK, + (const uint32_t[]){XCB_EVENT_MASK_NO_EVENT}); xcb_generic_error_t *e = xcb_request_check(m->conn, c); if (e != NULL) { - LOG_DBG( - "failed to de-register events on previous active window: %s", - xcb_error(e)); + LOG_DBG("failed to de-register events on previous active window: %s", xcb_error(e)); free(e); } m->active_win = 0; } - xcb_get_property_cookie_t c = xcb_get_property( - m->conn, 0, m->root_win, _NET_ACTIVE_WINDOW, XCB_ATOM_WINDOW, 0, 32); + xcb_get_property_cookie_t c = xcb_get_property(m->conn, 0, m->root_win, _NET_ACTIVE_WINDOW, XCB_ATOM_WINDOW, 0, 32); xcb_generic_error_t *e; xcb_get_property_reply_t *r = xcb_get_property_reply(m->conn, c, &e); @@ -78,9 +83,8 @@ update_active_window(struct private *m) free(r); if (m->active_win != 0) { - xcb_change_window_attributes( - m->conn, m->active_win, XCB_CW_EVENT_MASK, - (const uint32_t []){XCB_EVENT_MASK_PROPERTY_CHANGE}); + xcb_change_window_attributes(m->conn, m->active_win, XCB_CW_EVENT_MASK, + (const uint32_t[]){XCB_EVENT_MASK_PROPERTY_CHANGE}); } } @@ -97,8 +101,7 @@ update_application(struct module *mod) if (m->active_win == 0) return; - xcb_get_property_cookie_t c = xcb_get_property( - m->conn, 0, m->active_win, _NET_WM_PID, XCB_ATOM_CARDINAL, 0, 32); + xcb_get_property_cookie_t c = xcb_get_property(m->conn, 0, m->active_win, _NET_WM_PID, XCB_ATOM_CARDINAL, 0, 32); xcb_generic_error_t *e; xcb_get_property_reply_t *r = xcb_get_property_reply(m->conn, c, &e); @@ -127,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; @@ -156,12 +159,11 @@ update_title(struct module *mod) if (m->active_win == 0) return; - xcb_get_property_cookie_t c1 = xcb_get_property( - m->conn, 0, m->active_win, _NET_WM_VISIBLE_NAME, UTF8_STRING, 0, 1000); - xcb_get_property_cookie_t c2 = xcb_get_property( - m->conn, 0, m->active_win, _NET_WM_NAME, UTF8_STRING, 0, 1000); - xcb_get_property_cookie_t c3 = xcb_get_property( - m->conn, 0, m->active_win, XCB_ATOM_WM_NAME, XCB_ATOM_STRING, 0, 1000); + xcb_get_property_cookie_t c1 + = xcb_get_property(m->conn, 0, m->active_win, _NET_WM_VISIBLE_NAME, UTF8_STRING, 0, 1000); + xcb_get_property_cookie_t c2 = xcb_get_property(m->conn, 0, m->active_win, _NET_WM_NAME, UTF8_STRING, 0, 1000); + xcb_get_property_cookie_t c3 + = xcb_get_property(m->conn, 0, m->active_win, XCB_ATOM_WM_NAME, XCB_ATOM_STRING, 0, 1000); xcb_generic_error_t *e1, *e2, *e3; xcb_get_property_reply_t *r1 = xcb_get_property_reply(m->conn, c1, &e1); @@ -199,7 +201,7 @@ update_title(struct module *mod) free(r1); free(r2); free(r3); - } +} static int run(struct module *mod) @@ -219,19 +221,16 @@ run(struct module *mod) /* Need a window(?) to be able to process events */ m->monitor_win = xcb_generate_id(m->conn); - xcb_create_window(m->conn, screen->root_depth, m->monitor_win, screen->root, - -1, -1, 1, 1, - 0, - XCB_WINDOW_CLASS_INPUT_OUTPUT, screen->root_visual, - XCB_CW_OVERRIDE_REDIRECT, (const uint32_t []){1}); + xcb_create_window(m->conn, screen->root_depth, m->monitor_win, screen->root, -1, -1, 1, 1, 0, + XCB_WINDOW_CLASS_INPUT_OUTPUT, screen->root_visual, XCB_CW_OVERRIDE_REDIRECT, + (const uint32_t[]){1}); xcb_map_window(m->conn, m->monitor_win); /* Register for property changes on root window. This allows us to * catch e.g. window switches etc */ - xcb_change_window_attributes( - m->conn, screen->root, XCB_CW_EVENT_MASK, - (const uint32_t []){XCB_EVENT_MASK_PROPERTY_CHANGE}); + xcb_change_window_attributes(m->conn, screen->root, XCB_CW_EVENT_MASK, + (const uint32_t[]){XCB_EVENT_MASK_PROPERTY_CHANGE}); xcb_flush(m->conn); @@ -240,19 +239,25 @@ run(struct module *mod) update_title(mod); mod->bar->refresh(mod->bar); + int ret = 1; + int xcb_fd = xcb_get_file_descriptor(m->conn); while (true) { - struct pollfd fds[] = {{.fd = mod->abort_fd, .events = POLLIN}, - {.fd = xcb_fd, .events = POLLIN}}; - poll(fds, 2, -1); + struct pollfd fds[] = {{.fd = mod->abort_fd, .events = POLLIN}, {.fd = xcb_fd, .events = POLLIN}}; + if (poll(fds, sizeof(fds) / sizeof(fds[0]), -1) < 0) { + if (errno == EINTR) + continue; - if (fds[0].revents & POLLIN) + LOG_ERRNO("failed to poll"); break; + } - for (xcb_generic_event_t *_e = xcb_wait_for_event(m->conn); - _e != NULL; - _e = xcb_poll_for_event(m->conn)) - { + if (fds[0].revents & POLLIN) { + ret = 0; + break; + } + + for (xcb_generic_event_t *_e = xcb_wait_for_event(m->conn); _e != NULL; _e = xcb_poll_for_event(m->conn)) { switch (XCB_EVENT_RESPONSE_TYPE(_e)) { case 0: LOG_ERR("XCB: %s", xcb_error((const xcb_generic_error_t *)_e)); @@ -260,18 +265,13 @@ run(struct module *mod) case XCB_PROPERTY_NOTIFY: { xcb_property_notify_event_t *e = (xcb_property_notify_event_t *)_e; - if (e->atom == _NET_ACTIVE_WINDOW || - e->atom == _NET_CURRENT_DESKTOP) - { + if (e->atom == _NET_ACTIVE_WINDOW || e->atom == _NET_CURRENT_DESKTOP) { /* Active desktop and/or window changed */ update_active_window(m); update_application(mod); update_title(mod); mod->bar->refresh(mod->bar); - } else if (e->atom == _NET_WM_VISIBLE_NAME || - e->atom == _NET_WM_NAME || - e->atom == XCB_ATOM_WM_NAME) - { + } else if (e->atom == _NET_WM_VISIBLE_NAME || e->atom == _NET_WM_NAME || e->atom == XCB_ATOM_WM_NAME) { assert(e->window == m->active_win); update_title(mod); mod->bar->refresh(mod->bar); @@ -286,7 +286,7 @@ run(struct module *mod) xcb_destroy_window(m->conn, m->monitor_win); xcb_disconnect(m->conn); - return 0; + return ret; } static struct exposable * @@ -332,6 +332,7 @@ xwindow_new(struct particle *label) mod->run = &run; mod->destroy = &destroy; mod->content = &content; + mod->description = &description; return mod; } diff --git a/particle.c b/particle.c index be98e0e..f35b5d1 100644 --- a/particle.c +++ b/particle.c @@ -1,20 +1,21 @@ #include "particle.h" +#include +#include +#include #include #include #include -#include -#include -#include -#include -#include #include +#include +#include +#include #define LOG_MODULE "particle" #define LOG_ENABLE_DBG 0 -#include "log.h" #include "bar/bar.h" +#include "log.h" void particle_default_destroy(struct particle *particle) @@ -22,42 +23,49 @@ particle_default_destroy(struct particle *particle) if (particle->deco != NULL) particle->deco->destroy(particle->deco); fcft_destroy(particle->font); - free(particle->on_click_template); + for (size_t i = 0; i < MOUSE_BTN_COUNT; i++) + free(particle->on_click_templates[i]); free(particle); } struct particle * -particle_common_new(int left_margin, int right_margin, - const char *on_click_template, - struct fcft_font *font, pixman_color_t foreground, - struct deco *deco) +particle_common_new(int left_margin, int right_margin, char **on_click_templates, struct fcft_font *font, + enum font_shaping font_shaping, pixman_color_t foreground, struct deco *deco) { struct particle *p = calloc(1, sizeof(*p)); p->left_margin = left_margin; p->right_margin = right_margin; - p->on_click_template = - on_click_template != NULL ? strdup(on_click_template) : NULL; p->foreground = foreground; p->font = font; + p->font_shaping = font_shaping; p->deco = deco; + + if (on_click_templates != NULL) { + for (size_t i = 0; i < MOUSE_BTN_COUNT; i++) { + if (on_click_templates[i] != NULL) { + p->have_on_click_template = true; + p->on_click_templates[i] = on_click_templates[i]; + } + } + } + return p; } void exposable_default_destroy(struct exposable *exposable) { - free(exposable->on_click); + for (size_t i = 0; i < MOUSE_BTN_COUNT; i++) + free(exposable->on_click[i]); free(exposable); } void -exposable_render_deco(const struct exposable *exposable, - pixman_image_t *pix, int x, int y, int height) +exposable_render_deco(const struct exposable *exposable, pixman_image_t *pix, int x, int y, int height) { const struct deco *deco = exposable->particle->deco; if (deco != NULL) deco->expose(deco, pix, x, y, exposable->width, height); - } static bool @@ -102,9 +110,7 @@ tokenize_cmdline(char *cmdline, char ***argv) return false; } - if (!push_argv(argv, &argv_size, p, &idx) || - !push_argv(argv, &argv_size, NULL, &idx)) - { + if (!push_argv(argv, &argv_size, p, &idx) || !push_argv(argv, &argv_size, NULL, &idx)) { goto err; } else return true; @@ -140,21 +146,35 @@ err: } void -exposable_default_on_mouse(struct exposable *exposable, struct bar *bar, - enum mouse_event event, int x, int y) +exposable_default_on_mouse(struct exposable *exposable, struct bar *bar, enum mouse_event event, enum mouse_button btn, + int x, int y) { - LOG_DBG("on_mouse: exposable=%p, event=%s, x=%d, y=%d (on-click=%s)", - exposable, event == ON_MOUSE_MOTION ? "motion" : "click", x, y, - exposable->on_click); +#if defined(LOG_ENABLE_DBG) && LOG_ENABLE_DBG + static const char *button_name[] = { + [MOUSE_BTN_NONE] = "none", + [MOUSE_BTN_LEFT] = "left", + [MOUSE_BTN_MIDDLE] = "middle", + [MOUSE_BTN_RIGHT] = "right", + [MOUSE_BTN_COUNT] = "count", + [MOUSE_BTN_WHEEL_UP] = "wheel-up", + [MOUSE_BTN_WHEEL_DOWN] = "wheel-down", + [MOUSE_BTN_PREVIOUS] = "previous", + [MOUSE_BTN_NEXT] = "next", + }; + LOG_DBG("on_mouse: exposable=%p, event=%s, btn=%s, x=%d, y=%d (on-click=%s)", exposable, + event == ON_MOUSE_MOTION ? "motion" : "click", button_name[btn], x, y, exposable->on_click[btn]); +#endif /* If we have a handler, change cursor to a hand */ - bar->set_cursor(bar, exposable->on_click == NULL ? "left_ptr" : "hand2"); + const char *cursor + = (exposable->particle != NULL && exposable->particle->have_on_click_template) ? "hand2" : "left_ptr"; + bar->set_cursor(bar, cursor); /* If this is a mouse click, and we have a handler, execute it */ - if (exposable->on_click != NULL && event == ON_MOUSE_CLICK) { + if (exposable->on_click[btn] != NULL && event == ON_MOUSE_CLICK) { /* Need a writeable copy, whose scope *we* control */ - char *cmd = strdup(exposable->on_click); - LOG_DBG("cmd = \"%s\"", exposable->on_click); + char *cmd = strdup(exposable->on_click[btn]); + LOG_DBG("cmd = \"%s\"", exposable->on_click[btn]); char **argv; if (!tokenize_cmdline(cmd, &argv)) { @@ -172,15 +192,15 @@ exposable_default_on_mouse(struct exposable *exposable, struct bar *bar, int wstatus; if (waitpid(pid, &wstatus, 0) == -1) - LOG_ERRNO("%s: failed to wait for on_click handler", exposable->on_click); + LOG_ERRNO("%s: failed to wait for on_click handler", exposable->on_click[btn]); if (WIFEXITED(wstatus)) { if (WEXITSTATUS(wstatus) != 0) - LOG_ERRNO_P("%s: failed to execute", WEXITSTATUS(wstatus), exposable->on_click); + LOG_ERRNO_P(WEXITSTATUS(wstatus), "%s: failed to execute", exposable->on_click[btn]); } else - LOG_ERR("%s: did not exit normally", exposable->on_click); + LOG_ERR("%s: did not exit normally", exposable->on_click[btn]); - LOG_DBG("%s: launched", exposable->on_click); + LOG_DBG("%s: launched", exposable->on_click[btn]); } else { /* * Use a pipe with O_CLOEXEC to communicate exec() failure @@ -215,34 +235,41 @@ exposable_default_on_mouse(struct exposable *exposable, struct bar *bar, case 0: /* Child */ - close(pipe_fds[0]); /* Close read end */ + close(pipe_fds[0]); /* Close read end */ LOG_DBG("executing on-click handler: %s", cmd); + sigset_t mask; + sigemptyset(&mask); + + const struct sigaction sa = {.sa_handler = SIG_DFL}; + if (sigaction(SIGINT, &sa, NULL) < 0 || sigaction(SIGTERM, &sa, NULL) < 0 + || sigaction(SIGCHLD, &sa, NULL) < 0 || sigprocmask(SIG_SETMASK, &mask, NULL) < 0) { + goto fail; + } + /* Redirect stdin/stdout/stderr to /dev/null */ int dev_null_r = open("/dev/null", O_RDONLY | O_CLOEXEC); int dev_null_w = open("/dev/null", O_WRONLY | O_CLOEXEC); if (dev_null_r == -1 || dev_null_w == -1) { LOG_ERRNO("/dev/null: failed to open"); - (void)!write(pipe_fds[1], &errno, sizeof(errno)); - _exit(1); + goto fail; } - if (dup2(dev_null_r, STDIN_FILENO) == -1 || - dup2(dev_null_w, STDOUT_FILENO) == -1 || - dup2(dev_null_w, STDERR_FILENO) == -1) - { + if (dup2(dev_null_r, STDIN_FILENO) == -1 || dup2(dev_null_w, STDOUT_FILENO) == -1 + || dup2(dev_null_w, STDERR_FILENO) == -1) { LOG_ERRNO("failed to redirect stdin/stdout/stderr"); - (void)!write(pipe_fds[1], &errno, sizeof(errno)); - _exit(1); + goto fail; } execvp(argv[0], argv); + fail: /* Signal failure to parent process */ (void)!write(pipe_fds[1], &errno, sizeof(errno)); - _exit(1); + close(pipe_fds[1]); + _exit(errno); break; default: @@ -251,6 +278,8 @@ exposable_default_on_mouse(struct exposable *exposable, struct bar *bar, int _errno = 0; ssize_t ret = read(pipe_fds[0], &_errno, sizeof(_errno)); + close(pipe_fds[0]); + if (ret == 0) { /* Pipe was closed - child succeeded with exec() */ _exit(0); @@ -265,11 +294,14 @@ exposable_default_on_mouse(struct exposable *exposable, struct bar *bar, } struct exposable * -exposable_common_new(const struct particle *particle, const char *on_click) +exposable_common_new(const struct particle *particle, const struct tag_set *tags) { struct exposable *exposable = calloc(1, sizeof(*exposable)); exposable->particle = particle; - exposable->on_click = on_click != NULL ? strdup(on_click) : NULL; + + if (particle != NULL && particle->have_on_click_template) { + tags_expand_templates(exposable->on_click, (const char **)particle->on_click_templates, MOUSE_BTN_COUNT, tags); + } exposable->destroy = &exposable_default_destroy; exposable->on_mouse = &exposable_default_on_mouse; return exposable; diff --git a/particle.h b/particle.h index e89b9c4..bc8648d 100644 --- a/particle.h +++ b/particle.h @@ -5,28 +5,44 @@ #include "color.h" #include "decoration.h" +#include "font-shaping.h" #include "tag.h" +enum mouse_event { + ON_MOUSE_MOTION, + ON_MOUSE_CLICK, +}; + +enum mouse_button { + MOUSE_BTN_NONE, + MOUSE_BTN_LEFT, + MOUSE_BTN_MIDDLE, + MOUSE_BTN_RIGHT, + MOUSE_BTN_WHEEL_UP, + MOUSE_BTN_WHEEL_DOWN, + MOUSE_BTN_PREVIOUS, + MOUSE_BTN_NEXT, + + MOUSE_BTN_COUNT, +}; + struct bar; struct particle { void *private; int left_margin, right_margin; - char *on_click_template; + + bool have_on_click_template; + char *on_click_templates[MOUSE_BTN_COUNT]; pixman_color_t foreground; struct fcft_font *font; + enum font_shaping font_shaping; struct deco *deco; void (*destroy)(struct particle *particle); - struct exposable *(*instantiate)(const struct particle *particle, - const struct tag_set *tags); -}; - -enum mouse_event { - ON_MOUSE_MOTION, - ON_MOUSE_CLICK, + struct exposable *(*instantiate)(const struct particle *particle, const struct tag_set *tags); }; struct exposable { @@ -34,40 +50,35 @@ struct exposable { void *private; int width; /* Should be set by begin_expose(), at latest */ - char *on_click; + char *on_click[MOUSE_BTN_COUNT]; void (*destroy)(struct exposable *exposable); int (*begin_expose)(struct exposable *exposable); - void (*expose)(const struct exposable *exposable, pixman_image_t *pix, - int x, int y, int height); + void (*expose)(const struct exposable *exposable, pixman_image_t *pix, int x, int y, int height); - void (*on_mouse)(struct exposable *exposable, struct bar *bar, - enum mouse_event event, int x, int y); + void (*on_mouse)(struct exposable *exposable, struct bar *bar, enum mouse_event event, enum mouse_button btn, int x, + int y); }; -struct particle *particle_common_new( - int left_margin, int right_margin, const char *on_click_template, - struct fcft_font *font, pixman_color_t foreground, struct deco *deco); +struct particle *particle_common_new(int left_margin, int right_margin, char *on_click_templates[], + struct fcft_font *font, enum font_shaping font_shaping, pixman_color_t foreground, + struct deco *deco); void particle_default_destroy(struct particle *particle); -struct exposable *exposable_common_new( - const struct particle *particle, const char *on_click); +struct exposable *exposable_common_new(const struct particle *particle, const struct tag_set *tags); void exposable_default_destroy(struct exposable *exposable); -void exposable_render_deco( - const struct exposable *exposable, pixman_image_t *pix, int x, int y, int height); +void exposable_render_deco(const struct exposable *exposable, pixman_image_t *pix, int x, int y, int height); -void exposable_default_on_mouse( - struct exposable *exposable, struct bar *bar, - enum mouse_event event, int x, int y); +void exposable_default_on_mouse(struct exposable *exposable, struct bar *bar, enum mouse_event event, + enum mouse_button btn, int x, int y); /* List of attributes *all* particles implement */ -#define PARTICLE_COMMON_ATTRS \ - {"margin", false, &conf_verify_int}, \ - {"left-margin", false, &conf_verify_int}, \ - {"right-margin", false, &conf_verify_int}, \ - {"on-click", false, &conf_verify_string}, \ - {"font", false, &conf_verify_font}, \ - {"foreground", false, &conf_verify_color}, \ - {"deco", false, &conf_verify_decoration}, \ - {NULL, false, NULL} +#define PARTICLE_COMMON_ATTRS \ + {"margin", false, &conf_verify_unsigned}, {"left-margin", false, &conf_verify_unsigned}, \ + {"right-margin", false, &conf_verify_unsigned}, {"on-click", false, &conf_verify_on_click}, \ + {"font", false, &conf_verify_font}, {"font-shaping", false, &conf_verify_font_shaping}, \ + {"foreground", false, &conf_verify_color}, {"deco", false, &conf_verify_decoration}, \ + { \ + NULL, false, NULL \ + } diff --git a/particles/dynlist.c b/particles/dynlist.c index c04d610..fcd0066 100644 --- a/particles/dynlist.c +++ b/particles/dynlist.c @@ -1,12 +1,14 @@ #include "dynlist.h" +#include #include #define LOG_MODULE "dynlist" #include "../log.h" #include "../particle.h" -struct private { +struct private +{ int left_spacing; int right_spacing; @@ -36,15 +38,24 @@ dynlist_begin_expose(struct exposable *exposable) const struct private *e = exposable->private; exposable->width = 0; + bool have_at_least_one = false; for (size_t i = 0; i < e->count; i++) { struct exposable *ee = e->exposables[i]; e->widths[i] = ee->begin_expose(ee); - exposable->width += e->left_spacing + e->widths[i] + e->right_spacing; + assert(e->widths[i] >= 0); + + if (e->widths[i] > 0) { + exposable->width += e->left_spacing + e->widths[i] + e->right_spacing; + have_at_least_one = true; + } } - exposable->width -= e->left_spacing + e->right_spacing; + if (have_at_least_one) + exposable->width -= e->left_spacing + e->right_spacing; + else + assert(exposable->width == 0); return exposable->width; } @@ -61,42 +72,39 @@ dynlist_expose(const struct exposable *exposable, pixman_image_t *pix, int x, in for (size_t i = 0; i < e->count; i++) { const struct exposable *ee = e->exposables[i]; ee->expose(ee, pix, x + left_spacing, y, height); - x += left_spacing + e->widths[i] + right_spacing; + if (e->widths[i] > 0) + x += left_spacing + e->widths[i] + right_spacing; } } static void -on_mouse(struct exposable *exposable, struct bar *bar, - enum mouse_event event, int x, int y) +on_mouse(struct exposable *exposable, struct bar *bar, enum mouse_event event, enum mouse_button btn, int x, int y) { - //const struct particle *p = exposable->particle; const struct private *e = exposable->private; - if (exposable->on_click != NULL) { - exposable_default_on_mouse(exposable, bar, event, x, y); + if (exposable->on_click[btn] != NULL) { + exposable_default_on_mouse(exposable, bar, event, btn, x, y); return; } - int px = /*p->left_margin;*/0; + int px = /*p->left_margin;*/ 0; for (size_t i = 0; i < e->count; i++) { if (x >= px && x < px + e->exposables[i]->width) { if (e->exposables[i]->on_mouse != NULL) { - e->exposables[i]->on_mouse( - e->exposables[i], bar, event, x - px, y); + e->exposables[i]->on_mouse(e->exposables[i], bar, event, btn, x - px, y); } return; } - - px += e->left_spacing + e->exposables[i]->width + e->right_spacing; + if (e->exposables[i]->width > 0) + px += e->left_spacing + e->exposables[i]->width + e->right_spacing; } LOG_DBG("on_mouse missed all sub-particles"); - exposable_default_on_mouse(exposable, bar, event, x, y); + exposable_default_on_mouse(exposable, bar, event, btn, x, y); } struct exposable * -dynlist_exposable_new(struct exposable **exposables, size_t count, - int left_spacing, int right_spacing) +dynlist_exposable_new(struct exposable **exposables, size_t count, int left_spacing, int right_spacing) { struct private *e = calloc(1, sizeof(*e)); e->count = count; diff --git a/particles/dynlist.h b/particles/dynlist.h index 4867997..810df45 100644 --- a/particles/dynlist.h +++ b/particles/dynlist.h @@ -3,5 +3,5 @@ #include struct particle; -struct exposable *dynlist_exposable_new( - struct exposable **exposables, size_t count, int left_spacing, int right_spacing); +struct exposable *dynlist_exposable_new(struct exposable **exposables, size_t count, int left_spacing, + int right_spacing); diff --git a/particles/empty.c b/particles/empty.c index e97f929..052eacd 100644 --- a/particles/empty.c +++ b/particles/empty.c @@ -1,15 +1,14 @@ #include -#include "../config.h" #include "../config-verify.h" +#include "../config.h" #include "../particle.h" #include "../plugin.h" static int begin_expose(struct exposable *exposable) { - exposable->width = exposable->particle->left_margin + - exposable->particle->right_margin; + exposable->width = exposable->particle->left_margin + exposable->particle->right_margin; return exposable->width; } @@ -22,13 +21,9 @@ expose(const struct exposable *exposable, pixman_image_t *pix, int x, int y, int static struct exposable * instantiate(const struct particle *particle, const struct tag_set *tags) { - char *on_click = tags_expand_template(particle->on_click_template, tags); - - struct exposable *exposable = exposable_common_new(particle, on_click); + struct exposable *exposable = exposable_common_new(particle, tags); exposable->begin_expose = &begin_expose; exposable->expose = &expose; - - free(on_click); return exposable; } diff --git a/particles/list.c b/particles/list.c index 720e8a5..83b5d0c 100644 --- a/particles/list.c +++ b/particles/list.c @@ -2,13 +2,14 @@ #define LOG_MODULE "list" #define LOG_ENABLE_DBG 0 -#include "../log.h" -#include "../config.h" #include "../config-verify.h" +#include "../config.h" +#include "../log.h" #include "../particle.h" #include "../plugin.h" -struct private { +struct private +{ struct particle **particles; size_t count; int left_spacing, right_spacing; @@ -21,7 +22,6 @@ struct eprivate { int left_spacing, right_spacing; }; - static void exposable_destroy(struct exposable *exposable) { @@ -39,18 +39,29 @@ static int begin_expose(struct exposable *exposable) { const struct eprivate *e = exposable->private; + bool have_at_least_one = false; - exposable->width = exposable->particle->left_margin; + exposable->width = 0; for (size_t i = 0; i < e->count; i++) { struct exposable *ee = e->exposables[i]; e->widths[i] = ee->begin_expose(ee); - exposable->width += e->left_spacing + e->widths[i] + e->right_spacing; + assert(e->widths[i] >= 0); + + if (e->widths[i] > 0) { + exposable->width += e->left_spacing + e->widths[i] + e->right_spacing; + have_at_least_one = true; + } } - exposable->width -= e->left_spacing + e->right_spacing; - exposable->width += exposable->particle->right_margin; + if (have_at_least_one) { + exposable->width -= e->left_spacing + e->right_spacing; + exposable->width += exposable->particle->left_margin; + exposable->width += exposable->particle->right_margin; + } else + assert(exposable->width == 0); + return exposable->width; } @@ -69,20 +80,20 @@ expose(const struct exposable *exposable, pixman_image_t *pix, int x, int y, int for (size_t i = 0; i < e->count; i++) { const struct exposable *ee = e->exposables[i]; ee->expose(ee, pix, x + left_spacing, y, height); - x += left_spacing + e->widths[i] + right_spacing; + if (e->widths[i] > 0) + x += left_spacing + e->widths[i] + right_spacing; } } static void -on_mouse(struct exposable *exposable, struct bar *bar, - enum mouse_event event, int x, int y) +on_mouse(struct exposable *exposable, struct bar *bar, enum mouse_event event, enum mouse_button btn, int x, int y) { const struct particle *p = exposable->particle; const struct eprivate *e = exposable->private; - if (exposable->on_click != NULL) { + if ((event == ON_MOUSE_MOTION && exposable->particle->have_on_click_template) || exposable->on_click[btn] != NULL) { /* We have our own handler */ - exposable_default_on_mouse(exposable, bar, event, x, y); + exposable_default_on_mouse(exposable, bar, event, btn, x, y); return; } @@ -90,17 +101,16 @@ on_mouse(struct exposable *exposable, struct bar *bar, for (size_t i = 0; i < e->count; i++) { if (x >= px && x < px + e->exposables[i]->width) { if (e->exposables[i]->on_mouse != NULL) { - e->exposables[i]->on_mouse( - e->exposables[i], bar, event, x - px, y); + e->exposables[i]->on_mouse(e->exposables[i], bar, event, btn, x - px, y); } return; } - - px += e->left_spacing + e->exposables[i]->width + e->right_spacing; + if (e->exposables[i]->width > 0) + px += e->left_spacing + e->exposables[i]->width + e->right_spacing; } /* We're between sub-particles (or in the left/right margin) */ - exposable_default_on_mouse(exposable, bar, event, x, y); + exposable_default_on_mouse(exposable, bar, event, btn, x, y); } static struct exposable * @@ -118,18 +128,15 @@ instantiate(const struct particle *particle, const struct tag_set *tags) for (size_t i = 0; i < p->count; i++) { const struct particle *pp = p->particles[i]; e->exposables[i] = pp->instantiate(pp, tags); + assert(e->exposables[i] != NULL); } - char *on_click = tags_expand_template(particle->on_click_template, tags); - - struct exposable *exposable = exposable_common_new(particle, on_click); + struct exposable *exposable = exposable_common_new(particle, tags); exposable->private = e; exposable->destroy = &exposable_destroy; exposable->begin_expose = &begin_expose; exposable->expose = &expose; exposable->on_mouse = &on_mouse; - - free(on_click); return exposable; } @@ -145,9 +152,8 @@ particle_destroy(struct particle *particle) } struct particle * -particle_list_new(struct particle *common, - struct particle *particles[], size_t count, - int left_spacing, int right_spacing) +particle_list_new(struct particle *common, struct particle *particles[], size_t count, int left_spacing, + int right_spacing) { struct private *p = calloc(1, sizeof(*p)); p->particles = malloc(count * sizeof(p->particles[0])); @@ -172,21 +178,20 @@ from_conf(const struct yml_node *node, struct particle *common) const struct yml_node *_left_spacing = yml_get_value(node, "left-spacing"); const struct yml_node *_right_spacing = yml_get_value(node, "right-spacing"); - int left_spacing = spacing != NULL ? yml_value_as_int(spacing) : - _left_spacing != NULL ? yml_value_as_int(_left_spacing) : 0; - int right_spacing = spacing != NULL ? yml_value_as_int(spacing) : - _right_spacing != NULL ? yml_value_as_int(_right_spacing) : 2; + int left_spacing = spacing != NULL ? yml_value_as_int(spacing) + : _left_spacing != NULL ? yml_value_as_int(_left_spacing) + : 0; + int right_spacing = spacing != NULL ? yml_value_as_int(spacing) + : _right_spacing != NULL ? yml_value_as_int(_right_spacing) + : 2; size_t count = yml_list_length(items); struct particle *parts[count]; size_t idx = 0; - for (struct yml_list_iter it = yml_list_iter(items); - it.node != NULL; - yml_list_next(&it), idx++) - { - parts[idx] = conf_to_particle( - it.node, (struct conf_inherit){common->font, common->foreground}); + for (struct yml_list_iter it = yml_list_iter(items); it.node != NULL; yml_list_next(&it), idx++) { + parts[idx] + = conf_to_particle(it.node, (struct conf_inherit){common->font, common->font_shaping, common->foreground}); } return particle_list_new(common, parts, count, left_spacing, right_spacing); @@ -197,9 +202,9 @@ verify_conf(keychain_t *chain, const struct yml_node *node) { static const struct attr_info attrs[] = { {"items", true, &conf_verify_particle_list_items}, - {"spacing", false, &conf_verify_int}, - {"left-spacing", false, &conf_verify_int}, - {"right-spacing", false, &conf_verify_int}, + {"spacing", false, &conf_verify_unsigned}, + {"left-spacing", false, &conf_verify_unsigned}, + {"right-spacing", false, &conf_verify_unsigned}, PARTICLE_COMMON_ATTRS, }; diff --git a/particles/map.c b/particles/map.c index 2f5c460..c5510ff 100644 --- a/particles/map.c +++ b/particles/map.c @@ -1,21 +1,253 @@ +#include +#include #include #include -#include #define LOG_MODULE "map" -#include "../log.h" -#include "../config.h" #include "../config-verify.h" +#include "../config.h" +#include "../log.h" #include "../particle.h" #include "../plugin.h" +#include "dynlist.h" + +#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) +{ + switch (op) { + case MAP_OP_EQ: + return tag_value == cond_value; + case MAP_OP_NE: + return tag_value != cond_value; + case MAP_OP_LE: + return tag_value <= cond_value; + case MAP_OP_LT: + return tag_value < cond_value; + case MAP_OP_GE: + return tag_value >= cond_value; + case MAP_OP_GT: + return tag_value > cond_value; + case MAP_OP_SELF: + LOG_WARN("using int tag as bool"); + default: + return false; + } +} + +static bool +float_condition(const double tag_value, const double cond_value, enum map_op op) +{ + switch (op) { + case MAP_OP_EQ: + return tag_value == cond_value; + case MAP_OP_NE: + return tag_value != cond_value; + case MAP_OP_LE: + return tag_value <= cond_value; + case MAP_OP_LT: + return tag_value < cond_value; + case MAP_OP_GE: + return tag_value >= cond_value; + case MAP_OP_GT: + return tag_value > cond_value; + case MAP_OP_SELF: + LOG_WARN("using float tag as bool"); + default: + return false; + } +} + +static bool +str_condition(const char *tag_value, const char *cond_value, enum map_op op) +{ + switch (op) { + case MAP_OP_EQ: + return strcmp(tag_value, cond_value) == 0; + case MAP_OP_NE: + return strcmp(tag_value, cond_value) != 0; + case MAP_OP_LE: + return strcmp(tag_value, cond_value) <= 0; + case MAP_OP_LT: + return strcmp(tag_value, cond_value) < 0; + case MAP_OP_GE: + 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: + return false; + } +} + +static bool +eval_comparison(const struct map_condition *map_cond, const struct tag_set *tags) +{ + const struct tag *tag = tag_for_name(tags, map_cond->tag); + if (tag == NULL) { + LOG_WARN("tag %s not found", map_cond->tag); + return false; + } + + switch (tag->type(tag)) { + case TAG_TYPE_INT: { + errno = 0; + char *end; + const long cond_value = strtol(map_cond->value, &end, 0); + + if (errno == ERANGE) { + LOG_WARN("value %s is too large", map_cond->value); + return false; + } else if (*end != '\0') { + LOG_WARN("failed to parse %s into int", map_cond->value); + return false; + } + + const long tag_value = tag->as_int(tag); + return int_condition(tag_value, cond_value, map_cond->op); + } + case TAG_TYPE_FLOAT: { + errno = 0; + char *end; + const double cond_value = strtod(map_cond->value, &end); + + if (errno == ERANGE) { + LOG_WARN("value %s is too large", map_cond->value); + return false; + } else if (*end != '\0') { + LOG_WARN("failed to parse %s into float", map_cond->value); + return false; + } + + const double tag_value = tag->as_float(tag); + return float_condition(tag_value, cond_value, map_cond->op); + } + case TAG_TYPE_BOOL: + if (map_cond->op == MAP_OP_SELF) + return tag->as_bool(tag); + else { + LOG_WARN("boolean tag '%s' should be used directly", map_cond->tag); + return false; + } + case TAG_TYPE_STRING: { + const char *tag_value = tag->as_string(tag); + return str_condition(tag_value, map_cond->value, map_cond->op); + } + } + return false; +} + +static bool +eval_map_condition(const struct map_condition *map_cond, const struct tag_set *tags) +{ + switch (map_cond->op) { + case MAP_OP_NOT: + return !eval_map_condition(map_cond->cond1, tags); + + case MAP_OP_AND: + return eval_map_condition(map_cond->cond1, tags) && eval_map_condition(map_cond->cond2, tags); + + case MAP_OP_OR: + return eval_map_condition(map_cond->cond1, tags) || eval_map_condition(map_cond->cond2, tags); + + default: + return eval_comparison(map_cond, tags); + } +} + +void +free_map_condition(struct map_condition *c) +{ + switch (c->op) { + case MAP_OP_EQ: + case MAP_OP_NE: + case MAP_OP_LE: + case MAP_OP_LT: + case MAP_OP_GE: + case MAP_OP_LIKE: + case MAP_OP_GT: + free(c->value); + /* FALLTHROUGH */ + case MAP_OP_SELF: + free(c->tag); + break; + case MAP_OP_AND: + case MAP_OP_OR: + free_map_condition(c->cond2); + /* FALLTHROUGH */ + case MAP_OP_NOT: + free_map_condition(c->cond1); + break; + } + free(c); +} struct particle_map { - const char *tag_value; + struct map_condition *condition; struct particle *particle; }; -struct private { - char *tag; +struct private +{ struct particle *default_particle; struct particle_map *map; size_t count; @@ -40,11 +272,13 @@ begin_expose(struct exposable *exposable) { struct eprivate *e = exposable->private; - exposable->width = ( - exposable->particle->left_margin + - e->exposable->begin_expose(e->exposable) + - exposable->particle->right_margin); + int width = e->exposable->begin_expose(e->exposable); + assert(width >= 0); + if (width > 0) + width += exposable->particle->left_margin + exposable->particle->right_margin; + + exposable->width = width; return exposable->width; } @@ -54,75 +288,65 @@ expose(const struct exposable *exposable, pixman_image_t *pix, int x, int y, int struct eprivate *e = exposable->private; exposable_render_deco(exposable, pix, x, y, height); - e->exposable->expose( - e->exposable, pix, x + exposable->particle->left_margin, y, height); + e->exposable->expose(e->exposable, pix, x + exposable->particle->left_margin, y, height); } static void -on_mouse(struct exposable *exposable, struct bar *bar, enum mouse_event event, - int x, int y) +on_mouse(struct exposable *exposable, struct bar *bar, enum mouse_event event, enum mouse_button btn, int x, int y) { const struct particle *p = exposable->particle; const struct eprivate *e = exposable->private; - if (exposable->on_click != NULL) { + if ((event == ON_MOUSE_MOTION && exposable->particle->have_on_click_template) || exposable->on_click[btn] != NULL) { /* We have our own handler */ - exposable_default_on_mouse(exposable, bar, event, x, y); + exposable_default_on_mouse(exposable, bar, event, btn, x, y); return; } int px = p->left_margin; if (x >= px && x < px + e->exposable->width) { if (e->exposable->on_mouse != NULL) - e->exposable->on_mouse(e->exposable, bar, event, x - px, y); + e->exposable->on_mouse(e->exposable, bar, event, btn, x - px, y); return; } /* In the left- or right margin */ - exposable_default_on_mouse(exposable, bar, event, x, y); + exposable_default_on_mouse(exposable, bar, event, btn, x, y); } static struct exposable * instantiate(const struct particle *particle, const struct tag_set *tags) { const struct private *p = particle->private; - const struct tag *tag = tag_for_name(tags, p->tag); - assert(tag != NULL || p->default_particle != NULL); - - if (tag == NULL) - return p->default_particle->instantiate(p->default_particle, tags); - - - const char *tag_value = tag->as_string(tag); struct particle *pp = NULL; for (size_t i = 0; i < p->count; i++) { const struct particle_map *e = &p->map[i]; - if (strcmp(e->tag_value, tag_value) != 0) + if (!eval_map_condition(e->condition, tags)) continue; pp = e->particle; break; } - if (pp == NULL) { - assert(p->default_particle != NULL); - pp = p->default_particle; - } - struct eprivate *e = calloc(1, sizeof(*e)); - e->exposable = pp->instantiate(pp, tags); - char *on_click = tags_expand_template(particle->on_click_template, tags); - struct exposable *exposable = exposable_common_new(particle, on_click); + if (pp != NULL) + e->exposable = pp->instantiate(pp, tags); + else if (p->default_particle != NULL) + e->exposable = p->default_particle->instantiate(p->default_particle, tags); + else + e->exposable = dynlist_exposable_new(NULL, 0, 0, 0); + + assert(e->exposable != NULL); + + struct exposable *exposable = exposable_common_new(particle, tags); exposable->private = e; exposable->destroy = &exposable_destroy; exposable->begin_expose = &begin_expose; exposable->expose = &expose; exposable->on_mouse = &on_mouse; - - free(on_click); return exposable; } @@ -137,28 +361,25 @@ particle_destroy(struct particle *particle) for (size_t i = 0; i < p->count; i++) { struct particle *pp = p->map[i].particle; pp->destroy(pp); - free((char *)p->map[i].tag_value); + free_map_condition(p->map[i].condition); } free(p->map); - free(p->tag); free(p); particle_default_destroy(particle); } static struct particle * -map_new(struct particle *common, const char *tag, - const struct particle_map particle_map[], size_t count, +map_new(struct particle *common, const struct particle_map particle_map[], size_t count, struct particle *default_particle) { struct private *priv = calloc(1, sizeof(*priv)); - priv->tag = strdup(tag); priv->default_particle = default_particle; priv->count = count; priv->map = malloc(count * sizeof(priv->map[0])); for (size_t i = 0; i < count; i++) { - priv->map[i].tag_value = strdup(particle_map[i].tag_value); + priv->map[i].condition = particle_map[i].condition; priv->map[i].particle = particle_map[i].particle; } @@ -169,24 +390,30 @@ map_new(struct particle *common, const char *tag, } static bool -verify_map_values(keychain_t *chain, const struct yml_node *node) +verify_map_conditions(keychain_t *chain, const struct yml_node *node) { if (!yml_is_dict(node)) { - LOG_ERR( - "%s: must be a dictionary of workspace-name: particle mappings", - conf_err_prefix(chain, node)); + LOG_ERR("%s: must be a dictionary of workspace-name: particle mappings", conf_err_prefix(chain, node)); return false; } - for (struct yml_dict_iter it = yml_dict_iter(node); - it.key != NULL; - yml_dict_next(&it)) - { + bool result = true; + for (struct yml_dict_iter it = yml_dict_iter(node); it.key != NULL; yml_dict_next(&it)) { const char *key = yml_value_as_string(it.key); if (key == NULL) { LOG_ERR("%s: key must be a string", conf_err_prefix(chain, it.key)); return false; } + char *key_clone = strdup(key); + YY_BUFFER_STATE buffer = yy_scan_string(key_clone); + if (yyparse() != 0) { + LOG_ERR("%s: %s", conf_err_prefix(chain, it.key), MAP_PARSER_ERROR_MSG); + free(MAP_PARSER_ERROR_MSG); + result = false; + } else + free_map_condition(MAP_CONDITION_PARSE_RESULT); + yy_delete_buffer(buffer); + free(key_clone); if (!conf_verify_particle(chain_push(chain, key), it.value)) return false; @@ -194,46 +421,42 @@ verify_map_values(keychain_t *chain, const struct yml_node *node) chain_pop(chain); } - return true; + return result; } static struct particle * from_conf(const struct yml_node *node, struct particle *common) { - const struct yml_node *tag = yml_get_value(node, "tag"); - const struct yml_node *values = yml_get_value(node, "values"); + const struct yml_node *conditions = yml_get_value(node, "conditions"); const struct yml_node *def = yml_get_value(node, "default"); - struct particle_map particle_map[yml_dict_length(values)]; + struct particle_map particle_map[yml_dict_length(conditions)]; - struct conf_inherit inherited = { - .font = common->font, - .foreground = common->foreground - }; + struct conf_inherit inherited + = {.font = common->font, .font_shaping = common->font_shaping, .foreground = common->foreground}; size_t idx = 0; - for (struct yml_dict_iter it = yml_dict_iter(values); - it.key != NULL; - yml_dict_next(&it), idx++) - { - particle_map[idx].tag_value = yml_value_as_string(it.key); + for (struct yml_dict_iter it = yml_dict_iter(conditions); it.key != NULL; yml_dict_next(&it), idx++) { + /* Note we can skip the error checking here */ + char *key_clone = strdup(yml_value_as_string(it.key)); + YY_BUFFER_STATE buffer = yy_scan_string(key_clone); + yyparse(); + particle_map[idx].condition = MAP_CONDITION_PARSE_RESULT; + yy_delete_buffer(buffer); + free(key_clone); particle_map[idx].particle = conf_to_particle(it.value, inherited); } - struct particle *default_particle = def != NULL - ? conf_to_particle(def, inherited) : NULL; + struct particle *default_particle = def != NULL ? conf_to_particle(def, inherited) : NULL; - return map_new( - common, yml_value_as_string(tag), particle_map, yml_dict_length(values), - default_particle); + return map_new(common, particle_map, yml_dict_length(conditions), default_particle); } static bool verify_conf(keychain_t *chain, const struct yml_node *node) { static const struct attr_info attrs[] = { - {"tag", true, &conf_verify_string}, - {"values", true, &verify_map_values}, + {"conditions", true, &verify_map_conditions}, {"default", false, &conf_verify_particle}, PARTICLE_COMMON_ATTRS, }; diff --git a/particles/map.h b/particles/map.h new file mode 100644 index 0000000..1256744 --- /dev/null +++ b/particles/map.h @@ -0,0 +1,38 @@ +#pragma once + +enum map_op { + MAP_OP_EQ, + MAP_OP_NE, + MAP_OP_LE, + MAP_OP_LT, + MAP_OP_GE, + MAP_OP_GT, + MAP_OP_SELF, + MAP_OP_NOT, + MAP_OP_LIKE, + + MAP_OP_AND, + MAP_OP_OR, +}; + +struct map_condition { + union { + char *tag; + struct map_condition *cond1; + }; + enum map_op op; + union { + char *value; + struct map_condition *cond2; + }; +}; + +void free_map_condition(struct map_condition *c); + +typedef struct yy_buffer_state *YY_BUFFER_STATE; +YY_BUFFER_STATE yy_scan_string(const char *str); +int yyparse(); +void yy_delete_buffer(YY_BUFFER_STATE buffer); + +extern struct map_condition *MAP_CONDITION_PARSE_RESULT; +extern char *MAP_PARSER_ERROR_MSG; diff --git a/particles/map.l b/particles/map.l new file mode 100644 index 0000000..034353c --- /dev/null +++ b/particles/map.l @@ -0,0 +1,80 @@ +%{ +#include +#include "map.h" +#include "map.tab.h" +void yyerror(const char *s); +%} + +%option warn nodefault nounput noinput noyywrap + + char *quoted = NULL; + size_t quote_len = 0; + +%x QUOTE + +%% + +[[:alnum:]_-]+ yylval.str = strdup(yytext); return WORD; + +\" { + BEGIN(QUOTE); + quoted = calloc(1, sizeof(quoted[0])); +} + +[^\\\"]* { + /* printf("CAT: %s\n", yytext); */ + const size_t yy_length = strlen(yytext); + quoted = realloc(quoted, quote_len + yy_length + 1); + strcat(quoted, yytext); + quote_len += yy_length; +} + +\\\" { + /* printf("escaped quote\n"); */ + quoted = realloc(quoted, quote_len + 1 + 1); + strcat(quoted, "\""); + quote_len++; +} + +\\. { + /* printf("CAT: %s\n", yytext); */ + const size_t yy_length = strlen(yytext); + quoted = realloc(quoted, quote_len + yy_length + 1); + strcat(quoted, yytext); + quote_len += yy_length; +} + +\\ { + /* quoted string that ends with a backslash: "string\ */ + quoted = realloc(quoted, quote_len + 1 + 1); + strcat(quoted, "\\"); + quote_len++; +} + +\" { + /* printf("QUOTED=%s\n", quoted); */ + yylval.str = strdup(quoted); + + free(quoted); + quoted = NULL; + quote_len = 0; + + BEGIN(INITIAL); + return STRING; +} + +== yylval.op = MAP_OP_EQ; return CMP_OP; +!= yylval.op = MAP_OP_NE; return CMP_OP; +\<= yylval.op = MAP_OP_LE; return CMP_OP; +\< 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; +\( return L_PAR; +\) return R_PAR; +[ \t\n] ; +. yylval.str = strdup(yytext); return STRING; +%% diff --git a/particles/map.y b/particles/map.y new file mode 100644 index 0000000..8f3f46b --- /dev/null +++ b/particles/map.y @@ -0,0 +1,125 @@ +%{ +#include +#include + +#include "map.h" + +struct map_condition *MAP_CONDITION_PARSE_RESULT; +char *MAP_PARSER_ERROR_MSG; + +static const int NUM_TOKENS = 7; +int yylex(); +void yyerror(const char *str); +%} + +%define parse.lac full +%define parse.error custom + +%union { + char *str; + struct map_condition *condition; + enum map_op op; +} + +%token WORD STRING CMP_OP L_PAR R_PAR +%left BOOL_OP +%precedence NOT + +%destructor { free_map_condition($$); } condition +%destructor { free($$); } WORD +%destructor { free($$); } STRING + +%% +result: condition { MAP_CONDITION_PARSE_RESULT = $1; }; + +condition: + WORD { + $$ = malloc(sizeof(struct map_condition)); + $$->tag = $1; + $$->op = MAP_OP_SELF; + } + | + WORD CMP_OP WORD { + $$ = malloc(sizeof(struct map_condition)); + $$->tag = $1; + $$->op = $2; + $$->value = $3; + } + | + WORD CMP_OP STRING { + $$ = malloc(sizeof(struct map_condition)); + $$->tag = $1; + $$->op = $2; + $$->value = $3; + } + | + L_PAR condition R_PAR { $$ = $2; } + | + NOT condition { + $$ = malloc(sizeof(struct map_condition)); + $$->cond1 = $2; + $$->op = MAP_OP_NOT; + } + | + condition BOOL_OP condition { + $$ = malloc(sizeof(struct map_condition)); + $$->cond1 = $1; + $$->op = $2; + $$->cond2 = $3; + } + ; +%% + +void yyerror(const char *str) +{ + fprintf(stderr, "error: %s\n", str); +} + +static char const* +token_to_str(yysymbol_kind_t tkn) +{ + switch (tkn) { + case YYSYMBOL_CMP_OP: return "==, !=, <=, <, >=, >, ~~"; + case YYSYMBOL_BOOL_OP: return "||, &&"; + case YYSYMBOL_L_PAR: return "("; + case YYSYMBOL_R_PAR: return ")"; + case YYSYMBOL_NOT: return "~"; + default: return yysymbol_name(tkn); + } +} + +static int +yyreport_syntax_error (const yypcontext_t *ctx) +{ + int res = 0; + char *errmsg = malloc(1024); + errmsg[0] = '\0'; + + // Report the tokens expected at this point. + yysymbol_kind_t expected[NUM_TOKENS]; + int n = yypcontext_expected_tokens(ctx, expected, NUM_TOKENS); + if (n < 0) + res = n; // Forward errors to yyparse. + else { + for (int i = 0; i < n; ++i) { + strcat(errmsg, i == 0 ? "expected [" : ", "); + strcat(errmsg, token_to_str(expected[i])); + } + strcat(errmsg, "]"); + } + + // Report the unexpected token. + yysymbol_kind_t lookahead = yypcontext_token(ctx); + if (lookahead != YYSYMBOL_YYEMPTY) { + strcat(errmsg, ", found "); + if (!(lookahead == YYSYMBOL_STRING || lookahead == YYSYMBOL_WORD)) + strcat(errmsg, yysymbol_name(lookahead)); + else if (yylval.str != NULL) + strcat(errmsg, yylval.str); + else + strcat(errmsg, "nothing"); + } + + MAP_PARSER_ERROR_MSG = errmsg; + return res; +} diff --git a/particles/meson.build b/particles/meson.build index f571f12..091f551 100644 --- a/particles/meson.build +++ b/particles/meson.build @@ -1,20 +1,26 @@ -particle_sdk = declare_dependency(dependencies: [pixman, tllist, fcft]) +flex = find_program('flex', required: true) +bison = find_program('bison', required: true) -particles = [] -foreach particle : ['empty', 'list', 'map', 'progress-bar', 'ramp', 'string'] - if plugs_as_libs - shared_module('@0@'.format(particle), '@0@.c'.format(particle), - dependencies: particle_sdk, - name_prefix: 'particle_', - install: true, - install_dir: join_paths(get_option('libdir'), 'yambar')) - else - particles += [declare_dependency( - sources: '@0@.c'.format(particle), - dependencies: particle_sdk, - compile_args: '-DHAVE_PLUGIN_@0@'.format(particle.underscorify()))] - endif -endforeach +lgen = generator( + flex, + output : '@BASENAME@.yy.c', + arguments : ['-o', '@OUTPUT@', '@INPUT@'] +) +lfiles = lgen.process('map.l') + +pgen = generator( + bison, + output : ['@BASENAME@.tab.c', '@BASENAME@.tab.h'], + arguments : ['-Wall', + '-Wcounterexamples', + '--defines=@OUTPUT1@', + '--output=@OUTPUT0@', '@INPUT@'] +) +pfiles = pgen.process('map.y') + +map_parser = declare_dependency(sources: [pfiles, lfiles], include_directories: '.') + +particle_sdk = declare_dependency(dependencies: [pixman, tllist, fcft]) dynlist_lib = build_target( 'dynlist', 'dynlist.c', 'dynlist.h', dependencies: particle_sdk, @@ -25,3 +31,29 @@ dynlist_lib = build_target( ) dynlist = declare_dependency(link_with: dynlist_lib) + +# Particle name -> dep-list +deps = { + 'empty': [], + 'list': [], + 'map': [dynlist, map_parser], + 'progress-bar': [], + 'ramp': [], + 'string': [], +} + +particles = [] +foreach particle, particle_deps : deps + if plugs_as_libs + shared_module('@0@'.format(particle), '@0@.c'.format(particle), + dependencies: [particle_sdk] + particle_deps, + name_prefix: 'particle_', + install: true, + install_dir: join_paths(get_option('libdir'), 'yambar')) + else + particles += [declare_dependency( + sources: '@0@.c'.format(particle), + dependencies: [particle_sdk] + particle_deps, + compile_args: '-DHAVE_PLUGIN_@0@'.format(particle.underscorify()))] + endif +endforeach diff --git a/particles/progress-bar.c b/particles/progress-bar.c index 5c16802..f0bacbf 100644 --- a/particles/progress-bar.c +++ b/particles/progress-bar.c @@ -1,16 +1,17 @@ +#include #include #include -#include #define LOG_MODULE "progress_bar" #define LOG_ENABLE_DBG 0 -#include "../log.h" -#include "../config.h" #include "../config-verify.h" +#include "../config.h" +#include "../log.h" #include "../particle.h" #include "../plugin.h" -struct private { +struct private +{ char *tag; int width; @@ -57,14 +58,26 @@ static int begin_expose(struct exposable *exposable) { struct eprivate *e = exposable->private; + bool have_at_least_one = false; - /* Margins */ - exposable->width = exposable->particle->left_margin + - exposable->particle->right_margin; + exposable->width = 0; /* Sub-exposables */ - for (size_t i = 0; i < e->count; i++) - exposable->width += e->exposables[i]->begin_expose(e->exposables[i]); + for (size_t i = 0; i < e->count; i++) { + int width = e->exposables[i]->begin_expose(e->exposables[i]); + + assert(width >= 0); + if (width >= 0) { + exposable->width += width; + have_at_least_one = true; + } + } + + /* Margins */ + if (have_at_least_one) { + exposable->width += exposable->particle->left_margin + exposable->particle->right_margin; + } else + assert(exposable->width == 0); return exposable->width; } @@ -84,30 +97,8 @@ expose(const struct exposable *exposable, pixman_image_t *pix, int x, int y, int } static void -on_mouse(struct exposable *exposable, struct bar *bar, enum mouse_event event, - int x, int y) +on_mouse(struct exposable *exposable, struct bar *bar, enum mouse_event event, enum mouse_button btn, int x, int y) { - if (exposable->on_click == NULL) { - exposable_default_on_mouse(exposable, bar, event, x, y); - return; - } - - /* - * Hack-warning! - * - * In order to pass the *clicked* position to the on_click - * handler, we expand the handler *again* (first time would be - * when the particle instantiated us). - * - * We pass a single tag, "where", which is a percentage value. - * - * Keep a reference to the un-expanded string, to be able to reset - * it after executing the handler. - * - * Note that we only consider the actual progress bar to be - * clickable. This means we ignore the start and end markers. - */ - const struct particle *p = exposable->particle; const struct eprivate *e = exposable->private; @@ -120,7 +111,7 @@ on_mouse(struct exposable *exposable, struct bar *bar, enum mouse_event event, /* Mouse is over the start-marker */ struct exposable *start = e->exposables[0]; if (start->on_mouse != NULL) - start->on_mouse(start, bar, event, x - p->left_margin, y); + start->on_mouse(start, bar, event, btn, x - p->left_margin, y); } else { /* Mouse if over left margin */ bar->set_cursor(bar, "left_ptr"); @@ -139,7 +130,7 @@ on_mouse(struct exposable *exposable, struct bar *bar, enum mouse_event event, /* Mouse is over the end-marker */ struct exposable *end = e->exposables[e->count - 1]; if (end->on_mouse != NULL) - end->on_mouse(end, bar, event, x - x_offset - clickable_width, y); + end->on_mouse(end, bar, event, btn, x - x_offset - clickable_width, y); } else { /* Mouse is over the right margin */ bar->set_cursor(bar, "left_ptr"); @@ -147,30 +138,48 @@ on_mouse(struct exposable *exposable, struct bar *bar, enum mouse_event event, return; } + /* + * Hack-warning! + * + * In order to pass the *clicked* position to the on_click + * handler, we expand the handler *again* (first time would be + * when the particle instantiated us). + * + * We pass a single tag, "where", which is a percentage value. + * + * Keep a reference to the un-expanded string, to be able to + * reset it after executing the handler. + * + * Note that we only consider the actual progress bar to be + * clickable. This means we ignore the start and end markers. + */ + /* Remember the original handler, so that we can restore it */ - char *original = exposable->on_click; + char *original[MOUSE_BTN_COUNT]; + for (size_t i = 0; i < MOUSE_BTN_COUNT; i++) + original[i] = exposable->on_click[i]; if (event == ON_MOUSE_CLICK) { - long where = clickable_width > 0 - ? 100 * (x - x_offset) / clickable_width - : 0; + long where = clickable_width > 0 ? 100 * (x - x_offset) / clickable_width : 0; struct tag_set tags = { .tags = (struct tag *[]){tag_new_int(NULL, "where", where)}, .count = 1, }; - exposable->on_click = tags_expand_template(exposable->on_click, &tags); + tags_expand_templates(exposable->on_click, (const char **)exposable->on_click, MOUSE_BTN_COUNT, &tags); tag_set_destroy(&tags); } /* Call default implementation, which will execute our handler */ - exposable_default_on_mouse(exposable, bar, event, x, y); + exposable_default_on_mouse(exposable, bar, event, btn, x, y); if (event == ON_MOUSE_CLICK) { /* Reset handler string */ - free(exposable->on_click); - exposable->on_click = original; + for (size_t i = 0; i < MOUSE_BTN_COUNT; i++) { + free(exposable->on_click[i]); + exposable->on_click[i] = original[i]; + } } } @@ -179,24 +188,22 @@ instantiate(const struct particle *particle, const struct tag_set *tags) { const struct private *p = particle->private; const struct tag *tag = tag_for_name(tags, p->tag); - assert(tag != NULL); - long value = tag->as_int(tag); - long min = tag->min(tag); - long max = tag->max(tag); + long value = tag != NULL ? tag->as_int(tag) : 0; + long min = tag != NULL ? tag->min(tag) : 0; + long max = tag != NULL ? tag->max(tag) : 0; - LOG_DBG("%s: value=%ld, min=%ld, max=%ld", tag->name(tag), value, min, max); + LOG_DBG("%s: value=%ld, min=%ld, max=%ld", tag != NULL ? tag->name(tag) : "", value, min, max); long fill_count = max == min ? 0 : p->width * value / (max - min); long empty_count = p->width - fill_count; struct eprivate *epriv = calloc(1, sizeof(*epriv)); - epriv->count = ( - 1 + /* Start marker */ - fill_count + /* Before current position */ - 1 + /* Current position indicator */ - empty_count + /* After current position */ - 1); /* End marker */ + epriv->count = (1 + /* Start marker */ + fill_count + /* Before current position */ + 1 + /* Current position indicator */ + empty_count + /* After current position */ + 1); /* End marker */ epriv->exposables = malloc(epriv->count * sizeof(epriv->exposables[0])); @@ -210,11 +217,10 @@ instantiate(const struct particle *particle, const struct tag_set *tags) epriv->exposables[idx++] = p->end_marker->instantiate(p->end_marker, tags); assert(idx == epriv->count); + for (size_t i = 0; i < epriv->count; i++) + assert(epriv->exposables[i] != NULL); - char *on_click = tags_expand_template(particle->on_click_template, tags); - - struct exposable *exposable = exposable_common_new(particle, on_click); - free(on_click); + struct exposable *exposable = exposable_common_new(particle, tags); exposable->private = epriv; exposable->destroy = &exposable_destroy; @@ -222,6 +228,9 @@ instantiate(const struct particle *particle, const struct tag_set *tags) exposable->expose = &expose; exposable->on_mouse = &on_mouse; + if (tag == NULL) + return exposable; + enum tag_realtime_unit rt = tag->realtime(tag); if (rt == TAG_REALTIME_NONE) @@ -243,8 +252,7 @@ instantiate(const struct particle *particle, const struct tag_set *tags) LOG_DBG("tag: %s, value: %ld, " "units-per-segment: %f, units-filled: %f, units-til-next: %f", - tag->name(tag), value, - units_per_segment, units_filled, units_til_next_segment); + tag->name(tag), value, units_per_segment, units_filled, units_til_next_segment); #endif @@ -255,10 +263,8 @@ instantiate(const struct particle *particle, const struct tag_set *tags) } static struct particle * -progress_bar_new(struct particle *common, const char *tag, int width, - struct particle *start_marker, struct particle *end_marker, - struct particle *fill, struct particle *empty, - struct particle *indicator) +progress_bar_new(struct particle *common, const char *tag, int width, struct particle *start_marker, + struct particle *end_marker, struct particle *fill, struct particle *empty, struct particle *indicator) { struct private *priv = calloc(1, sizeof(*priv)); priv->tag = strdup(tag); @@ -288,18 +294,14 @@ from_conf(const struct yml_node *node, struct particle *common) struct conf_inherit inherited = { .font = common->font, + .font_shaping = common->font_shaping, .foreground = common->foreground, }; - return progress_bar_new( - common, - yml_value_as_string(tag), - yml_value_as_int(length), - conf_to_particle(start, inherited), - conf_to_particle(end, inherited), - conf_to_particle(fill, inherited), - conf_to_particle(empty, inherited), - conf_to_particle(indicator, inherited)); + return progress_bar_new(common, yml_value_as_string(tag), yml_value_as_int(length), + conf_to_particle(start, inherited), conf_to_particle(end, inherited), + conf_to_particle(fill, inherited), conf_to_particle(empty, inherited), + conf_to_particle(indicator, inherited)); } static bool @@ -307,7 +309,7 @@ verify_conf(keychain_t *chain, const struct yml_node *node) { static const struct attr_info attrs[] = { {"tag", true, &conf_verify_string}, - {"length", true, &conf_verify_int}, + {"length", true, &conf_verify_unsigned}, /* TODO: make these optional? Default to empty */ {"start", true, &conf_verify_particle}, {"end", true, &conf_verify_particle}, diff --git a/particles/ramp.c b/particles/ramp.c index b513681..befe1d9 100644 --- a/particles/ramp.c +++ b/particles/ramp.c @@ -1,16 +1,24 @@ +#include #include #include -#include #include -#include "../config.h" +#define LOG_MODULE "ramp" +#define LOG_ENABLE_DBG 0 #include "../config-verify.h" +#include "../config.h" +#include "../log.h" #include "../particle.h" #include "../plugin.h" -struct private { +struct private +{ char *tag; + bool use_custom_min; + long min; + bool use_custom_max; + long max; struct particle **particles; size_t count; }; @@ -34,11 +42,13 @@ begin_expose(struct exposable *exposable) { struct eprivate *e = exposable->private; - exposable->width = ( - exposable->particle->left_margin + - e->exposable->begin_expose(e->exposable) + - exposable->particle->right_margin); + int width = e->exposable->begin_expose(e->exposable); + assert(width >= 0); + if (width > 0) + width += exposable->particle->left_margin + exposable->particle->right_margin; + + exposable->width = width; return exposable->width; } @@ -48,32 +58,30 @@ expose(const struct exposable *exposable, pixman_image_t *pix, int x, int y, int struct eprivate *e = exposable->private; exposable_render_deco(exposable, pix, x, y, height); - e->exposable->expose( - e->exposable, pix, x + exposable->particle->left_margin, y, height); + e->exposable->expose(e->exposable, pix, x + exposable->particle->left_margin, y, height); } static void -on_mouse(struct exposable *exposable, struct bar *bar, enum mouse_event event, - int x, int y) +on_mouse(struct exposable *exposable, struct bar *bar, enum mouse_event event, enum mouse_button btn, int x, int y) { const struct particle *p = exposable->particle; const struct eprivate *e = exposable->private; - if (exposable->on_click != NULL) { + if ((event == ON_MOUSE_MOTION && exposable->particle->have_on_click_template) || exposable->on_click[btn] != NULL) { /* We have our own handler */ - exposable_default_on_mouse(exposable, bar, event, x, y); + exposable_default_on_mouse(exposable, bar, event, btn, x, y); return; } int px = p->left_margin; if (x >= px && x < px + e->exposable->width) { if (e->exposable->on_mouse != NULL) - e->exposable->on_mouse(e->exposable, bar, event, x - px, y); + e->exposable->on_mouse(e->exposable, bar, event, btn, x - px, y); return; } /* In the left- or right margin */ - exposable_default_on_mouse(exposable, bar, event, x, y); + exposable_default_on_mouse(exposable, bar, event, btn, x, y); } static void @@ -95,13 +103,35 @@ instantiate(const struct particle *particle, const struct tag_set *tags) { const struct private *p = particle->private; const struct tag *tag = tag_for_name(tags, p->tag); - assert(tag != NULL); assert(p->count > 0); - long value = tag->as_int(tag); - long min = tag->min(tag); - long max = tag->max(tag); + long value = tag != NULL ? tag->as_int(tag) : 0; + long min = tag != NULL ? tag->min(tag) : 0; + long max = tag != NULL ? tag->max(tag) : 0; + + min = p->use_custom_min ? p->min : min; + max = p->use_custom_max ? p->max : max; + + if (min > max) { + LOG_WARN("tag's minimum value is greater than its maximum: " + "tag=\"%s\", min=%ld, max=%ld", + p->tag, min, max); + min = max; + } + + if (value < min) { + LOG_WARN("tag's value is less than its minimum value: " + "tag=\"%s\", min=%ld, value=%ld", + p->tag, min, value); + value = min; + } + if (value > max) { + LOG_WARN("tag's value is greater than its maximum value: " + "tag=\"%s\", max=%ld, value=%ld", + p->tag, max, value); + value = max; + } assert(value >= min && value <= max); assert(max >= min); @@ -122,28 +152,30 @@ instantiate(const struct particle *particle, const struct tag_set *tags) struct eprivate *e = calloc(1, sizeof(*e)); e->exposable = pp->instantiate(pp, tags); + assert(e->exposable != NULL); - char *on_click = tags_expand_template(particle->on_click_template, tags); - struct exposable *exposable = exposable_common_new(particle, on_click); + struct exposable *exposable = exposable_common_new(particle, tags); exposable->private = e; exposable->destroy = &exposable_destroy; exposable->begin_expose = &begin_expose; exposable->expose = &expose; exposable->on_mouse = &on_mouse; - - free(on_click); return exposable; } static struct particle * -ramp_new(struct particle *common, const char *tag, - struct particle *particles[], size_t count) +ramp_new(struct particle *common, const char *tag, struct particle *particles[], size_t count, bool use_custom_min, + long min, bool use_custom_max, long max) { struct private *priv = calloc(1, sizeof(*priv)); priv->tag = strdup(tag); priv->particles = malloc(count * sizeof(priv->particles[0])); priv->count = count; + priv->use_custom_max = use_custom_max; + priv->max = max; + priv->use_custom_min = use_custom_min; + priv->min = min; for (size_t i = 0; i < count; i++) priv->particles[i] = particles[i]; @@ -159,20 +191,22 @@ from_conf(const struct yml_node *node, struct particle *common) { const struct yml_node *tag = yml_get_value(node, "tag"); const struct yml_node *items = yml_get_value(node, "items"); + const struct yml_node *min = yml_get_value(node, "min"); + const struct yml_node *max = yml_get_value(node, "max"); size_t count = yml_list_length(items); struct particle *parts[count]; size_t idx = 0; - for (struct yml_list_iter it = yml_list_iter(items); - it.node != NULL; - yml_list_next(&it), idx++) - { - parts[idx] = conf_to_particle( - it.node, (struct conf_inherit){common->font, common->foreground}); + for (struct yml_list_iter it = yml_list_iter(items); it.node != NULL; yml_list_next(&it), idx++) { + parts[idx] + = conf_to_particle(it.node, (struct conf_inherit){common->font, common->font_shaping, common->foreground}); } - return ramp_new(common, yml_value_as_string(tag), parts, count); + long min_v = min != NULL ? yml_value_as_int(min) : 0; + long max_v = max != NULL ? yml_value_as_int(max) : 0; + + return ramp_new(common, yml_value_as_string(tag), parts, count, min != NULL, min_v, max != NULL, max_v); } static bool @@ -181,6 +215,8 @@ verify_conf(keychain_t *chain, const struct yml_node *node) static const struct attr_info attrs[] = { {"tag", true, &conf_verify_string}, {"items", true, &conf_verify_particle_list_items}, + {"min", false, &conf_verify_int}, + {"max", false, &conf_verify_int}, PARTICLE_COMMON_ATTRS, }; diff --git a/particles/string.c b/particles/string.c index 5e98132..4922d7d 100644 --- a/particles/string.c +++ b/particles/string.c @@ -1,25 +1,36 @@ +#include #include #include -#include #define LOG_MODULE "string" #define LOG_ENABLE_DBG 0 -#include "../log.h" -#include "../config.h" +#include "../char32.h" #include "../config-verify.h" +#include "../config.h" +#include "../log.h" #include "../particle.h" #include "../plugin.h" -struct private { +struct text_run_cache { + uint64_t hash; + struct fcft_text_run *run; + int width; + bool in_use; +}; + +struct private +{ char *text; size_t max_len; + + size_t cache_size; + struct text_run_cache *cache; }; struct eprivate { - /* Set when instantiating */ - char *text; - + ssize_t cache_idx; const struct fcft_glyph **glyphs; + const struct fcft_glyph **allocated_glyphs; long *kern_x; int num_glyphs; }; @@ -29,8 +40,7 @@ exposable_destroy(struct exposable *exposable) { struct eprivate *e = exposable->private; - free(e->text); - free(e->glyphs); + free(e->allocated_glyphs); free(e->kern_x); free(e); exposable_default_destroy(exposable); @@ -40,43 +50,18 @@ static int begin_expose(struct exposable *exposable) { struct eprivate *e = exposable->private; - struct fcft_font *font = exposable->particle->font; + struct private *p = exposable->particle->private; - e->glyphs = NULL; - e->num_glyphs = 0; + exposable->width = exposable->particle->left_margin + exposable->particle->right_margin; - size_t chars = mbstowcs(NULL, e->text, 0); - if (chars != (size_t)-1) { - wchar_t wtext[chars + 1]; - mbstowcs(wtext, e->text, chars + 1); - - e->glyphs = malloc(chars * sizeof(e->glyphs[0])); - e->kern_x = calloc(chars, sizeof(e->kern_x[0])); - - /* Convert text to glyph masks/images. */ - for (size_t i = 0; i < chars; i++) { - const struct fcft_glyph *glyph = fcft_glyph_rasterize( - font, wtext[i], FCFT_SUBPIXEL_NONE); - - if (glyph == NULL) - continue; - - e->glyphs[e->num_glyphs++] = glyph; - - if (i == 0) - continue; - - fcft_kerning(font, wtext[i - 1], wtext[i], &e->kern_x[i], NULL); - } + if (e->cache_idx >= 0) { + exposable->width += p->cache[e->cache_idx].width; + } else { + /* Calculate the size we need to render the glyphs */ + for (int i = 0; i < e->num_glyphs; i++) + exposable->width += e->kern_x[i] + e->glyphs[i]->advance.x; } - exposable->width = exposable->particle->left_margin + - exposable->particle->right_margin; - - /* Calculate the size we need to render the glyphs */ - for (int i = 0; i < e->num_glyphs; i++) - exposable->width += e->kern_x[i] + e->glyphs[i]->advance.x; - return exposable->width; } @@ -88,6 +73,11 @@ expose(const struct exposable *exposable, pixman_image_t *pix, int x, int y, int const struct eprivate *e = exposable->private; const struct fcft_font *font = exposable->particle->font; + if (e->cache_idx >= 0) { + struct private *priv = exposable->particle->private; + priv->cache[e->cache_idx].in_use = false; + } + if (e->num_glyphs == 0) return; @@ -97,18 +87,17 @@ expose(const struct exposable *exposable, pixman_image_t *pix, int x, int y, int * its descent. This way, the part of the font *above* the * baseline is centered. * - * "EEEE" will typically be dead center, with the middle of each character being in the bar's center. + * "EEEE" will typically be dead center, with the middle of each character being in the bar's center. * "eee" will be slightly below the center. * "jjj" will be even further below the center. * * Finally, if the font's descent is negative, ignore it (except * for the height calculation). This is unfortunately not based on - * any real facts, but works very well with e.g. the "Awesome 5" + * any real facts, but works very well with e.g. the "Awesome 6" * font family. */ - const double baseline = (double)y + - (double)(height + font->ascent + font->descent) / 2.0 - - (font->descent > 0 ? font->descent : 0); + const double baseline + = (double)y + (double)(height + font->ascent + font->descent) / 2.0 - (font->descent > 0 ? font->descent : 0); x += exposable->particle->left_margin; @@ -121,17 +110,13 @@ expose(const struct exposable *exposable, pixman_image_t *pix, int x, int y, int if (pixman_image_get_format(glyph->pix) == PIXMAN_a8r8g8b8) { /* Glyph surface is a pre-rendered image (typically a color emoji...) */ - pixman_image_composite32( - PIXMAN_OP_OVER, glyph->pix, NULL, pix, 0, 0, 0, 0, - x + glyph->x, baseline - glyph->y, - glyph->width, glyph->height); + pixman_image_composite32(PIXMAN_OP_OVER, glyph->pix, NULL, pix, 0, 0, 0, 0, x + glyph->x, + baseline - glyph->y, glyph->width, glyph->height); } else { /* Glyph surface is an alpha mask */ pixman_image_t *src = pixman_image_create_solid_fill(&exposable->particle->foreground); - pixman_image_composite32( - PIXMAN_OP_OVER, src, glyph->pix, pix, 0, 0, 0, 0, - x + glyph->x, baseline - glyph->y, - glyph->width, glyph->height); + pixman_image_composite32(PIXMAN_OP_OVER, src, glyph->pix, pix, 0, 0, 0, 0, x + glyph->x, + baseline - glyph->y, glyph->width, glyph->height); pixman_image_unref(src); } @@ -139,48 +124,132 @@ expose(const struct exposable *exposable, pixman_image_t *pix, int x, int y, int } } +static uint64_t +sdbm_hash(const char *s) +{ + uint64_t hash = 0; + + for (; *s != '\0'; s++) { + int c = *s; + hash = c + (hash << 6) + (hash << 16) - hash; + } + + return hash; +} + static struct exposable * instantiate(const struct particle *particle, const struct tag_set *tags) { - const struct private *p = particle->private; + struct private *p = (struct private *)particle->private; struct eprivate *e = calloc(1, sizeof(*e)); + struct fcft_font *font = particle->font; - e->text = tags_expand_template(p->text, tags); - e->glyphs = NULL; + char32_t *wtext = NULL; + char *text = tags_expand_template(p->text, tags); + + e->glyphs = e->allocated_glyphs = NULL; e->num_glyphs = 0; + e->kern_x = NULL; + e->cache_idx = -1; - if (p->max_len > 0) { - const size_t len = strlen(e->text); - if (len > p->max_len) { + uint64_t hash = sdbm_hash(text); - size_t end = p->max_len; - if (end >= 3) { - /* "allocate" room for three dots at the end */ - end -= 3; - } + /* First, check if we have this string cached */ + for (size_t i = 0; i < p->cache_size; i++) { + if (p->cache[i].hash == hash) { + assert(p->cache[i].run != NULL); - /* Mucho importante - don't cut in the middle of a utf8 multibyte */ - while (end > 0 && e->text[end - 1] >> 7) - end--; - - if (p->max_len > 3) { - for (size_t i = 0; i < 3; i++) - e->text[end + i] = '.'; - e->text[end + 3] = '\0'; - } else - e->text[end] = '\0'; + p->cache[i].in_use = true; + e->cache_idx = i; + e->glyphs = p->cache[i].run->glyphs; + e->num_glyphs = p->cache[i].run->count; + e->kern_x = calloc(p->cache[i].run->count, sizeof(e->kern_x[0])); + goto done; } } - char *on_click = tags_expand_template(particle->on_click_template, tags); + /* Not in cache - we need to rasterize it. First, convert to char32_t */ + wtext = ambstoc32(text); + size_t chars = wtext != NULL ? c32len(wtext) : 0; - struct exposable *exposable = exposable_common_new(particle, on_click); + /* Truncate, if necessary */ + if (p->max_len > 0 && chars > p->max_len) { + chars = p->max_len; + if (p->max_len > 3) + wtext[p->max_len - 1] = U'…'; + wtext[p->max_len] = U'\0'; + } + + e->kern_x = calloc(chars, sizeof(e->kern_x[0])); + + if (particle->font_shaping == FONT_SHAPE_FULL && fcft_capabilities() & FCFT_CAPABILITY_TEXT_RUN_SHAPING) { + struct fcft_text_run *run = fcft_rasterize_text_run_utf32(font, chars, wtext, FCFT_SUBPIXEL_NONE); + + if (run != NULL) { + int w = 0; + for (size_t i = 0; i < run->count; i++) + w += run->glyphs[i]->advance.x; + + ssize_t cache_idx = -1; + for (size_t i = 0; i < p->cache_size; i++) { + if (p->cache[i].run == NULL || !p->cache[i].in_use) { + fcft_text_run_destroy(p->cache[i].run); + cache_idx = i; + break; + } + } + + if (cache_idx < 0) { + size_t new_size = p->cache_size + 1; + struct text_run_cache *new_cache = realloc(p->cache, new_size * sizeof(new_cache[0])); + + p->cache_size = new_size; + p->cache = new_cache; + cache_idx = new_size - 1; + } + + assert(cache_idx >= 0 && cache_idx < p->cache_size); + p->cache[cache_idx].hash = hash; + p->cache[cache_idx].run = run; + p->cache[cache_idx].width = w; + p->cache[cache_idx].in_use = true; + + e->cache_idx = cache_idx; + e->num_glyphs = run->count; + e->glyphs = run->glyphs; + } + } + + if (e->glyphs == NULL) { + e->allocated_glyphs = malloc(chars * sizeof(e->glyphs[0])); + + /* Convert text to glyph masks/images. */ + for (size_t i = 0; i < chars; i++) { + const struct fcft_glyph *glyph = fcft_rasterize_char_utf32(font, wtext[i], FCFT_SUBPIXEL_NONE); + + if (glyph == NULL) + continue; + + e->allocated_glyphs[e->num_glyphs++] = glyph; + + if (i == 0) + continue; + + fcft_kerning(font, wtext[i - 1], wtext[i], &e->kern_x[i], NULL); + } + + e->glyphs = e->allocated_glyphs; + } + +done: + free(wtext); + free(text); + + struct exposable *exposable = exposable_common_new(particle, tags); exposable->private = e; exposable->destroy = &exposable_destroy; exposable->begin_expose = &begin_expose; exposable->expose = &expose; - - free(on_click); return exposable; } @@ -188,6 +257,9 @@ static void particle_destroy(struct particle *particle) { struct private *p = particle->private; + for (size_t i = 0; i < p->cache_size; i++) + fcft_text_run_destroy(p->cache[i].run); + free(p->cache); free(p->text); free(p); particle_default_destroy(particle); @@ -199,6 +271,8 @@ string_new(struct particle *common, const char *text, size_t max_len) struct private *p = calloc(1, sizeof(*p)); p->text = strdup(text); p->max_len = max_len; + p->cache_size = 0; + p->cache = NULL; common->private = p; common->destroy = &particle_destroy; @@ -212,10 +286,7 @@ from_conf(const struct yml_node *node, struct particle *common) const struct yml_node *text = yml_get_value(node, "text"); const struct yml_node *max = yml_get_value(node, "max"); - return string_new( - common, - yml_value_as_string(text), - max != NULL ? yml_value_as_int(max) : 0); + return string_new(common, yml_value_as_string(text), max != NULL ? yml_value_as_int(max) : 0); } static bool @@ -223,7 +294,7 @@ verify_conf(keychain_t *chain, const struct yml_node *node) { static const struct attr_info attrs[] = { {"text", true, &conf_verify_string}, - {"max", false, &conf_verify_int}, + {"max", false, &conf_verify_unsigned}, PARTICLE_COMMON_ATTRS, }; diff --git a/plugin.c b/plugin.c index 7ae2fb4..2ed0a4f 100644 --- a/plugin.c +++ b/plugin.c @@ -1,50 +1,104 @@ #include "plugin.h" -#include #include +#include #include #define LOG_MODULE "plugin" #define LOG_ENABLE_DBG 0 -#include "log.h" #include "config.h" +#include "log.h" #if !defined(CORE_PLUGINS_AS_SHARED_LIBRARIES) -#define EXTERN_MODULE(plug_name) \ - extern const struct module_iface module_##plug_name##_iface; \ - extern bool plug_name##_verify_conf( \ - keychain_t *chain, const struct yml_node *node); \ - extern struct module *plug_name##_from_conf( \ - const struct yml_node *node, struct conf_inherit inherited); +#define EXTERN_MODULE(plug_name) \ + extern const struct module_iface module_##plug_name##_iface; \ + extern bool plug_name##_verify_conf(keychain_t *chain, const struct yml_node *node); \ + extern struct module *plug_name##_from_conf(const struct yml_node *node, struct conf_inherit inherited); -#define EXTERN_PARTICLE(plug_name) \ - extern const struct particle_iface particle_##plug_name##_iface; \ - extern bool plug_name##_verify_conf( \ - keychain_t *chain, const struct yml_node *node); \ - extern struct particle *plug_name##_from_conf( \ - const struct yml_node *node, struct particle *common); +#define EXTERN_PARTICLE(plug_name) \ + extern const struct particle_iface particle_##plug_name##_iface; \ + extern bool plug_name##_verify_conf(keychain_t *chain, const struct yml_node *node); \ + extern struct particle *plug_name##_from_conf(const struct yml_node *node, struct particle *common); -#define EXTERN_DECORATION(plug_name) \ - extern const struct deco_iface deco_##plug_name##_iface; \ - extern bool plug_name##_verify_conf( \ - keychain_t *chain, const struct yml_node *node); \ +#define EXTERN_DECORATION(plug_name) \ + extern const struct deco_iface deco_##plug_name##_iface; \ + extern bool plug_name##_verify_conf(keychain_t *chain, const struct yml_node *node); \ extern struct deco *plug_name##_from_conf(const struct yml_node *node); +#if defined(HAVE_PLUGIN_alsa) EXTERN_MODULE(alsa); +#endif +#if defined(HAVE_PLUGIN_backlight) EXTERN_MODULE(backlight); +#endif +#if defined(HAVE_PLUGIN_battery) EXTERN_MODULE(battery); +#endif +#if defined(HAVE_PLUGIN_clock) EXTERN_MODULE(clock); -EXTERN_MODULE(i3); -EXTERN_MODULE(label); +#endif +#if defined(HAVE_PLUGIN_cpu) +EXTERN_MODULE(cpu); +#endif +#if defined(HAVE_PLUGIN_disk_io) +EXTERN_MODULE(disk_io); +#endif +#if defined(HAVE_PLUGIN_dwl) +EXTERN_MODULE(dwl); +#endif +#if defined(HAVE_PLUGIN_foreign_toplevel) +EXTERN_MODULE(foreign_toplevel); +#endif +#if defined(HAVE_PLUGIN_mem) +EXTERN_MODULE(mem); +#endif +#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 +#if defined(HAVE_PLUGIN_label) +EXTERN_MODULE(label); +#endif +#if defined(HAVE_PLUGIN_network) EXTERN_MODULE(network); +#endif +#if defined(HAVE_PLUGIN_pipewire) +EXTERN_MODULE(pipewire); +#endif +#if defined(HAVE_PLUGIN_pulse) +EXTERN_MODULE(pulse); +#endif +#if defined(HAVE_PLUGIN_removables) EXTERN_MODULE(removables); +#endif +#if defined(HAVE_PLUGIN_river) EXTERN_MODULE(river); +#endif +#if defined(HAVE_PLUGIN_script) +EXTERN_MODULE(script); +#endif +#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 +#if defined(HAVE_PLUGIN_xwindow) EXTERN_MODULE(xwindow); +#endif EXTERN_PARTICLE(empty); EXTERN_PARTICLE(list); @@ -54,8 +108,10 @@ EXTERN_PARTICLE(ramp); EXTERN_PARTICLE(string); EXTERN_DECORATION(background); +EXTERN_DECORATION(border); EXTERN_DECORATION(stack); EXTERN_DECORATION(underline); +EXTERN_DECORATION(overline); #undef EXTERN_DECORATION #undef EXTERN_PARTICLE @@ -69,56 +125,113 @@ static const char * type2str(enum plugin_type type) { switch (type) { - case PLUGIN_MODULE: return "module"; - case PLUGIN_PARTICLE: return "particle"; - case PLUGIN_DECORATION: return "decoration"; + case PLUGIN_MODULE: + return "module"; + case PLUGIN_PARTICLE: + return "particle"; + case PLUGIN_DECORATION: + return "decoration"; } - return NULL; + assert(false && "invalid type"); + return ""; } -static void __attribute__((constructor)) -init(void) +static void __attribute__((constructor)) init(void) { #if !defined(CORE_PLUGINS_AS_SHARED_LIBRARIES) -#define REGISTER_CORE_PLUGIN(plug_name, func_prefix, plug_type) \ - do { \ - tll_push_back( \ - plugins, \ - ((struct plugin){ \ - .name = strdup(#plug_name), \ - .type = (plug_type), \ - .lib = RTLD_DEFAULT, \ - })); \ +#define REGISTER_CORE_PLUGIN(plug_name, func_prefix, plug_type) \ + do { \ + tll_push_back(plugins, ((struct plugin){ \ + .name = strdup(#plug_name), \ + .type = (plug_type), \ + .lib = RTLD_DEFAULT, \ + })); \ } while (0) -#define REGISTER_CORE_MODULE(plug_name, func_prefix) do { \ - REGISTER_CORE_PLUGIN(plug_name, func_prefix, PLUGIN_MODULE); \ - tll_back(plugins).module = &module_##func_prefix##_iface; \ +#define REGISTER_CORE_MODULE(plug_name, func_prefix) \ + do { \ + REGISTER_CORE_PLUGIN(plug_name, func_prefix, PLUGIN_MODULE); \ + tll_back(plugins).module = &module_##func_prefix##_iface; \ } while (0) -#define REGISTER_CORE_PARTICLE(plug_name, func_prefix) do { \ - REGISTER_CORE_PLUGIN(plug_name, func_prefix, PLUGIN_PARTICLE); \ - tll_back(plugins).particle = &particle_##func_prefix##_iface; \ +#define REGISTER_CORE_PARTICLE(plug_name, func_prefix) \ + do { \ + REGISTER_CORE_PLUGIN(plug_name, func_prefix, PLUGIN_PARTICLE); \ + tll_back(plugins).particle = &particle_##func_prefix##_iface; \ } while (0) -#define REGISTER_CORE_DECORATION(plug_name, func_prefix) do { \ - REGISTER_CORE_PLUGIN(plug_name, func_prefix, PLUGIN_DECORATION); \ - tll_back(plugins).decoration = &deco_##func_prefix##_iface; \ +#define REGISTER_CORE_DECORATION(plug_name, func_prefix) \ + do { \ + REGISTER_CORE_PLUGIN(plug_name, func_prefix, PLUGIN_DECORATION); \ + tll_back(plugins).decoration = &deco_##func_prefix##_iface; \ } while (0) +#if defined(HAVE_PLUGIN_alsa) REGISTER_CORE_MODULE(alsa, alsa); +#endif +#if defined(HAVE_PLUGIN_backlight) REGISTER_CORE_MODULE(backlight, backlight); +#endif +#if defined(HAVE_PLUGIN_battery) REGISTER_CORE_MODULE(battery, battery); +#endif +#if defined(HAVE_PLUGIN_clock) REGISTER_CORE_MODULE(clock, clock); - REGISTER_CORE_MODULE(i3, i3); - REGISTER_CORE_MODULE(label, label); +#endif +#if defined(HAVE_PLUGIN_cpu) + REGISTER_CORE_MODULE(cpu, cpu); +#endif +#if defined(HAVE_PLUGIN_disk_io) + REGISTER_CORE_MODULE(disk-io, disk_io); +#endif +#if defined(HAVE_PLUGIN_dwl) + REGISTER_CORE_MODULE(dwl, dwl); +#endif +#if defined(HAVE_PLUGIN_foreign_toplevel) + REGISTER_CORE_MODULE(foreign-toplevel, foreign_toplevel); +#endif +#if defined(HAVE_PLUGIN_mem) + REGISTER_CORE_MODULE(mem, mem); +#endif +#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 +#if defined(HAVE_PLUGIN_label) + REGISTER_CORE_MODULE(label, label); +#endif +#if defined(HAVE_PLUGIN_network) REGISTER_CORE_MODULE(network, network); +#endif +#if defined(HAVE_PLUGIN_pipewire) + REGISTER_CORE_MODULE(pipewire, pipewire); +#endif +#if defined(HAVE_PLUGIN_pulse) + REGISTER_CORE_MODULE(pulse, pulse); +#endif +#if defined(HAVE_PLUGIN_removables) REGISTER_CORE_MODULE(removables, removables); +#endif #if defined(HAVE_PLUGIN_river) REGISTER_CORE_MODULE(river, river); #endif +#if defined(HAVE_PLUGIN_script) + REGISTER_CORE_MODULE(script, script); +#endif +#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 @@ -134,8 +247,10 @@ init(void) REGISTER_CORE_PARTICLE(string, string); REGISTER_CORE_DECORATION(background, background); + REGISTER_CORE_DECORATION(border, border); REGISTER_CORE_DECORATION(stack, stack); REGISTER_CORE_DECORATION(underline, underline); + REGISTER_CORE_DECORATION(overline, overline); #undef REGISTER_CORE_DECORATION #undef REGISTER_CORE_PARTICLE @@ -159,16 +274,13 @@ free_plugin(struct plugin plug) free(plug.name); } -static void __attribute__((destructor)) -fini(void) -{ - tll_free_and_free(plugins, free_plugin); -} +static void __attribute__((destructor)) fini(void) { tll_free_and_free(plugins, free_plugin); } const struct plugin * plugin_load(const char *name, enum plugin_type type) { - tll_foreach(plugins, plug) { + tll_foreach(plugins, plug) + { if (plug->item.type == type && strcmp(plug->item.name, name) == 0) { LOG_DBG("%s: %s already loaded: %p", type2str(type), name, plug->item.lib); assert(plug->item.dummy != NULL); @@ -176,7 +288,6 @@ plugin_load(const char *name, enum plugin_type type) } } - char path[128]; snprintf(path, sizeof(path), "%s_%s.so", type2str(type), name); diff --git a/plugin.h b/plugin.h index 1b2da24..4c49caa 100644 --- a/plugin.h +++ b/plugin.h @@ -1,7 +1,7 @@ #pragma once -#include "config.h" #include "config-verify.h" +#include "config.h" #include "module.h" #include "particle.h" @@ -9,14 +9,12 @@ typedef bool (*verify_func_t)(keychain_t *chain, const struct yml_node *node); struct module_iface { verify_func_t verify_conf; - struct module *(*from_conf)( - const struct yml_node *node, struct conf_inherit inherited); + struct module *(*from_conf)(const struct yml_node *node, struct conf_inherit inherited); }; struct particle_iface { verify_func_t verify_conf; - struct particle *(*from_conf)( - const struct yml_node *node, struct particle *common); + struct particle *(*from_conf)(const struct yml_node *node, struct particle *common); }; struct deco_iface { diff --git a/subprojects/fcft.wrap b/subprojects/fcft.wrap new file mode 100644 index 0000000..d2709d4 --- /dev/null +++ b/subprojects/fcft.wrap @@ -0,0 +1,3 @@ +[wrap-git] +url = https://codeberg.org/dnkl/fcft.git +revision = master diff --git a/subprojects/tllist.wrap b/subprojects/tllist.wrap new file mode 100644 index 0000000..75f395a --- /dev/null +++ b/subprojects/tllist.wrap @@ -0,0 +1,3 @@ +[wrap-git] +url = https://codeberg.org/dnkl/tllist.git +revision = master diff --git a/tag.c b/tag.c index 52750fe..d6609af 100644 --- a/tag.c +++ b/tag.c @@ -1,16 +1,20 @@ #include "tag.h" -#include -#include -#include -#include #include +#include +#include +#include +#include +#include +#include +#include #define LOG_MODULE "tag" #define LOG_ENABLE_DBG 1 #include "log.h" #include "module.h" -struct private { +struct private +{ char *name; union { struct { @@ -32,6 +36,30 @@ tag_name(const struct tag *tag) return priv->name; } +static enum tag_type +bool_type(const struct tag *tag) +{ + return TAG_TYPE_BOOL; +} + +static enum tag_type +int_type(const struct tag *tag) +{ + return TAG_TYPE_INT; +} + +static enum tag_type +float_type(const struct tag *tag) +{ + return TAG_TYPE_FLOAT; +} + +static enum tag_type +string_type(const struct tag *tag) +{ + return TAG_TYPE_STRING; +} + static long unimpl_min_max(const struct tag *tag) { @@ -129,8 +157,8 @@ int_refresh_in(const struct tag *tag, long units) if (tag->owner == NULL || tag->owner->refresh_in == NULL) return false; - assert(priv->value_as_int.realtime_unit == TAG_REALTIME_SECS || - priv->value_as_int.realtime_unit == TAG_REALTIME_MSECS); + assert(priv->value_as_int.realtime_unit == TAG_REALTIME_SECS + || priv->value_as_int.realtime_unit == TAG_REALTIME_MSECS); long milli_seconds = units; if (priv->value_as_int.realtime_unit == TAG_REALTIME_SECS) @@ -242,15 +270,14 @@ tag_new_int(struct module *owner, const char *name, long value) } struct tag * -tag_new_int_range(struct module *owner, const char *name, long value, - long min, long max) +tag_new_int_range(struct module *owner, const char *name, long value, long min, long max) { return tag_new_int_realtime(owner, name, value, min, max, TAG_REALTIME_NONE); } struct tag * -tag_new_int_realtime(struct module *owner, const char *name, long value, - long min, long max, enum tag_realtime_unit unit) +tag_new_int_realtime(struct module *owner, const char *name, long value, long min, long max, + enum tag_realtime_unit unit) { struct private *priv = malloc(sizeof(*priv)); priv->name = strdup(name); @@ -264,6 +291,7 @@ tag_new_int_realtime(struct module *owner, const char *name, long value, tag->owner = owner; tag->destroy = &destroy_int_and_float; tag->name = &tag_name; + tag->type = &int_type; tag->min = &int_min; tag->max = &int_max; tag->realtime = &int_realtime; @@ -287,6 +315,7 @@ tag_new_bool(struct module *owner, const char *name, bool value) tag->owner = owner; tag->destroy = &destroy_int_and_float; tag->name = &tag_name; + tag->type = &bool_type; tag->min = &unimpl_min_max; tag->max = &unimpl_min_max; tag->realtime = &no_realtime; @@ -310,6 +339,7 @@ tag_new_float(struct module *owner, const char *name, double value) tag->owner = owner; tag->destroy = &destroy_int_and_float; tag->name = &tag_name; + tag->type = &float_type; tag->min = &unimpl_min_max; tag->max = &unimpl_min_max; tag->realtime = &no_realtime; @@ -333,6 +363,7 @@ tag_new_string(struct module *owner, const char *name, const char *value) tag->owner = owner; tag->destroy = &destroy_string; tag->name = &tag_name; + tag->type = &string_type; tag->min = &unimpl_min_max; tag->max = &unimpl_min_max; tag->realtime = &no_realtime; @@ -383,7 +414,7 @@ sbuf_append_at_most(struct sbuf *s1, const char *s2, size_t n) s1->size = 2 * required_size; s1->s = realloc(s1->s, s1->size); - //s1->s[s1->len] = '\0'; + // s1->s[s1->len] = '\0'; } memcpy(&s1->s[s1->len], s2, n); @@ -397,6 +428,21 @@ sbuf_append(struct sbuf *s1, const char *s2) sbuf_append_at_most(s1, s2, strlen(s2)); } +// stores the number in "*value" on success +static bool +is_number(const char *str, long *value) +{ + errno = 0; + + char *end; + long v = strtol(str, &end, 10); + if (errno != 0 || *end != '\0') + return false; + + *value = v; + return true; +} + char * tags_expand_template(const char *template, const struct tag_set *tags) { @@ -428,7 +474,7 @@ tags_expand_template(const char *template, const struct tag_set *tags) strncpy(tag_name_and_arg, begin + 1, end - begin - 1); tag_name_and_arg[end - begin - 1] = '\0'; - static const size_t MAX_TAG_ARGS = 3; + static const size_t MAX_TAG_ARGS = 4; const char *tag_name = NULL; const char *tag_args[MAX_TAG_ARGS]; memset(tag_args, 0, sizeof(tag_args)); @@ -446,20 +492,41 @@ tags_expand_template(const char *template, const struct tag_set *tags) } /* Lookup tag */ - const struct tag *tag = tag_for_name(tags, tag_name); - if (tag == NULL) { + const struct tag *tag = NULL; + + if (tag_name == NULL || (tag = tag_for_name(tags, tag_name)) == NULL) { /* No such tag, copy as-is instead */ sbuf_append_at_most(&formatted, template, begin - template + 1); template = begin + 1; continue; } - /* Copy characters preceeding the tag (name) */ + /* Copy characters preceding the tag (name) */ sbuf_append_at_most(&formatted, template, begin - template); /* Parse arguments */ - enum { FMT_DEFAULT, FMT_HEX, FMT_OCT } format = FMT_DEFAULT; - enum { VALUE_VALUE, VALUE_MIN, VALUE_MAX, VALUE_UNIT } kind = VALUE_VALUE; + enum { + FMT_DEFAULT, + FMT_HEX, + FMT_OCT, + FMT_PERCENT, + FMT_DIVIDE, + } format + = FMT_DEFAULT; + + enum { + VALUE_VALUE, + VALUE_MIN, + VALUE_MAX, + VALUE_UNIT, + } kind + = VALUE_VALUE; + + long digits = 0; + long decimals = 2; + long divider = 1; + bool zero_pad = false; + char *point = NULL; for (size_t i = 0; i < MAX_TAG_ARGS; i++) { if (tag_args[i] == NULL) @@ -468,27 +535,130 @@ tags_expand_template(const char *template, const struct tag_set *tags) format = FMT_HEX; else if (strcmp(tag_args[i], "oct") == 0) format = FMT_OCT; + else if (strcmp(tag_args[i], "%") == 0) + format = FMT_PERCENT; + 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) kind = VALUE_MAX; else if (strcmp(tag_args[i], "unit") == 0) kind = VALUE_UNIT; + else if (is_number(tag_args[i], &digits)) // i.e.: "{tag:3}" + zero_pad = tag_args[i][0] == '0'; + else if ((point = strchr(tag_args[i], '.')) != NULL) { + *point = '\0'; + + const char *digits_str = tag_args[i]; + const char *decimals_str = point + 1; + + if (digits_str[0] != '\0') { // guards against i.e. "{tag:.3}" + if (!is_number(digits_str, &digits)) { + LOG_WARN("tag `%s`: invalid field width formatter. Ignoring...", tag_name); + } + } + + if (decimals_str[0] != '\0') { // guards against i.e. "{tag:3.}" + if (!is_number(decimals_str, &decimals)) { + LOG_WARN("tag `%s`: invalid decimals formatter. Ignoring...", tag_name); + } + } + zero_pad = digits_str[0] == '0'; + } else + LOG_WARN("invalid tag formatter: %s", tag_args[i]); } /* Copy tag value */ switch (kind) { case VALUE_VALUE: switch (format) { - case FMT_DEFAULT: - sbuf_append(&formatted, tag->as_string(tag)); + case FMT_DEFAULT: { + switch (tag->type(tag)) { + case TAG_TYPE_FLOAT: { + const char *fmt = zero_pad ? "%0*.*f" : "%*.*f"; + char str[24]; + snprintf(str, sizeof(str), fmt, digits, decimals, tag->as_float(tag)); + sbuf_append(&formatted, str); + break; + } + + case TAG_TYPE_INT: { + const char *fmt = zero_pad ? "%0*ld" : "%*ld"; + char str[24]; + snprintf(str, sizeof(str), fmt, digits, tag->as_int(tag)); + sbuf_append(&formatted, str); + break; + } + + default: + sbuf_append(&formatted, tag->as_string(tag)); + break; + } + break; + } case FMT_HEX: case FMT_OCT: { + const char *fmt = format == FMT_HEX ? zero_pad ? "%0*lx" : "%*lx" : zero_pad ? "%0*lo" : "%*lo"; char str[24]; - snprintf(str, sizeof(str), format == FMT_HEX ? "%lx" : "%lo", - tag->as_int(tag)); + snprintf(str, sizeof(str), fmt, digits, tag->as_int(tag)); + sbuf_append(&formatted, str); + break; + } + + case FMT_PERCENT: { + const long min = tag->min(tag); + const long max = tag->max(tag); + const long cur = tag->as_int(tag); + + const char *fmt = zero_pad ? "%0*lu" : "%*lu"; + char str[4]; + snprintf(str, sizeof(str), fmt, digits, (cur - min) * 100 / (max - min)); + sbuf_append(&formatted, str); + break; + } + + case FMT_DIVIDE: { + char str[24]; + if (tag->type(tag) == TAG_TYPE_FLOAT) { + const char *fmt = zero_pad ? "%0*.*f" : "%*.*f"; + snprintf(str, sizeof(str), fmt, digits, decimals, tag->as_float(tag) / (double)divider); + } else { + const char *fmt = zero_pad ? "%0*lu" : "%*lu"; + snprintf(str, sizeof(str), fmt, digits, tag->as_int(tag) / divider); + } sbuf_append(&formatted, str); break; } @@ -497,17 +667,37 @@ tags_expand_template(const char *template, const struct tag_set *tags) case VALUE_MIN: case VALUE_MAX: { - const long value = kind == VALUE_MIN ? tag->min(tag) : tag->max(tag); + const long min = tag->min(tag); + const long max = tag->max(tag); + long value = kind == VALUE_MIN ? min : max; - const char *fmt; + const char *fmt = NULL; switch (format) { - case FMT_DEFAULT: fmt = "%ld"; break; - case FMT_HEX: fmt = "%lx"; break; - case FMT_OCT: fmt = "%lo"; break; + case FMT_DEFAULT: + fmt = zero_pad ? "%0*ld" : "%*ld"; + break; + case FMT_HEX: + fmt = zero_pad ? "%0*lx" : "%*lx"; + break; + case FMT_OCT: + fmt = zero_pad ? "%0*lo" : "%*lo"; + break; + case FMT_PERCENT: + value = (value - min) * 100 / (max - min); + fmt = zero_pad ? "%0*lu" : "%*lu"; + break; + + case FMT_DIVIDE: { + value /= divider; + fmt = zero_pad ? "%0*lu" : "%*lu"; + break; + } } + assert(fmt != NULL); + char str[24]; - snprintf(str, sizeof(str), fmt, value); + snprintf(str, sizeof(str), fmt, digits, value); sbuf_append(&formatted, str); break; } @@ -516,9 +706,15 @@ tags_expand_template(const char *template, const struct tag_set *tags) const char *value = NULL; switch (tag->realtime(tag)) { - case TAG_REALTIME_NONE: value = ""; break; - case TAG_REALTIME_SECS: value = "s"; break; - case TAG_REALTIME_MSECS: value = "ms"; break; + case TAG_REALTIME_NONE: + value = ""; + break; + case TAG_REALTIME_SECS: + value = "s"; + break; + case TAG_REALTIME_MSECS: + value = "ms"; + break; } sbuf_append(&formatted, value); @@ -532,3 +728,10 @@ tags_expand_template(const char *template, const struct tag_set *tags) return formatted.s; } + +void +tags_expand_templates(char *expanded[], const char *template[], size_t nmemb, const struct tag_set *tags) +{ + for (size_t i = 0; i < nmemb; i++) + expanded[i] = tags_expand_template(template[i], tags); +} diff --git a/tag.h b/tag.h index 2c629c5..6149b1e 100644 --- a/tag.h +++ b/tag.h @@ -1,7 +1,14 @@ #pragma once -#include #include +#include + +enum tag_type { + TAG_TYPE_BOOL, + TAG_TYPE_INT, + TAG_TYPE_FLOAT, + TAG_TYPE_STRING, +}; enum tag_realtime_unit { TAG_REALTIME_NONE, @@ -17,6 +24,7 @@ struct tag { void (*destroy)(struct tag *tag); const char *(*name)(const struct tag *tag); + enum tag_type (*type)(const struct tag *tag); const char *(*as_string)(const struct tag *tag); long (*as_int)(const struct tag *tag); bool (*as_bool)(const struct tag *tag); @@ -35,18 +43,16 @@ struct tag_set { }; struct tag *tag_new_int(struct module *owner, const char *name, long value); -struct tag *tag_new_int_range( - struct module *owner, const char *name, long value, long min, long max); -struct tag *tag_new_int_realtime( - struct module *owner, const char *name, long value, long min, - long max, enum tag_realtime_unit unit); +struct tag *tag_new_int_range(struct module *owner, const char *name, long value, long min, long max); +struct tag *tag_new_int_realtime(struct module *owner, const char *name, long value, long min, long max, + enum tag_realtime_unit unit); struct tag *tag_new_bool(struct module *owner, const char *name, bool value); struct tag *tag_new_float(struct module *owner, const char *name, double value); -struct tag *tag_new_string( - struct module *owner, const char *name, const char *value); +struct tag *tag_new_string(struct module *owner, const char *name, const char *value); const struct tag *tag_for_name(const struct tag_set *set, const char *name); void tag_set_destroy(struct tag_set *set); /* Utility functions */ char *tags_expand_template(const char *template, const struct tag_set *tags); +void tags_expand_templates(char *expanded[], const char *template[], size_t nmemb, const struct tag_set *tags); diff --git a/test/full-conf-good.yml b/test/full-conf-good.yml index 6270487..a6f6e99 100644 --- a/test/full-conf-good.yml +++ b/test/full-conf-good.yml @@ -39,8 +39,13 @@ bar: host: 127.0.0.1 content: {string: {text: "{state}"}} - network: - name: ldsjfdf - content: {string: {text: "{name}"}} + content: + map: + default: + string: {text: "{name}: {state} ({ipv4})"} + conditions: + ipv4 == "": + string: {text: "{name}: {state}"} - removables: content: {string: {text: "{label}"}} # - xkb: @@ -62,10 +67,9 @@ bar: - clock: content: map: - tag: date default: {string: {text: default value}} - values: - 1234: {string: {text: specific value}} + conditions: + date == 1234: {string: {text: specific value}} - clock: content: progress-bar: 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/xcb.c b/xcb.c index a157c1a..d3d2e30 100644 --- a/xcb.c +++ b/xcb.c @@ -1,16 +1,16 @@ #include "xcb.h" +#include #include #include #include -#include -#include #include #include +#include #if defined(HAVE_XCB_ERRORS) - #include +#include #endif #define LOG_MODULE "xcb" @@ -36,8 +36,7 @@ xcb_atom_t _NET_WM_NAME; static xcb_errors_context_t *err_context; #endif -static void __attribute__((destructor)) -fini(void) +static void __attribute__((destructor)) fini(void) { #if defined(HAVE_XCB_ERRORS) xcb_errors_context_free(err_context); @@ -63,19 +62,17 @@ xcb_init(void) /* Vendor release number */ unsigned release = setup->release_number; - unsigned major = release / 10000000; release %= 10000000; - unsigned minor = release / 100000; release %= 100000; + unsigned major = release / 10000000; + release %= 10000000; + unsigned minor = release / 100000; + release %= 100000; unsigned patch = release / 1000; #endif - LOG_DBG("%.*s %u.%u.%u (protocol: %u.%u)", - xcb_setup_vendor_length(setup), xcb_setup_vendor(setup), - major, minor, patch, - setup->protocol_major_version, - setup->protocol_minor_version); + LOG_DBG("%.*s %u.%u.%u (protocol: %u.%u)", xcb_setup_vendor_length(setup), xcb_setup_vendor(setup), major, minor, + patch, setup->protocol_major_version, setup->protocol_minor_version); - const xcb_query_extension_reply_t *randr = - xcb_get_extension_data(conn, &xcb_randr_id); + const xcb_query_extension_reply_t *randr = xcb_get_extension_data(conn, &xcb_randr_id); if (randr == NULL || !randr->present) { LOG_ERR("RANDR extension not present"); @@ -83,8 +80,7 @@ xcb_init(void) return false; } - const xcb_query_extension_reply_t *render = - xcb_get_extension_data(conn, &xcb_render_id); + const xcb_query_extension_reply_t *render = xcb_get_extension_data(conn, &xcb_render_id); if (render == NULL || !render->present) { LOG_ERR("RENDER extension not present"); @@ -92,18 +88,15 @@ xcb_init(void) return false; } - xcb_randr_query_version_cookie_t randr_cookie = - xcb_randr_query_version(conn, XCB_RANDR_MAJOR_VERSION, - XCB_RANDR_MINOR_VERSION); - xcb_render_query_version_cookie_t render_cookie = - xcb_render_query_version(conn, XCB_RENDER_MAJOR_VERSION, - XCB_RENDER_MINOR_VERSION); + xcb_randr_query_version_cookie_t randr_cookie + = xcb_randr_query_version(conn, XCB_RANDR_MAJOR_VERSION, XCB_RANDR_MINOR_VERSION); + xcb_render_query_version_cookie_t render_cookie + = xcb_render_query_version(conn, XCB_RENDER_MAJOR_VERSION, XCB_RENDER_MINOR_VERSION); xcb_flush(conn); xcb_generic_error_t *e; - xcb_randr_query_version_reply_t *randr_version = - xcb_randr_query_version_reply(conn, randr_cookie, &e); + xcb_randr_query_version_reply_t *randr_version = xcb_randr_query_version_reply(conn, randr_cookie, &e); if (e != NULL) { LOG_ERR("failed to query RANDR version: %s", xcb_error(e)); free(e); @@ -111,8 +104,7 @@ xcb_init(void) return false; } - xcb_render_query_version_reply_t *render_version = - xcb_render_query_version_reply(conn, render_cookie, &e); + xcb_render_query_version_reply_t *render_version = xcb_render_query_version_reply(conn, render_cookie, &e); if (e != NULL) { LOG_ERR("failed to query RENDER version: %s", xcb_error(e)); free(e); @@ -120,10 +112,8 @@ xcb_init(void) return false; } - LOG_DBG("RANDR: %u.%u", - randr_version->major_version, randr_version->minor_version); - LOG_DBG("RENDER: %u.%u", - render_version->major_version, render_version->minor_version); + LOG_DBG("RANDR: %u.%u", randr_version->major_version, randr_version->minor_version); + LOG_DBG("RENDER: %u.%u", render_version->major_version, render_version->minor_version); free(randr_version); free(render_version); @@ -131,7 +121,7 @@ xcb_init(void) /* Cache atoms */ UTF8_STRING = get_atom(conn, "UTF8_STRING"); _NET_WM_PID = get_atom(conn, "_NET_WM_PID"); - _NET_WM_WINDOW_TYPE = get_atom(conn, "_NET_WM_WINDOW_TYPE"); + _NET_WM_WINDOW_TYPE = get_atom(conn, "_NET_WM_WINDOW_TYPE"); _NET_WM_WINDOW_TYPE_DOCK = get_atom(conn, "_NET_WM_WINDOW_TYPE_DOCK"); _NET_WM_STATE = get_atom(conn, "_NET_WM_STATE"); _NET_WM_STATE_ABOVE = get_atom(conn, "_NET_WM_STATE_ABOVE"); @@ -153,10 +143,7 @@ xcb_atom_t get_atom(xcb_connection_t *conn, const char *name) { xcb_generic_error_t *e; - xcb_intern_atom_reply_t *reply = xcb_intern_atom_reply( - conn, - xcb_intern_atom(conn, 0, strlen(name), name), - &e); + xcb_intern_atom_reply_t *reply = xcb_intern_atom_reply(conn, xcb_intern_atom(conn, 0, strlen(name), name), &e); if (e != NULL) { LOG_ERR("%s: failed to get atom for %s", name, xcb_error(e)); @@ -182,8 +169,7 @@ char * get_atom_name(xcb_connection_t *conn, xcb_atom_t atom) { xcb_generic_error_t *e; - xcb_get_atom_name_reply_t *reply = xcb_get_atom_name_reply( - conn, xcb_get_atom_name(conn, atom), &e); + xcb_get_atom_name_reply_t *reply = xcb_get_atom_name_reply(conn, xcb_get_atom_name(conn, atom), &e); if (e != NULL) { LOG_ERR("failed to get atom name: %s", xcb_error(e)); @@ -192,8 +178,7 @@ get_atom_name(xcb_connection_t *conn, xcb_atom_t atom) return NULL; } - char *name = strndup( - xcb_get_atom_name_name(reply), xcb_get_atom_name_name_length(reply)); + char *name = strndup(xcb_get_atom_name_name(reply), xcb_get_atom_name_name_length(reply)); LOG_DBG("atom name: %s", name); @@ -207,22 +192,17 @@ xcb_error(const xcb_generic_error_t *error) static char msg[1024]; #if defined(HAVE_XCB_ERRORS) - const char *major = xcb_errors_get_name_for_major_code( - err_context, error->major_code); - const char *minor = xcb_errors_get_name_for_minor_code( - err_context, error->major_code, error->minor_code); + const char *major = xcb_errors_get_name_for_major_code(err_context, error->major_code); + const char *minor = xcb_errors_get_name_for_minor_code(err_context, error->major_code, error->minor_code); const char *extension; - const char *name = xcb_errors_get_name_for_error( - err_context, error->error_code, &extension); + const char *name = xcb_errors_get_name_for_error(err_context, error->error_code, &extension); - snprintf(msg, sizeof(msg), - "major=%s, minor=%s), code=%s, extension=%s, sequence=%u", - major, minor, name, extension, error->sequence); -#else - snprintf(msg, sizeof(msg), "op %hhu:%hu, code %hhu, sequence %hu", - error->major_code, error->minor_code, error->error_code, + snprintf(msg, sizeof(msg), "major=%s, minor=%s), code=%s, extension=%s, sequence=%u", major, minor, name, extension, error->sequence); +#else + snprintf(msg, sizeof(msg), "op %hhu:%hu, code %hhu, sequence %hu", error->major_code, error->minor_code, + error->error_code, error->sequence); #endif return msg; diff --git a/yml.c b/yml.c index 73e5083..4769d38 100644 --- a/yml.c +++ b/yml.c @@ -1,13 +1,15 @@ #include "yml.h" -#include -#include -#include -#include #include +#include +#include +#include +#include -#include #include +#include + +#define UNUSED __attribute__((unused)) enum yml_error { YML_ERR_NONE, @@ -76,7 +78,8 @@ clone_node(struct yml_node *parent, const struct yml_node *node) break; case DICT: - tll_foreach(node->dict.pairs, it) { + tll_foreach(node->dict.pairs, it) + { struct dict_pair p = { .key = clone_node(clone, it->item.key), .value = clone_node(clone, it->item.value), @@ -86,8 +89,7 @@ clone_node(struct yml_node *parent, const struct yml_node *node) break; case LIST: - tll_foreach(node->list.values, it) - tll_push_back(clone->list.values, clone_node(clone, it->item)); + tll_foreach(node->list.values, it) tll_push_back(clone->list.values, clone_node(clone, it->item)); break; case ROOT: @@ -117,7 +119,8 @@ dict_has_key(const struct yml_node *node, const struct yml_node *key) { assert(node->type == DICT); - tll_foreach(node->dict.pairs, pair) { + tll_foreach(node->dict.pairs, pair) + { if (node_equal(pair->item.key, key)) return true; } @@ -128,7 +131,7 @@ dict_has_key(const struct yml_node *node, const struct yml_node *key) static enum yml_error add_node(struct yml_node *parent, struct yml_node *new_node, yaml_mark_t loc) { - new_node->line = loc.line + 1; /* yaml uses 0-based line numbers */ + new_node->line = loc.line + 1; /* yaml uses 0-based line numbers */ new_node->column = loc.column; switch (parent->type) { @@ -166,8 +169,7 @@ add_node(struct yml_node *parent, struct yml_node *new_node, yaml_mark_t loc) } static void -add_anchor(struct yml_node *root, const char *anchor, - const struct yml_node *node) +add_anchor(struct yml_node *root, const char *anchor, const struct yml_node *node) { assert(root->type == ROOT); @@ -177,31 +179,34 @@ add_anchor(struct yml_node *root, const char *anchor, root->root.anchor_count++; } -static void -post_process(struct yml_node *node) +static bool +post_process(struct yml_node *node, char **error) { switch (node->type) { case ROOT: if (node->root.root != NULL) - post_process(node->root.root); + if (!post_process(node->root.root, error)) + return false; break; case SCALAR: - //assert(strcmp(node->scalar.value, "<<") != 0); + // assert(strcmp(node->scalar.value, "<<") != 0); break; case LIST: - tll_foreach(node->list.values, it) - post_process(it->item); + tll_foreach(node->list.values, it) if (!post_process(it->item, error)) return false; break; case DICT: - tll_foreach(node->dict.pairs, it) { - post_process(it->item.key); - post_process(it->item.value); + tll_foreach(node->dict.pairs, it) + { + if (!post_process(it->item.key, error) || !post_process(it->item.value, error)) { + return false; + } } - tll_foreach(node->dict.pairs, it) { + tll_foreach(node->dict.pairs, it) + { if (it->item.key->type != SCALAR) continue; @@ -213,22 +218,35 @@ post_process(struct yml_node *node) * Merge value is a list (of dictionaries) * e.g. <<: [*foo, *bar] */ - tll_foreach(it->item.value->list.values, v_it) { - assert(v_it->item->type == DICT); - tll_foreach(v_it->item->dict.pairs, vv_it) { + tll_foreach(it->item.value->list.values, v_it) + { + if (v_it->item->type != DICT) { + int cnt = snprintf(NULL, 0, "%zu:%zu: cannot merge non-dictionary anchor", v_it->item->line, + v_it->item->column); + *error = malloc(cnt + 1); + snprintf(*error, cnt + 1, "%zu:%zu: cannot merge non-dictionary anchor", v_it->item->line, + v_it->item->column); + return false; + } + + tll_foreach(v_it->item->dict.pairs, vv_it) + { struct dict_pair p = { .key = vv_it->item.key, .value = vv_it->item.value, }; - /* TODO: handle this. Is it an error? Or - * should we replace the existing key/value - * pair */ - assert(!dict_has_key(node, vv_it->item.key)); - tll_push_back(node->dict.pairs, p); + if (dict_has_key(node, vv_it->item.key)) { + /* Prefer value in target dictionary, over the + * value from the anchor */ + yml_destroy(vv_it->item.key); + yml_destroy(vv_it->item.value); + } else { + tll_push_back(node->dict.pairs, p); + } } - /* Destroy lits, but don't free (since its nodes + /* Destroy list, but don't free (since its nodes * have been moved to this node), *before* * destroying the key/value nodes. This ensures * the dict nodes aren't free:d in the @@ -240,18 +258,30 @@ post_process(struct yml_node *node) * Merge value is a dictionary only * e.g. <<: *foo */ - assert(it->item.value->type == DICT); - tll_foreach(it->item.value->dict.pairs, v_it) { + if (it->item.value->type != DICT) { + int cnt = snprintf(NULL, 0, "%zu:%zu: cannot merge non-dictionary anchor", it->item.value->line, + it->item.value->column); + *error = malloc(cnt + 1); + snprintf(*error, cnt + 1, "%zu:%zu: cannot merge non-dictionary anchor", it->item.value->line, + it->item.value->column); + return false; + } + + tll_foreach(it->item.value->dict.pairs, v_it) + { struct dict_pair p = { .key = v_it->item.key, .value = v_it->item.value, }; - /* TODO: handle this. Is it an error? Or should we - * replace the existing key/value pair */ - assert(!dict_has_key(node, v_it->item.key)); - - tll_push_back(node->dict.pairs, p); + if (dict_has_key(node, v_it->item.key)) { + /* Prefer value in target dictionary, over the + * value from the anchor */ + yml_destroy(v_it->item.key); + yml_destroy(v_it->item.value); + } else { + tll_push_back(node->dict.pairs, p); + } } /* Destroy list here, *without* freeing nodes (since @@ -269,13 +299,12 @@ post_process(struct yml_node *node) } break; } + + return true; } static const char * -format_error(enum yml_error err, - const struct yml_node *parent, - const struct yml_node *node, - const char *anchor) +format_error(enum yml_error err, const struct yml_node *parent, const struct yml_node *node, const char *anchor) { static char err_str[512]; @@ -286,11 +315,9 @@ format_error(enum yml_error err, case YML_ERR_DUPLICATE_KEY: { /* Find parent's key (i.e its name) */ - if (parent->parent != NULL && - parent->parent->type == DICT && - node->type == SCALAR) - { - tll_foreach(parent->parent->dict.pairs, pair) { + if (parent->parent != NULL && parent->parent->type == DICT && node->type == SCALAR) { + tll_foreach(parent->parent->dict.pairs, pair) + { if (pair->item.value != parent) continue; @@ -300,17 +327,14 @@ format_error(enum yml_error err, assert(pair->item.key->type == SCALAR); assert(node->type == SCALAR); - snprintf(err_str, sizeof(err_str), - "%s: duplicate key: '%s'", - pair->item.key->scalar.value, + snprintf(err_str, sizeof(err_str), "%s: duplicate key: '%s'", pair->item.key->scalar.value, node->scalar.value); return err_str; } } if (node->type == SCALAR) { - snprintf(err_str, sizeof(err_str), - "duplicate key: %s", node->scalar.value); + snprintf(err_str, sizeof(err_str), "duplicate key: %s", node->scalar.value); } else snprintf(err_str, sizeof(err_str), "duplicate key"); break; @@ -318,22 +342,20 @@ format_error(enum yml_error err, case YML_ERR_INVALID_ANCHOR: if (parent->parent != NULL && parent->parent->type == DICT) { - tll_foreach(parent->parent->dict.pairs, pair) { + tll_foreach(parent->parent->dict.pairs, pair) + { if (pair->item.value != parent) continue; if (pair->item.key->type != SCALAR) break; - snprintf(err_str, sizeof(err_str), - "%s: invalid anchor: %s", - pair->item.key->scalar.value, + snprintf(err_str, sizeof(err_str), "%s: invalid anchor: %s", pair->item.key->scalar.value, anchor != NULL ? anchor : ""); return err_str; } } - snprintf(err_str, sizeof(err_str), "invalid anchor: %s", - anchor != NULL ? anchor : ""); + snprintf(err_str, sizeof(err_str), "invalid anchor: %s", anchor != NULL ? anchor : ""); break; case YML_ERR_UNKNOWN: @@ -344,6 +366,47 @@ format_error(enum yml_error err, 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) { @@ -353,7 +416,7 @@ yml_load(FILE *yml, char **error) yaml_parser_set_input_file(&yaml, yml); bool done = false; - int indent = 0; + int indent UNUSED = 0; struct yml_node *root = malloc(sizeof(*root)); root->type = ROOT; @@ -368,22 +431,15 @@ yml_load(FILE *yml, char **error) yaml_event_t event; if (!yaml_parser_parse(&yaml, &event)) { if (error != NULL) { - int cnt = snprintf( - NULL, 0, "%zu:%zu: %s %s", - yaml.problem_mark.line + 1, - yaml.problem_mark.column, - yaml.problem, - yaml.context != NULL ? yaml.context : ""); + int cnt = snprintf(NULL, 0, "%zu:%zu: %s %s", yaml.problem_mark.line + 1, yaml.problem_mark.column, + yaml.problem, yaml.context != NULL ? yaml.context : ""); *error = malloc(cnt + 1); - snprintf(*error, cnt + 1, "%zu:%zu: %s %s", - yaml.problem_mark.line + 1, - yaml.problem_mark.column, - yaml.problem, - yaml.context != NULL ? yaml.context : ""); + snprintf(*error, cnt + 1, "%zu:%zu: %s %s", yaml.problem_mark.line + 1, yaml.problem_mark.column, + yaml.problem, yaml.context != NULL ? yaml.context : ""); } - goto err; + goto err_no_error_formatting; } switch (event.type) { @@ -431,9 +487,7 @@ yml_load(FILE *yml, char **error) } if (!got_match) { - error_str = format_error( - YML_ERR_INVALID_ANCHOR, n, NULL, - (const char *)event.data.alias.anchor); + error_str = format_error(YML_ERR_INVALID_ANCHOR, n, NULL, (const char *)event.data.alias.anchor); yaml_event_delete(&event); goto err; } @@ -443,8 +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) { @@ -526,30 +579,25 @@ yml_load(FILE *yml, char **error) yaml_parser_delete(&yaml); - post_process(root); + if (!post_process(root, error)) { + yml_destroy(root); + return NULL; + } return root; err: if (error_str != NULL) { - int cnt = snprintf( - NULL, 0, "%zu:%zu: %s", - yaml.mark.line + 1, - yaml.mark.column, - error_str); + int cnt = snprintf(NULL, 0, "%zu:%zu: %s", yaml.mark.line + 1, yaml.mark.column, error_str); *error = malloc(cnt + 1); - snprintf( - *error, cnt + 1, "%zu:%zu: %s", - yaml.mark.line + 1, - yaml.mark.column, - error_str); + snprintf(*error, cnt + 1, "%zu:%zu: %s", yaml.mark.line + 1, yaml.mark.column, error_str); } else { - int cnt = snprintf(NULL, 0, "%zu:%zu: unknown error", - yaml.mark.line + 1, yaml.mark.column); + int cnt = snprintf(NULL, 0, "%zu:%zu: unknown error", yaml.mark.line + 1, yaml.mark.column); *error = malloc(cnt + 1); - snprintf(*error, cnt + 1, "%zu:%zu: unknown error", - yaml.mark.line + 1, yaml.mark.column); + snprintf(*error, cnt + 1, "%zu:%zu: unknown error", yaml.mark.line + 1, yaml.mark.column); } +err_no_error_formatting: + yml_destroy(root); yaml_parser_delete(&yaml); return NULL; @@ -577,7 +625,8 @@ yml_destroy(struct yml_node *node) break; case DICT: - tll_foreach(node->dict.pairs, it) { + tll_foreach(node->dict.pairs, it) + { yml_destroy(it->item.key); yml_destroy(it->item.value); } @@ -606,9 +655,11 @@ yml_is_list(const struct yml_node *node) return node->type == LIST; } - const struct yml_node * -yml_get_value(const struct yml_node *node, const char *_path) +static struct yml_node const * +yml_get_(struct yml_node const *node, char const *_path, bool value) { + /* value: true for value, false for key */ + if (node != NULL && node->type == ROOT) node = node->root.root; @@ -617,18 +668,21 @@ yml_get_value(const struct yml_node *node, const char *_path) char *path = strdup(_path); - for (const char *part = strtok(path, "."), *next_part = strtok(NULL, "."); - part != NULL; - part = next_part, next_part = strtok(NULL, ".")) - { + for (const char *part = strtok(path, "."), *next_part = strtok(NULL, "."); part != NULL; + part = next_part, next_part = strtok(NULL, ".")) { assert(yml_is_dict(node)); - tll_foreach(node->dict.pairs, it) { + tll_foreach(node->dict.pairs, it) + { assert(yml_is_scalar(it->item.key)); if (strcmp(it->item.key->scalar.value, part) == 0) { if (next_part == NULL) { free(path); - return it->item.value; + + if (value) + return it->item.value; + else + return it->item.key; } node = it->item.value; @@ -641,11 +695,24 @@ yml_get_value(const struct yml_node *node, const char *_path) return NULL; } +const struct yml_node * +yml_get_value(const struct yml_node *node, const char *_path) +{ + return yml_get_(node, _path, true); +} + +struct yml_node const * +yml_get_key(struct yml_node const *node, char const *_path) +{ + return yml_get_(node, _path, false); +} + struct yml_list_iter yml_list_iter(const struct yml_node *list) { assert(yml_is_list(list)); - tll_foreach(list->list.values, it) { + tll_foreach(list->list.values, it) + { return (struct yml_list_iter){ .node = it->item, .private = it, @@ -678,9 +745,7 @@ yml_list_length(const struct yml_node *list) assert(yml_is_list(list)); size_t length = 0; - for (struct yml_list_iter it = yml_list_iter(list); - it.node != NULL; - yml_list_next(&it), length++) + for (struct yml_list_iter it = yml_list_iter(list); it.node != NULL; yml_list_next(&it), length++) ; return length; @@ -691,7 +756,8 @@ yml_dict_iter(const struct yml_node *dict) { assert(yml_is_dict(dict)); - tll_foreach(dict->dict.pairs, it) { + tll_foreach(dict->dict.pairs, it) + { return (struct yml_dict_iter){ .key = it->item.key, .value = it->item.value, @@ -699,7 +765,7 @@ yml_dict_iter(const struct yml_node *dict) }; } - return (struct yml_dict_iter) { + return (struct yml_dict_iter){ .key = NULL, .value = NULL, .private1 = NULL, @@ -770,18 +836,12 @@ _as_bool(const struct yml_node *value, bool *ret) return false; const char *v = yml_value_as_string(value); - if (strcasecmp(v, "y") == 0 || - strcasecmp(v, "yes") == 0 || - strcasecmp(v, "true") == 0 || - strcasecmp(v, "on") == 0) - { + if (strcasecmp(v, "y") == 0 || strcasecmp(v, "yes") == 0 || strcasecmp(v, "true") == 0 + || strcasecmp(v, "on") == 0) { *ret = true; return true; - } else if (strcasecmp(v, "n") == 0 || - strcasecmp(v, "no") == 0 || - strcasecmp(v, "false") == 0 || - strcasecmp(v, "off") == 0) - { + } else if (strcasecmp(v, "n") == 0 || strcasecmp(v, "no") == 0 || strcasecmp(v, "false") == 0 + || strcasecmp(v, "off") == 0) { *ret = false; return true; } @@ -828,7 +888,8 @@ _print_node(const struct yml_node *n, int indent) break; case DICT: - tll_foreach(n->dict.pairs, it) { + tll_foreach(n->dict.pairs, it) + { _print_node(it->item.key, indent); printf(": "); @@ -843,7 +904,8 @@ _print_node(const struct yml_node *n, int indent) break; case LIST: - tll_foreach(n->list.values, it) { + tll_foreach(n->list.values, it) + { printf("%*s- ", indent, ""); if (it->item->type != SCALAR) { printf("\n"); diff --git a/yml.h b/yml.h index 476d469..784252c 100644 --- a/yml.h +++ b/yml.h @@ -1,6 +1,6 @@ #pragma once -#include #include +#include struct yml_node; @@ -11,8 +11,8 @@ bool yml_is_scalar(const struct yml_node *node); bool yml_is_dict(const struct yml_node *node); bool yml_is_list(const struct yml_node *node); -const struct yml_node *yml_get_value( - const struct yml_node *node, const char *path); +const struct yml_node *yml_get_value(const struct yml_node *node, const char *path); +const struct yml_node *yml_get_key(struct yml_node const *node, char const *path); struct yml_list_iter { const struct yml_node *node;