diff --git a/.builds/alpine-x64.yml b/.builds/alpine-x64.yml index a5d4e9c..fc657ce 100644 --- a/.builds/alpine-x64.yml +++ b/.builds/alpine-x64.yml @@ -37,6 +37,10 @@ tasks: pip install codespell cd yambar ~/.local/bin/codespell README.md CHANGELOG.md *.c *.h doc/*.scd + - fcft: | + cd yambar/subprojects + git clone https://codeberg.org/dnkl/fcft.git + 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..528a36b --- /dev/null +++ b/.clang-format @@ -0,0 +1,17 @@ +--- +BasedOnStyle: GNU +IndentWidth: 4 +--- +Language: Cpp +PointerAlignment: Right +ColumnLimit: 120 +BreakBeforeBraces: Custom +BraceWrapping: + AfterEnum: false + AfterClass: false + SplitEmptyFunction: true + AfterFunction: true + AfterStruct: false + +SpaceBeforeParens: ControlStatements +Cpp11BracedListStyle: true 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/.gitlab-ci.yml b/.gitlab-ci.yml index 06df201..be21159 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -27,6 +27,9 @@ versions: debug: stage: build script: + - cd subprojects + - git clone https://codeberg.org/dnkl/fcft.git + - cd .. - apk add gcovr - mkdir -p bld/debug - cd bld/debug @@ -55,6 +58,9 @@ debug: release: stage: build script: + - cd subprojects + - git clone https://codeberg.org/dnkl/fcft.git + - cd .. - mkdir -p bld/release - cd bld/release - meson --buildtype=minsize ../../ @@ -64,6 +70,9 @@ release: x11_only: stage: build script: + - cd subprojects + - git clone https://codeberg.org/dnkl/fcft.git + - cd .. - mkdir -p bld/debug - cd bld/debug - meson --buildtype=debug -Dbackend-x11=enabled -Dbackend-wayland=disabled ../../ @@ -73,6 +82,9 @@ x11_only: wayland_only: stage: build script: + - cd subprojects + - git clone https://codeberg.org/dnkl/fcft.git + - cd .. - mkdir -p bld/debug - cd bld/debug - meson --buildtype=debug -Dbackend-x11=disabled -Dbackend-wayland=enabled ../../ @@ -82,6 +94,9 @@ wayland_only: plugins_as_shared_modules: stage: build script: + - cd subprojects + - git clone https://codeberg.org/dnkl/fcft.git + - cd .. - mkdir -p bld/debug - cd bld/debug - meson --buildtype=debug -Dcore-plugins-as-shared-libraries=true ../../ diff --git a/.woodpecker.yml b/.woodpecker.yml index 875213a..4ea84f1 100644 --- a/.woodpecker.yml +++ b/.woodpecker.yml @@ -1,6 +1,9 @@ pipeline: codespell: - when: { branch: master } + when: + branch: + - master + - releases/* image: alpine:latest commands: - apk add python3 @@ -9,7 +12,10 @@ pipeline: - codespell README.md CHANGELOG.md *.c *.h doc/*.scd subprojects: - when: { branch: master } + when: + branch: + - master + - releases/* image: alpine:latest commands: - apk add git @@ -19,7 +25,10 @@ pipeline: - cd .. x64: - when: { branch: master } + when: + branch: + - master + - releases/* group: build image: alpine:latest commands: @@ -74,7 +83,10 @@ pipeline: - cd ../.. x86: - when: { branch: master } + when: + branch: + - master + - releases/* group: build image: i386/alpine:latest commands: diff --git a/CHANGELOG.md b/CHANGELOG.md index 6990b6b..67195a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # Changelog * [Unreleased](#unreleased) +* [1.8.0](#1-8-0) * [1.7.0](#1-7-0) * [1.6.2](#1-6-2) * [1.6.1](#1-6-1) @@ -9,53 +10,166 @@ ## Unreleased + ### Added -* ramp: can now have custom min and max values - (https://codeberg.org/dnkl/yambar/issues/103). + +* 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]). + +[153]: https://codeberg.org/dnkl/yambar/issues/153 +[159]: https://codeberg.org/dnkl/yambar/issues/159 ### Changed -* Made `libmpdclient` an optional dependency -* battery: unknown battery states are now mapped to ‘unknown’, instead - of ‘discharging’. +* Minimum required meson version is now 0.58. +* **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: + ... + ``` + + For a more thorough explanation, see the updated map section in the + man page for yambar-particles([#137][137] and [#175][175]). + + [137]: https://codeberg.org/dnkl/yambar/issues/137 + [175]: https://codeberg.org/dnkl/yambar/issues/172 ### Deprecated ### Removed ### 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 - (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 (https://codeberg.org/dnkl/yambar/issues/116). +* 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]). + +[169]: https://codeberg.org/dnkl/yambar/issues/169 +[172]: https://codeberg.org/dnkl/yambar/issues/172 +[178]: https://codeberg.org/dnkl/yambar/issues/178 ### Security ### Contributors +* Horus + + +## 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 - (https://codeberg.org/dnkl/yambar/issues/72). + ([#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 - (https://codeberg.org/dnkl/yambar/issues/77). + ([#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 - (https://codeberg.org/dnkl/yambar/issues/84). + ([#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. @@ -67,7 +181,6 @@ * network: `ssid`, `signal`, `rx-bitrate` and `rx-bitrate` tags. * network: `poll-interval` option (for the new `signal` and `*-bitrate` tags). -* tags: percentage formatter, for range tags: `{tag_name:%}`. * 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. @@ -77,7 +190,7 @@ * 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 - (https://codeberg.org/dnkl/yambar/issues/86). + ([#86](https://codeberg.org/dnkl/yambar/issues/86)). ### Fixed @@ -87,7 +200,7 @@ * 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 (https://codeberg.org/dnkl/yambar/issues/61). + disconnected ([#61](https://codeberg.org/dnkl/yambar/issues/61)). ### Contributors @@ -103,39 +216,39 @@ * Text shaping support. * Support for middle and right mouse buttons, mouse wheel and trackpad - scrolling (https://codeberg.org/dnkl/yambar/issues/39). + scrolling ([#39](https://codeberg.org/dnkl/yambar/issues/39)). * script: polling mode. See the new `poll-interval` option - (https://codeberg.org/dnkl/yambar/issues/67). + ([#67](https://codeberg.org/dnkl/yambar/issues/67)). ### Changed * doc: split up **yambar-modules**(5) into multiple man pages, one for - each module (https://codeberg.org/dnkl/yambar/issues/15). + 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 - (https://codeberg.org/dnkl/yambar/issues/51). + ([#51](https://codeberg.org/dnkl/yambar/issues/51)). * battery: don’t terminate (causing last status to “freeze”) when failing to update; retry again later - (https://codeberg.org/dnkl/yambar/issues/44). + ([#44](https://codeberg.org/dnkl/yambar/issues/44)). * battery: differentiate "Not Charging" and "Discharging" in state tag of battery module. - (https://codeberg.org/dnkl/yambar/issues/57). + ([#57](https://codeberg.org/dnkl/yambar/issues/57)). * string: use HORIZONTAL ELLIPSIS instead of three regular periods when truncating a string - (https://codeberg.org/dnkl/yambar/issues/73). + ([#73](https://codeberg.org/dnkl/yambar/issues/73)). ### Fixed * Crash when merging non-dictionary anchors in the YAML configuration - (https://codeberg.org/dnkl/yambar/issues/32). + ([#32](https://codeberg.org/dnkl/yambar/issues/32)). * Crash in the `ramp` particle when the tag’s value was out-of-bounds - (https://codeberg.org/dnkl/yambar/issues/45). + ([#45](https://codeberg.org/dnkl/yambar/issues/45)). * Crash when a string particle contained `{}` - (https://codeberg.org/dnkl/yambar/issues/48). + ([#48](https://codeberg.org/dnkl/yambar/issues/48)). * `script` module rejecting range tag end values containing the digit - `9` (https://codeberg.org/dnkl/yambar/issues/60). + `9` ([#60](https://codeberg.org/dnkl/yambar/issues/60)). ### Contributors @@ -150,7 +263,7 @@ * i3: workspaces with numerical names are sorted separately from non-numerically named workspaces - (https://codeberg.org/dnkl/yambar/issues/30). + ([#30](https://codeberg.org/dnkl/yambar/issues/30)). ### Fixed @@ -158,7 +271,7 @@ * 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 (https://codeberg.org/dnkl/yambar/issues/25). + values ([#25](https://codeberg.org/dnkl/yambar/issues/25)). ## 1.6.0 @@ -167,17 +280,17 @@ * alsa: `percent` tag. This is an integer tag that represents the current volume as a percentage value - (https://codeberg.org/dnkl/yambar/issues/10). + ([#10](https://codeberg.org/dnkl/yambar/issues/10)). * river: added documentation - (https://codeberg.org/dnkl/yambar/issues/9). + ([#9](https://codeberg.org/dnkl/yambar/issues/9)). * script: new module, adds support for custom user scripts - (https://codeberg.org/dnkl/yambar/issues/11). + ([#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` - (https://codeberg.org/dnkl/yambar/issues/17). + ([#17](https://codeberg.org/dnkl/yambar/issues/17)). * i3: `mode` tag: the name of the currently active mode @@ -187,12 +300,12 @@ error”_. * Memory leak when a YAML parsing error was encountered. * clock: update every second when necessary - (https://codeberg.org/dnkl/yambar/issues/12). + ([#12](https://codeberg.org/dnkl/yambar/issues/12)). * mpd: fix compilation with clang - (https://codeberg.org/dnkl/yambar/issues/16). + ([#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 (https://codeberg.org/dnkl/yambar/issues/20) + disconnected ([#20](https://codeberg.org/dnkl/yambar/issues/20)) ### Contributors diff --git a/PKGBUILD b/PKGBUILD index e4b343b..13ddc83 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -1,5 +1,5 @@ pkgname=yambar -pkgver=1.7.0 +pkgver=1.8.0 pkgrel=1 pkgdesc="Simplistic and highly configurable status panel for X and Wayland" arch=('x86_64' 'aarch64') @@ -15,7 +15,7 @@ depends=( 'libudev.so' 'json-c' 'libmpdclient' - 'fcft>=2.4.0') + '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 d0963e4..5dc6cfd 100644 --- a/PKGBUILD.wayland-only +++ b/PKGBUILD.wayland-only @@ -1,5 +1,5 @@ pkgname=yambar-wayland -pkgver=1.7.0 +pkgver=1.8.0 pkgrel=1 pkgdesc="Simplistic and highly configurable status panel for Wayland" arch=('x86_64' 'aarch64') @@ -16,7 +16,7 @@ depends=( 'libudev.so' 'json-c' 'libmpdclient' - 'fcft>=2.4.0') + 'fcft>=3.0.0' 'fcft<4.0.0') source=() pkgver() { diff --git a/bar/bar.c b/bar/bar.c index 8b19a43..4f1895f 100644 --- a/bar/bar.c +++ b/bar/bar.c @@ -293,6 +293,7 @@ run(struct bar *_bar) } set_cursor(_bar, "left_ptr"); + expose(_bar); /* Start modules */ thrd_t thrd_left[max(bar->left.count, 1)]; @@ -323,7 +324,6 @@ run(struct bar *_bar) LOG_DBG("all modules started"); - refresh(_bar); bar->backend.iface->loop(_bar, &expose, &on_mouse); LOG_DBG("shutting down"); diff --git a/bar/bar.h b/bar/bar.h index c9f94f4..717b690 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 { @@ -26,6 +27,7 @@ struct bar_config { 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; diff --git a/bar/meson.build b/bar/meson.build index 90f050b..6ca5ec9 100644 --- a/bar/meson.build +++ b/bar/meson.build @@ -7,11 +7,11 @@ 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 = [] diff --git a/bar/wayland.c b/bar/wayland.c index 696f598..a57fdfa 100644 --- a/bar/wayland.c +++ b/bar/wayland.c @@ -7,6 +7,7 @@ #include #include #include +#include #include #include @@ -45,6 +46,7 @@ struct monitor { struct wl_output *output; struct zxdg_output_v1 *xdg; char *name; + uint32_t wl_name; int x; int y; @@ -95,6 +97,7 @@ struct wayland_backend { tll(struct monitor) monitors; const struct monitor *monitor; + char *last_mapped_monitor; int scale; @@ -112,11 +115,11 @@ struct wayland_backend { 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; double aggregated_scroll; bool have_discrete; - void (*bar_expose)(const struct bar *bar); void (*bar_on_mouse)(struct bar *bar, enum mouse_event event, enum mouse_button btn, int x, int y); }; @@ -492,12 +495,35 @@ output_scale(void *data, struct wl_output *wl_output, int32_t factor) } } +#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 @@ -519,6 +545,9 @@ xdg_output_handle_logical_size(void *data, struct zxdg_output_v1 *xdg_output, 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) { @@ -531,13 +560,40 @@ xdg_output_handle_done(void *data, struct zxdg_output_v1 *xdg_output) 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 @@ -610,6 +666,7 @@ handle_global(void *data, struct wl_registry *registry, tll_push_back(backend->monitors, ((struct monitor){ .backend = backend, + .wl_name = name, .output = output})); struct monitor *mon = &tll_back(backend->monitors); @@ -679,9 +736,23 @@ 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 = { @@ -703,22 +774,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 = { @@ -726,6 +785,82 @@ 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 = bar->layer == BAR_LAYER_BOTTOM + ? ZWLR_LAYER_SHELL_V1_LAYER_BOTTOM + : ZWLR_LAYER_SHELL_V1_LAYER_TOP; + + 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) { @@ -892,6 +1027,8 @@ update_size(struct wayland_backend *backend) const struct monitor *mon = backend->monitor; const int scale = mon != NULL ? mon->scale : guess_scale(backend); + assert(backend->surface != NULL); + if (backend->scale == scale) return true; @@ -942,8 +1079,6 @@ update_size(struct wayland_backend *backend) return true; } -static const struct wl_surface_listener surface_listener; - static bool setup(struct bar *_bar) { @@ -989,48 +1124,18 @@ 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); - - enum zwlr_layer_shell_v1_layer layer = bar->layer == BAR_LAYER_BOTTOM - ? ZWLR_LAYER_SHELL_V1_LAYER_BOTTOM - : ZWLR_LAYER_SHELL_V1_LAYER_TOP; - - 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); - - 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; } @@ -1050,16 +1155,6 @@ cleanup(struct bar *_bar) if (backend->pipe_fds[1] >= 0) close(backend->pipe_fds[1]); - tll_foreach(backend->buffers, it) { - if (it->item.wl_buf != NULL) - wl_buffer_destroy(it->item.wl_buf); - if (it->item.pix != NULL) - pixman_image_unref(it->item.pix); - - munmap(it->item.mmapped, it->item.size); - tll_remove(backend->buffers, it); - } - tll_foreach(backend->monitors, it) { struct monitor *mon = &it->item; free(mon->name); @@ -1070,6 +1165,7 @@ cleanup(struct bar *_bar) 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); @@ -1078,12 +1174,20 @@ cleanup(struct bar *_bar) seat_destroy(&it->item); tll_free(backend->seats); - if (backend->layer_surface != NULL) - zwlr_layer_surface_v1_destroy(backend->layer_surface); + 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) + pixman_image_unref(it->item.pix); + + munmap(it->item.mmapped, it->item.size); + tll_remove(backend->buffers, it); + } + 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) @@ -1108,14 +1212,19 @@ loop(struct bar *_bar, { 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) { @@ -1127,42 +1236,72 @@ 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 */ + 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 @@ -1195,7 +1334,15 @@ 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 = { @@ -1219,7 +1366,9 @@ frame_callback(void *data, struct wl_callback *wl_callback, uint32_t callback_da 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; @@ -1234,6 +1383,7 @@ 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 @@ -1248,6 +1398,9 @@ commit(const struct bar *_bar) //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); @@ -1275,6 +1428,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); @@ -1322,7 +1476,7 @@ set_cursor(struct bar *_bar, const char *cursor) } static const char * -output_name(const struct bar *_bar) +bar_output_name(const struct bar *_bar) { const struct private *bar = _bar->private; const struct wayland_backend *backend = bar->backend.data; @@ -1337,5 +1491,5 @@ const struct backend wayland_backend_iface = { .commit = &commit, .refresh = &refresh, .set_cursor = &set_cursor, - .output_name = &output_name, + .output_name = &bar_output_name, }; diff --git a/bar/xcb.c b/bar/xcb.c index 4b0e7c1..d8c5f9c 100644 --- a/bar/xcb.c +++ b/bar/xcb.c @@ -215,14 +215,14 @@ setup(struct bar *_bar) 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; diff --git a/char32.c b/char32.c new file mode 100644 index 0000000..0ca029a --- /dev/null +++ b/char32.c @@ -0,0 +1,84 @@ +#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..b01a7d3 --- /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/config-verify.c b/config-verify.c index 63efef3..68a50c8 100644 --- a/config-verify.c +++ b/config-verify.c @@ -50,6 +50,17 @@ conf_verify_int(keychain_t *chain, const struct yml_node *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) { @@ -217,6 +228,13 @@ conf_verify_font(keychain_t *chain, const struct yml_node *node) 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) { @@ -381,17 +399,17 @@ static bool verify_bar_border(keychain_t *chain, const struct yml_node *node) { static const struct attr_info attrs[] = { - {"width", false, &conf_verify_int}, - {"left-width", false, &conf_verify_int}, - {"right-width", false, &conf_verify_int}, - {"top-width", false, &conf_verify_int}, - {"bottom-width", false, &conf_verify_int}, + {"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_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}, + {"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}, }; @@ -422,30 +440,31 @@ 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_int}, + {"trackpad-sensitivity", false, &conf_verify_unsigned}, {NULL, false, NULL}, }; diff --git a/config-verify.h b/config-verify.h index 5afe3f6..0a4ae34 100644 --- a/config-verify.h +++ b/config-verify.h @@ -32,6 +32,7 @@ const char *conf_err_prefix( 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, @@ -44,6 +45,7 @@ bool conf_verify_dict(keychain_t *chain, const struct yml_node *node, 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 7f9583e..d2c11a6 100644 --- a/config.c +++ b/config.c @@ -4,6 +4,7 @@ #include #include #include +#include #include @@ -66,7 +67,76 @@ 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 * @@ -112,7 +182,8 @@ particle_simple_list_from_config(const struct yml_node *node, } struct particle *common = particle_common_new( - 0, 0, NULL, fcft_clone(inherited.font), inherited.foreground, NULL); + 0, 0, NULL, fcft_clone(inherited.font), inherited.font_shaping, + inherited.foreground, NULL); return particle_list_new(common, parts, count, 0, 2); } @@ -131,6 +202,7 @@ 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"); @@ -183,12 +255,14 @@ conf_to_particle(const struct yml_node *node, struct conf_inherit inherited) */ 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_templates, font, foreground, deco); + left, right, on_click_templates, font, font_shaping, foreground, deco); const struct particle_iface *iface = plugin_load_particle(type); @@ -205,6 +279,7 @@ 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, }; /* @@ -327,6 +402,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"); @@ -335,12 +411,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, }; @@ -370,12 +451,15 @@ 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_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, + .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, }; diff --git a/config.h b/config.h index 56f5b2e..ceb4b85 100644 --- a/config.h +++ b/config.h @@ -3,6 +3,7 @@ #include #include "yml.h" #include "bar/bar.h" +#include "font-shaping.h" struct bar; struct particle; @@ -16,9 +17,11 @@ 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; }; diff --git a/decorations/border.c b/decorations/border.c new file mode 100644 index 0000000..5868e88 --- /dev/null +++ b/decorations/border.c @@ -0,0 +1,93 @@ +#include + +#include "../config.h" +#include "../config-verify.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..1beb896 --- /dev/null +++ b/decorations/overline.c @@ -0,0 +1,72 @@ +#include + +#include "../config.h" +#include "../config-verify.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/underline.c b/decorations/underline.c index a700bec..5b8bbd3 100644 --- a/decorations/underline.c +++ b/decorations/underline.c @@ -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 a1550c4..e882f52 100644 --- a/doc/meson.build +++ b/doc/meson.build @@ -1,7 +1,7 @@ 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-alsa.5.scd', 'yambar-modules-backlight.5.scd', @@ -13,6 +13,8 @@ foreach man_src : ['yambar.1.scd', 'yambar.5.scd', 'yambar-decorations.5.scd', 'yambar-modules-script.5.scd', 'yambar-modules-sway-xkb.5.scd', 'yambar-modules-sway.5.scd', 'yambar-modules-xkb.5.scd', 'yambar-modules-xwindow.5.scd', 'yambar-modules.5.scd', + 'yambar-modules-cpu.5.scd', + 'yambar-modules-mem.5.scd', 'yambar-particles.5.scd', 'yambar-tags.5.scd'] parts = man_src.split('.') name = parts[-3] @@ -23,7 +25,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..24282b5 100644 --- a/doc/yambar-decorations.5.scd +++ b/doc/yambar-decorations.5.scd @@ -70,6 +70,71 @@ 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. diff --git a/doc/yambar-modules-battery.5.scd b/doc/yambar-modules-battery.5.scd index 42b5e00..3d55ed9 100644 --- a/doc/yambar-modules-battery.5.scd +++ b/doc/yambar-modules-battery.5.scd @@ -8,6 +8,17 @@ battery - This module reads battery status 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* diff --git a/doc/yambar-modules-cpu.5.scd b/doc/yambar-modules-cpu.5.scd new file mode 100644 index 0000000..2a05fdf --- /dev/null +++ b/doc/yambar-modules-cpu.5.scd @@ -0,0 +1,42 @@ +yambar-modules-cpu(5) + +# NAME +cpu - This module provides the CPU usage + +# TAGS + +[[ *Name* +:[ *Type* +:[ *Description* +| cpu +: range +: Current usage of the whole CPU in percent +| cpu<0..X> +: range +: Current usage of CPU core X in percent + +# CONFIGURATION + +[[ *Name* +:[ *Type* +:[ *Req* +:[ *Description* +| interval +: int +: no +: Refresh interval of the CPU usage stats in ms (default=500). Cannot be less then 500 ms + +# EXAMPLES + +``` +bar: + left: + - cpu: + interval: 2500 + content: + string: {text: "{cpu1}%"} +``` + +# 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 index 6a65ec3..94c4b84 100644 --- a/doc/yambar-modules-foreign-toplevel.5.scd +++ b/doc/yambar-modules-foreign-toplevel.5.scd @@ -67,10 +67,9 @@ bar: - foreign-toplevel: content: map: - tag: activated - values: - false: {empty: {}} - true: + conditions: + ~activated: {empty: {}} + activated: - string: {text: "{app-id}: {title}"} ``` diff --git a/doc/yambar-modules-i3.5.scd b/doc/yambar-modules-i3.5.scd index 8bf4a75..f82d131 100644 --- a/doc/yambar-modules-i3.5.scd +++ b/doc/yambar-modules-i3.5.scd @@ -35,6 +35,9 @@ with the _application_ and _title_ tags to replace the X11-only | 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: @@ -65,6 +68,10 @@ with the _application_ and _title_ tags to replace the X11-only : enum : no : How to sort the list of workspaces; one of _none_, _ascending_ or _descending_, defaults to _none_. +| 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 @@ -95,10 +102,9 @@ bar: content: "": map: - tag: state default: {string: {text: "{name}"}} - values: - focused: {string: {text: "{name}*"}} + conditions: + state == focused: {string: {text: "{name}*"}} current: { string: {text: "{application}: {title}"}} ``` diff --git a/doc/yambar-modules-mem.5.scd b/doc/yambar-modules-mem.5.scd new file mode 100644 index 0000000..cec575c --- /dev/null +++ b/doc/yambar-modules-mem.5.scd @@ -0,0 +1,51 @@ +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* +| interval +: string +: no +: Refresh interval of the memory usage stats in ms (default=500). Cannot be less then 500 ms + +# EXAMPLES + +``` +bar: + left: + - mem: + 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-removables.5.scd b/doc/yambar-modules-removables.5.scd index 4b984fa..ee7911f 100644 --- a/doc/yambar-modules-removables.5.scd +++ b/doc/yambar-modules-removables.5.scd @@ -22,6 +22,10 @@ instantiates the provided _content_ particle for each detected drive. | 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?*) @@ -70,13 +74,12 @@ bar: - removables: content: map: - tag: mounted - values: - false: + conditions: + ~mounted: string: on-click: udisksctl mount -b {device} text: "{label}" - true: + mounted: string: on-click: udisksctl unmount -b {device} text: "{label}" diff --git a/doc/yambar-modules-river.5.scd b/doc/yambar-modules-river.5.scd index 96be605..7ba2441 100644 --- a/doc/yambar-modules-river.5.scd +++ b/doc/yambar-modules-river.5.scd @@ -79,10 +79,9 @@ bar: title: {string: { text: "{seat} - {title}" }} content: map: - tag: occupied - values: - false: {empty: {}} - true: + conditions: + ~occupied: {empty: {}} + occupied: string: margin: 5 text: "{id}: {state}" diff --git a/doc/yambar-modules-script.5.scd b/doc/yambar-modules-script.5.scd index 7ba46d8..d021bf5 100644 --- a/doc/yambar-modules-script.5.scd +++ b/doc/yambar-modules-script.5.scd @@ -77,6 +77,7 @@ User defined. : Arguments to pass to the script/binary. | poll-interval : integer +: no : Number of seconds between each script run. If unset, continuous mode is used. @@ -132,10 +133,9 @@ bar: title|string|{{title}} content: map: - tag: status - values: - Paused: {empty: {}} - Playing: + conditions: + status == Paused: {empty: {}} + status == Playing: content: {string: {text: "{artist} - {title}"}} ``` diff --git a/doc/yambar-modules-sway-xkb.5.scd b/doc/yambar-modules-sway-xkb.5.scd index f79ff55..8543489 100644 --- a/doc/yambar-modules-sway-xkb.5.scd +++ b/doc/yambar-modules-sway-xkb.5.scd @@ -33,7 +33,7 @@ instantiated from this template, and represents an input device. | identifiers : list of strings : yes -: Identifiers of input devices to monitor. Use _swaymsg -t get_inputs | grep 'identifier.*keyboard' | cut -d'"' -f4_ to see available devices. +: Identifiers of input devices to monitor. Use _swaymsg -t get_inputs | grep 'identifier.\*keyboard' | cut -d'"' -f4_ to see available devices. | content : particle : yes diff --git a/doc/yambar-modules.5.scd b/doc/yambar-modules.5.scd index 266d9b7..ef47f62 100644 --- a/doc/yambar-modules.5.scd +++ b/doc/yambar-modules.5.scd @@ -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 diff --git a/doc/yambar-particles.5.scd b/doc/yambar-particles.5.scd index ab689a0..dba606a 100644 --- a/doc/yambar-particles.5.scd +++ b/doc/yambar-particles.5.scd @@ -31,20 +31,67 @@ 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. 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. | 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 @@ -161,48 +208,102 @@ content: # 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. + +For boolean tags, negation is done with a preceding '~': + +~ + +To match for empty strings, use ' "" ': + + == "" + +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 +| 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 diff --git a/doc/yambar.5.scd b/doc/yambar.5.scd index 47041fb..35c87b8 100644 --- a/doc/yambar.5.scd +++ b/doc/yambar.5.scd @@ -12,7 +12,8 @@ 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*: this is a comma separated list of fonts in _fontconfig_ + format. Example of valid values: - Font Awesome 5 Brands - Font Awesome 5 Free:style=solid - Dina:pixelsize=10:slant=italic @@ -124,7 +125,17 @@ 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 @@ -161,8 +172,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 index 0c31b20..573516c 100644 --- a/examples/configurations/laptop.conf +++ b/examples/configurations/laptop.conf @@ -46,82 +46,67 @@ bar: foreground: 000000ff deco: {stack: [background: {color: bc2b3fff}, <<: *std_underline]} - map: &i3_mode - tag: mode default: - string: margin: 5 text: "{mode}" deco: {background: {color: cc421dff}} - empty: {right-margin: 7} - values: - default: {empty: {}} + conditions: + mode == default: {empty: {}} content: "": map: - tag: state - values: - focused: {string: {<<: [*default, *focused]}} - unfocused: {string: {<<: *default}} - invisible: {string: {<<: [*default, *invisible]}} - urgent: {string: {<<: [*default, *urgent]}} + conditions: + state == focused: {string: {<<: [*default, *focused]}} + state == unfocused: {string: {<<: *default}} + state == invisible: {string: {<<: [*default, *invisible]}} + state == urgent: {string: {<<: [*default, *urgent]}} main: map: - tag: state - values: - focused: {string: {<<: [*main, *focused]}} - unfocused: {string: {<<: *main}} - invisible: {string: {<<: [*main, *invisible]}} - urgent: {string: {<<: [*main, *urgent]}} + conditions: + state == focused: {string: {<<: [*main, *focused]}} + state == unfocused: {string: {<<: *main}} + state == invisible: {string: {<<: [*main, *invisible]}} + state == urgent: {string: {<<: [*main, *urgent]}} surfing: map: - tag: state - values: - focused: {string: {<<: [*surfing, *focused]}} - unfocused: {string: {<<: *surfing}} - invisible: {string: {<<: [*surfing, *invisible]}} - urgent: {string: {<<: [*surfing, *urgent]}} + conditions: + state == focused: {string: {<<: [*surfing, *focused]}} + state == unfocused: {string: {<<: *surfing}} + state == invisible: {string: {<<: [*surfing, *invisible]}} + state == urgent: {string: {<<: [*surfing, *urgent]}} misc: map: - tag: state - values: - focused: {string: {<<: [*misc, *focused]}} - unfocused: {string: {<<: *misc}} - invisible: {string: {<<: [*misc, *invisible]}} - urgent: {string: {<<: [*misc, *urgent]}} + conditions: + state == focused: {string: {<<: [*misc, *focused]}} + state == unfocused: {string: {<<: *misc}} + state == invisible: {string: {<<: [*misc, *invisible]}} + state == urgent: {string: {<<: [*misc, *urgent]}} mail: map: - tag: state - values: - focused: {string: {<<: [*mail, *focused]}} - unfocused: {string: {<<: *mail}} - invisible: {string: {<<: [*mail, *invisible]}} - urgent: {string: {<<: [*mail, *urgent]}} + conditions: + state == focused: {string: {<<: [*mail, *focused]}} + state == unfocused: {string: {<<: *mail}} + state == invisible: {string: {<<: [*mail, *invisible]}} + state == 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: - "": - - map: {<<: *i3_mode} - - string: {text: "{title}"} - default: - list: - spacing: 0 - items: - - map: {<<: *i3_mode} - - string: {text: "{application}", max: 10, foreground: ffa0a0ff} - - string: {text: ": "} - - string: {text: "{title}", max: 35} + 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 @@ -130,32 +115,28 @@ bar: spacing: 0 items: - map: - tag: state - values: - playing: {string: {text: "{artist}"}} - paused: {string: {text: "{artist}", foreground: ffffff66}} + conditions: + state == playing: {string: {text: "{artist}"}} + state == paused: {string: {text: "{artist}", foreground: ffffff66}} - string: {text: " | ", foreground: ffffff66} - map: - tag: state - values: - playing: {string: {text: "{album}"}} - paused: {string: {text: "{album}", foreground: ffffff66}} + conditions: + state == playing: {string: {text: "{album}"}} + state == 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}} + conditions: + state == playing: {string: {text: "{title}", foreground: ffa0a0ff}} + state == 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} + 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: @@ -165,24 +146,21 @@ bar: spacing: 5 content: map: - tag: mounted - values: - false: + conditions: + ~mounted: map: - tag: optical on-click: udisksctl mount -b {device} - values: - false: [{string: *drive}, {string: {text: "{label}"}}] - true: [{string: *optical}, {string: {text: "{label}"}}] - true: + conditions: + ~optical: [{string: *drive}, {string: {text: "{label}"}}] + optical: [{string: *optical}, {string: {text: "{label}"}}] + mounted: map: - tag: optical on-click: udisksctl unmount -b {device} - values: - false: + conditions: + ~optical: - string: {<<: *drive, deco: *std_underline} - string: {text: "{label}"} - true: + optical: - string: {<<: *optical, deco: *std_underline} - string: {text: "{label}"} - sway-xkb: @@ -194,36 +172,31 @@ bar: name: enp1s0 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}} - network: name: wlp2s0 content: map: - tag: state default: {string: {text: , font: *awesome, foreground: ffffff66}} - values: - down: {string: {text: , font: *awesome, foreground: ff0000ff}} - up: + conditions: + state == down: {string: {text: , font: *awesome, foreground: ff0000ff}} + state == up: map: - tag: ipv4 default: - string: {text: , font: *awesome} - string: {text: "{ssid}"} - values: - "": + conditions: + ipv4 == "": - string: {text: , font: *awesome, foreground: ffffff66} - string: {text: "{ssid}", foreground: ffffff66} - alsa: @@ -231,23 +204,19 @@ bar: mixer: Master content: map: - tag: online - values: - false: {string: {text: , font: *awesome, foreground: ff0000ff}} - true: + 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" - tag: muted - values: - true: {string: {text: , font: *awesome, foreground: ffffff66}} - false: + conditions: + muted: {string: {text: , font: *awesome, foreground: ffffff66}} + ~muted: 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 @@ -255,11 +224,10 @@ bar: - battery: name: BAT0 poll-interval: 30 - content: - map: - tag: state - values: - discharging: + anchors: + discharging: &discharging + list: + items: - ramp: tag: capacity items: @@ -274,13 +242,20 @@ bar: - string: {text: , font: *awesome} - string: {text: , foreground: 00ff00ff, font: *awesome} - string: {text: "{capacity}% {estimate}"} - charging: + content: + map: + conditions: + state == unknown: + <<: *discharging + state == discharging: + <<: *discharging + state == charging: - string: {text: , foreground: 00ff00ff, font: *awesome} - string: {text: "{capacity}% {estimate}"} - full: + state == full: - string: {text: , foreground: 00ff00ff, font: *awesome} - string: {text: "{capacity}% full"} - not charging: + state == not charging: - ramp: tag: capacity items: diff --git a/examples/configurations/river-tags.conf b/examples/configurations/river-tags.conf new file mode 100644 index 0000000..462a329 --- /dev/null +++ b/examples/configurations/river-tags.conf @@ -0,0 +1,56 @@ +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: + map: + conditions: + ~occupied: {map: {<<: *river_base}} + occupied: {map: {<<: *river_base, deco: *bg_default}} + state == unfocused: + map: + <<: *river_base + state == invisible: + map: + conditions: + ~occupied: {empty: {}} + occupied: {map: {<<: *river_base, deco: {underline: {size: 3, color: ea6962ff}}}} diff --git a/examples/scripts/dwl-tags.sh b/examples/scripts/dwl-tags.sh index 395a790..bc24ac7 100755 --- a/examples/scripts/dwl-tags.sh +++ b/examples/scripts/dwl-tags.sh @@ -127,7 +127,7 @@ while true; do inotifywait -qq --event modify "${fname}" # Get info from the file - output="$(tail -n4 "${fname}")" + 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- )" 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/log.c b/log.c index 467b6fd..5864853 100644 --- a/log.c +++ b/log.c @@ -40,7 +40,9 @@ log_init(enum log_colorize _colorize, bool _do_syslog, [LOG_FACILITY_DAEMON] = LOG_DAEMON, }; - 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 : isatty(STDERR_FILENO); do_syslog = _do_syslog; log_level = _log_level; diff --git a/main.c b/main.c index 49d3985..7aab81a 100644 --- a/main.c +++ b/main.c @@ -296,8 +296,9 @@ main(int argc, char *const *argv) "fcft log level enum offset"); _Static_assert((int)LOG_COLORIZE_ALWAYS == (int)FCFT_LOG_COLORIZE_ALWAYS, "fcft colorize enum mismatch"); - fcft_log_init( - (enum fcft_log_colorize)log_colorize, log_syslog, (enum fcft_log_class)log_level); + 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); diff --git a/meson.build b/meson.build index ed4d0d1..ef28c58 100644 --- a/meson.build +++ b/meson.build @@ -1,7 +1,7 @@ project('yambar', 'c', - version: '1.7.0', + version: '1.8.0', license: 'MIT', - meson_version: '>=0.53.0', + meson_version: '>=0.58.0', default_options: ['c_std=c18', 'warning_level=1', 'werror=true', @@ -18,7 +18,7 @@ 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 @@ -71,7 +71,7 @@ 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.4.0', '<3.0.0'], fallback: 'fcft') +fcft = dependency('fcft', version: ['>=3.0.0', '<4.0.0'], fallback: 'fcft') add_project_arguments( ['-D_GNU_SOURCE'] + @@ -107,14 +107,16 @@ version = custom_target( 'generate_version', build_always_stale: true, output: 'version.h', - command: [env, 'LC_ALL=C', generate_version_sh, meson.project_version(), '@SOURCE_ROOT@', '@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', diff --git a/modules/backlight.c b/modules/backlight.c index 292b067..af58280 100644 --- a/modules/backlight.c +++ b/modules/backlight.c @@ -4,6 +4,7 @@ #include #include #include +#include #include #include @@ -178,15 +179,24 @@ 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); if (dev == NULL) @@ -209,7 +219,7 @@ run(struct module *mod) udev_unref(udev); close(current_fd); - return 0; + return ret; } static struct module * diff --git a/modules/battery.c b/modules/battery.c index 4f17de4..0833310 100644 --- a/modules/battery.c +++ b/modules/battery.c @@ -428,7 +428,15 @@ run(struct module *mod) {.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); + if (poll(fds, sizeof(fds) / sizeof(fds[0]), + m->poll_interval > 0 ? m->poll_interval * 1000 : -1) < 0) + { + if (errno == EINTR) + continue; + + LOG_ERRNO("failed to poll"); + break; + } if (fds[0].revents & POLLIN) { ret = 0; @@ -496,7 +504,7 @@ 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_unsigned}, MODULE_COMMON_ATTRS, }; diff --git a/modules/clock.c b/modules/clock.c index 2956a40..15392aa 100644 --- a/modules/clock.c +++ b/modules/clock.c @@ -2,6 +2,7 @@ #include #include #include +#include #include #include @@ -67,7 +68,6 @@ content(struct module *mod) return exposable; } -#include static int run(struct module *mod) { @@ -75,6 +75,8 @@ run(struct module *mod) const struct bar *bar = mod->bar; bar->refresh(bar); + int ret = 1; + while (true) { struct timespec _now; clock_gettime(CLOCK_REALTIME, &_now); @@ -120,19 +122,28 @@ run(struct module *mod) now.tv_sec, now.tv_usec, timeout_ms); struct pollfd fds[] = {{.fd = mod->abort_fd, .events = POLLIN}}; - poll(fds, 1, timeout_ms); + 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, bool utc) +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; diff --git a/modules/cpu.c b/modules/cpu.c new file mode 100644 index 0000000..b4067cd --- /dev/null +++ b/modules/cpu.c @@ -0,0 +1,278 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#define LOG_MODULE "cpu" +#define LOG_ENABLE_DBG 0 +#define SMALLEST_INTERVAL 500 +#include "../bar/bar.h" +#include "../config-verify.h" +#include "../config.h" +#include "../log.h" +#include "../plugin.h" +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 *label; + uint16_t interval; + struct cpu_stats cpu_stats; +}; + +static void +destroy(struct module *mod) +{ + struct private *m = mod->private; + m->label->destroy(m->label); + 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(struct module *mod) +{ + return "cpu"; +} + +static uint32_t +get_cpu_nb_cores() +{ + int nb_cores = get_nprocs(); + 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) +{ + uint32_t nb_cores = get_cpu_nb_cores(); + 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", "r"); + if (NULL == fp) { + LOG_ERRNO("unable to open /proc/stat"); + return; + } + + while ((read = getline(&line, &len, fp)) != -1 && core <= nb_cores) { + 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 *p = mod->private; + uint32_t nb_cores = get_cpu_nb_cores(); + + char cpu_name[32]; + struct tag_set tags; + tags.count = nb_cores + 1; + tags.tags = calloc(tags.count, sizeof(*tags.tags)); + mtx_lock(&mod->lock); + uint8_t cpu_usage = get_cpu_usage_percent(&p->cpu_stats, -1); + tags.tags[0] = tag_new_int_range(mod, "cpu", cpu_usage, 0, 100); + + for (uint32_t i = 0; i < nb_cores; ++i) { + uint8_t cpu_usage = get_cpu_usage_percent(&p->cpu_stats, i); + snprintf(cpu_name, sizeof(cpu_name), "cpu%u", i); + tags.tags[i + 1] = tag_new_int_range(mod, cpu_name, cpu_usage, 0, 100); + } + mtx_unlock(&mod->lock); + + struct exposable *exposable = p->label->instantiate(p->label, &tags); + + tag_set_destroy(&tags); + free(tags.tags); + 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, 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); + mtx_unlock(&mod->lock); + bar->refresh(bar); + } + + return 0; +} + +static struct module * +cpu_new(uint16_t interval, struct particle *label) +{ + struct private *p = calloc(1, sizeof(*p)); + p->label = label; + uint32_t nb_cores = get_cpu_nb_cores(); + p->interval = interval; + 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, "interval"); + const struct yml_node *c = yml_get_value(node, "content"); + + return cpu_new(interval == NULL ? SMALLEST_INTERVAL : yml_value_as_int(interval), conf_to_particle(c, inherited)); +} + +static bool +conf_verify_interval(keychain_t *chain, const struct yml_node *node) +{ + if (!conf_verify_unsigned(chain, node)) + return false; + + if (yml_value_as_int(node) < SMALLEST_INTERVAL) { + LOG_ERR("%s: interval value cannot be less than %d ms", conf_err_prefix(chain, node), SMALLEST_INTERVAL); + return false; + } + + return true; +} + +static bool +verify_conf(keychain_t *chain, const struct yml_node *node) +{ + static const struct attr_info attrs[] = { + {"interval", false, &conf_verify_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/i3.c b/modules/i3.c index 5e5c80a..22b6d4b 100644 --- a/modules/i3.c +++ b/modules/i3.c @@ -37,6 +37,7 @@ struct workspace { bool visible; bool focused; bool urgent; + bool empty; struct { unsigned id; @@ -59,6 +60,7 @@ struct private { size_t count; } ws_content; + bool strip_workspace_numbers; enum sort_mode sort_mode; tll(struct workspace) workspaces; @@ -70,6 +72,22 @@ 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; @@ -89,10 +107,15 @@ workspace_from_json(const struct json_object *json, struct workspace *ws) 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"); + 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); @@ -101,14 +124,22 @@ workspace_from_json(const struct json_object *json, struct workspace *ws) 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) { .name = strdup(name_as_string), - .name_as_int = workspace_name_as_int(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, .window = {.title = NULL, .pid = -1}, }; @@ -353,6 +384,7 @@ handle_workspace_event(int type, const struct json_object *json, void *_mod) else { workspace_free(ws); ws->name = strdup(current_name); + ws->empty = true; assert(ws->persistent); } } @@ -425,11 +457,12 @@ handle_window_event(int type, const struct json_object *json, void *_mod) } const char *change_str = json_object_get_string(change); + bool is_new = strcmp(change_str, "new") == 0; bool is_focus = strcmp(change_str, "focus") == 0; bool is_close = strcmp(change_str, "close") == 0; bool is_title = strcmp(change_str, "title") == 0; - if (!is_focus && !is_close && !is_title) + if (!is_new && !is_focus && !is_close && !is_title) return true; mtx_lock(&mod->lock); @@ -454,12 +487,19 @@ handle_window_event(int type, const struct json_object *json, void *_mod) ws->window.title = ws->window.application = NULL; ws->window.pid = -1; + /* May not be true, but e.g. a subsequent “focus” event will + * reset it... */ + ws->empty = true; + m->dirty = true; mtx_unlock(&mod->lock); return true; } + /* Non-close event - thus workspace cannot be empty */ + ws->empty = false; + struct json_object *container, *id, *name; if (!json_object_object_get_ex(json, "container", &container) || !json_object_object_get_ex(container, "id", &id) || @@ -581,7 +621,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; @@ -598,10 +638,18 @@ run(struct module *mod) 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 = { .name = strdup(name_as_string), - .name_as_int = workspace_name_as_int(name_as_string), + .name_as_int = name_as_int, .persistent = true, + .empty = true, }; workspace_add(m, ws); } @@ -695,12 +743,33 @@ content(struct module *mod) ws->visible ? ws->focused ? "focused" : "unfocused" : "invisible"; + LOG_DBG("%s: visible=%s, focused=%s, urgent=%s, empty=%s, state=%s, " + "application=%s, title=%s, mode=%s", + ws->name, + 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_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), @@ -708,7 +777,7 @@ content(struct module *mod) tag_new_string(mod, "mode", m->mode), }, - .count = 8, + .count = 9, }; if (ws->focused) { @@ -747,7 +816,8 @@ static struct module * 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]) + const char *persistent_workspaces[static persistent_count], + bool strip_workspace_numbers) { struct private *m = calloc(1, sizeof(*m)); @@ -763,6 +833,7 @@ 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; @@ -790,6 +861,8 @@ 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 *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; @@ -828,7 +901,9 @@ from_conf(const struct yml_node *node, struct conf_inherit inherited) } return i3_new(workspaces, yml_dict_length(c), left, right, sort_mode, - persistent_count, persistent_workspaces); + persistent_count, persistent_workspaces, + (strip_workspace_number != NULL + ? yml_value_as_bool(strip_workspace_number) : false)); } static bool @@ -878,11 +953,12 @@ 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}, diff --git a/modules/mem.c b/modules/mem.c new file mode 100644 index 0000000..d79861e --- /dev/null +++ b/modules/mem.c @@ -0,0 +1,192 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define LOG_MODULE "mem" +#define LOG_ENABLE_DBG 0 +#define SMALLEST_INTERVAL 500 +#include "../bar/bar.h" +#include "../config-verify.h" +#include "../config.h" +#include "../log.h" +#include "../plugin.h" + +struct private +{ + struct particle *label; + uint16_t interval; +}; + +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(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", "r"); + 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; + uint64_t mem_free = 0; + uint64_t mem_used = 0; + uint64_t mem_total = 0; + + if (!get_mem_stats(&mem_free, &mem_total)) { + LOG_ERR("unable to retrieve the memory stats"); + } + + 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); + 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; + + 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, "interval"); + const struct yml_node *c = yml_get_value(node, "content"); + + return mem_new(interval == NULL ? SMALLEST_INTERVAL : yml_value_as_int(interval), conf_to_particle(c, inherited)); +} + +static bool +conf_verify_interval(keychain_t *chain, const struct yml_node *node) +{ + if (!conf_verify_unsigned(chain, node)) + return false; + + if (yml_value_as_int(node) < SMALLEST_INTERVAL) { + LOG_ERR("%s: interval value cannot be less than %d ms", conf_err_prefix(chain, node), SMALLEST_INTERVAL); + return false; + } + + return true; +} + +static bool +verify_conf(keychain_t *chain, const struct yml_node *node) +{ + static const struct attr_info attrs[] = { + {"interval", false, &conf_verify_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 6b64958..192b094 100644 --- a/modules/meson.build +++ b/modules/meson.build @@ -17,6 +17,8 @@ mod_data = { 'backlight': [[], [m, udev]], 'battery': [[], [udev]], 'clock': [[], []], + 'cpu': [[], []], + 'mem': [[], []], 'i3': [['i3-common.c', 'i3-common.h'], [dynlist, json]], 'label': [[], []], 'network': [[], []], diff --git a/modules/mpd.c b/modules/mpd.c index bebd401..27992d3 100644 --- a/modules/mpd.c +++ b/modules/mpd.c @@ -223,7 +223,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); @@ -251,7 +251,13 @@ wait_for_socket_create(const struct module *mod) {.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; @@ -377,8 +383,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); @@ -396,7 +403,7 @@ run(struct module *mod) 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); @@ -414,16 +421,27 @@ 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]), 10 * 1000); - if (res == 1) { - assert(fds[0].revents & POLLIN); - aborted = true; + if (res < 0) { + if (errno == EINTR) + continue; + + LOG_ERRNO("failed to poll"); + ret = 1; + break; + } + + if (res == 1) { + assert(fds[0].revents & POLLIN); + aborted = true; + } } } - if (aborted) + if (aborted || ret != 0) break; /* Initial state (after establishing a connection) */ @@ -446,7 +464,14 @@ run(struct module *mod) 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; @@ -477,7 +502,7 @@ run(struct module *mod) m->conn = NULL; } - return 0; + return aborted ? 0 : ret; } struct refresh_context { @@ -616,7 +641,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/network.c b/modules/network.c index bf32c57..34980fa 100644 --- a/modules/network.c +++ b/modules/network.c @@ -167,7 +167,7 @@ nl_pid_value(void) static int 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; @@ -191,7 +191,7 @@ netlink_connect_rt(void) static int netlink_connect_genl(void) { - int sock = socket(AF_NETLINK, SOCK_RAW, NETLINK_GENERIC); + int sock = socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_GENERIC); if (sock == -1) { LOG_ERRNO("failed to create netlink socket"); return -1; @@ -310,6 +310,9 @@ send_nl80211_request(struct private *m, uint8_t cmd, uint16_t flags, uint32_t se if (m->ifindex < 0) return false; + if (m->nl80211.family_id == (uint16_t)-1) + return false; + const struct { struct nlmsghdr hdr; struct { @@ -692,6 +695,7 @@ handle_genl_ctrl(struct module *mod, uint16_t type, bool nested, case CTRL_ATTR_FAMILY_ID: { m->nl80211.family_id = *(const uint16_t *)payload; send_nl80211_get_interface(m); + send_nl80211_get_station(m); break; } @@ -1253,7 +1257,7 @@ 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_unsigned}, MODULE_COMMON_ATTRS, }; diff --git a/modules/removables.c b/modules/removables.c index 04d2695..d8fe7c9 100644 --- a/modules/removables.c +++ b/modules/removables.c @@ -5,6 +5,7 @@ #include #include #include +#include #include #include @@ -35,8 +36,8 @@ struct partition { char *label; uint64_t size; + bool audio_cd; - /*tll(char *) mount_points;*/ mount_point_list_t mount_points; }; @@ -141,13 +142,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); @@ -162,11 +164,17 @@ content(struct module *mod) 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); + int fd = open("/proc/self/mountinfo", O_RDONLY | O_CLOEXEC); + FILE *f = fd >= 0 ? fdopen(fd, "r") : NULL; + + if (fd < 0 || f == NULL) { + LOG_ERRNO("failed to open /proc/self/mountinfo"); + if (fd >= 0) + close(fd); + return; + } char line[4096]; - while (fgets(line, sizeof(line), f) != NULL) { char *dev = NULL, *path = NULL; @@ -277,6 +285,64 @@ add_partition(struct module *mod, struct block_device *block, .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); @@ -295,7 +361,9 @@ del_partition(struct module *mod, struct block_device *block, 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); @@ -350,8 +418,16 @@ 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 *_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 media = _fs_usage != NULL && strcmp(_fs_usage, "filesystem") == 0; + 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); @@ -373,8 +449,12 @@ add_device(struct module *mod, struct udev_device *dev) 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); } @@ -408,31 +488,53 @@ change_device(struct module *mod, struct udev_device *dev) const char *sys_path = udev_device_get_devpath(dev); mtx_lock(&mod->lock); + 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; } @@ -545,7 +647,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[] = { @@ -553,10 +657,18 @@ 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; @@ -587,7 +699,7 @@ run(struct module *mod) udev_monitor_unref(dev_mon); udev_unref(udev); - return 0; + return ret; } static struct module * @@ -652,9 +764,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/script.c b/modules/script.c index 2ddd722..f701e99 100644 --- a/modules/script.c +++ b/modules/script.c @@ -396,7 +396,7 @@ execute_script(struct module *mod) /* Stdout redirection pipe */ int comm_pipe[2]; - if (pipe(comm_pipe) < 0) { + if (pipe2(comm_pipe, O_CLOEXEC) < 0) { LOG_ERRNO("failed to create stdin/stdout redirection pipe"); close(exec_pipe[0]); close(exec_pipe[1]); @@ -444,7 +444,7 @@ execute_script(struct module *mod) close(comm_pipe[0]); /* Re-direct stdin/stdout */ - int dev_null = open("/dev/null", O_RDONLY); + int dev_null = open("/dev/null", O_RDONLY | O_CLOEXEC); if (dev_null < 0) goto fail; @@ -458,16 +458,6 @@ execute_script(struct module *mod) close(comm_pipe[1]); comm_pipe[1] = -1; - /* Close *all* other FDs */ - for (int i = STDERR_FILENO + 1; i < 65536; i++) { - if (i == exec_pipe[1]) { - /* Needed for error reporting. Automatically closed - * when execvp() succeeds */ - continue; - } - close(i); - } - execvp(m->path, argv); fail: @@ -709,7 +699,7 @@ 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_int}, + {"poll-interval", false, &conf_verify_unsigned}, MODULE_COMMON_ATTRS, }; diff --git a/modules/sway-xkb.c b/modules/sway-xkb.c index f4e7577..6632ab7 100644 --- a/modules/sway-xkb.c +++ b/modules/sway-xkb.c @@ -267,7 +267,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; @@ -374,9 +374,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, }; diff --git a/modules/xkb.c b/modules/xkb.c index 5c2c1f9..965664d 100644 --- a/modules/xkb.c +++ b/modules/xkb.c @@ -399,7 +399,14 @@ event_loop(struct module *mod, xcb_connection_t *conn, int xkb_event_base) }; /* 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; diff --git a/modules/xwindow.c b/modules/xwindow.c index 3925f4f..baf1239 100644 --- a/modules/xwindow.c +++ b/modules/xwindow.c @@ -2,6 +2,7 @@ #include #include #include +#include #include #include #include @@ -247,14 +248,24 @@ 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); + 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; + } for (xcb_generic_event_t *_e = xcb_wait_for_event(m->conn); _e != NULL; @@ -293,7 +304,7 @@ run(struct module *mod) xcb_destroy_window(m->conn, m->monitor_win); xcb_disconnect(m->conn); - return 0; + return ret; } static struct exposable * diff --git a/particle.c b/particle.c index 1b06f9a..5f6b04d 100644 --- a/particle.c +++ b/particle.c @@ -31,14 +31,15 @@ particle_default_destroy(struct particle *particle) struct particle * particle_common_new(int left_margin, int right_margin, const char **on_click_templates, - struct fcft_font *font, pixman_color_t foreground, - struct deco *deco) + 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->foreground = foreground; p->font = font; + p->font_shaping = font_shaping; p->deco = deco; if (on_click_templates != NULL) { @@ -276,11 +277,6 @@ exposable_default_on_mouse(struct exposable *exposable, struct bar *bar, goto fail; } - /* Close *all* other FDs (e.g. script modules' FDs) */ - for (int i = STDERR_FILENO + 1; i < 65536; i++) - if (i != pipe_fds[1]) - close(i); - execvp(argv[0], argv); fail: diff --git a/particle.h b/particle.h index 4ca520f..bdf01f2 100644 --- a/particle.h +++ b/particle.h @@ -5,6 +5,7 @@ #include "color.h" #include "decoration.h" +#include "font-shaping.h" #include "tag.h" enum mouse_event { @@ -35,6 +36,7 @@ struct particle { pixman_color_t foreground; struct fcft_font *font; + enum font_shaping font_shaping; struct deco *deco; void (*destroy)(struct particle *particle); @@ -61,7 +63,8 @@ struct exposable { struct particle *particle_common_new( int left_margin, int right_margin, const char *on_click_templates[], - struct fcft_font *font, pixman_color_t foreground, struct deco *deco); + struct fcft_font *font, enum font_shaping font_shaping, + pixman_color_t foreground, struct deco *deco); void particle_default_destroy(struct particle *particle); @@ -76,12 +79,13 @@ void exposable_default_on_mouse( 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_on_click}, \ - {"font", false, &conf_verify_font}, \ - {"foreground", false, &conf_verify_color}, \ - {"deco", false, &conf_verify_decoration}, \ +#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/list.c b/particles/list.c index 19ff15b..a2c37c6 100644 --- a/particles/list.c +++ b/particles/list.c @@ -197,7 +197,7 @@ from_conf(const struct yml_node *node, struct particle *common) yml_list_next(&it), idx++) { parts[idx] = conf_to_particle( - it.node, (struct conf_inherit){common->font, common->foreground}); + it.node, (struct conf_inherit){common->font, common->font_shaping, common->foreground}); } return particle_list_new(common, parts, count, left_spacing, right_spacing); @@ -208,9 +208,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 980bdd1..c748bea 100644 --- a/particles/map.c +++ b/particles/map.c @@ -1,5 +1,6 @@ #include #include +#include #include #define LOG_MODULE "map" @@ -10,13 +11,228 @@ #include "../plugin.h" #include "dynlist.h" +enum map_op{ + MAP_OP_EQ, + MAP_OP_NE, + MAP_OP_LE, + MAP_OP_LT, + MAP_OP_GE, + MAP_OP_GT, + /* The next two are for bool types */ + MAP_OP_SELF, + MAP_OP_NOT, +}; + +struct map_condition { + char *tag; + enum map_op op; + char *value; +}; + +static char * +trim(char *s) +{ + while (*s == ' ') + s++; + + char *end = s + strlen(s) - 1; + while (*end == ' ') { + *end = '\0'; + end--; + } + + return s; +} + +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; + default: return false; + } +} + +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; + default: return false; + } +} + +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; + default: return false; + } +} + +bool +eval_map_condition(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 if (map_cond->op == MAP_OP_NOT) + 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; +} + +struct map_condition* +map_condition_from_str(const char *str) +{ + struct map_condition *cond = malloc(sizeof(*cond)); + + /* This strdup is just to discard the 'const' qualifier */ + char *str_cpy = strdup(str); + char *op_str = strpbrk(str_cpy, "=!<>~"); + + /* we've already checked things in the verify functions, so these should all work */ + char *tag = str_cpy; + char *value = NULL; + + if (op_str == NULL) { + cond->tag = strdup(trim(tag)); + cond->op = MAP_OP_SELF; + cond->value = NULL; + free(str_cpy); + return cond; + } + + switch (op_str[0]) { + case '=': + cond->op = MAP_OP_EQ; + value = op_str + 2; + break; + case '!': + cond->op = MAP_OP_NE; + value = op_str + 2; + break; + case '<': + if (op_str[1] == '=') { + cond->op = MAP_OP_LE; + value = op_str + 2; + } else { + cond->op = MAP_OP_LT; + value = op_str + 1; + } + break; + case '>': + if (op_str[1] == '=') { + cond->op = MAP_OP_GE; + value = op_str + 2; + } else { + cond->op = MAP_OP_GT; + value = op_str + 1; + } + break; + case '~': + tag = op_str + 1; + cond->op = MAP_OP_NOT; + break; + } + + /* NULL terminate the tag value */ + op_str[0] = '\0'; + + cond->tag = strdup(trim(tag)); + + cond->value = NULL; + if (value != NULL) { + value = trim(value); + const size_t value_len = strlen(value); + if (value[0] == '"' && value[value_len - 1] == '"') { + value[value_len - 1] = '\0'; + ++value; + } + cond->value = strdup(value); + } + + free(str_cpy); + return cond; +} + +void +free_map_condition(struct map_condition* mc) +{ + free(mc->tag); + free(mc->value); + free(mc); +} + struct particle_map { - const char *tag_value; + struct map_condition *condition; struct particle *particle; }; struct private { - char *tag; struct particle *default_particle; struct particle_map *map; size_t count; @@ -92,21 +308,12 @@ 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); - - if (tag == NULL) { - return p->default_particle != NULL - ? p->default_particle->instantiate(p->default_particle, tags) - : dynlist_exposable_new(NULL, 0, 0, 0); - } - - 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; @@ -144,28 +351,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, - struct particle *default_particle) +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; } @@ -176,7 +380,103 @@ map_new(struct particle *common, const char *tag, } static bool -verify_map_values(keychain_t *chain, const struct yml_node *node) +verify_map_condition_syntax(keychain_t *chain, const struct yml_node *node, + const char *line) +{ + /* We need this strdup to discard the 'const' qualifier */ + char *cond = strdup(line); + char *op_str = strpbrk(cond, " =!<>~"); + if (op_str == NULL) { + char *tag = trim(cond); + if (tag[0] == '\0') + goto syntax_fail_missing_tag; + + free(cond); + return true; + } + op_str = trim(op_str); + + char *tag = cond; + char *value = NULL; + + switch (op_str[0]) { + case '=': + if (op_str[1] != '=') + goto syntax_fail_invalid_op; + + value = op_str + 2; + break; + + case '!': + if (op_str[1] != '=') + goto syntax_fail_invalid_op; + + value = op_str + 2; + break; + + case '<': + if (op_str[1] == '=') + value = op_str + 2; + else + value = op_str + 1; + break; + + case '>': + if (op_str[1] == '=') + value = op_str + 2; + else + value = op_str + 1; + break; + + case '~': + tag = op_str + 1; + if (strpbrk(tag, " =!<>~") != NULL) + goto syntax_fail_bad_not; + value = NULL; + break; + default: + goto syntax_fail_invalid_op; + } + + /* NULL terminate the tag value */ + op_str[0] = '\0'; + + tag = trim(tag); + if (tag[0] == '\0') + goto syntax_fail_missing_tag; + + value = value != NULL ? trim(value) : NULL; + + if (value != NULL && value[0] == '\0') + goto syntax_fail_missing_value; + + free(cond); + return true; + +syntax_fail_invalid_op: + LOG_ERR("%s: \"%s\" invalid operator", conf_err_prefix(chain, node), line); + goto err; + +syntax_fail_missing_tag: + LOG_ERR("%s: \"%s\" missing tag", conf_err_prefix(chain, node), line); + goto err; + +syntax_fail_missing_value: + LOG_ERR("%s: \"%s\" missing value", conf_err_prefix(chain, node), line); + goto err; + +syntax_fail_bad_not: + LOG_ERR("%s: \"%s\" '~' cannot be used with other operators", + conf_err_prefix(chain, node), line); + goto err; + +err: + free(cond); + return false; +} + +static bool +verify_map_conditions(keychain_t *chain, const struct yml_node *node) { if (!yml_is_dict(node)) { LOG_ERR( @@ -195,6 +495,9 @@ verify_map_values(keychain_t *chain, const struct yml_node *node) return false; } + if (!verify_map_condition_syntax(chain, it.key, key)) + return false; + if (!conf_verify_particle(chain_push(chain, key), it.value)) return false; @@ -207,40 +510,37 @@ verify_map_values(keychain_t *chain, const struct yml_node *node) 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, + .font_shaping = common->font_shaping, .foreground = common->foreground }; size_t idx = 0; - for (struct yml_dict_iter it = yml_dict_iter(values); + for (struct yml_dict_iter it = yml_dict_iter(conditions); it.key != NULL; yml_dict_next(&it), idx++) { - particle_map[idx].tag_value = yml_value_as_string(it.key); + particle_map[idx].condition = map_condition_from_str(yml_value_as_string(it.key)); particle_map[idx].particle = conf_to_particle(it.value, inherited); } 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/progress-bar.c b/particles/progress-bar.c index ebc51c3..f9e3999 100644 --- a/particles/progress-bar.c +++ b/particles/progress-bar.c @@ -304,6 +304,7 @@ 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, }; @@ -323,7 +324,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 7815d99..0127519 100644 --- a/particles/ramp.c +++ b/particles/ramp.c @@ -209,7 +209,7 @@ from_conf(const struct yml_node *node, struct particle *common) yml_list_next(&it), idx++) { parts[idx] = conf_to_particle( - it.node, (struct conf_inherit){common->font, common->foreground}); + it.node, (struct conf_inherit){common->font, common->font_shaping, common->foreground}); } long min_v = min != NULL ? yml_value_as_int(min) : 0; diff --git a/particles/string.c b/particles/string.c index c475d5b..f1e1faf 100644 --- a/particles/string.c +++ b/particles/string.c @@ -5,6 +5,7 @@ #define LOG_MODULE "string" #define LOG_ENABLE_DBG 0 #include "../log.h" +#include "../char32.h" #include "../config.h" #include "../config-verify.h" #include "../particle.h" @@ -87,7 +88,7 @@ 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. * @@ -149,7 +150,7 @@ instantiate(const struct particle *particle, const struct tag_set *tags) struct eprivate *e = calloc(1, sizeof(*e)); struct fcft_font *font = particle->font; - wchar_t *wtext = NULL; + char32_t *wtext = NULL; char *text = tags_expand_template(p->text, tags); e->glyphs = e->allocated_glyphs = NULL; @@ -173,17 +174,13 @@ instantiate(const struct particle *particle, const struct tag_set *tags) } } - /* Not in cache - we need to rasterize it. First, convert to wchar */ - size_t chars = mbstowcs(NULL, text, 0); - if (chars == (size_t)-1) - goto done; - - wtext = malloc((chars + 1) * sizeof(wtext[0])); - mbstowcs(wtext, text, chars + 1); + /* Not in cache - we need to rasterize it. First, convert to char32_t */ + wtext = ambstoc32(text); + size_t chars = c32len(wtext); /* Truncate, if necessary */ if (p->max_len > 0) { - const size_t len = wcslen(wtext); + const size_t len = c32len(wtext); if (len > p->max_len) { size_t end = p->max_len; @@ -193,11 +190,11 @@ instantiate(const struct particle *particle, const struct tag_set *tags) } if (p->max_len > 1) { - wtext[end] = L'…'; - wtext[end + 1] = L'\0'; + wtext[end] = U'…'; + wtext[end + 1] = U'\0'; chars = end + 1; } else { - wtext[end] = L'\0'; + wtext[end] = U'\0'; chars = 0; } } @@ -205,8 +202,10 @@ instantiate(const struct particle *particle, const struct tag_set *tags) e->kern_x = calloc(chars, sizeof(e->kern_x[0])); - if (fcft_capabilities() & FCFT_CAPABILITY_TEXT_RUN_SHAPING) { - struct fcft_text_run *run = fcft_text_run_rasterize( + 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) { @@ -250,7 +249,7 @@ instantiate(const struct particle *particle, const struct tag_set *tags) /* Convert text to glyph masks/images. */ for (size_t i = 0; i < chars; i++) { - const struct fcft_glyph *glyph = fcft_glyph_rasterize( + const struct fcft_glyph *glyph = fcft_rasterize_char_utf32( font, wtext[i], FCFT_SUBPIXEL_NONE); if (glyph == NULL) @@ -323,7 +322,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 00b0061..43e8672 100644 --- a/plugin.c +++ b/plugin.c @@ -49,6 +49,8 @@ EXTERN_MODULE(sway_xkb); EXTERN_MODULE(script); EXTERN_MODULE(xkb); EXTERN_MODULE(xwindow); +EXTERN_MODULE(cpu); +EXTERN_MODULE(mem); EXTERN_PARTICLE(empty); EXTERN_PARTICLE(list); @@ -58,8 +60,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 @@ -135,6 +139,8 @@ init(void) #if defined(HAVE_PLUGIN_xwindow) REGISTER_CORE_MODULE(xwindow, xwindow); #endif + REGISTER_CORE_MODULE(mem, mem); + REGISTER_CORE_MODULE(cpu, cpu); REGISTER_CORE_PARTICLE(empty, empty); REGISTER_CORE_PARTICLE(list, list); @@ -144,8 +150,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 diff --git a/tag.c b/tag.c index 2943423..5c5684b 100644 --- a/tag.c +++ b/tag.c @@ -32,6 +32,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) { @@ -264,6 +288,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 +312,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 +336,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 +360,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; @@ -545,12 +573,12 @@ tags_expand_template(const char *template, const struct tag_set *tags) case FMT_MIBYTE: case FMT_GIBYTE: { const long divider = - format == FMT_KBYTE ? 1024 : - format == FMT_MBYTE ? 1024 * 1024 : - format == FMT_GBYTE ? 1024 * 1024 * 1024 : - format == FMT_KIBYTE ? 1000 : - format == FMT_MIBYTE ? 1000 * 1000 : - format == FMT_GIBYTE ? 1000 * 1000 * 1000 : + format == FMT_KBYTE ? 1000 : + format == FMT_MBYTE ? 1000 * 1000 : + format == FMT_GBYTE ? 1000 * 1000 * 1000 : + format == FMT_KIBYTE ? 1024 : + format == FMT_MIBYTE ? 1024 * 1024 : + format == FMT_GIBYTE ? 1024 * 1024 * 1024 : 1; char str[24]; diff --git a/tag.h b/tag.h index d6bfe6a..1e9742a 100644 --- a/tag.h +++ b/tag.h @@ -3,6 +3,13 @@ #include #include +enum tag_type { + TAG_TYPE_BOOL, + TAG_TYPE_INT, + TAG_TYPE_FLOAT, + TAG_TYPE_STRING, +}; + enum tag_realtime_unit { TAG_REALTIME_NONE, TAG_REALTIME_SECS, @@ -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); diff --git a/test/full-conf-good.yml b/test/full-conf-good.yml index 6270487..d89d494 100644 --- a/test/full-conf-good.yml +++ b/test/full-conf-good.yml @@ -62,10 +62,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: