pax_global_header00006660000000000000000000000064144751233600014517gustar00rootroot0000000000000052 comment=4ba25d6d48ade53c94e921d47b6ccd467f4d73dd fnott-1.4.1+ds/000077500000000000000000000000001447512336000132565ustar00rootroot00000000000000fnott-1.4.1+ds/.builds/000077500000000000000000000000001447512336000146165ustar00rootroot00000000000000fnott-1.4.1+ds/.builds/alpine-x64.yml000066400000000000000000000016751447512336000172410ustar00rootroot00000000000000image: alpine/latest packages: - musl-dev - linux-headers - meson - ninja - gcc - scdoc - pixman-dev - freetype-dev - fontconfig-dev - libpng-dev - dbus-dev - wayland-dev - wayland-protocols - wlroots-dev - python3 - py3-pip sources: - https://git.sr.ht/~dnkl/fnott # triggers: # - action: email # condition: failure # to: tasks: - fcft: | cd fnott/subprojects git clone https://codeberg.org/dnkl/fcft.git cd ../.. - debug: | mkdir -p bld/debug meson --buildtype=debug fnott bld/debug ninja -C bld/debug -k0 meson test -C bld/debug --print-errorlogs - release: | mkdir -p bld/release meson --buildtype=release fnott bld/release ninja -C bld/release -k0 meson test -C bld/release --print-errorlogs - codespell: | pip install codespell cd fnott ~/.local/bin/codespell README.md CHANGELOG.md *.c *.h doc/*.scd fnott-1.4.1+ds/.builds/freebsd-x64.yml000066400000000000000000000012541447512336000173740ustar00rootroot00000000000000image: freebsd/latest packages: - meson - ninja - scdoc - pkgconf - pixman - freetype2 - fontconfig - png - dbus - wayland - wayland-protocols - evdev-proto - wlroots sources: - https://git.sr.ht/~dnkl/fnott tasks: - fcft: | cd fnott/subprojects git clone https://codeberg.org/dnkl/fcft.git cd ../.. - debug: | mkdir -p bld/debug meson --buildtype=debug fnott bld/debug ninja -C bld/debug -k0 meson test -C bld/debug --print-errorlogs - release: | mkdir -p bld/release meson --buildtype=release fnott bld/release ninja -C bld/release -k0 meson test -C bld/release --print-errorlogs fnott-1.4.1+ds/.gitignore000066400000000000000000000001151447512336000152430ustar00rootroot00000000000000/bld/ /pkg/ /src/ /subprojects/* !/subprojects/*.wrap /compile_commands.json fnott-1.4.1+ds/.gitmodules000066400000000000000000000002251447512336000154320ustar00rootroot00000000000000[submodule "external/wlr-protocols"] path = external/wlr-protocols url = https://gitlab.freedesktop.org/wlroots/wlr-protocols.git branch = master fnott-1.4.1+ds/.woodpecker.yml000066400000000000000000000046411447512336000162260ustar00rootroot00000000000000pipeline: codespell: when: branch: - master - releases/* image: alpine:latest commands: - apk add python3 - apk add py3-pip - pip install codespell - codespell README.md CHANGELOG.md *.c *.h doc/*.scd subprojects: when: branch: - master - releases/* image: alpine:latest commands: - apk add git - mkdir -p subprojects && cd subprojects - git clone https://codeberg.org/dnkl/tllist.git - git clone https://codeberg.org/dnkl/fcft.git - cd .. x64: when: branch: - master - releases/* group: build image: alpine:latest commands: - apk update - apk add musl-dev linux-headers meson ninja gcc clang scdoc - apk add pixman-dev freetype-dev fontconfig-dev libpng-dev dbus-dev - apk add wayland-dev wayland-protocols wlroots-dev - apk add git # Debug - mkdir -p bld/debug-x64 - meson --buildtype=debug . bld/debug-x64 - ninja -C bld/debug-x64 -v -k0 - bld/debug-x64/fnott --version # Release (gcc) - mkdir -p bld/release-x64 - meson --buildtype=release . bld/release-x64 - ninja -C bld/release-x64 -v -k0 - bld/release-x64/fnott --version # Release (clang) - mkdir -p bld/release-x64-clang - CC=clang meson --buildtype=release . bld/release-x64-clang - ninja -C bld/release-x64-clang -v -k0 - bld/release-x64-clang/fnott --version x86: when: branch: - master - releases/* group: build image: i386/alpine:latest commands: - apk update - apk add musl-dev linux-headers meson ninja gcc clang scdoc - apk add pixman-dev freetype-dev fontconfig-dev libpng-dev dbus-dev - apk add wayland-dev wayland-protocols wlroots-dev - apk add git # Debug - mkdir -p bld/debug-x86 - meson --buildtype=debug . bld/debug-x86 - ninja -C bld/debug-x86 -v -k0 - bld/debug-x86/fnott --version # Release (gcc) - mkdir -p bld/release-x86 - meson --buildtype=release . bld/release-x86 - ninja -C bld/release-x86 -v -k0 - bld/release-x86/fnott --version # Release (clang) - mkdir -p bld/release-x86-clang - CC=clang meson --buildtype=release . bld/release-x86-clang - ninja -C bld/release-x86-clang -v -k0 - bld/release-x86-clang/fnott --version fnott-1.4.1+ds/CHANGELOG.md000066400000000000000000000205311447512336000150700ustar00rootroot00000000000000# Changelog * [1.4.1](#1-4-1) * [1.4.0](#1-4-0) * [1.3.0](#1-3-0) * [1.2.1](#1-2-1) * [1.2.0](#1-2-0) * [1.1.2](#1-1-2) * [1.1.1](#1-1-1) * [1.1.0](#1-1-0) * [1.0.1](#1-0-1) * [1.0.0](#1-0-0) ## 1.4.1 ### Fixed * Compilation errors with clang 15.x ([#96][96]) * Notifications initially positioned outside the screen not being visible after being moved up in the notification stack. [96]: https://codeberg.org/dnkl/fnott/issues/96 ## 1.4.0 ### Added * `idle-timeout` option to specify the amount of time you need to be idle before notifications are prevented from timing out ([#16][16]). * `icon` option, to specify icon to use when none is provided by the notification itself ([#82][82]). * Support for `image-path` hints ([#84][84]). * `dpi-aware=no|yes|auto` option ([#80][80]). [16]: https://codeberg.org/dnkl/fnott/issues/16 [82]: https://codeberg.org/dnkl/fnott/issues/82 [84]: https://codeberg.org/dnkl/fnott/issues/84 [80]: https://codeberg.org/dnkl/fnott/issues/80 ### Changed * Default value of `max-width` and `max-height` is now `0` (unlimited). * When determining initial font size, do FontConfig config substitution if the user-provided font pattern has no {pixel}size option ([#1287][foot-1287]). [foot-1287]: https://codeberg.org/dnkl/foot/issues/1287 ### Fixed * file:// URIs, in icon paths ([#84][84]) * _Replace ID_ being ignored if there were no prior notification with that ID. * Wayland protocol violation when output scaling was enabled. * Notification expiration (timeout) and dismissal are now deferred while the action selection helper is running ([#90][90]). [84]: https://codeberg.org/dnkl/fnott/issues/84 [90]: https://codeberg.org/dnkl/fnott/issues/90 ### Contributors * Leonardo Hernández Hernández ## 1.3.0 ### Added * Support for a “progress” hints, `notify-send -h int:value:20 ...`, ([#51][51]). * `title-format`, `summary-format` and `body-format` options, allowing you to customize the rendered strings. In this release, the `%a`, `%s`, `%b` and `%%` formatters, as well as `\n`, are recognized. ([#39][39]). * Added configuration option `layer` to specify the layer on which notifications are displayed. Values include `background`, `top`, `bottom`, and `overlay` ([#71][71]). [51]: https://codeberg.org/dnkl/fnott/issues/51 [39]: https://codeberg.org/dnkl/fnott/issues/39 [71]: https://codeberg.org/dnkl/fnott/issues/71 ### Changed * Minimum required meson version is now 0.58. * Notification text is now truncated instead of running into, and past, the vertical padding ([#52][52]). * All color configuration options have been changed from (A)RGB (i.e. ARGB, where the alpha component is optional), to RGBA. This means **all** color values **must** be specified with 8 digits ([#47][47]). [52]: https://codeberg.org/dnkl/fnott/issues/52 [47]: https://codeberg.org/dnkl/fnott/issues/47 ### Removed * `$XDG_CONFIG_HOME/fnottrc` and `~/.config/fnottrc`. Use `$XDG_CONFIG_HOME/fnott/fnott.ini` (defaulting to `~/.config/fnott/fnott.ini`) instead ([#7][7]). [7]: https://codeberg.org/dnkl/fnott/issues/7 ### Fixed * Scale not being applied to the notification’s size when first instantiated ([#54][54]). * Fallback to `/etc/xdg` if `XDG_CONFIG_DIRS` is unset. * Icon lookup is now better at following the XDG specification ([#64][64]). * Setting `max-width` and/or `max-height` to 0 no longer causes fnott to crash. Instead, a zero max-width/height means there is no limit ([#66][66]). [54]: https://codeberg.org/dnkl/fnott/issues/54 [64]: https://codeberg.org/dnkl/fnott/issues/64 [66]: https://codeberg.org/dnkl/fnott/issues/66 ### Contributors * bagnaram * Humm * Leonardo Hernández Hernández * Mark Stosberg * merkix ## 1.2.1 ### Fixed * Crash when receiving notification with inline image data ([#44][44]). [44]: https://codeberg.org/dnkl/fnott/issues/44 ## 1.2.0 ### Added * Configurable padding of notification text. New `fnottrc` options: `padding-vertical` and `padding-horizontal` ([#35][35]). [35]: https://codeberg.org/dnkl/fnott/issues/35 ### Changed * Default padding is now fixed at 20, instead of depending on the font size. This is due to the new `padding-horizontal|vertical` options. ### Fixed * `fnottctl actions` exiting without receiving a reply. * Fnott is now much better at surviving monitors being disabled and re-enabled ([#25][25]). * Wrong font being used when the body and summary (or title and body, or title and summary) is set to the same text ([#36][36]). * Fnott no longer allocates the vertical padding space between summary and body text, if the body text is empty ([#41][41]). [25]: https://codeberg.org/dnkl/fnott/issues/25 [36]: https://codeberg.org/dnkl/fnott/issues/36 [41]: https://codeberg.org/dnkl/fnott/issues/41 ### Contributors * fauxmight * Rishabh Das ## 1.1.2 ### Fixed * `max-timeout` not having any effect when the timeout is 0 ([#32][32]). [32]: https://codeberg.org/dnkl/fnott/issues/32 ## 1.1.1 ### Added * `default-timeout` option, to adjust the timeout when applications ask us to pick the timeout ([#27][27]). * `max-timeout` option ([#29][29]). [27]: https://codeberg.org/dnkl/fnott/issues/27 [29]: https://codeberg.org/dnkl/fnott/issues/29 ### Changed * Updated nanosvg to ccdb1995134d340a93fb20e3a3d323ccb3838dd0 (20210903). ### Removed * `timeout` option (replaced with `max-timeout`, [#29][29]). ### Fixed * Icons not being searched for in all icon theme instances ([#17][17]). * fnott crashing when a notification was received while no monitor was attached to the wayland session. * Wrong colors in (semi-)transparent areas of SVG icons. [17]: https://codeberg.org/dnkl/fnott/issues/17 ### Contributors * Julian Scheel * polykernel * Stanislav Ochotnický ## 1.1.0 ### Added * Configurable minimal width of notifications. New `fnottrc` option: `min-width` * Configurable anchor point and margins. New `fnottrc` options: `anchor=top-left|top-right|bottom-left|bottom-right`, `edge-margin-vertical`, `edge-margin-horizontal` and `notification-margin` ([#4][4]). * `-c,--config=PATH` command line option ([#10][10]). * Text shaping support ([#13][13]). * `play-sound` to `fnott.ini`, specifying the command to execute to play a sound ([#12][12]). * `sound-file`, a per-urgency option in `fnott.ini`, specifying the path to an audio file to play when a notification is received ([#12][12]). [4]: https://codeberg.org/dnkl/fnott/issues/4 [10]: https://codeberg.org/dnkl/fnott/issues/10 [13]: https://codeberg.org/dnkl/fnott/issues/13 [12]: https://codeberg.org/dnkl/fnott/issues/12 ### Changed * Fnott now searches for its configuration in `$XDG_DATA_DIRS/fnott/fnott.ini`, if no configuration is found in `$XDG_CONFIG_HOME/fnott/fnott.ini` or in `$XDG_CONFIG_HOME/fnottrc` ([#7][7]). * Assume a DPI of 96 if the monitor’s DPI is 0 (seen on certain emulated displays). * There is now an empty line between the ‘summary’ and ‘body’. [7]: https://codeberg.org/dnkl/fnott/issues/7 ### Deprecated * `$XDG_CONFIG_HOME/fnottrc` and `~/.config/fnottrc`. Use `$XDG_CONFIG_HOME/fnott/fnott.ini` (defaulting to `~/.config/fnott/fnott.ini`) instead ([#7][7]). [7]: https://codeberg.org/dnkl/fnott/issues/7 ### Removed * `margin` option from `fnottrc` ### Fixed * Notification sometimes not being rendered with the correct subpixel mode, until updated. ### Contributors - yyp (Alexey Yerin) - Julian Scheel ## 1.0.1 ### Added * `timeout` option to `fnottrc`. This option can be set on a per-urgency basis. If both the user has set a timeout, and the notification provides its own timeout, the shortest one is used ([#2][2]). * FreeBSD port ([#1][1]). [1]: https://codeberg.org/dnkl/fnott/issues/1 ### Fixed * PPI being incorrectly calculated. * Crash due to bug in Sway-1.5 when a notification is dismissed, either with `fnottctl` or through its timeout, while the cursor is above it. ### Contributors * jbeich ## 1.0.0 Initial release - no changelog. Rough list of features: * Application title, summary and body fonts can be configured individually * Icon support, both inline and name referenced (PNG + SVG). * Actions (requires a dmenu-like utility to display and let user select action - e.g. [fuzzel](https://codeberg.org/dnkl/fuzzel)) * Urgency (custom colors and fonts for different urgency levels) * Markup (**bold**, _italic_ and underline) * Timeout (notification is automatically dismissed) fnott-1.4.1+ds/LICENSE000066400000000000000000000020561447512336000142660ustar00rootroot00000000000000MIT License Copyright (c) 2019 Daniel Eklöf Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. fnott-1.4.1+ds/PKGBUILD000066400000000000000000000012251447512336000144020ustar00rootroot00000000000000pkgname=fnott pkgver=1.4.1 pkgrel=1 pkgdesc="Lightweight notification daemon for Wayland" arch=('x86_64' 'aarch64') url=https://codeberg.org/dnkl/fnott license=(mit) makedepends=('meson' 'ninja' 'scdoc' 'tllist>=1.0.1') depends=( 'wayland' 'pixman' 'libpng' 'dbus' 'fcft>=3.0.0' 'fcft<4.0.0') source=() changelog=CHANGELOG.md pkgver() { cd ../.git &> /dev/null && git describe --tags --long | sed 's/^v//;s/\([^-]*-g\)/r\1/;s/-/./g' || head -3 ../meson.build | grep version | cut -d "'" -f 2 } build() { meson --prefix=/usr --buildtype=release --wrap-mode=nofallback -Db_lto=true .. ninja } package() { DESTDIR="${pkgdir}/" ninja install } fnott-1.4.1+ds/README.md000066400000000000000000000064551447512336000145470ustar00rootroot00000000000000[![CI status](https://ci.codeberg.org/api/badges/dnkl/fnott/status.svg)](https://ci.codeberg.org/dnkl/fnott) # Fnott Fnott is a keyboard driven and lightweight notification daemon for wlroots-based Wayland compositors. It implements (parts of) the [Desktop Notifications Specification](https://specifications.freedesktop.org/notification-spec/latest/). [![Packaging status](https://repology.org/badge/vertical-allrepos/fnott.svg)](https://repology.org/project/fnott/versions) Supports styling and progress hints: ![screenshot](screenshot.png "Hello World screenshot") Notifications are automatically sized (with the possibility of limiting their max width and height): ![screenshot-2](screenshot-2.png "Screenshot of multiple notifications") ## Supported features * Summary * Body * Actions (requires a dmenu-like utility to display and let user select action) * Urgency * Icons - PNGs (using libpng) - SVGs (using bundled [nanosvg](https://github.com/memononen/nanosvg)) * Markup * Timeout More documentation is available in the installed man pages: * [man fnott](./doc/fnott.1.scd) documents the server * [man fnottctl](./doc/fnottctl.1.scd) documents the client * [man fnott.ini](./doc/fnott.ini.5.scd) documents the configuration ## Requirements ### Running * fontconfig * freetype * pixman * libpng * wayland (_client_ and _cursor_ libraries) * wlroots\* * dbus * [fcft](https://codeberg.org/dnkl/fcft), _unless_ built as a subproject \* Fnott must be run in a Wayland compositor that implements the wlroots protocols. ### Building In addition to the dev variant of the packages above, you need: * meson * ninja * scdoc * wayland-protocols * [tllist](https://codeberg.org/dnkl/tllist), _unless_ built as a subproject ## Usage Copy the example `fnott.ini` to `${HOME}/.config/fnott/fnott.ini` and edit to your liking. Start the daemon by running `fnott`. Note that it does **not** daemonize or background itself. Test it with e.g. `notify-send "this is the summary" "this is the body"`. Use `fnottctl dismiss` to dismiss the highest priority notification (usually the oldest), `fnottctl dismiss all` to dismiss **all** notifications, or `fnottctl dismiss ` to dismiss a specific notification (use `fnottctl list` to list currently active notifications). Additionally if you compositor implements either the KDE idle protocol, or the newer idle-notify protocol, fnott will not dismiss any notification if you are idle by the amount of time configured in `fnott.ini` You can also click on a notification to dismiss it. Note: you probably want to bind at least `fnottctl dismiss` to a keyboard shortcut in your Wayland compositor configuration. ## Installation To build, first, create a build directory, and switch to it: ```sh mkdir -p bld/release && cd bld/release ``` Second, configure the build (if you intend to install it globally, you might also want `--prefix=/usr`): ```sh meson --buildtype=release ../.. ``` Three, build it: ```sh ninja ``` You can now run it directly from the build directory: ```sh ./fnott ``` Test that it works: ```sh notify-send -a "MyApplicationName" "This Is The Summary" "hello world" ``` Optionally, install it: ```sh ninja install ``` ## License Fnott is released under the [MIT license](LICENSE). Fnott uses nanosvg, released under the [Zlib license](3rd-party/nanosvg/LICENSE.txt). fnott-1.4.1+ds/char32.c000066400000000000000000000070551447512336000145130ustar00rootroot00000000000000#include "char32.h" #include #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); } int c32ncasecmp(const char32_t *s1, const char32_t *s2, size_t n) { return wcsncasecmp((const wchar_t *)s1, (const wchar_t *)s2, n); } char32_t * c32dup(const char32_t *s) { return (char32_t *)wcsdup((const wchar_t *)s); } size_t mbsntoc32(char32_t *dst, const char *src, size_t nms, size_t len) { mbstate_t ps = {0}; char32_t *out = dst; const char *in = src; size_t consumed = 0; size_t chars = 0; size_t rc; while ((out == NULL || chars < len) && consumed < nms && (rc = mbrtoc32(out, in, nms - consumed, &ps)) != 0) { switch (rc) { case 0: goto done; case (size_t)-1: case (size_t)-2: case (size_t)-3: goto err; } in += rc; consumed += rc; chars++; if (out != NULL) out++; } done: return chars; err: return (char32_t)-1; } 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; } char * ac32tombs(const char32_t *src) { if (src == NULL) return NULL; const size_t src_len = c32len(src); size_t allocated = src_len + 1; char *ret = malloc(allocated); if (ret == NULL) return NULL; mbstate_t ps = {0}; char *out = ret; const char32_t *const end = src + src_len + 1; size_t bytes = 0; char mb[MB_CUR_MAX]; for (const char32_t *in = src; in < end; in++) { size_t rc = c32rtomb(mb, *in, &ps); switch (rc) { case (size_t)-1: goto err; } if (bytes + rc > allocated) { allocated *= 2; ret = realloc(ret, allocated); out = &ret[bytes]; } for (size_t i = 0; i < rc; i++, out++) *out = mb[i]; bytes += rc; } assert(ret[bytes - 1] == '\0'); ret = realloc(ret, bytes); return ret; err: free(ret); return NULL; } bool isc32space(char32_t c32) { return iswspace((wint_t)c32); } fnott-1.4.1+ds/char32.h000066400000000000000000000006401447512336000145110ustar00rootroot00000000000000#pragma once #include #include #include #include size_t c32len(const char32_t *s); char32_t *c32dup(const char32_t *s); int c32ncasecmp(const char32_t *s1, const char32_t *s2, size_t n); size_t mbsntoc32(char32_t *dst, const char *src, size_t nms, size_t len); char32_t *ambstoc32(const char *src); char *ac32tombs(const char32_t *src); bool isc32space(char32_t c32); fnott-1.4.1+ds/completions/000077500000000000000000000000001447512336000156125ustar00rootroot00000000000000fnott-1.4.1+ds/completions/meson.build000066400000000000000000000002771447512336000177620ustar00rootroot00000000000000zsh_install_dir = join_paths(get_option('datadir'), 'zsh/site-functions') install_data('zsh/_fnott', install_dir: zsh_install_dir) install_data('zsh/_fnottctl', install_dir: zsh_install_dir) fnott-1.4.1+ds/completions/zsh/000077500000000000000000000000001447512336000164165ustar00rootroot00000000000000fnott-1.4.1+ds/completions/zsh/_fnott000066400000000000000000000011321447512336000176270ustar00rootroot00000000000000#compdef fnott _arguments \ -s \ '(-c --config)'{-c,--config}'[path to configuration file (XDG_CONFIG_HOME/fnott/fnott.ini)]:config:_files' \ '(-p --print-pid)'{-p,--print-pid}'[print PID to this file or FD when up and running]:pidfile:_files' \ '(-l --log-colorize)'{-l,--log-colorize}'[enable or disable colorization of log output on stderr]:logcolor:(never always auto)' \ '(-s --log-no-syslog)'{-s,--log-no-syslog}'[disable syslog logging]' \ '(-v --version)'{-v,--version}'[show the version number and quit]' \ '(-h --help)'{-h,--help}'[show help message and quit]' \ fnott-1.4.1+ds/completions/zsh/_fnottctl000066400000000000000000000017641447512336000203450ustar00rootroot00000000000000#compdef fnottctl _arguments \ -C \ '1:cmd:->cmds' \ ':id:->id' case ${state} in (cmds) local -a commands commands=('dismiss:dismiss a notification' 'actions:show and apply actions for a notification' 'list:list IDs of all active notifications' 'quit:stop the fnott daemon') _arguments \ -s \ '(-v --version)'{-v,--version}'[show the version number and quit]' \ '(-h --help)'{-h,--help}'[show help message and quit]' _describe 'command' commands ret=0 ;; (id) case ${line[1]} in (actions|dismiss) local -a ids ids=("${(f)$(fnottctl list)}") if [[ "${line[1]}" == "dismiss" ]]; then ids+=('all:all notifications') fi _describe 'notification' ids ret=0 ;; esac ;; esac return 1 fnott-1.4.1+ds/config.c000066400000000000000000001062201447512336000146700ustar00rootroot00000000000000#include "config.h" #include #include #include #include #include #include #include #include #include #include #include #define LOG_MODULE "config" #define LOG_ENABLE_DBG 0 #include "log.h" #include "tokenize.h" struct config_file { char *path; int fd; }; static const char * get_user_home_dir(void) { const struct passwd *passwd = getpwuid(getuid()); if (passwd == NULL) return NULL; return passwd->pw_dir; } static struct config_file open_config(void) { char *path = NULL; struct config_file ret = {.path = NULL, .fd = -1}; const char *xdg_config_home = getenv("XDG_CONFIG_HOME"); const char *xdg_config_dirs = getenv("XDG_CONFIG_DIRS"); const char *home_dir = get_user_home_dir(); char *xdg_config_dirs_copy = NULL; /* First, check XDG_CONFIG_HOME (or .config, if unset) */ if (xdg_config_home != NULL && xdg_config_home[0] != '\0') { if (asprintf(&path, "%s/fnott/fnott.ini", xdg_config_home) < 0) { LOG_ERRNO("failed to build fnott.ini path"); goto done; } } else if (home_dir != NULL) { if (asprintf(&path, "%s/.config/fnott/fnott.ini", home_dir) < 0) { LOG_ERRNO("failed to build fnott.ini path"); goto done; } } if (path != NULL) { LOG_DBG("checking for %s", path); int fd = open(path, O_RDONLY | O_CLOEXEC); if (fd >= 0) { ret = (struct config_file) {.path = path, .fd = fd}; path = NULL; goto done; } } xdg_config_dirs_copy = xdg_config_dirs != NULL && xdg_config_dirs[0] != '\0' ? strdup(xdg_config_dirs) : strdup("/etc/xdg"); if (xdg_config_dirs_copy == NULL || xdg_config_dirs_copy[0] == '\0') goto done; for (const char *conf_dir = strtok(xdg_config_dirs_copy, ":"); conf_dir != NULL; conf_dir = strtok(NULL, ":")) { free(path); path = NULL; if (asprintf(&path, "%s/fnott/fnott.ini", conf_dir) < 0) { LOG_ERRNO("failed to build fnott.ini path"); goto done; } LOG_DBG("checking for %s", path); int fd = open(path, O_RDONLY | O_CLOEXEC); if (fd >= 0) { ret = (struct config_file){.path = path, .fd = fd}; path = NULL; goto done; } } done: free(xdg_config_dirs_copy); free(path); return ret; } static bool str_to_bool(const char *s, bool *res) { static const char *const yes[] = {"on", "true", "yes", "1"}; static const char *const no[] = {"off", "false", "no", "0"}; for (size_t i = 0; i < sizeof(yes) / sizeof(yes[0]); i++) { if (strcasecmp(s, yes[i]) == 0) { *res = true; return true; } } for (size_t i = 0; i < sizeof(no) / sizeof(no[0]); i++) { if (strcasecmp(s, no[i]) == 0) { *res = false; return true; } } return false; } static bool str_to_ulong(const char *s, int base, unsigned long *res) { if (s == NULL) return false; errno = 0; char *end = NULL; *res = strtoul(s, &end, base); return errno == 0 && *end == '\0'; } static inline pixman_color_t color_hex_to_pixman_with_alpha(uint32_t color, uint16_t alpha) { return (pixman_color_t){ .red = ((color >> 16 & 0xff) | (color >> 8 & 0xff00)) * alpha / 0xffff, .green = ((color >> 8 & 0xff) | (color >> 0 & 0xff00)) * alpha / 0xffff, .blue = ((color >> 0 & 0xff) | (color << 8 & 0xff00)) * alpha / 0xffff, .alpha = alpha, }; } static bool str_to_color(const char *s, pixman_color_t *color, const char *path, int lineno) { if (strlen(s) != 8) { LOG_ERR("%s:%d: %s: invalid RGBA color (not 8 digits)", path, lineno, s); return false; } unsigned long value; if (!str_to_ulong(s, 16, &value)) { LOG_ERRNO("%s:%d: invalid color: %s", path, lineno, s); return false; } uint32_t rgb = value >> 8; uint16_t alpha = value & 0xff; alpha |= alpha << 8; *color = color_hex_to_pixman_with_alpha(rgb, alpha); return true; } static bool str_to_spawn_template(struct config *conf, const char *s, struct config_spawn_template *template, const char *path, int lineno) { free(template->raw_cmd); free(template->argv); template->raw_cmd = NULL; template->argv = NULL; if (strlen(s) == 0) return true; char *raw_cmd = strdup(s); char **argv = NULL; if (!tokenize_cmdline(raw_cmd, &argv)) { LOG_ERR("%s:%d: syntax error in command line", path, lineno); return false; } template->raw_cmd = raw_cmd; template->argv = argv; return true; } static bool config_font_parse(const char *pattern, struct config_font *font) { FcPattern *pat = FcNameParse((const FcChar8 *)pattern); if (pat == NULL) return false; /* * First look for user specified {pixel}size option * e.g. “font-name:size=12” */ double pt_size = -1.0; FcResult have_pt_size = FcPatternGetDouble(pat, FC_SIZE, 0, &pt_size); int px_size = -1; FcResult have_px_size = FcPatternGetInteger(pat, FC_PIXEL_SIZE, 0, &px_size); if (have_pt_size != FcResultMatch && have_px_size != FcResultMatch) { /* * Apply fontconfig config. Can’t do that until we’ve first * checked for a user provided size, since we may end up with * both “size” and “pixelsize” being set, and we don’t know * which one takes priority. */ FcPattern *pat_copy = FcPatternDuplicate(pat); if (pat_copy == NULL || !FcConfigSubstitute(NULL, pat_copy, FcMatchPattern)) { LOG_WARN("%s: failed to do config substitution", pattern); } else { have_pt_size = FcPatternGetDouble(pat_copy, FC_SIZE, 0, &pt_size); have_px_size = FcPatternGetInteger(pat_copy, FC_PIXEL_SIZE, 0, &px_size); } FcPatternDestroy(pat_copy); if (have_pt_size != FcResultMatch && have_px_size != FcResultMatch) pt_size = 8.0; } FcPatternRemove(pat, FC_SIZE, 0); FcPatternRemove(pat, FC_PIXEL_SIZE, 0); char *stripped_pattern = (char *)FcNameUnparse(pat); FcPatternDestroy(pat); LOG_DBG("%s: pt-size=%.2f, px-size=%d", stripped_pattern, pt_size, px_size); *font = (struct config_font){ .pattern = stripped_pattern, .pt_size = pt_size, .px_size = px_size }; return true; } static bool parse_section_urgency(const char *key, const char *value, struct urgency_config *conf, const char *path, unsigned lineno) { if (strcmp(key, "background") == 0) { pixman_color_t bg; if (!str_to_color(value, &bg, path, lineno)) return false; conf->bg = bg; } else if (strcmp(key, "border-color") == 0) { pixman_color_t color; if (!str_to_color(value, &color, path, lineno)) return false; conf->border.color = color; } else if (strcmp(key, "border-size") == 0) { unsigned long sz; if (!str_to_ulong(value, 10, &sz)) { LOG_ERR("%s:%u: invalid border-size (expected an integer): %s", path, lineno, value); return false; } conf->border.size = sz; } else if (strcmp(key, "padding-vertical") == 0) { unsigned long p; if (!str_to_ulong(value, 10, &p)) { LOG_ERR("%s:%u: invalid padding-vertical (expected an integer): %s", path, lineno, value); return false; } conf->padding.vertical = p; } else if (strcmp(key, "padding-horizontal") == 0) { unsigned long p; if (!str_to_ulong(value, 10, &p)) { LOG_ERR("%s:%u: invalid padding-horizontal (expected an integer): %s", path, lineno, value); return false; } conf->padding.horizontal = p; } else if (strcmp(key, "title-font") == 0 || strcmp(key, "summary-font") == 0 || strcmp(key, "body-font") == 0 || strcmp(key, "action-font") == 0) { struct config_font *font = strcmp(key, "title-font") == 0 ? &conf->app.font : strcmp(key, "summary-font") == 0 ? &conf->summary.font : strcmp(key, "body-font") == 0 ? &conf->body.font : strcmp(key, "action-font") == 0 ? &conf->action.font : NULL; assert(font != NULL); free(font->pattern); config_font_parse(value, font); } else if (strcmp(key, "title-color") == 0 || strcmp(key, "summary-color") == 0 || strcmp(key, "body-color") == 0 || strcmp(key, "action-color") == 0) { pixman_color_t color; if (!str_to_color(value, &color, path, lineno)) return false; pixman_color_t *c = strcmp(key, "title-color") == 0 ? &conf->app.color : strcmp(key, "summary-color") == 0 ? &conf->summary.color : strcmp(key, "body-color") == 0 ? &conf->body.color : strcmp(key, "action-color") == 0 ? &conf->action.color : NULL; assert(c != NULL); *c = color; } else if (strcmp(key, "title-format") == 0) { free(conf->app.format); conf->app.format = ambstoc32(value); } else if (strcmp(key, "summary-format") == 0) { free(conf->summary.format); conf->summary.format = ambstoc32(value); } else if (strcmp(key, "body-format") == 0) { free(conf-> body.format); conf->body.format = ambstoc32(value); } else if (strcmp(key, "progress-bar-color") == 0) { pixman_color_t color; if (!str_to_color(value, &color, path, lineno)) return false; conf->progress.color = color; } else if (strcmp(key, "progress-bar-height") == 0) { unsigned long height; if (!str_to_ulong(value, 10, &height)) { LOG_ERR( "%s:%d: invalid progress-bar-height (expected an integer): %s", path, lineno, value); return false; } conf->progress.height = height; } else if (strcmp(key, "max-timeout") == 0) { unsigned long max_timeout_secs; if (!str_to_ulong(value, 10, &max_timeout_secs)) { LOG_ERR("%s:%d: invalid max-timeout (expected an integer): %s", path, lineno, value); return false; } conf->max_timeout_secs = max_timeout_secs; } else if (strcmp(key, "default-timeout") == 0) { unsigned long default_timeout_secs; if (!str_to_ulong(value, 10, &default_timeout_secs)) { LOG_ERR("%s:%d: invalid default-timeout (expected an integer): %s", path, lineno, value); return false; } conf->default_timeout_secs = default_timeout_secs; } else if (strcmp(key, "idle-timeout") == 0) { unsigned long idle_timeout_secs; if (!str_to_ulong(value, 10, &idle_timeout_secs)) { LOG_ERR("%s:%d: invalid idle-timeout (expected an integer): %s", path, lineno, value); return false; } conf->idle_timeout_secs = idle_timeout_secs; } else if (strcmp(key, "sound-file") == 0) { free(conf->sound_file); conf->sound_file = strlen(value) > 0 ? strdup(value) : NULL; } else if (strcmp(key, "icon") == 0) { free(conf->icon); conf->icon = strlen(value) > 0 ? strdup(value) : NULL; } else { LOG_ERR("%s:%u: invalid key: %s", path, lineno, key); return false; } return true; } static bool parse_section_low(const char *key, const char *value, struct config *conf, const char *path, unsigned lineno) { return parse_section_urgency( key, value, &conf->by_urgency[0], path, lineno); } static bool parse_section_normal(const char *key, const char *value, struct config *conf, const char *path, unsigned lineno) { return parse_section_urgency( key, value, &conf->by_urgency[1], path, lineno); } static bool parse_section_critical(const char *key, const char *value, struct config *conf, const char *path, unsigned lineno) { return parse_section_urgency( key, value, &conf->by_urgency[2], path, lineno); } static bool parse_section_main(const char *key, const char *value, struct config *conf, const char *path, unsigned lineno) { if (strcmp(key, "output") == 0) { free(conf->output); conf->output = strdup(value); } else if (strcmp(key, "max-width") == 0) { unsigned long w; if (!str_to_ulong(value, 10, &w)) { LOG_ERR("%s:%u: invalid max-width (expected an integer): %s", path, lineno, value); return false; } conf->max_width = w; } else if (strcmp(key, "min-width") == 0) { unsigned long w; if (!str_to_ulong(value, 10, &w)) { LOG_ERR("%s:%u: invalid min-width (expected an integer): %s", path, lineno, value); return false; } conf->min_width = w; } else if (strcmp(key, "max-height") == 0) { unsigned long h; if (!str_to_ulong(value, 10, &h)) { LOG_ERR("%s:%u: invalid max-height (expected an integer): %s", path, lineno, value); return false; } conf->max_height = h; } else if (strcmp(key, "dpi-aware") == 0) { if (strcasecmp(value, "auto") == 0) conf->dpi_aware = DPI_AWARE_AUTO; else { bool enabled; if (str_to_bool(value, &enabled)) conf->dpi_aware = enabled ? DPI_AWARE_YES : DPI_AWARE_NO; else { LOG_ERR("%s:%d: %s: invalid boolean value", path, lineno, value); return false; } } } else if (strcmp(key, "icon-theme") == 0) { free(conf->icon_theme_name); conf->icon_theme_name = strdup(value); } else if (strcmp(key, "max-icon-size") == 0) { unsigned long sz; if (!str_to_ulong(value, 10, &sz)) { LOG_ERR("%s:%u: invalid max-height (expected an integer): %s", path, lineno, value); return false; } conf->max_icon_size = sz; } else if (strcmp(key, "stacking-order") == 0) { if (strcasecmp(value, "bottom-up") == 0) conf->stacking_order = STACK_BOTTOM_UP; else if (strcasecmp(value, "top-down") == 0) conf->stacking_order = STACK_TOP_DOWN; else { LOG_ERR("%s:%u: %s: invalid stacking-order value, must be one of " "\"bottom-up\", " "\"top-down\"", path, lineno, value); return false; } } else if (strcmp(key, "layer") == 0) { if (strcasecmp(value, "background") == 0) conf->layer = ZWLR_LAYER_SHELL_V1_LAYER_BACKGROUND; else if (strcasecmp(value, "top") == 0) conf->layer = ZWLR_LAYER_SHELL_V1_LAYER_TOP; else if (strcasecmp(value, "bottom") == 0) conf->layer = ZWLR_LAYER_SHELL_V1_LAYER_BOTTOM; else if (strcasecmp(value, "overlay") == 0) conf->layer = ZWLR_LAYER_SHELL_V1_LAYER_OVERLAY; else { LOG_ERR( "%s:%u: %s: invalid layer value, must be one of " "\"background\", " "\"bottom\", " "\"top\" or " "\"overlay\"", path, lineno, value); return false; } } else if (strcmp(key, "anchor") == 0) { if (strcasecmp(value, "top-left") == 0) conf->anchor = ANCHOR_TOP_LEFT; else if (strcasecmp(value, "top-right") == 0) conf->anchor = ANCHOR_TOP_RIGHT; else if (strcasecmp(value, "bottom-left") == 0) conf->anchor = ANCHOR_BOTTOM_LEFT; else if (strcasecmp(value, "bottom-right") == 0) conf->anchor = ANCHOR_BOTTOM_RIGHT; else { LOG_ERR( "%s:%u: %s: invalid anchor value, must be one of " "\"top-left\", " "\"top-right\", " "\"bottom-left\" or " "\"bottom-right\"", path, lineno, value); return false; } } else if (strcmp(key, "edge-margin-vertical") == 0) { unsigned long m; if (!str_to_ulong(value, 10, &m)) { LOG_ERR( "%s:%u: invalid edge-margin-vertical (expected an integer): %s", path, lineno, value); return false; } conf->margins.vertical = m; } else if (strcmp(key, "edge-margin-horizontal") == 0) { unsigned long m; if (!str_to_ulong(value, 10, &m)) { LOG_ERR( "%s:%u: invalid edge-margin-horizontal (expected an integer): %s", path, lineno, value); return false; } conf->margins.horizontal = m; } else if (strcmp(key, "notification-margin") == 0) { unsigned long m; if (!str_to_ulong(value, 10, &m)) { LOG_ERR( "%s:%u: invalid nofication-margin (expected an integer): %s", path, lineno, value); return false; } conf->margins.between = m; } else if (strcmp(key, "selection-helper") == 0) { free(conf->selection_helper); conf->selection_helper = strdup(value); } else if (strcmp(key, "play-sound") == 0) { if (!str_to_spawn_template(conf, value, &conf->play_sound, path, lineno)) return false; } else if (strcmp(key, "background") == 0) { pixman_color_t bg; if (!str_to_color(value, &bg, path, lineno)) return false; for (int i = 0; i < 3; i++) conf->by_urgency[i].bg = bg; } else if (strcmp(key, "border-color") == 0) { pixman_color_t color; if (!str_to_color(value, &color, path, lineno)) return false; for (int i = 0; i < 3; i++) conf->by_urgency[i].border.color = color; } else if (strcmp(key, "border-size") == 0) { unsigned long sz; if (!str_to_ulong(value, 10, &sz)) { LOG_ERR("%s:%u: invalid border-size (expected an integer): %s", path, lineno, value); return false; } for (int i = 0; i < 3; i++) conf->by_urgency[i].border.size = sz; } else if (strcmp(key, "padding-vertical") == 0) { unsigned long p; if (!str_to_ulong(value, 10, &p)) { LOG_ERR("%s:%u: invalid padding-vertical (expected an integer): %s", path, lineno, value); return false; } for (int i = 0; i < 3; i++) conf->by_urgency[i].padding.vertical = p; } else if (strcmp(key, "padding-horizontal") == 0) { unsigned long p; if (!str_to_ulong(value, 10, &p)) { LOG_ERR("%s:%u: invalid padding-horizontal (expected an integer): %s", path, lineno, value); return false; } for (int i = 0; i < 3; i++) conf->by_urgency[i].padding.horizontal = p; } else if (strcmp(key, "title-font") == 0 || strcmp(key, "summary-font") == 0 || strcmp(key, "body-font") == 0 || strcmp(key, "action-font") == 0) { for (int i = 0; i < 3; i++) { struct urgency_config *urgency = &conf->by_urgency[i]; struct config_font *font = strcmp(key, "title-font") == 0 ? &urgency->app.font : strcmp(key, "summary-font") == 0 ? &urgency->summary.font : strcmp(key, "body-font") == 0 ? &urgency->body.font : strcmp(key, "action-font") == 0 ? &urgency->action.font : NULL; assert(font != NULL); free(font->pattern); config_font_parse(value, font); } } else if (strcmp(key, "title-color") == 0 || strcmp(key, "summary-color") == 0 || strcmp(key, "body-color") == 0 || strcmp(key, "action-color") == 0) { pixman_color_t color; if (!str_to_color(value, &color, path, lineno)) return false; for (int i = 0; i < 3; i++) { struct urgency_config *urgency = &conf->by_urgency[i]; pixman_color_t *c = strcmp(key, "title-color") == 0 ? &urgency->app.color : strcmp(key, "summary-color") == 0 ? &urgency->summary.color : strcmp(key, "body-color") == 0 ? &urgency->body.color : strcmp(key, "action-color") == 0 ? &urgency->action.color : NULL; assert(c != NULL); *c = color; } } else if (strcmp(key, "title-format") == 0) { for (int i = 0; i < 3; i++) { struct urgency_config *urgency = &conf->by_urgency[i]; free(urgency->app.format); urgency->app.format = ambstoc32(value); } } else if (strcmp(key, "summary-format") == 0) { for (int i = 0; i < 3; i++) { struct urgency_config *urgency = &conf->by_urgency[i]; free(urgency->summary.format); urgency->summary.format = ambstoc32(value); } } else if (strcmp(key, "body-format") == 0) { for (int i = 0; i < 3; i++) { struct urgency_config *urgency = &conf->by_urgency[i]; free(urgency->body.format); urgency->body.format = ambstoc32(value); } } else if (strcmp(key, "progress-bar-color") == 0) { pixman_color_t color; if (!str_to_color(value, &color, path, lineno)) return false; for (int i = 0; i < 3; i++) { struct urgency_config *urgency = &conf->by_urgency[i]; urgency->progress.color = color; } } else if (strcmp(key, "progress-bar-height") == 0) { unsigned long height; if (!str_to_ulong(value, 10, &height)) { LOG_ERR( "%s:%d: invalid progress-bar-height (expected an integer): %s", path, lineno, value); return false; } for (int i = 0; i < 3; i++) { struct urgency_config *urgency = &conf->by_urgency[i]; urgency->progress.height = height; } } else if (strcmp(key, "max-timeout") == 0) { unsigned long max_timeout_secs; if (!str_to_ulong(value, 10, &max_timeout_secs)) { LOG_ERR("%s:%d: invalid max-timeout (expected an integer): %s", path, lineno, value); return false; } for (int i = 0; i < 3; i++) { struct urgency_config *urgency = &conf->by_urgency[i]; urgency->max_timeout_secs = max_timeout_secs; } } else if (strcmp(key, "default-timeout") == 0) { unsigned long default_timeout_secs; if (!str_to_ulong(value, 10, &default_timeout_secs)) { LOG_ERR("%s:%d: invalid default-timeout (expected an integer): %s", path, lineno, value); return false; } for (int i = 0; i < 3; i++) { struct urgency_config *urgency = &conf->by_urgency[i]; urgency->default_timeout_secs = default_timeout_secs; } } else if (strcmp(key, "idle-timeout") == 0) { unsigned long idle_timeout_secs; if (!str_to_ulong(value, 10, &idle_timeout_secs)) { LOG_ERR("%s:%d: invalid idle-timeout (expected an integer): %s", path, lineno, value); return false; } for (int i = 0; i < 3; i++) { struct urgency_config *urgency = &conf->by_urgency[i]; urgency->idle_timeout_secs = idle_timeout_secs; } } else if (strcmp(key, "sound-file") == 0) { for (int i = 0; i < 3; i++) { struct urgency_config *urgency = &conf->by_urgency[i]; free(urgency->sound_file); urgency->sound_file = strlen(value) > 0 ? strdup(value) : NULL; } } else if (strcmp(key, "icon") == 0) { for (int i = 0; i < 3; i++) { struct urgency_config *urgency = &conf->by_urgency[i]; free(urgency->icon); urgency->icon = strlen(value) > 0 ? strdup(value) : NULL; } } else { LOG_ERR("%s:%u: invalid key: %s", path, lineno, key); return false; } return true; } static bool parse_config_file(FILE *f, struct config *conf, const char *path) { enum section { SECTION_MAIN, SECTION_LOW, SECTION_NORMAL, SECTION_CRITICAL, SECTION_COUNT, } section = SECTION_MAIN; /* Function pointer, called for each key/value line */ typedef bool (*parser_fun_t)( const char *key, const char *value, struct config *conf, const char *path, unsigned lineno); /* Maps sections to line parser functions */ static const parser_fun_t section_parser_map[] = { [SECTION_MAIN] = &parse_section_main, [SECTION_LOW] = &parse_section_low, [SECTION_NORMAL] = &parse_section_normal, [SECTION_CRITICAL] = &parse_section_critical, }; static const char *const section_names[] = { [SECTION_MAIN] = "main", [SECTION_LOW] = "low", [SECTION_NORMAL] = "normal", [SECTION_CRITICAL] = "critical", }; unsigned lineno = 0; char *_line = NULL; size_t count = 0; while (true) { errno = 0; lineno++; ssize_t ret = getline(&_line, &count, f); if (ret < 0) { if (errno != 0) { LOG_ERRNO("failed to read from configuration"); goto err; } break; } /* Strip whitespace */ char *line = _line; { while (isspace(*line)) line++; if (line[0] != '\0') { char *end = line + strlen(line) - 1; while (isspace(*end)) end--; *(end + 1) = '\0'; } } /* Empty line, or comment */ if (line[0] == '\0' || line[0] == '#') continue; /* Split up into key/value pair + trailing comment */ char *key_value = strtok(line, "#"); char *comment __attribute__((unused)) = strtok(NULL, "\n"); /* Check for new section */ if (key_value[0] == '[') { char *end = strchr(key_value, ']'); if (end == NULL) { LOG_ERR("%s:%d: syntax error: %s", path, lineno, key_value); goto err; } *end = '\0'; for (section = SECTION_MAIN; section < SECTION_COUNT; ++section) { if (strcmp(&key_value[1], section_names[section]) == 0) { break; }; } if (section == SECTION_COUNT) { LOG_ERR("%s:%d: invalid section name: %s", path, lineno, &key_value[1]); goto err; } continue; } char *key = strtok(key_value, "="); if (key == NULL) { LOG_ERR("%s:%d: syntax error: no key specified", path, lineno); goto err; } char *value = strtok(NULL, "\n"); if (value == NULL) { /* Empty value, i.e. "key=" */ value = key + strlen(key); } /* Strip trailing whitespace from key (leading stripped earlier) */ { assert(!isspace(*key)); char *end = key + strlen(key) - 1; while (isspace(*end)) end--; *(end + 1) = '\0'; } /* Strip leading whitespace from value (trailing stripped earlier) */ { while (isspace(*value)) value++; if (value[0] != '\0') { char *end = value + strlen(value) - 1; while (isspace(*end)) end--; *(end + 1) = '\0'; } } LOG_DBG("section=%s, key='%s', value='%s'", section_names[section], key, value); parser_fun_t section_parser = section_parser_map[section]; assert(section_parser != NULL); if (!section_parser(key, value, conf, path, lineno)) goto err; } free(_line); return true; err: free(_line); return false; } bool config_load(struct config *conf, const char *path) { const char *const default_font_name = "sans serif"; *conf = (struct config){ .output = NULL, .min_width = 0, .max_width = 0, .max_height = 0, .dpi_aware = DPI_AWARE_AUTO, .icon_theme_name = strdup("hicolor"), .max_icon_size = 48, .stacking_order = STACK_BOTTOM_UP, .anchor = ANCHOR_TOP_RIGHT, .layer = ZWLR_LAYER_SHELL_V1_LAYER_TOP, .margins = { .vertical = 10, .horizontal = 10, .between = 10, }, .by_urgency = { /* urgency == low */ { .bg = {0x2b2b, 0x2b2b, 0x2b2b, 0xffff}, .border = { .color = {0x9090, 0x9090, 0x9090, 0xffff}, .size = 1, }, .padding = { .vertical = 20, .horizontal = 20, }, .app = { .color = {0x8888, 0x8888, 0x8888, 0xffff}, .format = c32dup(U"%a%A"), }, .summary = { .color = {0x8888, 0x8888, 0x8888, 0xffff}, .format = c32dup(U"%s\\n"), }, .body = { .color = {0x8888, 0x8888, 0x8888, 0xffff}, .format = c32dup(U"%b"), }, .action = { .color = {0x8888, 0x8888, 0x8888, 0xffff}, }, .progress = { .height = 20, .color = {0xffff, 0xffff, 0xffff, 0xffff}, }, .max_timeout_secs = 0, .default_timeout_secs = 0, .idle_timeout_secs = 0, .icon = NULL, }, { .bg = {0x3f3f, 0x5f5f, 0x3f3f, 0xffff}, .border = { .color = {0x9090, 0x9090, 0x9090, 0xffff}, .size = 1, }, .padding = { .vertical = 20, .horizontal = 20, }, .app = { .color = {0xffff, 0xffff, 0xffff, 0xffff}, .format = c32dup(U"%a%A"), }, .summary = { .color = {0xffff, 0xffff, 0xffff, 0xffff}, .format = c32dup(U"%s\\n"), }, .body = { .color = {0xffff, 0xffff, 0xffff, 0xffff}, .format = c32dup(U"%b"), }, .action = { .color = {0xffff, 0xffff, 0xffff, 0xffff}, }, .progress = { .height = 20, .color = {0xffff, 0xffff, 0xffff, 0xffff}, }, .max_timeout_secs = 0, .default_timeout_secs = 0, .idle_timeout_secs = 0, .icon = NULL, }, { .bg = {0x6c6c, 0x3333, 0x3333, 0xffff}, .border = { .color = {0x9090, 0x9090, 0x9090, 0xffff}, .size = 1, }, .padding = { .vertical = 20, .horizontal = 20, }, .app = { .color = {0xffff, 0xffff, 0xffff, 0xffff}, .format = c32dup(U"%a%A"), }, .summary = { .color = {0xffff, 0xffff, 0xffff, 0xffff}, .format = c32dup(U"%s\\n"), }, .body = { .color = {0xffff, 0xffff, 0xffff, 0xffff}, .format = c32dup(U"%b"), }, .action = { .color = {0xffff, 0xffff, 0xffff, 0xffff}, }, .progress = { .height = 20, .color = {0xffff, 0xffff, 0xffff, 0xffff}, }, .max_timeout_secs = 0, .default_timeout_secs = 0, .idle_timeout_secs = 0, .icon = NULL, }, }, .selection_helper = strdup("dmenu"), }; for (size_t i = 0; i < sizeof(conf->by_urgency) / sizeof(conf->by_urgency[0]); i++) { config_font_parse(default_font_name, &conf->by_urgency[i].app.font); config_font_parse(default_font_name, &conf->by_urgency[i].summary.font); config_font_parse(default_font_name, &conf->by_urgency[i].body.font); config_font_parse(default_font_name, &conf->by_urgency[i].action.font); } conf->play_sound.raw_cmd = strdup("aplay ${filename}"); tokenize_cmdline(conf->play_sound.raw_cmd, &conf->play_sound.argv); bool ret = false; struct config_file conf_file = {.path = NULL, .fd = -1}; if (path != NULL) { int fd = open(path, O_RDONLY); if (fd < 0) { LOG_ERRNO("%s: failed to open", path); goto out; } conf_file.path = strdup(path); conf_file.fd = fd; } else { conf_file = open_config(); if (conf_file.fd < 0) { /* Default conf */ LOG_WARN("no configuration found, using defaults"); ret = true; goto out; } } assert(conf_file.path != NULL); assert(conf_file.fd >= 0); LOG_INFO("loading configuration from %s", conf_file.path); FILE *f = fdopen(conf_file.fd, "r"); if (f == NULL) { LOG_ERR("%s: failed to open", conf_file.path); goto out; } ret = parse_config_file(f, conf, conf_file.path); fclose(f); out: free(conf_file.path); return ret; } static void free_spawn_template(struct config_spawn_template *template) { free(template->raw_cmd); free(template->argv); } void config_destroy(struct config conf) { free(conf.output); free(conf.icon_theme_name); free(conf.selection_helper); free_spawn_template(&conf.play_sound); for (int i = 0; i < 3; i++) { struct urgency_config *uconf = &conf.by_urgency[i]; free(uconf->app.font.pattern); free(uconf->app.format); free(uconf->summary.font.pattern); free(uconf->summary.format); free(uconf->body.font.pattern); free(uconf->body.format); free(uconf->action.font.pattern); free(uconf->sound_file); free(uconf->icon); } } fnott-1.4.1+ds/config.h000066400000000000000000000036451447512336000147040ustar00rootroot00000000000000#pragma once #include #include #include "char32.h" #include struct config_font { char *pattern; float pt_size; int px_size; }; struct urgency_config { pixman_color_t bg; struct { pixman_color_t color; int size; } border; struct { int vertical; int horizontal; } padding; struct { struct config_font font; pixman_color_t color; char32_t *format; } app; struct { struct config_font font; pixman_color_t color; char32_t *format; } summary; struct { struct config_font font; pixman_color_t color; char32_t *format; } body; struct { struct config_font font; pixman_color_t color; } action; struct { int height; pixman_color_t color; } progress; int max_timeout_secs; int default_timeout_secs; int idle_timeout_secs; char *sound_file; /* Path to user-configured sound to play on notification */ char *icon; }; struct config_spawn_template { char *raw_cmd; char **argv; }; struct config { char *output; int min_width; int max_width; int max_height; enum {DPI_AWARE_AUTO, DPI_AWARE_YES, DPI_AWARE_NO} dpi_aware; char *icon_theme_name; int max_icon_size; enum { STACK_BOTTOM_UP, STACK_TOP_DOWN, } stacking_order; enum zwlr_layer_shell_v1_layer layer; enum { ANCHOR_TOP_LEFT, ANCHOR_TOP_RIGHT, ANCHOR_BOTTOM_LEFT, ANCHOR_BOTTOM_RIGHT, } anchor; struct { int vertical; int horizontal; int between; } margins; struct urgency_config by_urgency[3]; char *selection_helper; struct config_spawn_template play_sound; }; bool config_load(struct config *conf, const char *path); void config_destroy(struct config conf); fnott-1.4.1+ds/ctrl-protocol.h000066400000000000000000000006201447512336000162300ustar00rootroot00000000000000#pragma once #include enum ctrl_command { CTRL_QUIT, CTRL_LIST, CTRL_DISMISS_BY_ID, CTRL_DISMISS_ALL, CTRL_ACTIONS_BY_ID, }; struct ctrl_request { enum ctrl_command cmd; uint32_t id; } __attribute__((packed)); enum ctrl_result { CTRL_OK, CTRL_INVALID_ID, CTRL_NO_ACTIONS, CTRL_ERROR }; struct ctrl_reply { enum ctrl_result result; } __attribute__((packed)); fnott-1.4.1+ds/ctrl.c000066400000000000000000000216061447512336000143730ustar00rootroot00000000000000#include "ctrl.h" #include #include #include #include #include #include #include #include #include #include #define LOG_MODULE "ctrl" #define LOG_ENABLE_DBG 0 #include "log.h" #include "ctrl-protocol.h" #include "dbus.h" extern volatile sig_atomic_t aborted; struct ctrl; struct client { struct ctrl *ctrl; int fd; struct { union { struct ctrl_request cmd; uint8_t raw[sizeof(struct ctrl_request)]; }; size_t idx; } recv; }; struct ctrl { struct fdm *fdm; struct notif_mgr *notif_mgr; struct dbus *bus; int server_fd; char *socket_path; tll(struct client) clients; }; static bool send_reply(int fd, const struct ctrl_reply *reply) { LOG_DBG("client: FD=%d, reply: result=%s", fd, reply->result == CTRL_OK ? "ok" : "fail"); if (write(fd, reply, sizeof(*reply)) != sizeof(*reply)) { LOG_ERRNO("client: FD=%d: failed to send reply", fd); return false; } return true; } static void client_disconnected(struct ctrl *ctrl, int fd) { LOG_DBG("client: FD=%d disconnected", fd); fdm_del(ctrl->fdm, fd); tll_foreach(ctrl->clients, it) { if (it->item.fd == fd) { tll_remove(ctrl->clients, it); break; } } } struct actions_cb_data { struct ctrl *ctrl; int client_fd; }; static void actions_complete(uint32_t notif_id, const char *action_id, void *data) { struct actions_cb_data *info = data; struct ctrl *ctrl = info->ctrl; int fd = info->client_fd; struct ctrl_reply reply = {.result = CTRL_INVALID_ID}; LOG_DBG("actions callback: notif=%u, ID=%s", notif_id, action_id); if (action_id == NULL) goto out; reply.result = dbus_signal_action(info->ctrl->bus, notif_id, action_id) ? CTRL_OK : CTRL_ERROR; out: send_reply(fd, &reply); client_disconnected(ctrl, fd); free(info); } static enum ctrl_result actions_by_id(struct ctrl *ctrl, int fd, uint32_t id) { struct notif *notif = notif_mgr_get_notif(ctrl->notif_mgr, id); if (notif == NULL) return CTRL_INVALID_ID; if (notif_action_count(notif) == 0) return CTRL_NO_ACTIONS; struct actions_cb_data *info = malloc(sizeof(*info)); *info = (struct actions_cb_data){.ctrl = ctrl, .client_fd = fd}; notif_select_action(notif, &actions_complete, info); return CTRL_OK; } static bool fdm_client(struct fdm *fdm, int fd, int events, void *data) { struct client *client = data; struct ctrl *ctrl = client->ctrl; assert(client->fd == fd); bool ret = false; uint32_t *ids = NULL; int64_t id_count = -1; size_t left = sizeof(client->recv.cmd) - client->recv.idx; ssize_t count = recv(fd, &client->recv.raw[client->recv.idx], left, 0); if (count < 0) { LOG_ERRNO("client: FD=%d: failed to receive command", fd); return false; } client->recv.idx += count; if (client->recv.idx < sizeof(client->recv.cmd)) { /* Haven’t received a full command yet */ goto no_err; } assert(client->recv.idx == sizeof(client->recv.cmd)); /* TODO: endianness */ struct ctrl_reply reply; switch (client->recv.cmd.cmd) { case CTRL_QUIT: LOG_DBG("client: FD=%d, quit", fd); aborted = 1; reply.result = CTRL_OK; break; case CTRL_LIST: id_count = notif_mgr_get_ids(ctrl->notif_mgr, NULL, 0); LOG_INFO("got %"PRIi64" IDs", id_count); reply.result = id_count >= 0 ? CTRL_OK : CTRL_ERROR; if (id_count >= 0) { if (id_count > 0) ids = calloc(id_count, sizeof(ids[0])); notif_mgr_get_ids(ctrl->notif_mgr, ids, id_count); } break; case CTRL_DISMISS_BY_ID: LOG_DBG("client: FD=%d, dismiss by-id: %u", fd, client->recv.cmd.id); reply.result = notif_mgr_dismiss_id(ctrl->notif_mgr, client->recv.cmd.id) ? CTRL_OK : CTRL_INVALID_ID; break; case CTRL_DISMISS_ALL: LOG_DBG("client: FD=%d, dismiss all", fd); reply.result = notif_mgr_dismiss_all(ctrl->notif_mgr) ? CTRL_OK : CTRL_ERROR; break; case CTRL_ACTIONS_BY_ID: LOG_DBG("client: FD=%d, actions by-id: %u", fd, client->recv.cmd.id); if ((reply.result = actions_by_id(ctrl, fd, client->recv.cmd.id)) == CTRL_OK) { /* Action selection helper successfully started, wait for * response before sending reply to fnottctl */ goto no_reply; } } if (!send_reply(fd, &reply)) goto err; if (reply.result == CTRL_OK && id_count >= 0) { if (write(fd, &id_count, sizeof(id_count)) != sizeof(id_count)) { LOG_ERRNO("failed to write 'list' response"); goto err; } for (size_t i = 0; i < id_count; i++) { char *summary = NULL; uint32_t len = 0; const struct notif *notif = notif_mgr_get_notif(ctrl->notif_mgr, ids[i]); if (notif != NULL) { summary = notif_get_summary(notif); len = strlen(summary); } if (write(fd, &ids[i], sizeof(ids[i])) != sizeof(ids[i]) || write(fd, &len, sizeof(len)) != sizeof(len) || write(fd, summary != NULL ? summary : "", len) != len) { LOG_ERRNO("failed to write 'list' response"); free(summary); goto err; } free(summary); } } no_err: ret = true; err: free(ids); if ((events & EPOLLHUP) || client->recv.idx >= sizeof(client->recv.cmd)) { /* Client disconnected */ client_disconnected(ctrl, fd); } return ret; no_reply: free(ids); return true; } static bool fdm_server(struct fdm *fdm, int fd, int events, void *data) { if (events & EPOLLHUP) { LOG_ERR("disconnected from controller UNIX socket"); return false; } struct ctrl *ctrl = data; struct sockaddr_un addr; socklen_t addr_size = sizeof(addr); int client_fd = accept4(ctrl->server_fd, (struct sockaddr *)&addr, &addr_size, SOCK_CLOEXEC); if (client_fd == -1) { LOG_ERRNO("failed to accept client connection"); return false; } LOG_DBG("client FD=%d connected", client_fd); tll_push_back(ctrl->clients, ((struct client){.ctrl = ctrl, .fd = client_fd})); struct client *client = &tll_back(ctrl->clients) ; if (!fdm_add(ctrl->fdm, client_fd, EPOLLIN, &fdm_client, client)) { LOG_ERR("failed to register client FD with FDM"); tll_pop_back(ctrl->clients); close(client_fd); return false; } return true; } static char * get_socket_path(void) { const char *xdg_runtime = getenv("XDG_RUNTIME_DIR"); if (xdg_runtime == NULL) return strdup("/tmp/fnott.sock"); const char *wayland_display = getenv("WAYLAND_DISPLAY"); if (wayland_display == NULL) { char *path = malloc(strlen(xdg_runtime) + 1 + strlen("fnott.sock") + 1); sprintf(path, "%s/fnott.sock", xdg_runtime); return path; } char *path = malloc(strlen(xdg_runtime) + 1 + strlen("fnott-.sock") + strlen(wayland_display) + 1); sprintf(path, "%s/fnott-%s.sock", xdg_runtime, wayland_display); return path; } struct ctrl * ctrl_init(struct fdm *fdm, struct notif_mgr *notif_mgr, struct dbus *bus) { int fd = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0); if (fd == -1) { LOG_ERRNO("failed to create UNIX socket"); return NULL; } char *sock_path = get_socket_path(); if (sock_path == NULL) goto err; unlink(sock_path); struct sockaddr_un addr = {.sun_family = AF_UNIX}; strncpy(addr.sun_path, sock_path, sizeof(addr.sun_path) - 1); if (bind(fd, (const struct sockaddr *)&addr, sizeof(addr)) < 0) { LOG_ERRNO("%s: failed to bind", addr.sun_path); goto err; } if (listen(fd, 5) < 0) { LOG_ERRNO("%s: failed to listen", addr.sun_path); goto err; } struct ctrl *ctrl = malloc(sizeof(*ctrl)); *ctrl = (struct ctrl){ .fdm = fdm, .bus = bus, .notif_mgr = notif_mgr, .server_fd = fd, .socket_path = sock_path, .clients = tll_init(), }; if (!fdm_add(fdm, fd, EPOLLIN, &fdm_server, ctrl)) { LOG_ERR("failed to register with FDM"); goto err; } return ctrl; err: if (sock_path) free(sock_path); if (fd != -1) close(fd); return NULL; } void ctrl_destroy(struct ctrl *ctrl) { if (ctrl == NULL) return; fdm_del(ctrl->fdm, ctrl->server_fd); if (ctrl->socket_path != NULL) unlink(ctrl->socket_path); free(ctrl->socket_path); free(ctrl); } int ctrl_poll_fd(const struct ctrl *ctrl) { return ctrl->server_fd; } fnott-1.4.1+ds/ctrl.h000066400000000000000000000004361447512336000143760ustar00rootroot00000000000000#pragma once #include #include "notification.h" #include "fdm.h" #include "dbus.h" struct ctrl; struct ctrl *ctrl_init( struct fdm *fdm, struct notif_mgr *notif_mgr, struct dbus *bus); void ctrl_destroy(struct ctrl *ctrl); int ctrl_poll_fd(const struct ctrl *ctrl); fnott-1.4.1+ds/dbus.c000066400000000000000000000532441447512336000143670ustar00rootroot00000000000000#include "dbus.h" #include #include #include #include #include #include #include #include #define LOG_MODULE "dbus" #define LOG_ENABLE_DBG 0 #include "log.h" #include "icon.h" #include "notification.h" #include "uri.h" #include "version.h" #define min(x, y) ((x) < (y) ? (x) : (y)) #define max(x, y) ((x) > (y) ? (x) : (y)) struct dbus { DBusConnection *conn; const struct config *conf; struct fdm *fdm; struct wayland *wayl; struct notif_mgr *notif_mgr; const icon_theme_list_t *icon_theme; int bus_fd; }; bool get_server_information(struct dbus *bus, DBusMessage *msg) { LOG_DBG("get_server_information"); bool ret = false; DBusMessage *reply = dbus_message_new_method_return(msg); if (reply == NULL) return false; DBusMessageIter iter; dbus_message_iter_init_append(reply, &iter); if (!dbus_message_iter_append_basic(&iter, DBUS_TYPE_STRING, &(const char *){"fnott"}) || !dbus_message_iter_append_basic(&iter, DBUS_TYPE_STRING, &(const char *){"dnkl"}) || !dbus_message_iter_append_basic(&iter, DBUS_TYPE_STRING, &(const char *){FNOTT_VERSION}) || !dbus_message_iter_append_basic(&iter, DBUS_TYPE_STRING, &(const char *){"1.2"})) { goto err; } if (!dbus_connection_send(bus->conn, reply, NULL)) goto err; if (dbus_connection_has_messages_to_send(bus->conn)) fdm_event_add(bus->fdm, bus->bus_fd, EPOLLOUT); ret = true; err: dbus_message_unref(reply); return ret; } static bool get_capabilities(struct dbus *bus, DBusMessage *msg) { DBusMessage *reply = dbus_message_new_method_return(msg); if (reply == NULL) return false; bool ret = false; DBusMessageIter iter, arr; dbus_message_iter_init_append(reply, &iter); dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY, DBUS_TYPE_STRING_AS_STRING, &arr); dbus_message_iter_append_basic(&arr, DBUS_TYPE_STRING, &(const char *){"body"}); dbus_message_iter_append_basic(&arr, DBUS_TYPE_STRING, &(const char *){"body-markup"}); dbus_message_iter_append_basic(&arr, DBUS_TYPE_STRING, &(const char *){"actions"}); dbus_message_iter_append_basic(&arr, DBUS_TYPE_STRING, &(const char *){"icon-static"}); dbus_message_iter_close_container(&iter, &arr); if (!dbus_connection_send(bus->conn, reply, NULL)) goto err; if (dbus_connection_has_messages_to_send(bus->conn)) fdm_event_add(bus->fdm, bus->bus_fd, EPOLLOUT); ret = true; err: dbus_message_unref(reply); return ret; } static bool notify(struct dbus *bus, DBusMessage *msg) { DBusError dbus_error; dbus_error_init(&dbus_error); DBusMessage *reply = NULL; pixman_image_t *pix = NULL; struct action { const char *id; const char *label; }; /* D-Bus arguments */ dbus_uint32_t replaces_id; char *app_name, *app_icon, *summary, *body; tll(struct action) actions = tll_init(); enum urgency urgency = URGENCY_NORMAL; int8_t progress_percent = -1; if (!dbus_message_get_args( msg, &dbus_error, DBUS_TYPE_STRING, &app_name, DBUS_TYPE_UINT32, &replaces_id, DBUS_TYPE_STRING, &app_icon, DBUS_TYPE_STRING, &summary, DBUS_TYPE_STRING, &body, DBUS_TYPE_INVALID)) { return false; } size_t len = strlen(app_name); while (len > 0 && isspace(app_name[len - 1])) app_name[--len] = '\0'; len = strlen(summary); while (len > 0 && isspace(summary[len - 1])) summary[--len] = '\0'; len = strlen(body); while (len > 0 && isspace(body[len - 1])) body[--len] = '\0'; if (dbus_error_is_set(&dbus_error)) { LOG_ERR("Notify: failed to parse arguments: %s", dbus_error.message); dbus_error_free(&dbus_error); return false; } bool ret = false; LOG_DBG("app: %s, icon: %s, summary: %s, body: %s", app_name, app_icon, summary, body); { char *app_name_allocated = NULL; const char *icon_name = NULL; const size_t app_icon_len = strlen(app_icon); if (app_icon_len > 0) { char *scheme = NULL, *host = NULL, *path = NULL; if (uri_parse(app_icon, app_icon_len, &scheme, NULL, NULL, &host, NULL, &path, NULL, NULL) && strcmp(scheme, "file") == 0 && hostname_is_localhost(host)) { icon_name = app_name_allocated = path; path = NULL; } else icon_name = app_icon; free(scheme); free(host); free(path); } else { app_name_allocated = malloc(strlen(app_name) + 1); for (size_t i = 0; i < strlen(app_name); i++) app_name_allocated[i] = tolower(app_name[i]); app_name_allocated[strlen(app_name)] = '\0'; icon_name = app_name_allocated; } pix = icon_load(icon_name, bus->conf->max_icon_size, bus->icon_theme); free(app_name_allocated); } DBusMessageIter args_iter; dbus_message_iter_init(msg, &args_iter); dbus_message_iter_next(&args_iter); /* app name */ dbus_message_iter_next(&args_iter); /* replaces ID */ dbus_message_iter_next(&args_iter); /* app icon */ dbus_message_iter_next(&args_iter); /* summary */ dbus_message_iter_next(&args_iter); /* body */ if (dbus_message_iter_get_arg_type(&args_iter) != DBUS_TYPE_ARRAY) goto err; DBusMessageIter actions_iter; dbus_message_iter_recurse(&args_iter, &actions_iter); while (dbus_message_iter_get_arg_type(&actions_iter) != DBUS_TYPE_INVALID) { if (dbus_message_iter_get_arg_type(&actions_iter) != DBUS_TYPE_STRING) goto err; const char *id; dbus_message_iter_get_basic(&actions_iter, &id); dbus_message_iter_next(&actions_iter); if (dbus_message_iter_get_arg_type(&actions_iter) != DBUS_TYPE_STRING) goto err; const char *label; dbus_message_iter_get_basic(&actions_iter, &label); dbus_message_iter_next(&actions_iter); LOG_DBG("action: %s %s", id, label); tll_push_back(actions, ((struct action){id, label})); } dbus_message_iter_next(&args_iter); if (dbus_message_iter_get_arg_type(&args_iter) != DBUS_TYPE_ARRAY) goto err; DBusMessageIter hints_iter; dbus_message_iter_recurse(&args_iter, &hints_iter); while (dbus_message_iter_get_arg_type(&hints_iter) != DBUS_TYPE_INVALID) { DBusMessageIter entry_iter; if (dbus_message_iter_get_arg_type(&hints_iter) != DBUS_TYPE_DICT_ENTRY) goto err; dbus_message_iter_recurse(&hints_iter, &entry_iter); dbus_message_iter_next(&hints_iter); if (dbus_message_iter_get_arg_type(&entry_iter) != DBUS_TYPE_STRING) goto err; const char *name; dbus_message_iter_get_basic(&entry_iter, &name); dbus_message_iter_next(&entry_iter); if (dbus_message_iter_get_arg_type(&entry_iter) != DBUS_TYPE_VARIANT) goto err; DBusMessageIter value_iter; dbus_message_iter_recurse(&entry_iter, &value_iter); dbus_message_iter_next(&entry_iter); LOG_DBG("hint: %s", name); if (strcmp(name, "urgency") == 0) { if (dbus_message_iter_get_arg_type(&value_iter) != DBUS_TYPE_BYTE) goto err; /* low=0, normal=1, critical=2 */ uint8_t level; dbus_message_iter_get_basic(&value_iter, &level); LOG_DBG("hint: urgency=%hhu", level); urgency = level; } else if (strcmp(name, "value") == 0) { if (dbus_message_iter_get_arg_type(&value_iter) != DBUS_TYPE_INT32) goto err; dbus_int32_t progress; dbus_message_iter_get_basic(&value_iter, &progress); LOG_DBG("hint: progress=%d", progress); progress_percent = min(100, max(0, progress)); } else if (strcmp(name, "image-path") == 0 || strcmp(name, "image_path") == 0) { if (dbus_message_iter_get_arg_type(&value_iter) != DBUS_TYPE_STRING) goto err; const char *image_path; dbus_message_iter_get_basic(&value_iter, &image_path); LOG_DBG("image-path: %s", image_path); char *scheme = NULL, *host = NULL, *path = NULL; if (uri_parse(image_path, strlen(image_path), &scheme, NULL, NULL, &host, NULL, &path, NULL, NULL) && strcmp(scheme, "file") == 0 && hostname_is_localhost(host)) { image_path = path; } if (pix != NULL) { free(pixman_image_get_data(pix)); pixman_image_unref(pix); pix = NULL; } pix = icon_load(image_path, bus->conf->max_icon_size, bus->icon_theme); free(scheme); free(host); free(path); } else if (strcmp(name, "image-data") == 0 || strcmp(name, "image_data") == 0 || strcmp(name, "icon_data") == 0) { if (dbus_message_iter_get_arg_type(&value_iter) != DBUS_TYPE_STRUCT) goto err; DBusMessageIter img_iter; dbus_message_iter_recurse(&value_iter, &img_iter); #define iter_get(dest) \ do { \ if (dbus_message_iter_get_arg_type(&img_iter) != DBUS_TYPE_INT32) \ goto err; \ dbus_message_iter_get_basic(&img_iter, &dest); \ dbus_message_iter_next(&img_iter); \ } while (0) dbus_bool_t has_alpha; dbus_int32_t width, height, stride, bpp, channels; iter_get(width); iter_get(height); iter_get(stride); if (dbus_message_iter_get_arg_type(&img_iter) != DBUS_TYPE_BOOLEAN) goto err; dbus_message_iter_get_basic(&img_iter, &has_alpha); dbus_message_iter_next(&img_iter); iter_get(bpp); iter_get(channels); #undef iter_get LOG_DBG("image: width=%u, height=%u, stride=%u, has-alpha=%d, bpp=%u, channels=%u", width, height, stride, has_alpha, bpp, channels); if (dbus_message_iter_get_arg_type(&img_iter) != DBUS_TYPE_ARRAY) goto err; if (width * channels * bpp / 8 > stride) LOG_WARN("image width exceeds image stride"); size_t image_size = stride * height; uint8_t *image_data = malloc(image_size); DBusMessageIter data_iter; dbus_message_iter_recurse(&img_iter, &data_iter); for (size_t i = 0; i < image_size; i++) { int type = dbus_message_iter_get_arg_type(&data_iter); if (type == DBUS_TYPE_INVALID) { LOG_WARN("image data truncated"); break; } if (type != DBUS_TYPE_BYTE) goto err; dbus_message_iter_get_basic(&data_iter, &image_data[i]); dbus_message_iter_next(&data_iter); } if (dbus_message_iter_get_arg_type(&data_iter) != DBUS_TYPE_INVALID) LOG_WARN("image data exceeds specified size"); pixman_format_code_t format = 0; if (bpp == 8 && channels == 4) format = has_alpha ? PIXMAN_a8b8g8r8 : PIXMAN_x8b8g8r8; else if (bpp == 8 && channels == 3) { /* Untested */ format = PIXMAN_b8g8r8; } else { LOG_WARN("unimplemented image format: bpp=%u, channels=%u", bpp, channels); free(image_data); } if (format != 0) { if (pix != NULL) { free(pixman_image_get_data(pix)); pixman_image_unref(pix); pix = NULL; } pix = pixman_image_create_bits_no_clear( format, width, height, (uint32_t *)image_data, stride); } } } dbus_message_iter_next(&args_iter); if (dbus_message_iter_get_arg_type(&args_iter) != DBUS_TYPE_INT32) goto err; /* -1 - up to server (us), 0 - never expire */ dbus_int32_t timeout_ms; dbus_message_iter_get_basic(&args_iter, &timeout_ms); LOG_DBG("timeout = %dms", timeout_ms); struct notif *notif = notif_mgr_create_notif(bus->notif_mgr, replaces_id); if (notif == NULL) goto err; notif_set_application(notif, app_name); notif_set_summary(notif, summary); notif_set_body(notif, body); notif_set_urgency(notif, urgency); notif_set_progress(notif, progress_percent); if (timeout_ms >= 0) notif_set_timeout(notif, timeout_ms); if (pix != NULL) notif_set_image(notif, pix); tll_foreach(actions, it) notif_add_action(notif, it->item.id, it->item.label); notif_play_sound(notif); notif_mgr_refresh(bus->notif_mgr); if ((reply = dbus_message_new_method_return(msg)) == NULL) goto err; DBusMessageIter iter; dbus_message_iter_init_append(reply, &iter); if (!dbus_message_iter_append_basic( &iter, DBUS_TYPE_UINT32, &(dbus_uint32_t){notif_id(notif)})) goto err; if (!dbus_connection_send(bus->conn, reply, NULL)) goto err; if (dbus_connection_has_messages_to_send(bus->conn)) fdm_event_add(bus->fdm, bus->bus_fd, EPOLLOUT); ret = true; goto out; err: if (pix != NULL) { free(pixman_image_get_data(pix)); pixman_image_unref(pix); } out: tll_free(actions); if (reply != NULL) dbus_message_unref(reply); return ret; } static bool close_notification(struct dbus *bus, DBusMessage *msg) { DBusError dbus_error; dbus_error_init(&dbus_error); dbus_uint32_t id; if (!dbus_message_get_args( msg, &dbus_error, DBUS_TYPE_UINT32, &id, DBUS_TYPE_INVALID)) { return false; } if (dbus_error_is_set(&dbus_error)) { LOG_ERR("CloseNotification: failed to parse arguments: %s", dbus_error.message); dbus_error_free(&dbus_error); return false; } LOG_DBG("CloseNotification: id=%u", id); bool success = notif_mgr_del_notif(bus->notif_mgr, id); if (success) { notif_mgr_refresh(bus->notif_mgr); dbus_signal_closed(bus, id); } bool ret = false; DBusMessage *reply = success ? dbus_message_new_method_return(msg) : dbus_message_new_error(msg, DBUS_ERROR_FAILED, "invalid notification ID"); if (reply == NULL) goto err; if (!dbus_connection_send(bus->conn, reply, NULL)) goto err; if (dbus_connection_has_messages_to_send(bus->conn)) fdm_event_add(bus->fdm, bus->bus_fd, EPOLLOUT); ret = true; err: dbus_message_unref(reply); return ret; } static DBusHandlerResult dbus_handler(DBusConnection *conn, DBusMessage *msg, void *data) { struct dbus *bus = data; const char *iface __attribute__((unused)) = dbus_message_get_interface(msg); const char *member = dbus_message_get_member(msg); LOG_DBG("%s:%s", iface, member); static const struct { const char *name; bool (*handler)(struct dbus *bus, DBusMessage *msg); } handlers[] = { {"GetServerInformation", &get_server_information}, {"GetCapabilities", &get_capabilities}, {"Notify", ¬ify}, {"CloseNotification", &close_notification}, }; for (size_t i = 0; i < sizeof(handlers) / sizeof(handlers[0]); i++) { if (strcmp(handlers[i].name, member) != 0) continue; return handlers[i].handler(bus, msg) ? DBUS_HANDLER_RESULT_HANDLED: DBUS_HANDLER_RESULT_NOT_YET_HANDLED; } return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; } static bool signal_notification_closed(struct dbus *bus, uint32_t id, uint32_t reason) { DBusMessage *signal = dbus_message_new_signal( "/org/freedesktop/Notifications", "org.freedesktop.Notifications", "NotificationClosed"); if (signal == NULL) return false; DBusMessageIter iter; dbus_message_iter_init_append(signal, &iter); dbus_message_iter_append_basic(&iter, DBUS_TYPE_UINT32, &(dbus_uint32_t){id}); dbus_message_iter_append_basic(&iter, DBUS_TYPE_UINT32, &(dbus_uint32_t){reason}); dbus_connection_send(bus->conn, signal, NULL); dbus_message_unref(signal); if (dbus_connection_has_messages_to_send(bus->conn)) fdm_event_add(bus->fdm, bus->bus_fd, EPOLLOUT); return true; } bool dbus_signal_expired(struct dbus *bus, uint32_t id) { return signal_notification_closed(bus, id, 0); } bool dbus_signal_dismissed(struct dbus *bus, uint32_t id) { return signal_notification_closed(bus, id, 1); } bool dbus_signal_closed(struct dbus *bus, uint32_t id) { return signal_notification_closed(bus, id, 2); } bool dbus_signal_action(struct dbus *bus, uint32_t id, const char *action_id) { DBusMessage *signal = dbus_message_new_signal( "/org/freedesktop/Notifications", "org.freedesktop.Notifications", "ActionInvoked"); if (signal == NULL) return false; DBusMessageIter iter; dbus_message_iter_init_append(signal, &iter); dbus_message_iter_append_basic(&iter, DBUS_TYPE_UINT32, &(dbus_uint32_t){id}); dbus_message_iter_append_basic(&iter, DBUS_TYPE_STRING, &action_id); dbus_connection_send(bus->conn, signal, NULL); dbus_message_unref(signal); if (dbus_connection_has_messages_to_send(bus->conn)) fdm_event_add(bus->fdm, bus->bus_fd, EPOLLOUT); return true; } static bool fdm_handler(struct fdm *fdm, int fd, int events, void *data) { bool ret = false; struct dbus *bus = data; bool had_outgoing = dbus_connection_has_messages_to_send(bus->conn); /* A sending function added EPOLLOUT when it wasn't necessary */ if ((events & EPOLLOUT) && !had_outgoing) { LOG_WARN("EPOLLOUT set, but no outgoing messages"); fdm_event_del(bus->fdm, bus->bus_fd, EPOLLOUT); } if (!dbus_connection_read_write(bus->conn, 0)) { LOG_ERRNO("failed to read/write dbus connection"); goto err; } /* Remove EPOLLOUT when no longer needed */ if (had_outgoing && !dbus_connection_has_messages_to_send(bus->conn)) fdm_event_del(bus->fdm, bus->bus_fd, EPOLLOUT); while (dbus_connection_dispatch(bus->conn) != DBUS_DISPATCH_COMPLETE) ; ret = true; err: if (events & EPOLLHUP) { LOG_INFO("disconnected from DBus"); return false; } return ret; } struct dbus * dbus_init(const struct config *conf, struct fdm *fdm, struct wayland *wayl, struct notif_mgr *notif_mgr, const icon_theme_list_t *icon_theme) { struct dbus *bus = NULL; DBusError dbus_error; dbus_error_init(&dbus_error); DBusConnection *conn; conn = dbus_bus_get(DBUS_BUS_SESSION, &dbus_error); if (dbus_error_is_set(&dbus_error)) { LOG_ERR("failed to connect to D-Bus session bus: %s", dbus_error.message); dbus_error_free(&dbus_error); } if (conn == NULL) return NULL; int ret = dbus_bus_request_name( conn, "org.freedesktop.Notifications", DBUS_NAME_FLAG_DO_NOT_QUEUE, &dbus_error); if (dbus_error_is_set(&dbus_error)) { LOG_ERR("failed to acquire service name: %s", dbus_error.message); dbus_error_free(&dbus_error); goto err; } if (ret != DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER) { LOG_ERR( "failed to acquire service name: not primary owner, ret = %d", ret); if (ret == DBUS_REQUEST_NAME_REPLY_EXISTS) LOG_ERR("is a notification daemon already running?"); goto err; } bus = malloc(sizeof(*bus)); *bus = (struct dbus) { .conn = conn, .conf = conf, .fdm = fdm, .wayl = wayl, .notif_mgr = notif_mgr, .icon_theme = icon_theme, .bus_fd = -1, /* TODO: use watches */ }; static const DBusObjectPathVTable handler = { .message_function = &dbus_handler, }; if (!dbus_connection_register_object_path( conn, "/org/freedesktop/Notifications", &handler, bus)) { LOG_ERR("failed to register vtable"); goto err; } /* TODO: use watches */ if (!dbus_connection_get_unix_fd(conn, &bus->bus_fd)) { if (!dbus_connection_get_socket(conn, &bus->bus_fd)) { LOG_ERR("failed to get socket or UNIX FD"); goto err; } } assert(bus->bus_fd != -1); if (!fdm_add(fdm, bus->bus_fd, EPOLLIN, fdm_handler, bus)) { LOG_ERR("failed to register with FDM"); goto err; } return bus; err: if (conn != NULL) dbus_connection_unref(conn); if (bus != NULL) free(bus); return NULL; } void dbus_destroy(struct dbus *bus) { if (bus == NULL) return; fdm_del_no_close(bus->fdm, bus->bus_fd); dbus_connection_unref(bus->conn); free(bus); } int dbus_poll_fd(const struct dbus *bus) { /* TODO: use watches */ int fd = -1; if (!dbus_connection_get_unix_fd(bus->conn, &fd)) { if (!dbus_connection_get_socket(bus->conn, &fd)) { LOG_ERR("failed to get socket or UNIX FD"); return -1; } } assert(fd != -1); return fd; } fnott-1.4.1+ds/dbus.h000066400000000000000000000011661447512336000143700ustar00rootroot00000000000000#pragma once #include #include "notification.h" #include "wayland.h" #include "fdm.h" #include "icon.h" struct dbus; struct dbus *dbus_init( const struct config *conf, struct fdm *fdm, struct wayland *wayl, struct notif_mgr *notif_mgr, const icon_theme_list_t *icon_theme); void dbus_destroy(struct dbus *bus); bool dbus_signal_expired(struct dbus *bus, uint32_t id); bool dbus_signal_dismissed(struct dbus *bus, uint32_t id); bool dbus_signal_closed(struct dbus *bus, uint32_t id); bool dbus_signal_action(struct dbus *bus, uint32_t id, const char *action_id); int dbus_poll_fd(const struct dbus *bus); fnott-1.4.1+ds/doc/000077500000000000000000000000001447512336000140235ustar00rootroot00000000000000fnott-1.4.1+ds/doc/fnott.1.scd000066400000000000000000000034321447512336000160110ustar00rootroot00000000000000fnott(1) # NAME fnott - keyboard driven notification daemon for Wayland # SYNOPSIS *fnott*++ *fnott* *--version* # OPTIONS *-c*,*--config*=_PATH_ Path to configuration file. Default: *$XDG_CONFIG_HOME/fnott/fnott.ini*. *-p*,*--print-pid*=_FILE_|_FD_ Print PID to this file, or FD, when successfully started. The file (or FD) is closed immediately after writing the PID. When a _FILE_ has been specified, the file is unlinked on exit. *-l*,*--log-colorize*=[{*never*,*always*,*auto*}] Enables or disables colorization of log output on stderr. *-s*,*--log-no-syslog* Disables syslog logging. Logging is only done on stderr. *-v*,*--version* Show the version number and quit # DESCRIPTION *fnott* is a keyboard driven and lightweight notification daemon for wlroots-based Wayland compositors, implementing (parts of) the Desktop Notification Specification. *fnott* is the daemon part and should be launched when you log in to your desktop. You should be able to configure your Wayland compositor to autolaunch it for you. With the daemon running, you will be able to receive and display notifications: notify-send "this is the summary" "this is the body" Use *fnottctl*(1) to interact with it: fnottctl dismiss Or simply click on a notification to dismiss it. Notifications are prioritized in the following way: - urgency - normal notifications always have higher priority than low-urgency notifications, and critical notifications always have higher priority than normal notifications. - time - older notifications have higher priority than newer notifications. Notifications are displayed with the lowest priority ones at the top, and the highest priority ones at the bottom (this is subject to change). # CONFIGURATION See *fnott.ini*(5) # SEE ALSO *fnott.ini*(5), *fnottctl*(1) fnott-1.4.1+ds/doc/fnott.ini.5.scd000066400000000000000000000217561447512336000166040ustar00rootroot00000000000000fnott(5) # NAME fnott - configuration file # DESCRIPTION *fnott* uses the standard _unix configuration format_, with section based key/value pairs. The default (global) section is unnamed (i.e. not prefixed with a _[section]_). fnott will search for a configuration file in the following locations, in this order: - *XDG_CONFIG_HOME/fnott/fnott.ini* (defaulting to *~/.config/fnott/fnott.ini* if unset) - *XDG_CONFIG_DIRS/fnott/fnott.ini* (defaulting to */etc/xdg/fnott/fnott.ini* if unset) # SECTION: default ## Global options *output* The output to display notifications on. If left unspecified, the compositor will choose one for us. Note that if you *do not* specify an output, and the output chosen by the compositor is scaled, then each new notification will flash a low-res frame before re-rendering with the correct scale factor. This is because fnott has no way of knowing what the scale factor is until *after* the notification has been mapped (i.e. shown). Default: _unspecified_. In Sway, you can use *swaymsg -t get_outputs* to get a list of available outputs. *min-width* Minimum notification width, in pixels. Default: _0_. *max-width* Maximum notification width, in pixels. 0 means unlimited. Note that fnott will automatically word-wrap the notification text if set to a non-zero value. Default: _0_. *max-height* Maximum notification height, in pixels. 0 means unlimited. Default: _0_. *icon-theme* Icon theme to use when a notification has requested an non-embedded icon. Default: _hicolor_. *max-icon-size* Maximum icon size, in pixels. Icons larger than this will be scaled down. Default: _32_. *stacking-order* How to stack multiple notifications. - *bottom-up* : oldest, highest priority furthest away from the anchor point. - *top-down*: oldest, highest priority at the anchor point. Thus, if the notifications are anchored at the top, *bottom-up* will have the most recent notification in the upper corner, while the oldest notification is in the bottom of the stack. Default: _bottom-up_. *anchor* Where notifications are positioned: *top-left*, *top-right*, *bottom-left* or *bottom-right*. Default: _top-right_. *layer* Layer on which notifications will appear: *background*, *bottom*, *top* or *overlay*. Default: _top_. *edge-margin-vertical* Vertical margin, in pixels, between the screen edge (top or bottom, depending on anchor pointer) and notifications. Default: _10_. *edge-margin-horizontal* Horizontal margin, in pixels, between the screen edge (left or right, depending on anchor pointer) and notifications. Default: _10_. *notification-margin* Margin between notifications. Default: _10_. *selection-helper* Command (and optionally arguments) to execute to display actions and let the user select an action to run. The utility should accept (action) entries to display on stdin (newline separated), and write the selected entry on stdout. Default: _dmenu_. *play-sound* Command to execute to play notification sounds. _${filename}_ will be expanded to the path to the audio file to play. Default: _aplay ${filename}_. ## Per-urgency default options These options can also be specified in an _urgency_ section, in which case they override the values specified in the default section. *background* Background color of the notification, in RGBA format. Default: _3f5f3fff_. *border-color* Border color of the notification, in RGBA format. Default: _909090ff_. *border-size* Border size, in pixels. Default: _1_. *padding-vertical* Vertical padding, in pixels, between the notification edge (top or bottom) and notification text. Default: _20_. *padding-horizontal* Horizontal padding, in pixels, between the notification edge (left or right) and notification text. Default: _20_. *dpi-aware* *auto*, *yes*, or *no*. When set to *yes*, fonts are sized using the monitor's DPI, making a font of a given size have the same physical size, regardless of monitor. In this mode, the monitor's scaling factor is ignored; doubling the scaling factor will *not* double the font size. When set to *no*, the monitor's DPI is ignored. The font is instead sized using the monitor's scaling factor; doubling the scaling factor *does* double the font size. Finally, if set to *auto*, fonts will be sized using the monitor's DPI if _all_ monitors have a scaling factor of 1. If at least one monitor as a scaling factor larger than 1 (regardless of whether the fnott window is mapped on that monitor or not), fonts will be scaled using the scaling factor. Note that this option typically does not work with bitmap fonts, which only contains a pre-defined set of sizes, and cannot be dynamically scaled. Whichever size (of the available ones) that best matches the DPI or scaling factor, will be used. Also note that if the font size has been specified in pixels (*:pixelsize=*_N_, instead of *:size=*_N_), DPI scaling (*dpi-aware=yes*) will have no effect (the specified pixel size will be used as is). But, if the monitor's scaling factor is used to size the font (*dpi-aware=no*), the font's pixel size will be multiplied with the scaling factor. Default: _auto_ *title-font* Font to use for the application title, in fontconfig format (see *FONT FORMAT*). Default: _sans serif_. *title-color* Text color to use for the application title, in RGBA format. Default: _ffffffff_. *title-format* Template string for the title portion of the notification (see *FORMAT STRINGS*). Default: _%a%A_. *summary-font* Font to use for the summary, in fontconfig format (see *FONT FORMAT*). Default: _sans serif_. *summary-color* Text color to use for the summary, in RGBA format. Default: _ffffffff_. *summary-format* Template string for the summary portion of the notification (see *FORMAT STRINGS*). Default: _%s\\n_. *body-font* Font to use for the text body, in fontconfig format (see *FONT FORMAT*). Default: _sans serif_. *body-color* Text color to use for the text body, in RGBA format. Default: _ffffffff_. *body-format* Template string for the body portion of the notification (see *FORMAT STRINGS*). Default: _%b_. *progress-bar-height* Height, in pixels, of progress bars (rendered when a notification has an _int:value_ hint). Default: _20_. *progress-bar-color* Color, in RGBA format, of progress bars. Default: _ffffffff_. *max-timeout* Time limit, in seconds, before notifications are automatically dismissed. Applications can provide their own timeout when they create a notification. This option can be used to limit that timeout. A value of 0 disables the limit. Default: _0_. *default-timeout* Time, in seconds, before notifications are automatically dismissed if the notifying application does not specify a timeout. A value of 0 disables the timeout. I.e. if the application does not provide a timeout, the notification is never automatically dismissed (unless *max-timeout* has been set). Default: _0_. *idle-timeout* Time, in seconds, that you must be idle to prevent any notification from being dismissed. A value of 0 disables the timeout. Default: _0_. *sound-file* Absolute path to audio file to play when a notification is received. If unset, no sound is played. Default: _unset_. *icon* Icon to use when none is provided by the notifications themselves. Can be either an absolute path, or a name (without extension). In the latter case, it will be searched for in the selected icon theme, using the fallback rules defined by the XDG icon theme specification. Default: _unset_. # SECTION: low This section allows you to override the options listed under *per-urgency default options* for _low_ priority notifications. By default, the following options are already overridden: - *background*: _2b2b2bff_ - *title-color* _888888ff_ - *summary-color*: _888888ff_ - *body-color*: _888888ff_ # SECTION: normal This section allows you to override the options listed under *per-urgency default options* for _normal_ priority notifications. By default, the following options are already overridden: _none_. # SECTION: critical This section allows you to override the options listed under *per-urgency default options* for _critical_ priority notifications. By default, the following options are already overridden: - *background*: _6c3333ff_ # FONT FORMAT The font is specified in FontConfig syntax. That is, a colon-separated list of font name and font options. _Examples_: - Dina:weight=bold:slant=italic - Courier New:size=12 # FORMAT STRINGS The *title-format*, *summary-format* and *body-format* options allow you to configure what to display for the corresponding portion of the notification. They are strings with placeholders that are expanded with attributes from the notification: - *%a* application name - *%s* notification summary - *%b* notification body text - *%A* action indicator ('\*' if actions are present, empty string otherwise) - *%%* a literal '%' - *\\n* a literal newline Also supported are the following markup tags: - ** bold - ** italic - ** underline # SEE ALSO *fnott*(1) fnott-1.4.1+ds/doc/fnottctl.1.scd000066400000000000000000000022431447512336000165130ustar00rootroot00000000000000fnottctl(1) # NAME fnottctl - utility to interact with *fnott*(1) # SYNOPSIS *fnottctl* *dismiss* [_id_]++ *fnottctl* *actions* [_id_]++ *fnottctl* *list*++ *fnottctl* *quit*++ *fnottctl* *--version* # OPTIONS *id* The notification ID to dismiss or show actions for. Use *fnottctl list* to see the IDs of currently active notifications. *-v*,*--version* Show the version number and quit # DESCRIPTION *fnottctl* is used to interact (dismiss notifications, show and select action for notifications) with *fnott*(1). The most common operation is *fnottctl dismiss*. This will dismiss the highest priority notification. You might want to bind this to a keyboard shortcut in your Wayland compositor configuration. To see, and select between, actions associated with the notification, use *fnottctl actions*. This requires a dmenu-like utility to have been configured in *fnott.ini*(5). You can optionally specify a notification ID, to dismiss (or show actions for) a specific notification instead of the highest priority one. For _dismiss_, there is also the special ID _all_ which will, not unsurprisingly, dismiss *all* notifications. # SEE ALSO *fnott*(1), *fnott.ini*(5) fnott-1.4.1+ds/doc/meson.build000066400000000000000000000012741447512336000161710ustar00rootroot00000000000000sh = find_program('sh', native: true) scdoc = dependency('scdoc', native: true) scdoc_prog = find_program(scdoc.get_variable('scdoc'), native: true) foreach man_src : [{'name': 'fnott', 'section': 1}, {'name': 'fnott.ini', 'section': 5}, {'name': 'fnottctl', 'section': 1}] name = man_src['name'] section = man_src['section'] out = '@0@.@1@'.format(name, section) custom_target( out, output: out, input: '@0@.@1@.scd'.format(name, section), 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))) endforeach fnott-1.4.1+ds/external/000077500000000000000000000000001447512336000151005ustar00rootroot00000000000000fnott-1.4.1+ds/external/idle.xml000066400000000000000000000045621447512336000165460ustar00rootroot00000000000000 . ]]> This interface allows to monitor user idle time on a given seat. The interface allows to register timers which trigger after no user activity was registered on the seat for a given interval. It notifies when user activity resumes. This is useful for applications wanting to perform actions when the user is not interacting with the system, e.g. chat applications setting the user as away, power management features to dim screen, etc.. fnott-1.4.1+ds/external/wlr-layer-shell-unstable-v1.xml000066400000000000000000000440361447512336000230130ustar00rootroot00000000000000 Copyright © 2017 Drew DeVault Permission to use, copy, modify, distribute, and sell this software and its documentation for any purpose is hereby granted without fee, provided that the above copyright notice appear in all copies and that both that copyright notice and this permission notice appear in supporting documentation, and that the name of the copyright holders not be used in advertising or publicity pertaining to distribution of the software without specific, written prior permission. The copyright holders make no representations about the suitability of this software for any purpose. It is provided "as is" without express or implied warranty. THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. Clients can use this interface to assign the surface_layer role to wl_surfaces. Such surfaces are assigned to a "layer" of the output and rendered with a defined z-depth respective to each other. They may also be anchored to the edges and corners of a screen and specify input handling semantics. This interface should be suitable for the implementation of many desktop shell components, and a broad number of other applications that interact with the desktop. Create a layer surface for an existing surface. This assigns the role of layer_surface, or raises a protocol error if another role is already assigned. Creating a layer surface from a wl_surface which has a buffer attached or committed is a client error, and any attempts by a client to attach or manipulate a buffer prior to the first layer_surface.configure call must also be treated as errors. After creating a layer_surface object and setting it up, the client must perform an initial commit without any buffer attached. The compositor will reply with a layer_surface.configure event. The client must acknowledge it and is then allowed to attach a buffer to map the surface. You may pass NULL for output to allow the compositor to decide which output to use. Generally this will be the one that the user most recently interacted with. Clients can specify a namespace that defines the purpose of the layer surface. These values indicate which layers a surface can be rendered in. They are ordered by z depth, bottom-most first. Traditional shell surfaces will typically be rendered between the bottom and top layers. Fullscreen shell surfaces are typically rendered at the top layer. Multiple surfaces can share a single layer, and ordering within a single layer is undefined. This request indicates that the client will not use the layer_shell object any more. Objects that have been created through this instance are not affected. An interface that may be implemented by a wl_surface, for surfaces that are designed to be rendered as a layer of a stacked desktop-like environment. Layer surface state (layer, size, anchor, exclusive zone, margin, interactivity) is double-buffered, and will be applied at the time wl_surface.commit of the corresponding wl_surface is called. Attaching a null buffer to a layer surface unmaps it. Unmapping a layer_surface means that the surface cannot be shown by the compositor until it is explicitly mapped again. The layer_surface returns to the state it had right after layer_shell.get_layer_surface. The client can re-map the surface by performing a commit without any buffer attached, waiting for a configure event and handling it as usual. Sets the size of the surface in surface-local coordinates. The compositor will display the surface centered with respect to its anchors. If you pass 0 for either value, the compositor will assign it and inform you of the assignment in the configure event. You must set your anchor to opposite edges in the dimensions you omit; not doing so is a protocol error. Both values are 0 by default. Size is double-buffered, see wl_surface.commit. Requests that the compositor anchor the surface to the specified edges and corners. If two orthogonal edges are specified (e.g. 'top' and 'left'), then the anchor point will be the intersection of the edges (e.g. the top left corner of the output); otherwise the anchor point will be centered on that edge, or in the center if none is specified. Anchor is double-buffered, see wl_surface.commit. Requests that the compositor avoids occluding an area with other surfaces. The compositor's use of this information is implementation-dependent - do not assume that this region will not actually be occluded. A positive value is only meaningful if the surface is anchored to one edge or an edge and both perpendicular edges. If the surface is not anchored, anchored to only two perpendicular edges (a corner), anchored to only two parallel edges or anchored to all edges, a positive value will be treated the same as zero. A positive zone is the distance from the edge in surface-local coordinates to consider exclusive. Surfaces that do not wish to have an exclusive zone may instead specify how they should interact with surfaces that do. If set to zero, the surface indicates that it would like to be moved to avoid occluding surfaces with a positive exclusive zone. If set to -1, the surface indicates that it would not like to be moved to accommodate for other surfaces, and the compositor should extend it all the way to the edges it is anchored to. For example, a panel might set its exclusive zone to 10, so that maximized shell surfaces are not shown on top of it. A notification might set its exclusive zone to 0, so that it is moved to avoid occluding the panel, but shell surfaces are shown underneath it. A wallpaper or lock screen might set their exclusive zone to -1, so that they stretch below or over the panel. The default value is 0. Exclusive zone is double-buffered, see wl_surface.commit. Requests that the surface be placed some distance away from the anchor point on the output, in surface-local coordinates. Setting this value for edges you are not anchored to has no effect. The exclusive zone includes the margin. Margin is double-buffered, see wl_surface.commit. Types of keyboard interaction possible for layer shell surfaces. The rationale for this is twofold: (1) some applications are not interested in keyboard events and not allowing them to be focused can improve the desktop experience; (2) some applications will want to take exclusive keyboard focus. This value indicates that this surface is not interested in keyboard events and the compositor should never assign it the keyboard focus. This is the default value, set for newly created layer shell surfaces. This is useful for e.g. desktop widgets that display information or only have interaction with non-keyboard input devices. Request exclusive keyboard focus if this surface is above the shell surface layer. For the top and overlay layers, the seat will always give exclusive keyboard focus to the top-most layer which has keyboard interactivity set to exclusive. If this layer contains multiple surfaces with keyboard interactivity set to exclusive, the compositor determines the one receiving keyboard events in an implementation- defined manner. In this case, no guarantee is made when this surface will receive keyboard focus (if ever). For the bottom and background layers, the compositor is allowed to use normal focus semantics. This setting is mainly intended for applications that need to ensure they receive all keyboard events, such as a lock screen or a password prompt. This requests the compositor to allow this surface to be focused and unfocused by the user in an implementation-defined manner. The user should be able to unfocus this surface even regardless of the layer it is on. Typically, the compositor will want to use its normal mechanism to manage keyboard focus between layer shell surfaces with this setting and regular toplevels on the desktop layer (e.g. click to focus). Nevertheless, it is possible for a compositor to require a special interaction to focus or unfocus layer shell surfaces (e.g. requiring a click even if focus follows the mouse normally, or providing a keybinding to switch focus between layers). This setting is mainly intended for desktop shell components (e.g. panels) that allow keyboard interaction. Using this option can allow implementing a desktop shell that can be fully usable without the mouse. Set how keyboard events are delivered to this surface. By default, layer shell surfaces do not receive keyboard events; this request can be used to change this. This setting is inherited by child surfaces set by the get_popup request. Layer surfaces receive pointer, touch, and tablet events normally. If you do not want to receive them, set the input region on your surface to an empty region. Keyboard interactivity is double-buffered, see wl_surface.commit. This assigns an xdg_popup's parent to this layer_surface. This popup should have been created via xdg_surface::get_popup with the parent set to NULL, and this request must be invoked before committing the popup's initial state. See the documentation of xdg_popup for more details about what an xdg_popup is and how it is used. When a configure event is received, if a client commits the surface in response to the configure event, then the client must make an ack_configure request sometime before the commit request, passing along the serial of the configure event. If the client receives multiple configure events before it can respond to one, it only has to ack the last configure event. A client is not required to commit immediately after sending an ack_configure request - it may even ack_configure several times before its next surface commit. A client may send multiple ack_configure requests before committing, but only the last request sent before a commit indicates which configure event the client really is responding to. This request destroys the layer surface. The configure event asks the client to resize its surface. Clients should arrange their surface for the new states, and then send an ack_configure request with the serial sent in this configure event at some point before committing the new surface. The client is free to dismiss all but the last configure event it received. The width and height arguments specify the size of the window in surface-local coordinates. The size is a hint, in the sense that the client is free to ignore it if it doesn't resize, pick a smaller size (to satisfy aspect ratio or resize in steps of NxM pixels). If the client picks a smaller size and is anchored to two opposite anchors (e.g. 'top' and 'bottom'), the surface will be centered on this axis. If the width or height arguments are zero, it means the client should decide its own window dimension. The closed event is sent by the compositor when the surface will no longer be shown. The output may have been destroyed or the user may have asked for it to be removed. Further changes to the surface will be ignored. The client should destroy the resource after receiving this event, and create a new surface if they so choose. Change the layer that the surface is rendered on. Layer is double-buffered, see wl_surface.commit. fnott-1.4.1+ds/external/wlr-protocols/000077500000000000000000000000001447512336000177265ustar00rootroot00000000000000fnott-1.4.1+ds/fdm.c000066400000000000000000000116561447512336000142010ustar00rootroot00000000000000#include "fdm.h" #include #include #include #include #include #include #include #define LOG_MODULE "fdm" #define LOG_ENABLE_DBG 0 #include "log.h" struct handler { int fd; int events; fdm_handler_t callback; void *callback_data; bool deleted; }; struct fdm { int epoll_fd; bool is_polling; tll(struct handler *) fds; tll(struct handler *) deferred_delete; }; struct fdm * fdm_init(void) { int epoll_fd = epoll_create1(EPOLL_CLOEXEC); if (epoll_fd == -1) { LOG_ERRNO("failed to create epoll FD"); return NULL; } struct fdm *fdm = malloc(sizeof(*fdm)); *fdm = (struct fdm){ .epoll_fd = epoll_fd, .is_polling = false, .fds = tll_init(), .deferred_delete = tll_init(), }; return fdm; } void fdm_destroy(struct fdm *fdm) { if (fdm == NULL) return; if (tll_length(fdm->fds) > 0) LOG_WARN("FD list not empty"); assert(tll_length(fdm->fds) == 0); assert(tll_length(fdm->deferred_delete) == 0); tll_free(fdm->fds); tll_free(fdm->deferred_delete); close(fdm->epoll_fd); free(fdm); } bool fdm_add(struct fdm *fdm, int fd, int events, fdm_handler_t handler, void *data) { #if defined(_DEBUG) tll_foreach(fdm->fds, it) { if (it->item->fd == fd) { LOG_ERR("FD=%d already registered", fd); return false; } } #endif struct handler *fd_data = malloc(sizeof(*fd_data)); *fd_data = (struct handler) { .fd = fd, .events = events, .callback = handler, .callback_data = data, .deleted = false, }; tll_push_back(fdm->fds, fd_data); struct epoll_event ev = { .events = events, .data = {.ptr = fd_data}, }; if (epoll_ctl(fdm->epoll_fd, EPOLL_CTL_ADD, fd, &ev) < 0) { LOG_ERRNO("failed to register FD=%d with epoll", fd); free(fd_data); tll_pop_back(fdm->fds); return false; } return true; } static bool fdm_del_internal(struct fdm *fdm, int fd, bool close_fd) { if (fd == -1) return true; tll_foreach(fdm->fds, it) { if (it->item->fd != fd) continue; if (epoll_ctl(fdm->epoll_fd, EPOLL_CTL_DEL, fd, NULL) < 0) LOG_ERRNO("failed to unregister FD=%d from epoll", fd); if (close_fd) close(it->item->fd); it->item->deleted = true; if (fdm->is_polling) tll_push_back(fdm->deferred_delete, it->item); else free(it->item); tll_remove(fdm->fds, it); return true; } LOG_ERR("no such FD: %d", fd); return false; } bool fdm_del(struct fdm *fdm, int fd) { return fdm_del_internal(fdm, fd, true); } bool fdm_del_no_close(struct fdm *fdm, int fd) { return fdm_del_internal(fdm, fd, false); } static bool event_modify(struct fdm *fdm, struct handler *fd, int new_events) { if (new_events == fd->events) return true; struct epoll_event ev = { .events = new_events, .data = {.ptr = fd}, }; if (epoll_ctl(fdm->epoll_fd, EPOLL_CTL_MOD, fd->fd, &ev) < 0) { LOG_ERRNO("failed to modify FD=%d with epoll (events 0x%08x -> 0x%08x)", fd->fd, fd->events, new_events); return false; } fd->events = new_events; return true; } bool fdm_event_add(struct fdm *fdm, int fd, int events) { tll_foreach(fdm->fds, it) { if (it->item->fd != fd) continue; return event_modify(fdm, it->item, it->item->events | events); } LOG_ERR("FD=%d not registered with the FDM", fd); return false; } bool fdm_event_del(struct fdm *fdm, int fd, int events) { tll_foreach(fdm->fds, it) { if (it->item->fd != fd) continue; return event_modify(fdm, it->item, it->item->events & ~events); } LOG_ERR("FD=%d not registered with the FDM", fd); return false; } bool fdm_poll(struct fdm *fdm) { assert(!fdm->is_polling && "nested calls to fdm_poll() not allowed"); if (fdm->is_polling) { LOG_ERR("nested calls to fdm_poll() not allowed"); return false; } struct epoll_event events[tll_length(fdm->fds)]; int r = epoll_wait(fdm->epoll_fd, events, tll_length(fdm->fds), -1); if (r == -1) { if (errno == EINTR) return true; LOG_ERRNO("failed to epoll"); return false; } bool ret = true; fdm->is_polling = true; for (int i = 0; i < r; i++) { struct handler *fd = events[i].data.ptr; if (fd->deleted) continue; if (!fd->callback(fdm, fd->fd, events[i].events, fd->callback_data)) { ret = false; break; } } fdm->is_polling = false; tll_foreach(fdm->deferred_delete, it) { free(it->item); tll_remove(fdm->deferred_delete, it); } return ret; } fnott-1.4.1+ds/fdm.h000066400000000000000000000010031447512336000141670ustar00rootroot00000000000000#pragma once #include struct fdm; typedef bool (*fdm_handler_t)(struct fdm *fdm, int fd, int events, void *data); struct fdm *fdm_init(void); void fdm_destroy(struct fdm *fdm); bool fdm_add(struct fdm *fdm, int fd, int events, fdm_handler_t handler, void *data); bool fdm_del(struct fdm *fdm, int fd); bool fdm_del_no_close(struct fdm *fdm, int fd); bool fdm_event_add(struct fdm *fdm, int fd, int events); bool fdm_event_del(struct fdm *fdm, int fd, int events); bool fdm_poll(struct fdm *fdm); fnott-1.4.1+ds/fnott.desktop000066400000000000000000000002531447512336000160030ustar00rootroot00000000000000[Desktop Entry] Type=Application Exec=fnott Icon=notifications-symbolic Terminal=false Categories=System;Monitor;Core Name=Fnott Comment=Lightweight notifications daemon fnott-1.4.1+ds/fnott.ini000066400000000000000000000021541447512336000151130ustar00rootroot00000000000000# -*- conf -*- # For documentation on these options, see `man fnott.ini` # Global values # output=# # min-width=0 # max-width=0 # max-height=0 # stacking-order=bottom-up # anchor=top-right # edge-margin-vertical=10 # edge-margin-horizontal=10 # notification-margin=10 # icon-theme=hicolor # max-icon-size=32 # selection-helper=dmenu # play-sound=aplay ${filename} # layer=top # Default values, may be overridden in 'urgency' specific sections # background=3f5f3fff # border-color=909090ff # border-size=1 # padding-vertical=20 # padding-horizontal=20 # dpi-aware=auto # title-font=sans serif # title-color=ffffffff # title-format=%a%A # summary-font=sans serif # summary-color=ffffffff # summary-format=%s\n # body-font=sans serif # body-color=ffffffff # body-format=%b # progress-bar-height=20 # progress-bar-color=ffffffff # sound-file= # icon= # Timeout values are in seconds. 0 to disable # max-timeout=0 # default-timeout=0 # idle-timeout=0 # [low] # background=2b2b2bff # title-color=888888ff # summary-color=888888ff # body-color=888888ff # [normal] # [critical] # background=6c3333ff fnott-1.4.1+ds/fnottctl.c000066400000000000000000000140111447512336000152540ustar00rootroot00000000000000#include #include #include #include #include #include #include #include #include #define LOG_MODULE "main" #define LOG_ENABLE_DBG 0 #include "log.h" #include "ctrl-protocol.h" #include "version.h" static void print_usage(const char *prog) { printf("Usage: %s dismiss | actions []\n" " %s list | quit\n" " %s --version\n" "\n" "Options:\n" " id notification ID to dismiss or show actions for\n" " -v,--version show the version number and quit\n", prog, prog, prog); } int main(int argc, char *const *argv) { const char *const prog = argv[0]; static const struct option longopts[] = { {"version", no_argument, 0, 'v'}, {"help", no_argument, 0, 'h'}, {NULL, no_argument, 0, 0}, }; while (true) { int c = getopt_long(argc, argv, "+:vh", longopts, NULL); if (c == -1) break; switch (c) { case 'v': printf("fnottctl version %s\n", FNOTT_VERSION); return EXIT_SUCCESS; case 'h': print_usage(prog); return EXIT_SUCCESS; case ':': fprintf(stderr, "error: -%c: missing required argument\n", optopt); return EXIT_FAILURE; case '?': fprintf(stderr, "error: -%c: invalid option\n", optopt); return EXIT_FAILURE; } } argc -= optind; argv += optind; if (argc < 1) { print_usage(prog); return EXIT_FAILURE; } log_init(LOG_COLORIZE_AUTO, false, LOG_FACILITY_USER, LOG_CLASS_DEBUG); bool have_id = argc >= 2; const char *cmd_word = argv[0]; const char *id_str = have_id ? argv[1] : NULL; /* Which command should we execute? */ enum ctrl_command cmd_type; if (strcmp(cmd_word, "quit") == 0) cmd_type = CTRL_QUIT; else if (strcmp(cmd_word, "dismiss") == 0) { cmd_type = have_id && strcmp(id_str, "all") == 0 ? CTRL_DISMISS_ALL : CTRL_DISMISS_BY_ID; } else if (strcmp(cmd_word, "actions") == 0) cmd_type = CTRL_ACTIONS_BY_ID; else if (strcmp(cmd_word, "list") == 0) cmd_type = CTRL_LIST; else { LOG_ERR("%s: invalid command", cmd_word); return EXIT_FAILURE; } /* With which ID? */ uint32_t id; switch (cmd_type) { case CTRL_DISMISS_BY_ID: case CTRL_ACTIONS_BY_ID: if (have_id) { char *end = NULL; errno = 0; id = strtoul(id_str, &end, 0); if (errno != 0 || *end != '\0') { LOG_ERR( "%s: invalid notification ID (expected an integer)", id_str); return EXIT_FAILURE; } } else id = 0; break; case CTRL_QUIT: case CTRL_LIST: case CTRL_DISMISS_ALL: id = 0; break; } int ret = EXIT_FAILURE; int fd = socket(AF_UNIX, SOCK_STREAM, 0); if (fd == -1) { LOG_ERRNO("failed to create socket"); goto err; } bool connected = false; struct sockaddr_un addr = {.sun_family = AF_UNIX}; const char *xdg_runtime = getenv("XDG_RUNTIME_DIR"); if (xdg_runtime != NULL) { const char *wayland_display = getenv("WAYLAND_DISPLAY"); if (wayland_display != NULL) snprintf(addr.sun_path, sizeof(addr.sun_path), "%s/fnott-%s.sock", xdg_runtime, wayland_display); else snprintf(addr.sun_path, sizeof(addr.sun_path), "%s/fnott.sock", xdg_runtime); if (connect(fd, (const struct sockaddr *)&addr, sizeof(addr)) == 0) connected = true; else LOG_WARN("%s: failed to connect, will now try /tmp/fnott.sock", addr.sun_path); } if (!connected) { strncpy(addr.sun_path, "/tmp/fnott.sock", sizeof(addr.sun_path) - 1); if (connect(fd, (const struct sockaddr *)&addr, sizeof(addr)) < 0) { LOG_ERRNO("failed to connect; is fnott running?"); goto err; } } /* TODO: endianness */ struct ctrl_request cmd = { .cmd = cmd_type, .id = id, }; ssize_t sent = send(fd, &cmd, sizeof(cmd), 0); if (sent == -1 || sent != sizeof(cmd)) { LOG_ERRNO("failed to send command"); goto err; } struct ctrl_reply reply; ssize_t rcvd = read(fd, &reply, sizeof(reply)); if (rcvd != sizeof(reply)) { LOG_ERRNO("failed to read reply"); goto err; } if (reply.result == CTRL_OK && cmd_type == CTRL_LIST) { uint64_t count; if (read(fd, &count, sizeof(count)) != sizeof(count)) { LOG_ERRNO("failed to read 'list' response"); goto err; } for (size_t i = 0; i < count; i++) { uint32_t notif_id; uint32_t summary_len; if (read(fd, ¬if_id, sizeof(notif_id)) != sizeof(notif_id) || read(fd, &summary_len, sizeof(summary_len)) != sizeof(summary_len)) { LOG_ERRNO("failed to read 'list' response"); goto err; } char *summary = malloc(summary_len + 1); if (read(fd, summary, summary_len) != summary_len) { LOG_ERRNO("failed to read 'list' response"); free(summary); goto err; } printf("%u: %.*s\n", notif_id, summary_len, summary); } } switch (reply.result) { case CTRL_OK: break; case CTRL_INVALID_ID: fprintf(stderr, "%u: invalid ID\n", id); break; case CTRL_NO_ACTIONS: fprintf(stderr, "%u: no actions\n", id); break; case CTRL_ERROR: fprintf(stderr, "unknown error\n"); break; } ret = reply.result == CTRL_OK ? EXIT_SUCCESS : EXIT_FAILURE; err: if (fd != -1) close(fd); return ret; } fnott-1.4.1+ds/generate-version.sh000077500000000000000000000020051447512336000170670ustar00rootroot00000000000000#!/bin/sh set -e default_version=${1} src_dir=${2} out_file=${3} # echo "default version: ${default_version}" # echo "source directory: ${src_dir}" # echo "output file: ${out_file}" if [ -d "${src_dir}/.git" ] && command -v git > /dev/null; then workdir=$(pwd) cd "${src_dir}" if git describe --tags > /dev/null 2>&1; then git_version=$(git describe --always --tags) else # No tags available, happens in e.g. CI builds git_version="${default_version}" fi git_branch=$(git rev-parse --abbrev-ref HEAD) cd "${workdir}" new_version="${git_version} ($(date "+%b %d %Y"), branch '${git_branch}')" else new_version="${default_version}" fi new_version="#define FNOTT_VERSION \"${new_version}\"" if [ -f "${out_file}" ]; then old_version=$(cat "${out_file}") else old_version="" fi # echo "old version: ${old_version}" # echo "new version: ${new_version}" if [ "${old_version}" != "${new_version}" ]; then echo "${new_version}" > "${out_file}" fi fnott-1.4.1+ds/icon.c000066400000000000000000000347361447512336000143670ustar00rootroot00000000000000#include "icon.h" #include #include #include #include #include #include #include #include #include #include #include #include #define LOG_MODULE "icon" #define LOG_ENABLE_DBG 0 #include "log.h" #include "png-fnott.h" #include "svg.h" #include "xdg.h" enum icon_type { ICON_NONE, ICON_PNG, ICON_SVG }; typedef tll(char *) theme_names_t; pixman_image_t * icon_load(const char *name, int icon_size, const icon_theme_list_t *themes) { pixman_image_t *pix = NULL; if (name[0] == '/') { if ((pix = svg_load(name, icon_size)) != NULL) { LOG_DBG("%s: absolute path SVG", name); return pix; } if ((pix = png_load(name)) != NULL) { LOG_DBG("%s: abslute path PNG", name); return pix; } } const size_t file_name_len = strlen(name) + 4; char file_name[file_name_len + 1]; strcpy(file_name, name); strcat(file_name, ".xxx"); struct { int diff; const struct xdg_data_dir *xdg_dir; const struct icon_theme *theme; const struct icon_dir *icon_dir; enum icon_type type; } min_diff = {.diff = INT_MAX}; /* For details, see * https://specifications.freedesktop.org/icon-theme-spec/icon-theme-spec-latest.html#icon_lookup */ xdg_data_dirs_t xdg_dirs = xdg_data_dirs(); tll_foreach(*themes, theme_it) { const struct icon_theme *theme = &theme_it->item; /* Fallback icon to use if there aren’t any exact matches */ /* Assume sorted */ tll_foreach(theme->dirs, icon_dir_it) { const struct icon_dir *icon_dir = &icon_dir_it->item; char theme_relative_path[ 5 + 1 + /* “icons” */ strlen(theme->name) + 1 + strlen(icon_dir->path) + 1]; sprintf(theme_relative_path, "icons/%s/%s", theme->name, icon_dir->path); tll_foreach(xdg_dirs, xdg_dir_it) { const struct xdg_data_dir *xdg_dir = &xdg_dir_it->item; const int scale = icon_dir->scale; const int size = icon_dir->size * scale; const int min_size = icon_dir->min_size * scale; const int max_size = icon_dir->max_size * scale; const int threshold = icon_dir->threshold * scale; const enum icon_dir_type type = icon_dir->type; bool is_exact_match = false;; int diff = INT_MAX; /* See if this directory is usable for the requested icon size */ switch (type) { case ICON_DIR_FIXED: is_exact_match = size == icon_size; diff = abs(size - icon_size); break; case ICON_DIR_THRESHOLD: is_exact_match = (size - threshold) <= icon_size && (size + threshold) >= icon_size; diff = icon_size < (size - threshold) ? (size - threshold) - icon_size : icon_size - (size + threshold); break; case ICON_DIR_SCALABLE: is_exact_match = min_size <= icon_size && max_size >= icon_size; diff = icon_size < min_size ? min_size - icon_size : icon_size - max_size; break; } int dir_fd = openat( xdg_dir->fd, theme_relative_path, O_RDONLY | O_DIRECTORY); if (dir_fd < 0) continue; if (!is_exact_match && min_diff.diff <= diff) { close(dir_fd); continue; } const size_t len = file_name_len; char *path = file_name; path[len - 4] = '.'; path[len - 3] = 'p'; path[len - 2] = 'n'; path[len - 1] = 'g'; if (faccessat(dir_fd, path, R_OK, 0) < 0) { path[len - 3] = 's'; path[len - 2] = 'v'; path[len - 1] = 'g'; if (faccessat(dir_fd, path, R_OK, 0) < 0) { close(dir_fd); continue; } } if (!is_exact_match) { assert(diff < min_diff.diff); min_diff.diff = diff; min_diff.xdg_dir = xdg_dir; min_diff.theme = theme; min_diff.icon_dir = icon_dir; min_diff.type = path[len - 3] == 's' ? ICON_SVG : ICON_PNG; close(dir_fd); continue; } size_t path_len = strlen(xdg_dir->path) + 1 + 5 + 1 + /* “icons” */ strlen(theme->name) + 1 + strlen(icon_dir->path) + 1 + len; char full_path[path_len + 1]; sprintf(full_path, "%s/icons/%s/%s/%s", xdg_dir->path, theme->name, icon_dir->path, path); if ((path[len - 3] == 's' && (pix = svg_load(full_path, icon_size)) != NULL) || (path[len - 3] == 'p' && (pix = png_load(full_path)) != NULL)) { LOG_DBG("%s: %s", icon.name, full_path); close(dir_fd); goto done; } close(dir_fd); } } /* Try loading fallbacks for those icons we didn’t find an * exact match */ if (min_diff.type == ICON_NONE) { assert(min_diff.xdg_dir == NULL); continue; } size_t path_len = strlen(min_diff.xdg_dir->path) + 1 + 5 + 1 + /* “icons” */ strlen(min_diff.theme->name) + 1 + strlen(min_diff.icon_dir->path) + 1 + strlen(name) + 4; char full_path[path_len + 1]; sprintf(full_path, "%s/icons/%s/%s/%s.%s", min_diff.xdg_dir->path, min_diff.theme->name, min_diff.icon_dir->path, name, min_diff.type == ICON_SVG ? "svg" : "png"); if ((min_diff.type == ICON_SVG && (pix = svg_load(full_path, icon_size)) != NULL) || (min_diff.type == ICON_PNG && (pix = png_load(full_path)) != NULL)) { LOG_DBG("%s: %s (fallback)", icon.name, full_path); goto done; } else { /* Reset diff data, before checking the parent theme(s) */ min_diff.diff = INT_MAX; min_diff.xdg_dir = NULL; min_diff.theme = NULL; min_diff.icon_dir = NULL; min_diff.type = ICON_NONE; } } /* Finally, look in XDG_DATA_DIRS/pixmaps */ tll_foreach(xdg_dirs, it) { const size_t len = strlen(it->item.path) + 1 + strlen("pixmaps") + 1 + strlen(name) + strlen(".svg"); char path[len + 1]; /* Try SVG variant first */ sprintf(path, "%s/pixmaps/%s.svg", it->item.path, name); if ((pix = svg_load(path, icon_size)) != NULL) { LOG_DBG("%s: %s (pixmaps)", icon.name, path); goto done; } /* No SVG, look for PNG instead */ path[len - 3] = 'p'; path[len - 2] = 'n'; path[len - 1] = 'g'; if ((pix = png_load(path)) != NULL) { LOG_DBG("%s: %s (pixmaps)", icon.name, path); goto done; } } done: xdg_data_dirs_destroy(xdg_dirs); return pix; } static void parse_theme(FILE *index, struct icon_theme *theme, theme_names_t *themes_to_load) { char *section = NULL; int size = -1; int min_size = -1; int max_size = -1; int scale = 1; int threshold = 2; char *context = NULL; enum icon_dir_type type = ICON_DIR_THRESHOLD; while (true) { char *line = NULL; size_t sz = 0; ssize_t len = getline(&line, &sz, index); if (len == -1) { free(line); break; } if (len == 0) { free(line); continue; } if (line[len - 1] == '\n') { line[len - 1] = '\0'; len--; } if (len == 0) { free(line); continue; } if (line[0] == '[' && line[len - 1] == ']') { tll_foreach(theme->dirs, it) { struct icon_dir *d = &it->item; if (section == NULL || strcmp(d->path, section) != 0) continue; d->size = size; d->min_size = min_size >= 0 ? min_size : size; d->max_size = max_size >= 0 ? max_size : size; d->scale = scale; d->threshold = threshold; d->type = type; } free(section); free(context); size = min_size = max_size = -1; scale = 1; section = NULL; context = NULL; type = ICON_DIR_THRESHOLD; threshold = 2; section = malloc(len - 2 + 1); memcpy(section, &line[1], len - 2); section[len - 2] = '\0'; free(line); continue; } char *tok_ctx = NULL; const char *key = strtok_r(line, "=", &tok_ctx); char *value = strtok_r(NULL, "=", &tok_ctx); if (strcasecmp(key, "inherits") == 0) { char *ctx = NULL; for (const char *theme_name = strtok_r(value, ",", &ctx); theme_name != NULL; theme_name = strtok_r(NULL, ",", &ctx)) { tll_push_back(*themes_to_load, strdup(theme_name)); } } if (strcasecmp(key, "directories") == 0) { char *save = NULL; for (const char *d = strtok_r(value, ",", &save); d != NULL; d = strtok_r(NULL, ",", &save)) { struct icon_dir dir = {.path = strdup(d)}; tll_push_back(theme->dirs, dir); } } else if (strcasecmp(key, "size") == 0) sscanf(value, "%d", &size); else if (strcasecmp(key, "minsize") == 0) sscanf(value, "%d", &min_size); else if (strcasecmp(key, "maxsize") == 0) sscanf(value, "%d", &max_size); else if (strcasecmp(key, "scale") == 0) sscanf(value, "%d", &scale); else if (strcasecmp(key, "context") == 0) context = strdup(value); else if (strcasecmp(key, "threshold") == 0) sscanf(value, "%d", &threshold); else if (strcasecmp(key, "type") == 0) { if (strcasecmp(value, "fixed") == 0) type = ICON_DIR_FIXED; else if (strcasecmp(value, "scalable") == 0) type = ICON_DIR_SCALABLE; else if (strcasecmp(value, "threshold") == 0) type = ICON_DIR_THRESHOLD; else { LOG_WARN( "ignoring unrecognized icon theme directory type: %s", value); } } free(line); } tll_foreach(theme->dirs, it) { struct icon_dir *d = &it->item; if (section == NULL || strcmp(d->path, section) != 0) continue; d->size = size; d->min_size = min_size >= 0 ? min_size : size; d->max_size = max_size >= 0 ? max_size : size; d->scale = scale; d->threshold = threshold; d->type = type; } tll_foreach(theme->dirs, it) { if (it->item.size == 0) { free(it->item.path); tll_remove(theme->dirs, it); } } free(section); free(context); } static bool load_theme_in(const char *dir, struct icon_theme *theme, theme_names_t *themes_to_load) { char path[PATH_MAX]; snprintf(path, sizeof(path), "%s/index.theme", dir); FILE *index = fopen(path, "r"); if (index == NULL) return false; parse_theme(index, theme, themes_to_load); fclose(index); return true; } icon_theme_list_t icon_load_theme(const char *name) { /* List of themes; first item is the primary theme, subsequent * items are inherited items (i.e. fallback themes) */ icon_theme_list_t themes = tll_init(); /* List of themes to try to load. This list will be appended to as * we go, and find 'Inherits' values in the theme index files. */ theme_names_t themes_to_load = tll_init(); tll_push_back(themes_to_load, strdup(name)); xdg_data_dirs_t dirs = xdg_data_dirs(); while (tll_length(themes_to_load) > 0) { char *theme_name = tll_pop_front(themes_to_load); /* * Check if we've already loaded this theme. Example: * "Arc" inherits "Moka,Faba,elementary,Adwaita,ghome,hicolor * "Moka" inherits "Faba" * "Faba" inherits "elementary,gnome,hicolor" */ bool theme_already_loaded = false; tll_foreach(themes, it) { if (strcasecmp(it->item.name, theme_name) == 0) { theme_already_loaded = true; break; } } if (theme_already_loaded) { free(theme_name); continue; } tll_foreach(dirs, dir_it) { char path[strlen(dir_it->item.path) + 1 + strlen("icons") + 1 + strlen(theme_name) + 1]; sprintf(path, "%s/icons/%s", dir_it->item.path, theme_name); struct icon_theme theme = {0}; if (load_theme_in(path, &theme, &themes_to_load)) { theme.name = strdup(theme_name); tll_push_back(themes, theme); } } free(theme_name); } xdg_data_dirs_destroy(dirs); return themes; } static void theme_destroy(struct icon_theme theme) { free(theme.name); tll_foreach(theme.dirs, it) { free(it->item.path); tll_remove(theme.dirs, it); } } void icon_themes_destroy(icon_theme_list_t themes) { tll_foreach(themes, it) { theme_destroy(it->item); tll_remove(themes, it); } } fnott-1.4.1+ds/icon.h000066400000000000000000000012531447512336000143600ustar00rootroot00000000000000#pragma once #include #include #include "tllist.h" enum icon_dir_type { ICON_DIR_FIXED, ICON_DIR_SCALABLE, ICON_DIR_THRESHOLD, }; struct icon_dir { char *path; /* Relative to theme's base path */ int size; int min_size; int max_size; int scale; int threshold; enum icon_dir_type type; }; struct icon_theme { char *name; tll(struct icon_dir) dirs; }; typedef tll(struct icon_theme) icon_theme_list_t; icon_theme_list_t icon_load_theme(const char *name); void icon_themes_destroy(icon_theme_list_t themes); pixman_image_t *icon_load( const char *name, int icon_size, const icon_theme_list_t *themes); fnott-1.4.1+ds/log.c000066400000000000000000000107641447512336000142130ustar00rootroot00000000000000#include "log.h" #include #include #include #include #include #include #include #include #include static bool colorize = false; static bool do_syslog = true; void log_init(enum log_colorize _colorize, bool _do_syslog, enum log_facility syslog_facility, enum log_class syslog_level) { static const int facility_map[] = { [LOG_FACILITY_USER] = LOG_USER, [LOG_FACILITY_DAEMON] = LOG_DAEMON, }; static const int level_map[] = { [LOG_CLASS_ERROR] = LOG_ERR, [LOG_CLASS_WARNING] = LOG_WARNING, [LOG_CLASS_INFO] = LOG_INFO, [LOG_CLASS_DEBUG] = LOG_DEBUG, }; colorize = _colorize == LOG_COLORIZE_NEVER ? false : _colorize == LOG_COLORIZE_ALWAYS ? true : isatty(STDERR_FILENO); do_syslog = _do_syslog; if (do_syslog) { openlog(NULL, /*LOG_PID*/0, facility_map[syslog_facility]); setlogmask(LOG_UPTO(level_map[syslog_level])); } } void log_deinit(void) { if (do_syslog) closelog(); } static void _log(enum log_class log_class, const char *module, const char *file, int lineno, const char *fmt, int sys_errno, va_list va) { const char *class = "abcd"; int class_clr = 0; switch (log_class) { case LOG_CLASS_ERROR: class = " err"; class_clr = 31; break; case LOG_CLASS_WARNING: class = "warn"; class_clr = 33; break; case LOG_CLASS_INFO: class = "info"; class_clr = 97; break; case LOG_CLASS_DEBUG: class = " dbg"; class_clr = 36; break; } char clr[16]; snprintf(clr, sizeof(clr), "\e[%dm", class_clr); fprintf(stderr, "%s%s%s: ", colorize ? clr : "", class, colorize ? "\e[0m" : ""); if (colorize) fprintf(stderr, "\e[2m"); fprintf(stderr, "%s:%d: ", file, lineno); if (colorize) fprintf(stderr, "\e[0m"); vfprintf(stderr, fmt, va); if (sys_errno != 0) fprintf(stderr, ": %s", strerror(sys_errno)); fprintf(stderr, "\n"); } static void _sys_log(enum log_class log_class, const char *module, const char *file __attribute__((unused)), int lineno __attribute__((unused)), const char *fmt, int sys_errno, va_list va) { if (!do_syslog) return; /* Map our log level to syslog's level */ int level = -1; switch (log_class) { case LOG_CLASS_ERROR: level = LOG_ERR; break; case LOG_CLASS_WARNING: level = LOG_WARNING; break; case LOG_CLASS_INFO: level = LOG_INFO; break; case LOG_CLASS_DEBUG: level = LOG_DEBUG; break; } assert(level != -1); const char *sys_err = sys_errno != 0 ? strerror(sys_errno) : NULL; va_list va2; va_copy(va2, va); /* Calculate required size of buffer holding the entire log message */ int required_len = 0; required_len += strlen(module) + 2; /* "%s: " */ required_len += vsnprintf(NULL, 0, fmt, va2); va_end(va2); if (sys_errno != 0) required_len += strlen(sys_err) + 2; /* ": %s" */ /* Format the msg */ char *msg = malloc(required_len + 1); int idx = 0; idx += snprintf(&msg[idx], required_len + 1 - idx, "%s: ", module); idx += vsnprintf(&msg[idx], required_len + 1 - idx, fmt, va); if (sys_errno != 0) { snprintf( &msg[idx], required_len + 1 - idx, ": %s", strerror(sys_errno)); } syslog(level, "%s", msg); free(msg); } void log_msg(enum log_class log_class, const char *module, const char *file, int lineno, const char *fmt, ...) { va_list ap1, ap2; va_start(ap1, fmt); va_copy(ap2, ap1); _log(log_class, module, file, lineno, fmt, 0, ap1); _sys_log(log_class, module, file, lineno, fmt, 0, ap2); va_end(ap1); va_end(ap2); } void log_errno(enum log_class log_class, const char *module, const char *file, int lineno, const char *fmt, ...) { va_list ap1, ap2; va_start(ap1, fmt); va_copy(ap2, ap1); _log(log_class, module, file, lineno, fmt, errno, ap1); _sys_log(log_class, module, file, lineno, fmt, errno, ap2); va_end(ap1); va_end(ap2); } void log_errno_provided(enum log_class log_class, const char *module, const char *file, int lineno, int _errno, const char *fmt, ...) { va_list ap1, ap2; va_start(ap1, fmt); va_copy(ap2, ap1); _log(log_class, module, file, lineno, fmt, _errno, ap1); _sys_log(log_class, module, file, lineno, fmt, _errno, ap2); va_end(ap1); va_end(ap2); } fnott-1.4.1+ds/log.h000066400000000000000000000034501447512336000142120ustar00rootroot00000000000000#pragma once #include enum log_colorize { LOG_COLORIZE_NEVER, LOG_COLORIZE_ALWAYS, LOG_COLORIZE_AUTO }; enum log_facility { LOG_FACILITY_USER, LOG_FACILITY_DAEMON }; enum log_class { LOG_CLASS_ERROR, LOG_CLASS_WARNING, LOG_CLASS_INFO, LOG_CLASS_DEBUG }; void log_init(enum log_colorize colorize, bool do_syslog, enum log_facility syslog_facility, enum log_class syslog_level); void log_deinit(void); void log_msg(enum log_class log_class, const char *module, const char *file, int lineno, const char *fmt, ...) __attribute__((format (printf, 5, 6))); void log_errno(enum log_class log_class, const char *module, const char *file, int lineno, const char *fmt, ...) __attribute__((format (printf, 5, 6))); void log_errno_provided( enum log_class log_class, const char *module, const char *file, int lineno, int _errno, const char *fmt, ...) __attribute__((format (printf, 6, 7))); #define LOG_ERR(fmt, ...) \ log_msg(LOG_CLASS_ERROR, LOG_MODULE, __FILE__, __LINE__, fmt, ## __VA_ARGS__) #define LOG_ERRNO(fmt, ...) \ log_errno(LOG_CLASS_ERROR, LOG_MODULE, __FILE__, __LINE__, fmt, ## __VA_ARGS__) #define LOG_ERRNO_P(fmt, _errno, ...) \ log_errno_provided(LOG_CLASS_ERROR, LOG_MODULE, __FILE__, __LINE__, \ _errno, fmt, ## __VA_ARGS__) #define LOG_WARN(fmt, ...) \ log_msg(LOG_CLASS_WARNING, LOG_MODULE, __FILE__, __LINE__, fmt, ## __VA_ARGS__) #define LOG_INFO(fmt, ...) \ log_msg(LOG_CLASS_INFO, LOG_MODULE, __FILE__, __LINE__, fmt, ## __VA_ARGS__) #if defined(LOG_ENABLE_DBG) && LOG_ENABLE_DBG #define LOG_DBG(fmt, ...) \ log_msg(LOG_CLASS_DEBUG, LOG_MODULE, __FILE__, __LINE__, fmt, ## __VA_ARGS__) #else #define LOG_DBG(fmt, ...) #endif fnott-1.4.1+ds/main.c000066400000000000000000000134111447512336000143460ustar00rootroot00000000000000#include #include #include #include #include #include #include #include #include #include #include #include #include #include #define LOG_MODULE "main" #define LOG_ENABLE_DBG 0 #include "log.h" #include "ctrl.h" #include "dbus.h" #include "fdm.h" #include "icon.h" #include "wayland.h" #include "version.h" volatile sig_atomic_t aborted = 0; static void sig_handler(int signo) { aborted = 1; } static void print_usage(const char *prog) { printf("Usage: %s\n" " %s --version\n" "\n" "Options:\n" " -c,--config=PATH load configuration from PATH ($XDG_CONFIG_HOME/fnott/fnott.ini)\n" " -p,--print-pid=FILE|FD print PID to file or FD\n" " -l,--log-colorize=[never|always|auto] enable/disable colorization of log output on stderr\n" " -s,--log-no-syslog disable syslog logging\n" " -v,--version show the version number and quit\n", prog, prog); } static bool print_pid(const char *pid_file, bool *unlink_at_exit) { LOG_DBG("printing PID to %s", pid_file); errno = 0; char *end; int pid_fd = strtoul(pid_file, &end, 10); if (errno != 0 || *end != '\0') { if ((pid_fd = open(pid_file, O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)) < 0) { LOG_ERRNO("%s: failed to open", pid_file); return false; } else *unlink_at_exit = true; } if (pid_fd >= 0) { char pid[32]; snprintf(pid, sizeof(pid), "%u\n", getpid()); ssize_t bytes = write(pid_fd, pid, strlen(pid)); close(pid_fd); if (bytes < 0) { LOG_ERRNO("failed to write PID to FD=%u", pid_fd); return false; } LOG_DBG("wrote %zd bytes to FD=%d", bytes, pid_fd); return true; } else return false; } int main(int argc, char *const *argv) { static const struct option longopts[] = { {"config", required_argument, 0, 'c'}, {"print-pid", required_argument, 0, 'p'}, {"log-colorize", optional_argument, 0, 'l'}, {"log-no-syslog", no_argument, 0, 's'}, {"version", no_argument, 0, 'v'}, {"help", no_argument, 0, 'h'}, {NULL, no_argument, 0, 0}, }; bool unlink_pid_file = false; const char *pid_file = NULL; const char *conf_path = NULL; enum log_colorize log_colorize = LOG_COLORIZE_AUTO; bool log_syslog = true; while (true) { int c = getopt_long(argc, argv, ":c:p:l::svh", longopts, NULL); if (c == -1) break; switch (c) { case 'c': conf_path = optarg; break; case 'p': pid_file = optarg; break; case 'l': if (optarg == NULL || strcmp(optarg, "auto") == 0) log_colorize = LOG_COLORIZE_AUTO; else if (strcmp(optarg, "never") == 0) log_colorize = LOG_COLORIZE_NEVER; else if (strcmp(optarg, "always") == 0) log_colorize = LOG_COLORIZE_ALWAYS; else { fprintf(stderr, "%s: argument must be one of 'never', 'always' or 'auto'\n", optarg); return EXIT_FAILURE; } break; case 's': log_syslog = false; break; case 'v': printf("fnott version %s\n", FNOTT_VERSION); return EXIT_SUCCESS; case 'h': print_usage(argv[0]); return EXIT_SUCCESS; case ':': fprintf(stderr, "error: -%c: missing required argument\n", optopt); return EXIT_FAILURE; case '?': fprintf(stderr, "error: -%c: invalid option\n", optopt); return EXIT_FAILURE; } } log_init(log_colorize, log_syslog, LOG_FACILITY_DAEMON, LOG_CLASS_DEBUG); fcft_init((enum fcft_log_colorize)log_colorize, log_syslog, FCFT_LOG_CLASS_DEBUG); atexit(&fcft_fini); int ret = EXIT_FAILURE; struct config conf = {}; struct fdm *fdm = NULL; struct ctrl *ctrl = NULL; struct dbus *bus = NULL; struct wayland *wayl = NULL; struct notif_mgr *mgr = NULL; icon_theme_list_t icon_theme = tll_init(); if (!config_load(&conf, conf_path)) goto err; icon_theme = icon_load_theme(conf.icon_theme_name); setlocale(LC_CTYPE, ""); if ((fdm = fdm_init()) == NULL) goto err; if ((mgr = notif_mgr_new(&conf, fdm, &icon_theme)) == NULL) goto err; if ((wayl = wayl_init(&conf, fdm, mgr)) == NULL) goto err; if ((bus = dbus_init(&conf, fdm, wayl, mgr, &icon_theme)) == NULL) goto err; if ((ctrl = ctrl_init(fdm, mgr, bus)) == NULL) goto err; notif_mgr_configure(mgr, wayl, bus); const struct sigaction sigact = { .sa_handler = &sig_handler, }; sigaction(SIGINT, &sigact, NULL); sigaction(SIGTERM, &sigact, NULL); if (pid_file != NULL) { if (!print_pid(pid_file, &unlink_pid_file)) goto err; } while (!aborted) { wayl_flush(wayl); if (!fdm_poll(fdm)) break; } if (aborted) ret = EXIT_SUCCESS; err: icon_themes_destroy(icon_theme); ctrl_destroy(ctrl); notif_mgr_destroy(mgr); dbus_destroy(bus); wayl_destroy(wayl); fdm_destroy(fdm); config_destroy(conf); if (unlink_pid_file) unlink(pid_file); log_deinit(); return ret; } fnott-1.4.1+ds/meson.build000066400000000000000000000107671447512336000154330ustar00rootroot00000000000000project('fnott', 'c', version: '1.4.1', license: 'MIT', meson_version: '>=0.58.0', default_options: [ 'c_std=c18', 'warning_level=1', 'werror=true', 'b_ndebug=if-release']) is_debug_build = get_option('buildtype').startswith('debug') add_project_arguments( ['-D_GNU_SOURCE=200809L'] + (is_debug_build ? ['-D_DEBUG'] : []), language: 'c') cc = meson.get_compiler('c') if cc.has_function('memfd_create') add_project_arguments('-DMEMFD_CREATE', language: 'c') endif # Compute the relative path used by compiler invocations. source_root = meson.current_source_dir().split('/') build_root = meson.global_build_root().split('/') relative_dir_parts = [] i = 0 in_prefix = true foreach p : build_root if i >= source_root.length() or not in_prefix or p != source_root[i] in_prefix = false relative_dir_parts += '..' endif i += 1 endforeach i = 0 in_prefix = true foreach p : source_root if i >= build_root.length() or not in_prefix or build_root[i] != p in_prefix = false relative_dir_parts += p endif i += 1 endforeach relative_dir = join_paths(relative_dir_parts) + '/' if cc.has_argument('-fmacro-prefix-map=/foo=') add_project_arguments('-fmacro-prefix-map=@0@='.format(relative_dir), language: 'c') endif math = cc.find_library('m', required : false) threads = dependency('threads') libepoll = dependency('epoll-shim', required: false) pixman = dependency('pixman-1') png = dependency('libpng') wayland_protocols = dependency('wayland-protocols') wayland_client = dependency('wayland-client') wayland_cursor = dependency('wayland-cursor') dbus = dependency('dbus-1') fontconfig = dependency('fontconfig') tllist = dependency('tllist', version: '>=1.0.1', fallback: 'tllist') fcft = dependency('fcft', version: ['>=3.0.0', '<4.0.0'], fallback: 'fcft') wayland_protocols_datadir = wayland_protocols.get_variable('pkgdatadir') wscanner = dependency('wayland-scanner', native: true) wscanner_prog = find_program( wscanner.get_variable('wayland_scanner'), native: true) wl_xmls = [ 'external/wlr-layer-shell-unstable-v1.xml', 'external/idle.xml', wayland_protocols_datadir + '/stable/xdg-shell/xdg-shell.xml', wayland_protocols_datadir + '/unstable/xdg-output/xdg-output-unstable-v1.xml', ] if wayland_protocols.version().version_compare('>=1.27') wl_xmls += [wayland_protocols_datadir + '/staging/ext-idle-notify/ext-idle-notify-v1.xml'] add_project_arguments('-DFNOTT_HAVE_IDLE_NOTIFY', language: 'c') endif wl_proto_headers = [] wl_proto_src = [] foreach prot : wl_xmls wl_proto_headers += custom_target( prot.underscorify() + '-client-header', output: '@BASENAME@.h', input: prot, command: [wscanner_prog, 'client-header', '@INPUT@', '@OUTPUT@']) wl_proto_src += custom_target( prot.underscorify() + '-private-code', output: '@BASENAME@.c', input: prot, command: [wscanner_prog, 'private-code', '@INPUT@', '@OUTPUT@']) endforeach env = find_program('env', native: true) generate_version_sh = files('generate-version.sh') version = custom_target( 'generate_version', build_always_stale: true, output: 'version.h', command: [env, 'LC_ALL=C', generate_version_sh, meson.project_version(), '@CURRENT_SOURCE_DIR@', '@OUTPUT@']) nanosvg = declare_dependency( sources: ['nanosvg.c', '3rd-party/nanosvg/src/nanosvg.h', 'nanosvgrast.c', '3rd-party/nanosvg/src/nanosvgrast.h'], include_directories: '3rd-party/nanosvg/src', dependencies: math) executable( 'fnott', 'main.c', 'char32.c', 'char32.h', 'config.c', 'config.h', 'ctrl.c', 'ctrl.h', 'dbus.c', 'dbus.h', 'fdm.c', 'fdm.h', 'icon.c', 'icon.h', 'log.c', 'log.h', 'notification.c', 'notification.h', 'png.c', 'png-fnott.h', 'shm.c', 'shm.h', 'spawn.c', 'spawn.h', 'svg.c', 'svg.h', 'stride.h', 'tokenize.c', 'tokenize.h', 'uri.c', 'uri.h', 'wayland.c', 'wayland.h', 'xdg.c', 'xdg.h', wl_proto_src + wl_proto_headers, version, dependencies: [threads, libepoll, pixman, png, wayland_client, wayland_cursor, dbus, fontconfig, tllist, fcft, nanosvg], install: true) executable( 'fnottctl', 'fnottctl.c', 'ctrl-protocol.h', 'log.c', 'log.h', version, install: true) install_data( 'LICENSE', 'README.md', install_dir: join_paths(get_option('datadir'), 'doc', 'fnott')) install_data('fnott.desktop', install_dir: join_paths(get_option('datadir'), 'applications')) install_data('fnott.ini', install_dir: join_paths(get_option('datadir'), 'fnott')) subdir('completions') subdir('doc') fnott-1.4.1+ds/nanosvg.c000066400000000000000000000002201447512336000150670ustar00rootroot00000000000000#include #include #include #define NANOSVG_ALL_COLOR_KEYWORDS #define NANOSVG_IMPLEMENTATION #include fnott-1.4.1+ds/nanosvgrast.c000066400000000000000000000001721447512336000157670ustar00rootroot00000000000000#include #include #include #define NANOSVGRAST_IMPLEMENTATION #include fnott-1.4.1+ds/notification.c000066400000000000000000002052231447512336000161140ustar00rootroot00000000000000#include "notification.h" #include "config.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define LOG_MODULE "notification" #define LOG_ENABLE_DBG 0 #include "log.h" #include "char32.h" #include "dbus.h" #include "icon.h" #include "spawn.h" #include "wayland.h" #define max(x, y) ((x) > (y) ? (x) : (y)) #define min(x, y) ((x) < (y) ? (x) : (y)) #ifndef CLOCK_BOOTTIME #ifdef CLOCK_UPTIME /* DragonFly and FreeBSD */ #define CLOCK_BOOTTIME CLOCK_UPTIME #else #define CLOCK_BOOTTIME CLOCK_MONOTONIC #endif #endif struct notif_mgr; struct font_set { struct fcft_font *regular; struct fcft_font *bold; struct fcft_font *italic; struct fcft_font *bold_italic; }; struct action { char *id; char *label; char32_t *wid; char32_t *wlabel; }; struct text_run_cache { struct fcft_text_run *run; const struct fcft_font *font; uint64_t hash; enum fcft_subpixel subpixel; size_t ofs; }; struct notif { struct notif_mgr *mgr; struct wl_surface *surface; struct zwlr_layer_surface_v1 *layer_surface; bool is_configured; uint32_t id; enum urgency urgency; char32_t *app; char32_t *summary; char32_t *body; tll(struct action) actions; int8_t progress; int timeout_ms; /* Timeout provided by the notification itself */ int timeout_fd; enum {DISMISS_IMMEDIATELY, DISMISS_DEFER, DISMISS_DELAYED} deferred_dismissal; enum {EXPIRE_IMMEDIATELY, EXPIRE_DEFER, EXPIRE_DELAYED} deferred_expiral; struct { float dpi; bool dpi_aware; enum urgency urgency; struct font_set app; struct font_set summary; struct font_set body; struct font_set action; } fonts; pixman_image_t *pix; int image_width; int image_height; bool image_is_custom; int scale; enum fcft_subpixel subpixel; struct buffer *pending; struct wl_callback *frame_callback; int y; const struct monitor *mon; tll(struct text_run_cache) text_run_cache; }; struct notif_mgr { struct config *conf; struct fdm *fdm; struct wayland *wayl; struct dbus *bus; const icon_theme_list_t *icon_theme; regex_t html_entity_re; tll(struct notif *) notifs; }; static size_t next_id = 1; struct notif_mgr * notif_mgr_new(struct config *conf, struct fdm *fdm, const icon_theme_list_t *icon_theme) { struct notif_mgr *mgr = malloc(sizeof(*mgr)); *mgr = (struct notif_mgr) { .conf = conf, .fdm = fdm, .wayl = NULL, /* notif_mgr_configure() */ .bus = NULL, /* notif_mgr_configure() */ .icon_theme = icon_theme, .notifs = tll_init(), }; int r = regcomp( &mgr->html_entity_re, /* Entity names (there's a *lot* of these - we only support the common ones */ "&\\(nbsp\\|lt\\|gt\\|amp\\|quot\\|apos\\|cent\\|pound\\|yen\\|euro\\|copy\\|reg\\);\\|" /* Decimal entity number: ' */ "&#\\([0-9]\\+\\);\\|" /* Hexadecimal entity number: ' */ "&#x\\([0-9a-fA-F]\\+\\);" , 0); if (r != 0) { char err[1024]; regerror(r, &mgr->html_entity_re, err, sizeof(err)); LOG_ERR("failed to compile HTML entity regex: %s (%d)", err, r); regfree(&mgr->html_entity_re); free(mgr); return NULL; } return mgr; } void notif_mgr_destroy(struct notif_mgr *mgr) { if (mgr == NULL) return; regfree(&mgr->html_entity_re); tll_foreach(mgr->notifs, it) notif_destroy(it->item); tll_free(mgr->notifs); free(mgr); } void notif_mgr_configure(struct notif_mgr *mgr, struct wayland *wayl, struct dbus *bus) { assert(mgr->wayl == NULL); assert(mgr->bus == NULL); mgr->wayl = wayl; mgr->bus = bus; } static bool notif_reload_default_icon(struct notif *notif); static bool notif_reload_fonts(struct notif *notif); static bool notif_reload_timeout(struct notif *notif); static int notif_show(struct notif *notif, int y); static void surface_enter(void *data, struct wl_surface *wl_surface, struct wl_output *wl_output) { struct notif *notif = data; const struct monitor *mon = wayl_monitor_get(notif->mgr->wayl, wl_output); if (notif->mon == mon) return; const int old_scale = notif->scale; const float old_dpi = notif->fonts.dpi; const enum fcft_subpixel old_subpixel = notif->subpixel; notif->mon = mon; notif_reload_fonts(notif); int new_scale = mon != NULL ? mon->scale : wayl_guess_scale(notif->mgr->wayl); enum fcft_subpixel new_subpixel = mon != NULL ? (enum fcft_subpixel)mon->subpixel : wayl_guess_subpixel(notif->mgr->wayl); if (old_scale == new_scale && old_subpixel == new_subpixel && old_dpi == mon->dpi) { return; } /* Need to reload fonts, if not DPI aware */ notif_reload_fonts(notif); assert(notif->scale == new_scale); notif->subpixel = new_subpixel; notif_show(notif, notif->y); } static void surface_leave(void *data, struct wl_surface *wl_surface, struct wl_output *wl_output) { struct notif *notif = data; notif->mon = NULL; } static const struct wl_surface_listener surface_listener = { .enter = &surface_enter, .leave = &surface_leave, }; static void frame_callback( void *data, struct wl_callback *wl_callback, uint32_t callback_data); static const struct wl_callback_listener frame_listener = { .done = &frame_callback, }; static void commit_buffer(struct notif *notif, struct buffer *buf) { struct wayland *wayl = notif->mgr->wayl; assert(notif->scale >= 1); assert(buf->busy); wl_surface_set_buffer_scale(notif->surface, notif->scale); wl_surface_attach(notif->surface, buf->wl_buf, 0, 0); wl_surface_damage_buffer(notif->surface, 0, 0, buf->width, buf->height); assert(notif->frame_callback == NULL); notif->frame_callback = wl_surface_frame(notif->surface); wl_callback_add_listener(notif->frame_callback, &frame_listener, notif); wl_surface_commit(notif->surface); wayl_flush(wayl); } static void layer_surface_configure(void *data, struct zwlr_layer_surface_v1 *surface, uint32_t serial, uint32_t w, uint32_t h) { LOG_DBG("configure: width=%u, height=%u", w, h); struct notif *notif = data; notif->is_configured = true; zwlr_layer_surface_v1_ack_configure(surface, serial); if (notif->pending != NULL && notif->frame_callback == NULL) { commit_buffer(notif, notif->pending); notif->pending = NULL; } else { /* ack *must* be followed by a commit */ notif_show(notif, notif->y); } } static void notif_destroy_surfaces(struct notif *notif); static void layer_surface_closed(void *data, struct zwlr_layer_surface_v1 *surface) { struct notif *notif = data; notif_destroy_surfaces(notif); } static const struct zwlr_layer_surface_v1_listener layer_surface_listener = { .configure = &layer_surface_configure, .closed = &layer_surface_closed, }; struct notif * notif_mgr_get_notif(struct notif_mgr *mgr, uint32_t id) { if (id == 0 && tll_length(mgr->notifs) > 0) return tll_front(mgr->notifs); tll_foreach(mgr->notifs, it) { if (it->item->id == id) return it->item; } return NULL; } struct notif * notif_mgr_get_notif_for_surface(struct notif_mgr *mgr, const struct wl_surface *surface) { tll_foreach(mgr->notifs, it) { if (it->item->surface == surface) return it->item; } return NULL; } /* Instantiates a new notification. You *must* call * notif_mgr_refresh() "soon" (after configuring the notification). */ struct notif * notif_mgr_create_notif(struct notif_mgr *mgr, uint32_t replaces_id) { int notif_id; if (replaces_id != 0) { struct notif *old_notif = notif_mgr_get_notif(mgr, replaces_id); if (old_notif != NULL) return old_notif; notif_id = replaces_id; } else notif_id = next_id++; struct notif *notif = malloc(sizeof(*notif)); *notif = (struct notif) { .mgr = mgr, .id = notif_id, .urgency = URGENCY_NORMAL, .app = c32dup(U""), .summary = c32dup(U""), .body = c32dup(U""), .actions = tll_init(), .timeout_ms = -1, /* -1, up to us, 0 - never expire */ .timeout_fd = -1, .deferred_dismissal = DISMISS_IMMEDIATELY, .deferred_expiral = EXPIRE_IMMEDIATELY, }; notif_reload_default_icon(notif); notif_reload_fonts(notif); notif_reload_timeout(notif); tll_rforeach(mgr->notifs, it) { if (it->item->urgency >= notif->urgency) { tll_insert_after(mgr->notifs, it, notif); return notif; } } tll_push_front(mgr->notifs, notif); return notif; } bool notif_mgr_del_notif(struct notif_mgr *mgr, uint32_t id) { if (id == 0) return false; tll_foreach(mgr->notifs, it) { if (it->item->id != id) continue; notif_destroy(it->item); tll_remove(mgr->notifs, it); return true; } return false; } static void notif_destroy_surfaces(struct notif *notif) { if (notif->frame_callback != NULL) wl_callback_destroy(notif->frame_callback); if (notif->layer_surface) zwlr_layer_surface_v1_destroy(notif->layer_surface); if (notif->surface) wl_surface_destroy(notif->surface); notif->is_configured = false; notif->surface = NULL; notif->layer_surface = NULL; notif->frame_callback = NULL; notif->mon = NULL; notif->scale = 0; notif->fonts.dpi = 0; notif->subpixel = FCFT_SUBPIXEL_DEFAULT; } void notif_destroy(struct notif *notif) { if (notif == NULL) return; notif_destroy_surfaces(notif); fdm_del(notif->mgr->fdm, notif->timeout_fd); if (notif->pix != NULL) { free(pixman_image_get_data(notif->pix)); pixman_image_unref(notif->pix); } tll_foreach(notif->actions, it) { free(it->item.id); free(it->item.wid); free(it->item.label); free(it->item.wlabel); tll_remove(notif->actions, it); } tll_foreach(notif->text_run_cache, it) { fcft_text_run_destroy(it->item.run); tll_remove(notif->text_run_cache, it); } free(notif->app); free(notif->summary); free(notif->body); free(notif); } static float get_dpi(const struct notif *notif) { if (notif->mon != NULL) return notif->mon->dpi > 0 ? notif->mon->dpi : 96.; else return wayl_dpi_guess(notif->mgr->wayl); } static int get_scale(const struct notif *notif) { if (notif->mon != NULL) return notif->mon->scale; else return wayl_guess_scale(notif->mgr->wayl); } static void font_set_destroy(struct font_set *set) { fcft_destroy(set->regular); fcft_destroy(set->bold); fcft_destroy(set->italic); fcft_destroy(set->bold_italic); set->regular = set->bold = set->italic = set->bold_italic = NULL; } static bool reload_one_font_set(const struct config_font *font, struct font_set *set, bool dpi_aware, int scale, float dpi) { scale = dpi_aware ? 1 : scale; dpi = dpi_aware ? dpi : 96.; char size[64]; if (font->px_size > 0) { snprintf(size, sizeof(size), "pixelsize=%d", font->px_size * scale); } else { snprintf(size, sizeof(size), "size=%.2f", font->pt_size * (double)scale); } char attrs0[256], attrs1[256], attrs2[256], attrs3[256]; snprintf(attrs0, sizeof(attrs0), "dpi=%.2f:%s", dpi, size); snprintf(attrs1, sizeof(attrs1), "dpi=%.2f:weight=bold:%s", dpi, size); snprintf(attrs2, sizeof(attrs2), "dpi=%.2f:slant=italic:%s", dpi, size); snprintf(attrs3, sizeof(attrs3), "dpi=%.2f:weight=bold:slant=italic:%s", dpi, size); const char *names[1] = {font->pattern}; struct fcft_font *regular = fcft_from_name(1, names, attrs0); if (regular == NULL) { LOG_ERR("%s: failed to load font", font->pattern); return false; } struct fcft_font *bold = fcft_from_name(1, names, attrs1); struct fcft_font *italic = fcft_from_name(1, names, attrs2); struct fcft_font *bold_italic = fcft_from_name(1, names, attrs3); set->regular = regular; set->bold = bold; set->italic = italic; set->bold_italic = bold_italic; return true; } static bool be_dpi_aware(const struct config *conf, const struct wayland *wayl) { switch (conf->dpi_aware) { case DPI_AWARE_NO: return false; case DPI_AWARE_YES: return true; case DPI_AWARE_AUTO: return wayl_all_monitors_have_scale_one(wayl); } assert(false); return false; } static bool notif_reload_fonts(struct notif *notif) { const float old_dpi = notif->fonts.dpi; const float new_dpi = get_dpi(notif); const int old_scale = notif->scale; const int new_scale = get_scale(notif); const enum urgency old_urgency = notif->fonts.urgency; const enum urgency new_urgency = notif->urgency; const bool was_dpi_aware = notif->fonts.dpi_aware; const bool is_dpi_aware = be_dpi_aware(notif->mgr->conf, notif->mgr->wayl); notif->scale = new_scale; notif->fonts.dpi = new_dpi; notif->fonts.dpi_aware = is_dpi_aware; notif->fonts.urgency = notif->urgency; /* Skip font reload if none of the parameters affecting font * rendering has changed */ if (notif->fonts.app.regular != NULL && was_dpi_aware == is_dpi_aware && (is_dpi_aware ? old_dpi == new_dpi : old_scale == new_scale) && old_urgency == new_urgency) { LOG_DBG("skipping font reloading (DPI-aware: %d/%d, DPI: %.2f/%.2f, " "scale: %d/%d, urgency: %d/%d)", was_dpi_aware, is_dpi_aware, old_dpi, new_dpi, old_scale, new_scale, old_urgency, new_urgency); return false; } const struct urgency_config *urgency = ¬if->mgr->conf->by_urgency[notif->urgency]; struct font_set app; if (reload_one_font_set( &urgency->app.font, &app, is_dpi_aware, new_scale, new_dpi)) { font_set_destroy(¬if->fonts.app); notif->fonts.app = app; } struct font_set summary; if (reload_one_font_set( &urgency->summary.font, &summary, is_dpi_aware, new_scale, new_dpi)) { font_set_destroy(¬if->fonts.summary); notif->fonts.summary = summary; } struct font_set body; if (reload_one_font_set( &urgency->body.font, &body, is_dpi_aware, new_scale, new_dpi)) { font_set_destroy(¬if->fonts.body); notif->fonts.body = body; } struct font_set action; if (reload_one_font_set( &urgency->action.font, &action, is_dpi_aware, new_scale, new_dpi)) { font_set_destroy(¬if->fonts.action); notif->fonts.action = action; } return true; } static void notif_reset_image(struct notif *notif) { if (notif->pix == NULL) return; free(pixman_image_get_data(notif->pix)); pixman_image_unref(notif->pix); notif->pix = NULL; notif->image_is_custom = false; } static void notif_set_image_internal(struct notif *notif, pixman_image_t *pix, bool custom) { const int max_size = notif->mgr->conf->max_icon_size; notif_reset_image(notif); notif->image_is_custom = custom; notif->pix = pix; notif->image_width = pixman_image_get_width(pix); notif->image_height = pixman_image_get_height(pix); if (notif->image_width <= max_size && notif->image_height <= max_size) return; double scale_w = notif->image_width / max_size; double scale_h = notif->image_height / max_size; double scale = scale_w > scale_h ? scale_w : scale_h; notif->image_width /= scale; notif->image_height /= scale; LOG_DBG("image re-scaled: %dx%d -> %dx%d", pixman_image_get_width(pix), pixman_image_get_height(pix), notif->image_width, notif->image_height); struct pixman_transform t; pixman_transform_init_scale( &t, pixman_double_to_fixed(scale), pixman_double_to_fixed(scale)); pixman_image_set_transform(pix, &t); } static bool notif_reload_default_icon(struct notif *notif) { if (notif->image_is_custom) return true; const struct config *conf = notif->mgr->conf; const char *icon = conf->by_urgency[notif->urgency].icon; if (icon == NULL) { notif_reset_image(notif); return true; } pixman_image_t *pix = icon_load( icon, conf->max_icon_size, notif->mgr->icon_theme); assert(pix != NULL); notif_set_image_internal(notif, pix, false); return true; } static bool fdm_timeout(struct fdm *fdm, int fd, int events, void *data) { if (events & EPOLLHUP) return false; struct notif *notif = data; uint64_t unused; ssize_t ret = read(fd, &unused, sizeof(unused)); if (ret < 0) { if (errno == EAGAIN) return true; LOG_ERRNO("failed to read notification timeout timer"); return false; } notif_mgr_expire_id(notif->mgr, notif->id); return true; } static bool notif_reload_timeout(struct notif *notif) { const struct urgency_config *urgency = ¬if->mgr->conf->by_urgency[notif->urgency]; const int notif_timeout_ms = notif->timeout_ms; const int max_timeout_ms = urgency->max_timeout_secs * 1000; const int default_timeout_ms = urgency->default_timeout_secs * 1000; int timeout_ms = notif_timeout_ms == -1 ? default_timeout_ms : notif_timeout_ms; assert(timeout_ms >= 0); if (max_timeout_ms > 0) { if (timeout_ms > 0) timeout_ms = min(timeout_ms, max_timeout_ms); else timeout_ms = max_timeout_ms; } LOG_DBG("timeout=%dms " "(notif-timeout=%dms, max-timeout=%dms, default-timeout=%dms)", timeout_ms, notif_timeout_ms, max_timeout_ms, default_timeout_ms); /* Remove existing timer */ if (notif->timeout_fd >= 0) { fdm_del(notif->mgr->fdm, notif->timeout_fd); notif->timeout_fd = -1; } if (wayl_is_idle_for_urgency(notif->mgr->wayl, notif->urgency)) { LOG_DBG("removed timer for notification with id %d, because urgency level %d is idle", notif->id, notif->urgency); return true; } if (timeout_ms == 0) { /* No timeout */ return true; } notif->timeout_fd = timerfd_create( CLOCK_BOOTTIME, TFD_CLOEXEC | TFD_NONBLOCK); if (notif->timeout_fd < 0) { LOG_ERRNO("failed to create notification timeout timer FD"); return false; } long nsecs = (long)timeout_ms * 1000000; time_t secs = nsecs / 1000000000l; nsecs %= 1000000000l; struct itimerspec timeout = { .it_value = {.tv_sec = secs, .tv_nsec = nsecs} }; if (timerfd_settime(notif->timeout_fd, 0, &timeout, NULL) < 0) { LOG_ERRNO("failed to configure notification timeout timer FD"); goto fail; } if (!fdm_add(notif->mgr->fdm, notif->timeout_fd, EPOLLIN, &fdm_timeout, notif)) { LOG_ERR("failed to add notification timeout timer to FDM"); goto fail; } return true; fail: if (notif->timeout_fd != -1) close(notif->timeout_fd); notif->timeout_fd = -1; return false; } uint32_t notif_id(const struct notif *notif) { return notif->id; } const struct monitor * notif_monitor(const struct notif *notif) { return notif->mon; } static char32_t * decode_html_entities(const struct notif_mgr *mgr, const char *s) { /* Guesstimate initial size */ size_t sz = strlen(s) + 1; char32_t *result = malloc(sz * sizeof(char32_t)); /* Output so far */ size_t len = 0; char32_t *out = result; #define ensure_size(new_size) \ do { \ while (sz < (new_size)) { \ sz *= 2; \ result = realloc(result, sz * sizeof(char32_t)); \ out = &result[len]; \ } \ } while (0) #define append(wc) \ do { \ ensure_size(len + 1); \ *out++ = wc; \ len++; \ } while (0) #define append_u8(s, s_len) \ do { \ size_t _w_len = mbsntoc32(NULL, s, s_len, 0); \ if (_w_len > 0) { \ ensure_size(len + _w_len + 1); \ mbsntoc32(out, s, s_len, _w_len + 1); \ out += _w_len; \ len += _w_len; \ } \ } while (0) while (true) { regmatch_t matches[mgr->html_entity_re.re_nsub + 1]; if (regexec(&mgr->html_entity_re, s, mgr->html_entity_re.re_nsub + 1, matches, 0) == REG_NOMATCH) { append_u8(s, strlen(s)); break; } const regmatch_t *all = &matches[0]; const regmatch_t *named = &matches[1]; const regmatch_t *decimal = &matches[2]; const regmatch_t *hex = &matches[3]; append_u8(s, all->rm_so); if (named->rm_so >= 0) { size_t match_len = named->rm_eo - named->rm_so; const char *match = &s[named->rm_so]; if (strncmp(match, "nbsp", match_len) == 0) append(U' '); else if (strncmp(match, "lt", match_len) == 0) append(U'<'); else if (strncmp(match, "gt", match_len) == 0) append(U'>'); else if (strncmp(match, "amp", match_len) == 0) append(U'&'); else if (strncmp(match, "quot", match_len) == 0) append(U'"'); else if (strncmp(match, "apos", match_len) == 0) append(U'\''); else if (strncmp(match, "cent", match_len) == 0) append(U'¢'); else if (strncmp(match, "pound", match_len) == 0) append(U'£'); else if (strncmp(match, "yen", match_len) == 0) append(U'¥'); else if (strncmp(match, "euro", match_len) == 0) append(U'€'); else if (strncmp(match, "copy", match_len) == 0) append(U'©'); else if (strncmp(match, "reg", match_len) == 0) append(U'®'); else assert(false); } else if (decimal->rm_so >= 0 || hex->rm_so >= 0) { bool is_hex = hex->rm_so >= 0; const char *match = is_hex ? &s[hex->rm_so] : &s[decimal->rm_so]; /* Convert string to integer */ errno = 0; char *end; unsigned long v = strtoul(match, &end, is_hex ? 16 : 10); if (errno == 0) { assert(*end == ';'); append((char32_t)v); } } s += all->rm_eo; } #undef append #undef append_n #undef ensure_size result[len] = U'\0'; return result; } void notif_set_application(struct notif *notif, const char *text) { free(notif->app); notif->app = ambstoc32(text); } void notif_set_summary(struct notif *notif, const char *text) { free(notif->summary); notif->summary = decode_html_entities(notif->mgr, text); } char * notif_get_summary(const struct notif *notif) { if (notif->summary == NULL) return NULL; return ac32tombs(notif->summary); } void notif_set_body(struct notif *notif, const char *text) { free(notif->body); notif->body = decode_html_entities(notif->mgr, text); } void notif_set_urgency(struct notif *notif, enum urgency urgency) { if (notif->urgency == urgency) return; notif->urgency = urgency; notif_reload_timeout(notif); notif_reload_fonts(notif); notif_reload_default_icon(notif); if (tll_length(notif->mgr->notifs) <= 1) return; tll_foreach(notif->mgr->notifs, it) { if (it->item == notif) { tll_remove(notif->mgr->notifs, it); break; } } tll_rforeach(notif->mgr->notifs, it) { if (it->item->urgency >= notif->urgency) { tll_insert_after(notif->mgr->notifs, it, notif); return; } } tll_push_front(notif->mgr->notifs, notif); } void notif_set_progress(struct notif *notif, int8_t progress) { if (notif->progress == progress) return; notif->progress = progress; } void notif_set_image(struct notif *notif, pixman_image_t *pix) { notif_set_image_internal(notif, pix, true); } void notif_set_timeout(struct notif *notif, int timeout_ms) { /* 0 - never expire */ notif->timeout_ms = timeout_ms; notif_reload_timeout(notif); } void notif_add_action(struct notif *notif, const char *id, const char *label) { char32_t *wid = ambstoc32(id); char32_t *wlabel = ambstoc32(label); if (wid == NULL || wlabel == NULL) { free(wid); free(wlabel); return; } tll_push_back( notif->actions, ((struct action){ .id = strdup(id), .wid = wid, .label = strdup(label), .wlabel = wlabel})); } void notif_play_sound(struct notif *notif) { const struct config *conf = notif->mgr->conf; const struct urgency_config *uconf = &conf->by_urgency[notif->urgency]; if (conf->play_sound.raw_cmd == NULL || uconf->sound_file == NULL) return; size_t argc; char **argv; if (!spawn_expand_template( &conf->play_sound, 1, (const char *[]){"filename"}, (const char *[]){uconf->sound_file}, &argc, &argv)) { return; } spawn(NULL, argv, -1, -1, -1); for (size_t i = 0; i < argc; i++) free(argv[i]); free(argv); } static void frame_callback(void *data, struct wl_callback *wl_callback, uint32_t callback_data) { struct notif *notif = data; LOG_DBG("frame callback"); assert(notif->frame_callback == wl_callback); notif->frame_callback = NULL; wl_callback_destroy(wl_callback); if (notif->pending != NULL) { commit_buffer(notif, notif->pending); notif->pending = NULL; } } static bool notif_instantiate_surface(struct notif *notif, int *width, int *height) { assert(notif->surface == NULL); assert(notif->layer_surface == NULL); struct notif_mgr *mgr = notif->mgr; struct wayland *wayl = mgr->wayl; const struct monitor *mon = wayl_preferred_monitor(wayl); /* Will be updated, if necessary, once we’ve been mapped */ const int scale = mon != NULL ? mon->scale : wayl_guess_scale(wayl); struct wl_surface *surface = wl_compositor_create_surface(wayl_compositor(wayl)); if (surface == NULL) { LOG_ERR("failed to create wayland surface"); return false; } const struct config *conf = mgr->conf; struct zwlr_layer_surface_v1 *layer_surface = zwlr_layer_shell_v1_get_layer_surface( wayl_layer_shell(wayl), surface, mon != NULL ? mon->output : NULL, conf->layer, "notifications"); if (layer_surface == NULL) { LOG_ERR("failed to create layer shell surface"); wl_surface_destroy(surface); return false; } /* Width/height must be divisible with the scale */ *width = (*width + scale - 1) / scale * scale; *height = (*height + scale - 1) / scale * scale; enum zwlr_layer_surface_v1_anchor anchor = (conf->anchor == ANCHOR_TOP_LEFT || conf->anchor == ANCHOR_TOP_RIGHT ? ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP : ZWLR_LAYER_SURFACE_V1_ANCHOR_BOTTOM) | (conf->anchor == ANCHOR_TOP_LEFT || conf->anchor == ANCHOR_BOTTOM_LEFT ? ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT : ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT); zwlr_layer_surface_v1_set_anchor(layer_surface, anchor); zwlr_layer_surface_v1_set_size(layer_surface, *width / scale, *height / scale); wl_surface_add_listener(surface, &surface_listener, notif); zwlr_layer_surface_v1_add_listener( layer_surface, &layer_surface_listener, notif); wl_surface_commit(surface); notif->surface = surface; notif->layer_surface = layer_surface; notif->mon = mon; notif->scale = mon != NULL ? mon->scale : wayl_guess_scale(wayl); notif->subpixel = mon != NULL ? (enum fcft_subpixel)mon->subpixel : wayl_guess_subpixel(wayl); return true; } struct glyph_run { size_t count; int *cluster; const struct fcft_glyph **glyphs; bool underline; struct fcft_font *font; bool free_arrays; }; static uint64_t sdbm_hash(size_t len, const char32_t s[static len]) { uint64_t hash = 0; for (size_t i = 0; i < len; i++) { int c = s[i]; hash = c + (hash << 6) + (hash << 16) - hash; } return hash; } static struct glyph_run notify_rasterize_text_run(struct notif *notif, struct fcft_font *font, enum fcft_subpixel subpixel, size_t len, const char32_t text[static len], size_t ofs) { uint64_t hash = sdbm_hash(len, text); tll_foreach(notif->text_run_cache, it) { if (it->item.hash != hash) continue; if (it->item.font != font) continue; if (it->item.subpixel != subpixel) continue; if (it->item.ofs != ofs) { /* TODO: we could still re-use the run, but need to deal * with cluster offsets */ continue; } return (struct glyph_run){ .count = it->item.run->count, .cluster = it->item.run->cluster, .glyphs = it->item.run->glyphs, .font = font, .free_arrays = false, }; } struct fcft_text_run *run = fcft_rasterize_text_run_utf32( font, len, text, subpixel); if (run == NULL) return (struct glyph_run){0}; for (size_t i = 0; i < run->count; i++) run->cluster[i] += ofs; struct text_run_cache cache = { .run = run, .font = font, .hash = hash, .subpixel = subpixel, .ofs = ofs, }; tll_push_front(notif->text_run_cache, cache); return (struct glyph_run){ .count = run->count, .cluster = run->cluster, .glyphs = run->glyphs, .font = font, .free_arrays = false, }; } static struct glyph_run notify_rasterize_glyphs(struct fcft_font *font, enum fcft_subpixel subpixel, size_t len, const char32_t text[static len], size_t ofs) { int *cluster = malloc(len * sizeof(cluster[0])); const struct fcft_glyph **glyphs = malloc(len * sizeof(glyphs[0])); struct glyph_run run = { .count = 0, .cluster = cluster, .glyphs = glyphs, .font = font, .free_arrays = true, }; for (size_t i = 0; i < len; i++) { const struct fcft_glyph *glyph = fcft_rasterize_char_utf32( font, text[i], subpixel); if (glyph == NULL) continue; cluster[run.count] = ofs + i; glyphs[run.count] = glyph; run.count++; } return run; } static struct glyph_run notify_rasterize(struct notif *notif, struct fcft_font *font, enum fcft_subpixel subpixel, size_t len, const char32_t text[static len], size_t ofs) { if (len == 0) return (struct glyph_run){0}; return fcft_capabilities() & FCFT_CAPABILITY_TEXT_RUN_SHAPING ? notify_rasterize_text_run(notif, font, subpixel, len, text, ofs) : notify_rasterize_glyphs(font, subpixel, len, text, ofs); } struct glyph_layout { const struct fcft_glyph *glyph; const pixman_color_t *color; int x, y; struct { bool draw; int y; int thickness; } underline; }; typedef tll(struct glyph_layout) glyph_list_t; static void notif_layout(struct notif *notif, struct font_set *fonts, const pixman_color_t *color, enum fcft_subpixel subpixel, const char32_t *text, int left_pad, int right_pad, int y, int max_y, int *width, int *height, glyph_list_t *glyph_list) { *width = 0; *height = 0; const struct config *conf = notif->mgr->conf; bool bold = false; bool italic = false; bool underline = false; tll(struct glyph_run) runs = tll_init(); size_t total_glyph_count = 0; /* Rasterize whole runs, if possible. Need to look for font * formatters since we need to use different fonts for those */ const char32_t *_t = text; for (const char32_t *wc = _t; true; wc++) { if (!(*wc == U'\0' || c32ncasecmp(wc, U"", 3) == 0 || c32ncasecmp(wc, U"", 3) == 0 || c32ncasecmp(wc, U"", 3) == 0 || c32ncasecmp(wc, U"", 4) == 0 || c32ncasecmp(wc, U"", 4) == 0 || c32ncasecmp(wc, U"", 4) == 0)) { continue; } /* Select font based on formatters currently enabled */ struct fcft_font *font = NULL; if (bold && italic) font = fonts->bold_italic; else if (bold) font = fonts->bold; else if (italic) font = fonts->italic; if (font == NULL) font = fonts->regular; size_t len = wc - _t; size_t ofs = _t - text; struct glyph_run run = notify_rasterize(notif, font, subpixel, len, _t, ofs); total_glyph_count += run.count; if (run.count > 0) { run.underline = underline; tll_push_back(runs, run); } if (*wc == U'\0') break; /* Update formatter state */ bool new_value = wc[1] == U'/' ? false : true; char32_t formatter = wc[1] == U'/' ? wc[2] : wc[1]; if (formatter == U'b' || formatter == U'B') bold = new_value; if (formatter == U'i' || formatter == U'I') italic = new_value; if (formatter == U'u' || formatter == U'U') underline = new_value; _t = wc + (wc[1] == U'/' ? 4 : 3); } /* Distance from glyph to next word boundary. Note: only the * *first* glyph in a word has a non-zero distance */ int distance[total_glyph_count]; { /* Need flat cluster+glyph arrays for this... */ int cluster[total_glyph_count]; const struct fcft_glyph *glyphs[total_glyph_count]; size_t idx = 0; tll_foreach(runs, it) { const struct glyph_run *run = &it->item; for (size_t i = 0; i < run->count; i++, idx++) { cluster[idx] = it->item.cluster[i]; glyphs[idx] = it->item.glyphs[i]; } } /* Loop glyph runs, looking for word boundaries */ idx = 0; tll_foreach(runs, it) { const struct glyph_run *run = &it->item; for (size_t i = 0; i < run->count; i++, idx++) { distance[idx] = 0; if (!isc32space(text[run->cluster[i]])) continue; /* Calculate distance to *this* space for all * preceding glyphs (up til the previous space) */ for (ssize_t j = idx - 1, dist = 0; j >= 0; j--) { if (isc32space(text[cluster[j]])) break; if (j == 0 || (j > 0 && isc32space(text[cluster[j - 1]]))) { /* * Store non-zero distance only in first character in a word * This ensures the layouting doesn't produce output like: * * x * x * x * x * * for very long words, that doesn't fit at all on single line. */ distance[j] = dist; } dist += glyphs[j]->advance.x; } } } /* Calculate distance for the last word */ for (ssize_t j = total_glyph_count - 1, dist = 0; j >= 0; j--) { if (isc32space(text[cluster[j]])) break; if (j == 0 || (j > 0 && isc32space(text[cluster[j - 1]]))) { /* Store non-zero distance only in first character in a word */ distance[j] = dist; } dist += glyphs[j]->advance.x; } } int x = left_pad; if (conf->min_width != 0) *width = conf->min_width; /* * Finally, lay out the glyphs * * This is done by looping the glyphs, and inserting a newline * whenever a word cannot be fitted in the remaining space. */ size_t idx = 0; tll_foreach(runs, it) { struct glyph_run *run = &it->item; for (size_t i = 0; i < run->count; i++, idx++) { const char32_t wc = text[run->cluster[i]]; const struct fcft_glyph *glyph = run->glyphs[i]; struct fcft_font *font = run->font; const int dist = distance[idx]; if ((x > left_pad && conf->max_width > 0 && x + glyph->advance.x + dist + right_pad > conf->max_width) || wc == U'\n') { *width = max(*width, x + right_pad); *height += fonts->regular->height; x = left_pad; y += fonts->regular->height; if (isc32space(wc)) { /* Don't render trailing whitespace */ continue; } } if (max_y >= 0 && y + fonts->regular->height > max_y) break; if (glyph->cols <= 0) continue; struct glyph_layout layout = { .glyph = glyph, .color = color, .x = x, .y = y + font->ascent, .underline = { .draw = run->underline, .y = y + font->ascent - font->underline.position, .thickness = font->underline.thickness, }, }; tll_push_back(*glyph_list, layout); x += glyph->advance.x; } if (run->free_arrays) { free(run->cluster); free(run->glyphs); } tll_remove(runs, it); } *width = max(*width, x + right_pad); *height += fonts->regular->height; } static char32_t * expand_format_string(const struct notif *notif, const char32_t *fmt) { if (fmt == NULL) return NULL; const size_t fmt_len = c32len(fmt); size_t ret_len = fmt_len; char32_t *ret = malloc(ret_len * sizeof(ret[0])); if (ret == NULL) return NULL; enum { ESCAPE_NONE, ESCAPE_PERCENT, ESCAPE_BACKSLASH} escape = ESCAPE_NONE; char32_t scratch[16]; size_t ret_idx = 0; for (const char32_t *src = fmt; src < &fmt[fmt_len]; src++) { const char32_t *append_str = NULL; size_t append_len = 0; switch (escape) { case ESCAPE_NONE: switch (*src) { case U'%': escape = ESCAPE_PERCENT; continue; case U'\\': escape = ESCAPE_BACKSLASH; continue; default: append_str = src; append_len = 1; break; } break; case ESCAPE_PERCENT: switch (*src) { case U'a': append_str = notif->app; append_len = c32len(append_str); break; case U's': append_str = notif->summary; append_len = c32len(append_str); break; case U'b': append_str = notif->body; append_len = c32len(append_str); break; case U'A': if (tll_length(notif->actions) > 0) { scratch[0] = U'*'; append_str = scratch; append_len = 1; } break; case U'%': append_str = src; append_len = 1; break; } escape = ESCAPE_NONE; break; case ESCAPE_BACKSLASH: switch (*src) { case U'n': scratch[0] = U'\n'; append_str = scratch; append_len = 1; break; } escape = ESCAPE_NONE; break; } if (append_str == NULL) continue; while (ret_idx + append_len + 1> ret_len) { size_t new_ret_len = ret_len * 2; char32_t *new_ret = realloc(ret, new_ret_len * sizeof(new_ret[0])); if (new_ret == NULL) { free(ret); return NULL; } ret = new_ret; ret_len = new_ret_len; } assert(ret_idx + append_len <= ret_len); memcpy(&ret[ret_idx], append_str, append_len * sizeof(ret[0])); ret_idx += append_len; } if (ret_idx == 0) { LOG_DBG("expand: %ls -> NULL", (const wchar_t *)fmt); free(ret); return NULL; } assert(ret_idx + 1 <= ret_len); ret[ret_idx] = U'\0'; LOG_DBG("expand: %ls -> %ls", (const wchar_t *)fmt, (const wchar_t *)ret); return ret; } static int notif_show(struct notif *notif, int y) { struct notif_mgr *mgr = notif->mgr; struct config *conf = mgr->conf; struct urgency_config *urgency = &conf->by_urgency[notif->urgency]; struct wayland *wayl = notif->mgr->wayl; const enum fcft_subpixel subpixel = urgency->bg.alpha == 0xffff ? notif->subpixel : FCFT_SUBPIXEL_NONE; const int pad_horizontal = urgency->padding.horizontal; const int pad_vertical = urgency->padding.vertical; const int pbar_height = urgency->progress.height; int pbar_y = -1; int width = 0; int height = pad_vertical; glyph_list_t glyphs = tll_init(); int indent = pad_horizontal; int _w, _h; if (notif->pix != NULL) indent += notif->image_width + pad_horizontal; char32_t *title = expand_format_string(notif, urgency->app.format); char32_t *summary = expand_format_string(notif, urgency->summary.format); char32_t *body = expand_format_string(notif, urgency->body.format); if (title != NULL && title[0] != U'\0') { notif_layout( notif, ¬if->fonts.app, &urgency->app.color, subpixel, title, indent, pad_horizontal, height, conf->max_height > 0 ? conf->max_height - pad_vertical : -1, &_w, &_h, &glyphs); width = max(width, _w); height += _h; } if (summary != NULL && summary[0] != U'\0') { notif_layout( notif, ¬if->fonts.summary, &urgency->summary.color, subpixel, summary, indent, pad_horizontal, height, conf->max_height > 0 ? conf->max_height - pad_vertical : -1, &_w, &_h, &glyphs); width = max(width, _w); height += _h; } if (body != NULL && body[0] != U'\0') { notif_layout( notif, ¬if->fonts.body, &urgency->body.color, subpixel, body, indent, pad_horizontal, height, conf->max_height > 0 ? conf->max_height - pad_vertical : -1, &_w, &_h, &glyphs); width = max(width, _w); height += _h; } free(title); free(summary); free(body); #if 0 /* App name */ if (notif->app != NULL && notif->app[0] != U'\0') { notif_layout( notif, ¬if->fonts.app, &urgency->app.color, subpixel, notif->app, indent, pad_horizontal, height, conf->max_height > 0 ? conf->max_height - pad_vertical : -1, &_w, &_h, &glyphs); if (tll_length(notif->actions) > 0) { /* TODO: better 'action' indicator */ int _a, _b; notif_layout( notif, ¬if->fonts.app, &urgency->app.color, subpixel, U"*", _w - pad_horizontal, pad_horizontal, height, conf->max_height > 0 ? conf->max_height - pad_vertical : -1, &_a, &_b, &glyphs); } width = max(width, _w); height += _h; } /* Summary */ if (notif->summary != NULL && notif->summary[0] != U'\0') { notif_layout( notif, ¬if->fonts.summary, &urgency->summary.color, subpixel, notif->summary, indent, pad_horizontal, height, conf->max_height > 0 ? conf->max_height - pad_vertical : -1, &_w, &_h, &glyphs); width = max(width, _w); height += _h; } /* Body */ if (notif->body != NULL && notif->body[0] != U'\0') { /* Empty line between summary and body */ height += notif->fonts.body.regular->height; notif_layout( notif, ¬if->fonts.body, &urgency->body.color, subpixel, notif->body, indent, pad_horizontal, height, conf->max_height > 0 ? conf->max_height - pad_vertical : -1, &_w, &_h, &glyphs); width = max(width, _w); height += _h; } #endif if (notif->pix != NULL) { height = max(height, pad_vertical + notif->image_height + pad_vertical); width = max(width, pad_horizontal + notif->image_width + pad_horizontal); } if (notif->progress >= 0) { const int bar_y = height + notif->fonts.body.regular->height; if (conf->max_height == 0 || bar_y + pbar_height <= conf->max_height - pad_vertical) { pbar_y = bar_y; height += notif->fonts.body.regular->height + pbar_height; width = max(width, 3 * pad_horizontal); } } height += pad_vertical; if (conf->max_height > 0) height = min(height, conf->max_height); bool top_anchored = conf->anchor == ANCHOR_TOP_LEFT || conf->anchor == ANCHOR_TOP_RIGHT; int scale; /* Resize and position */ if (notif->surface == NULL) { if (!notif_instantiate_surface(notif, &width, &height)) return 0; scale = notif->scale; } else { scale = notif->scale; width = (width + scale - 1) / scale * scale; height = (height + scale - 1) / scale * scale; zwlr_layer_surface_v1_set_size(notif->layer_surface, width / scale, height / scale); } LOG_DBG("show: y = %d, width = %d, height = %d (scale = %d)", y, width, height, scale); zwlr_layer_surface_v1_set_margin( notif->layer_surface, (top_anchored ? y : conf->margins.vertical) / scale, /* top */ conf->margins.horizontal / scale, /* right */ (!top_anchored ? y : conf->margins.between) / scale, /* bottom */ conf->margins.horizontal / scale); /* left */ struct buffer *buf = wayl_get_buffer(wayl, width, height); const int brd_sz = urgency->border.size;; pixman_region32_t clip; pixman_region32_init_rect(&clip, 0, 0, width, height); pixman_image_set_clip_region32(buf->pix, &clip); pixman_region32_fini(&clip); /* Border */ pixman_image_fill_rectangles( PIXMAN_OP_SRC, buf->pix, &urgency->border.color, 4, (pixman_rectangle16_t []){ {0, 0, buf->width, brd_sz}, /* top */ {buf->width - brd_sz, 0, brd_sz, buf->height}, /* right */ {0, buf->height - brd_sz, buf->width, brd_sz}, /* bottom */ {0, 0, brd_sz, buf->height}, /* left */ }); /* Background */ pixman_image_fill_rectangles( PIXMAN_OP_SRC, buf->pix, &urgency->bg, 1, &(pixman_rectangle16_t){ brd_sz, brd_sz, buf->width - 2 * brd_sz, buf->height - 2 * brd_sz} ); /* Image */ if (notif->pix != NULL) { pixman_image_composite32( PIXMAN_OP_OVER, notif->pix, NULL, buf->pix, 0, 0, 0, 0, pad_horizontal, (height - notif->image_height - (pbar_y >= 0 ? pbar_height : 0)) / 2, notif->image_width, notif->image_height); } /* Text */ tll_foreach(glyphs, it) { const struct fcft_glyph *glyph = it->item.glyph; if (pixman_image_get_format(glyph->pix) == PIXMAN_a8r8g8b8) { /* Glyph surface is a fully rendered bitmap */ pixman_image_composite32( PIXMAN_OP_OVER, glyph->pix, NULL, buf->pix, 0, 0, 0, 0, it->item.x + glyph->x, it->item.y - glyph->y, glyph->width, glyph->height); } else { /* Glyph surface is an alpha mask */ pixman_image_t *src = pixman_image_create_solid_fill(it->item.color); pixman_image_composite32( PIXMAN_OP_OVER, src, glyph->pix, buf->pix, 0, 0, 0, 0, it->item.x + glyph->x, it->item.y - glyph->y, glyph->width, glyph->height); pixman_image_unref(src); } if (it->item.underline.draw) { pixman_image_fill_rectangles( PIXMAN_OP_OVER, buf->pix, it->item.color, 1, &(pixman_rectangle16_t){ it->item.x, it->item.underline.y, glyph->advance.x, it->item.underline.thickness}); } tll_remove(glyphs, it); } /* Progress bar */ if (pbar_y >= 0) { const int full_width = buf->width - pad_horizontal * 2; const int width = full_width * notif->progress / 100; const int border = pbar_height > 2 * scale && width > 2 * scale ? 1 * scale : 0; pixman_image_fill_rectangles( PIXMAN_OP_OVER, buf->pix, &urgency->progress.color, 5, (pixman_rectangle16_t []){ /* Edges: left, top, bottom, right */ {pad_horizontal, pbar_y, border, pbar_height}, {pad_horizontal + border, pbar_y, full_width - border * 2, border}, {pad_horizontal + border, pbar_y + pbar_height - border, full_width - border * 2, border}, {pad_horizontal + full_width - border, pbar_y, border, pbar_height}, /* The bar */ {pad_horizontal + border, pbar_y + border, width - border * 2, pbar_height - border * 2} }); } if (!notif->is_configured || notif->frame_callback != NULL) { if (notif->pending != NULL) notif->pending->busy = false; notif->pending = buf; /* Commit size+margins, but not the new buffer */ wl_surface_commit(notif->surface); } else { assert(notif->pending == NULL); commit_buffer(notif, buf); } notif->y = y; return height; } void notif_mgr_refresh(struct notif_mgr *mgr) { int y = mgr->conf->margins.vertical; switch (mgr->conf->stacking_order) { case STACK_BOTTOM_UP: tll_rforeach(mgr->notifs, it) y += notif_show(it->item, y) + mgr->conf->margins.between; break; case STACK_TOP_DOWN: tll_foreach(mgr->notifs, it) y += notif_show(it->item, y) + mgr->conf->margins.between; break; } } void notif_mgr_notifs_reload_timeout(const struct notif_mgr *mgr) { tll_foreach(mgr->notifs, it) notif_reload_timeout(it->item); } ssize_t notif_mgr_get_ids(const struct notif_mgr *mgr, uint32_t *ids, size_t max) { size_t count = 0; tll_foreach(mgr->notifs, it) { if (++count <= max && ids != NULL) ids[count - 1] = it->item->id; } return count; } static bool notif_dismiss(struct notif *notif) { dbus_signal_dismissed(notif->mgr->bus, notif->id); notif_destroy(notif); return true; } static bool notif_expire(struct notif *notif) { dbus_signal_expired(notif->mgr->bus, notif->id); notif_destroy(notif); return true; } static bool notif_mgr_expire_current(struct notif_mgr *mgr) { if (tll_length(mgr->notifs) == 0) return false; struct notif *notif = tll_pop_front(mgr->notifs); switch (notif->deferred_expiral) { case EXPIRE_IMMEDIATELY: break; case EXPIRE_DEFER: notif->deferred_expiral = EXPIRE_DELAYED; tll_push_front(mgr->notifs, notif); return true; case EXPIRE_DELAYED: /* Already marked for expiration */ return true; } bool ret = notif_expire(notif); notif_mgr_refresh(mgr); return ret; } bool notif_mgr_expire_id(struct notif_mgr *mgr, uint32_t id) { if (id == 0) return notif_mgr_expire_current(mgr); tll_foreach(mgr->notifs, it) { if (it->item->id != id) continue; struct notif *notif = it->item; switch (notif->deferred_expiral) { case EXPIRE_IMMEDIATELY: break; case EXPIRE_DEFER: notif->deferred_expiral = EXPIRE_DELAYED; return true; case EXPIRE_DELAYED: /* Already marked for expiration */ return true; } tll_remove(mgr->notifs, it); bool ret = notif_expire(notif); notif_mgr_refresh(mgr); return ret; } return false; } static bool notif_mgr_dismiss_current(struct notif_mgr *mgr) { if (tll_length(mgr->notifs) == 0) return false; struct notif *notif = tll_pop_front(mgr->notifs); switch (notif->deferred_dismissal) { case DISMISS_IMMEDIATELY: break; case DISMISS_DEFER: notif->deferred_dismissal = DISMISS_DELAYED; tll_push_front(mgr->notifs, notif); return true; case DISMISS_DELAYED: /* Already marked for dismissal */ return true; } bool ret = notif_dismiss(notif); notif_mgr_refresh(mgr); return ret; } static bool notif_mgr_dismiss_id_internal(struct notif_mgr *mgr, uint32_t id, bool refresh) { if (id == 0) return notif_mgr_dismiss_current(mgr); tll_foreach(mgr->notifs, it) { if (it->item->id != id) continue; struct notif *notif = it->item; switch (notif->deferred_dismissal) { case DISMISS_IMMEDIATELY: break; case DISMISS_DEFER: notif->deferred_dismissal = DISMISS_DELAYED; return true; case DISMISS_DELAYED: /* Already marked for dismissal */ return true; } tll_remove(mgr->notifs, it); bool ret = notif_dismiss(notif); if (refresh) notif_mgr_refresh(mgr); return ret; } return false; } bool notif_mgr_dismiss_id(struct notif_mgr *mgr, uint32_t id) { return notif_mgr_dismiss_id_internal(mgr, id, true); } bool notif_mgr_dismiss_all(struct notif_mgr *mgr) { bool ret = true; tll_foreach(mgr->notifs, it) { struct notif *notif = it->item; bool do_dismiss = true; switch (notif->deferred_dismissal) { case DISMISS_IMMEDIATELY: break; case DISMISS_DEFER: notif->deferred_dismissal = DISMISS_DELAYED; do_dismiss = false; break; case DISMISS_DELAYED: /* Already marked for dismissal */ do_dismiss = false; break; } if (do_dismiss) { if (!notif_dismiss(notif)) ret = false; tll_remove(mgr->notifs, it); } } notif_mgr_refresh(mgr); return ret; } void notif_mgr_monitor_removed(struct notif_mgr *mgr, const struct monitor *mon) { tll_foreach(mgr->notifs, it) { if (it->item->mon == mon) it->item->mon = NULL; } } /* Returns true if the update is a reason to refresh */ bool notif_mgr_monitor_updated(struct notif_mgr *mgr, const struct monitor *mon) { bool refresh_needed = false; tll_foreach(mgr->notifs, it) { struct notif *notif = it->item; if (notif->surface == NULL) refresh_needed = true; const int old_notif_scale = notif->scale; if (notif_reload_fonts(notif)) refresh_needed = true; else if (old_notif_scale != notif->scale) { /* for set_buffer_scale() */ refresh_needed = true; } if (notif->mon != NULL && notif->mon == mon && notif->subpixel != (enum fcft_subpixel)mon->subpixel) { notif->subpixel = (enum fcft_subpixel)mon->subpixel; refresh_needed = true; } } return refresh_needed; } struct action_async { struct fdm *fdm; struct notif_mgr *mgr; /* The notification may be dismissed while we're waiting for the * action selection. So, store the ID, and re-retreieve the * notification when we're done */ uint32_t notif_id; pid_t pid; int to_child; /* Child's stdin */ int from_child; /* Child's stdout */ char *input; /* Data to be sent to child (action labels) */ size_t input_idx; /* Where to start next write() */ size_t input_len; /* Total amount of data */ char *output; /* Output from child */ size_t output_len; /* Amount of output received (so far) */ notif_select_action_cb completion_cb; void *data; }; static bool fdm_action_writer(struct fdm *fdm, int fd, int events, void *data) { struct action_async *async = data; ssize_t count = write( async->to_child, &async->input[async->input_idx], async->input_len - async->input_idx); if (count < 0) { LOG_ERRNO("could not write actions to actions selection helper"); goto done; } async->input_idx += count; if (async->input_idx >= async->input_len) { /* Close child's stdin, to signal there are no more labels */ LOG_DBG("all input sent to child"); goto done; } return true; done: fdm_del(async->fdm, async->to_child); async->to_child = -1; return true; } static bool fdm_action_reader(struct fdm *fdm, int fd, int events, void *data) { struct action_async *async = data; const size_t chunk_sz = 128; char buf[chunk_sz]; ssize_t count = read(async->from_child, buf, chunk_sz); if (count < 0) { LOG_ERRNO("failed to read from actions selection helper"); goto check_pollhup; } /* Append to previously received response */ size_t new_len = async->output_len + count; async->output = realloc(async->output, new_len); memcpy(&async->output[async->output_len], buf, count); async->output_len = new_len; check_pollhup: if (!(events & EPOLLHUP)) return true; /* Strip trailing spaces/newlines */ while (async->output_len > 0 && isspace(async->output[--async->output_len])) async->output[async->output_len] = '\0'; /* Extract the data we need from the info struct, then free it */ struct notif_mgr *mgr = async->mgr; pid_t pid = async->pid; uint32_t notif_id = async->notif_id; notif_select_action_cb completion_cb = async->completion_cb; void *cb_data = async->data; char *chosen = async->output; size_t chosen_len = async->output_len; if (async->to_child != -1) { /* This is an error case - normally, the writer should have * completed and closed this already */ fdm_del(async->fdm, async->to_child); } fdm_del(async->fdm, async->from_child); free(async->input); free(async); /* Wait for child to die */ int status; waitpid(pid, &status, 0); LOG_DBG("child exited with status 0x%08x", status); const char *action_id = NULL; struct notif *notif = notif_mgr_get_notif(mgr, notif_id); if (!WIFEXITED(status)) { LOG_ERR("child did not exit normally"); goto done; } if (WEXITSTATUS(status) != 0) { uint8_t code = WEXITSTATUS(status); if (code >> 1) LOG_ERRNO_P("failed to execute action selection helper", code & 0x7f); goto done; } if (notif == NULL) { LOG_WARN("notification was dismissed before we could signal action: %.*s", (int)chosen_len, chosen); goto done; } /* Map returned label to action ID */ tll_foreach(notif->actions, it) { if (strncmp(it->item.label, chosen, chosen_len) == 0) { action_id = it->item.id; goto done; } } LOG_WARN("could not map chosen action label to action ID: %.*s", (int)chosen_len, chosen); done: completion_cb(notif_id, action_id, cb_data); free(chosen); if (notif->deferred_expiral == EXPIRE_DELAYED) { notif->deferred_expiral = EXPIRE_IMMEDIATELY; notif_mgr_expire_id(mgr, notif->id); } else { notif->deferred_expiral = EXPIRE_IMMEDIATELY; if (notif->deferred_dismissal == DISMISS_DELAYED) { notif->deferred_dismissal = DISMISS_IMMEDIATELY; notif_mgr_dismiss_id(mgr, notif->id); } else notif->deferred_dismissal = DISMISS_IMMEDIATELY; } return true; } static bool push_argv(char ***argv, size_t *size, char *arg, size_t *argc) { if (arg != NULL && arg[0] == '%') return true; if (*argc >= *size) { size_t new_size = *size > 0 ? 2 * *size : 10; char **new_argv = realloc(*argv, new_size * sizeof(new_argv[0])); if (new_argv == NULL) return false; *argv = new_argv; *size = new_size; } (*argv)[(*argc)++] = arg; return true; } static bool tokenize_cmdline(char *cmdline, char ***argv) { *argv = NULL; size_t argv_size = 0; bool first_token_is_quoted = cmdline[0] == '"' || cmdline[0] == '\''; char delim = first_token_is_quoted ? cmdline[0] : ' '; char *p = first_token_is_quoted ? &cmdline[1] : &cmdline[0]; size_t idx = 0; while (*p != '\0') { char *end = strchr(p, delim); if (end == NULL) { if (delim != ' ') { LOG_ERR("unterminated %s quote\n", delim == '"' ? "double" : "single"); free(*argv); return false; } if (!push_argv(argv, &argv_size, p, &idx) || !push_argv(argv, &argv_size, NULL, &idx)) { goto err; } else return true; } *end = '\0'; if (!push_argv(argv, &argv_size, p, &idx)) goto err; p = end + 1; while (*p == delim) p++; while (*p == ' ') p++; if (*p == '"' || *p == '\'') { delim = *p; p++; } else delim = ' '; } if (!push_argv(argv, &argv_size, NULL, &idx)) goto err; return true; err: free(*argv); return false; } size_t notif_action_count(const struct notif *notif) { return tll_length(notif->actions); } void notif_select_action( struct notif *notif, notif_select_action_cb completion_cb, void *data) { char *copy = strdup(notif->mgr->conf->selection_helper); char **argv = NULL; int to_child[2] = {-1, -1}; /* Pipe to child's STDIN */ int from_child[2] = {-1, -1}; /* Pipe to child's STDOUT */ if (tll_length(notif->actions) == 0) goto err_before_fork; if (!tokenize_cmdline(copy, &argv)) goto err_before_fork; if (pipe(to_child) < 0 || pipe(from_child) < 0) { LOG_ERRNO("failed to create pipe"); goto err_before_fork; } int pid = fork(); if (pid == -1) { LOG_ERRNO("failed to fork"); goto err_before_fork; } notif->deferred_dismissal = DISMISS_DEFER; notif->deferred_expiral = EXPIRE_DEFER; if (pid == 0) { /* * Child */ close(to_child[1]); close(from_child[0]); /* Rewire pipes to child's STDIN/STDOUT */ if (dup2(to_child[0], STDIN_FILENO) < 0 || dup2(from_child[1], STDOUT_FILENO) < 0) { goto child_exit; } close(to_child[0]); close(from_child[1]); execvp(argv[0], argv); child_exit: _exit(1 << 7 | errno); } assert(pid > 0); /* * Parent */ free(copy); free(argv); close(to_child[0]); close(from_child[1]); /* * Writing the action labels and waiting for the response can take * a *very* long time, and we can't block execution. * * Make our pipe ends non-blocking, and use the FDM to write/read * them asynchronously. */ struct action_async *async = NULL; size_t input_len = 0; char *input = NULL; if (fcntl(to_child[1], F_SETFL, fcntl(to_child[1], F_GETFL) | O_NONBLOCK) < 0 || fcntl(from_child[0], F_SETFL, fcntl(from_child[0], F_GETFL) | O_NONBLOCK) < 0) { LOG_ERRNO("failed to make pipes non blocking"); goto err_in_parent; } /* Construct a single string consisting of all the action labels * separated by newlines */ tll_foreach(notif->actions, it) input_len += strlen(it->item.label) + 1; input = malloc(input_len + 1); input[0] = '\0'; tll_foreach(notif->actions, it) { strcat(input, it->item.label); strcat(input, "\n"); } /* FDM callback data. Shared by both the write and read callback, * but *only* freed by the *read* handler. */ async = malloc(sizeof(*async)); *async = (struct action_async) { .fdm = notif->mgr->fdm, .mgr = notif->mgr, .notif_id = notif->id, .to_child = to_child[1], .from_child = from_child[0], .input = input, .input_len = input_len, .input_idx = 0, .output = NULL, .output_len = 0, .completion_cb = completion_cb, .data = data, }; if (!fdm_add(notif->mgr->fdm, to_child[1], EPOLLOUT, &fdm_action_writer, async) || !fdm_add(notif->mgr->fdm, from_child[0], EPOLLIN, &fdm_action_reader, async)) { goto err_in_parent; } return; err_before_fork: if (to_child[0] != -1) close(to_child[0]); if (to_child[1] != -1) close(to_child[1]); if (from_child[0] != -1) close(from_child[0]); if (from_child[1] != -1) close(from_child[1]); free(copy); free(argv); completion_cb(notif->id, NULL, data); notif->deferred_dismissal = DISMISS_IMMEDIATELY; notif->deferred_expiral = EXPIRE_IMMEDIATELY; return; err_in_parent: free(async); free(input); fdm_del(notif->mgr->fdm, to_child[1]); fdm_del(notif->mgr->fdm, from_child[0]); completion_cb(notif->id, NULL, data); notif->deferred_dismissal = DISMISS_IMMEDIATELY; notif->deferred_expiral = EXPIRE_IMMEDIATELY; return; } fnott-1.4.1+ds/notification.h000066400000000000000000000050011447512336000161110ustar00rootroot00000000000000#pragma once #include #include #include #include #include "config.h" #include "fdm.h" #include "icon.h" #include "tllist.h" /* forward declarations, to avoid circular includes */ struct wayland; struct dbus; struct notif_mgr; struct notif_mgr *notif_mgr_new(struct config *conf, struct fdm *fdm, const icon_theme_list_t *icon_theme); void notif_mgr_destroy(struct notif_mgr *mgr); void notif_mgr_configure( struct notif_mgr *mgr, struct wayland *wayl, struct dbus *bus); void notif_mgr_refresh(struct notif_mgr *mgr); void notif_mgr_notifs_reload_timeout(const struct notif_mgr *mgr); ssize_t notif_mgr_get_ids(const struct notif_mgr *mgr, uint32_t *ids, size_t max); bool notif_mgr_expire_id(struct notif_mgr *mgr, uint32_t id); bool notif_mgr_dismiss_id(struct notif_mgr *mgr, uint32_t id); bool notif_mgr_dismiss_all(struct notif_mgr *mgr); struct monitor; void notif_mgr_monitor_removed(struct notif_mgr *mgr, const struct monitor *mon); bool notif_mgr_monitor_updated(struct notif_mgr *mgr, const struct monitor *mon); enum urgency { URGENCY_LOW, URGENCY_NORMAL, URGENCY_CRITICAL }; struct notif; struct notif *notif_mgr_create_notif(struct notif_mgr *mgr, uint32_t replaces_id); struct notif *notif_mgr_get_notif(struct notif_mgr *mgr, uint32_t id); struct notif *notif_mgr_get_notif_for_surface( struct notif_mgr *mgr, const struct wl_surface *surface); bool notif_mgr_del_notif(struct notif_mgr *mgr, uint32_t id); void notif_destroy(struct notif *notif); uint32_t notif_id(const struct notif *notif); const struct monitor *notif_monitor(const struct notif *notif); void notif_set_application(struct notif *notif, const char *text); void notif_set_summary(struct notif *notif, const char *text); void notif_set_body(struct notif *notif, const char *text); void notif_set_urgency(struct notif *notif, enum urgency urgency); void notif_set_image(struct notif *notif, pixman_image_t *pix); void notif_set_timeout(struct notif *notif, int timeout_ms); void notif_set_progress(struct notif *notif, int8_t progress); void notif_add_action(struct notif *notif, const char *id, const char *label); void notif_play_sound(struct notif *notif); char *notif_get_summary(const struct notif *notif); typedef void (*notif_select_action_cb)( uint32_t notif_id, const char *action_id, void *data); size_t notif_action_count(const struct notif *notif); void notif_select_action( struct notif *notif, notif_select_action_cb completion_cb, void *data); fnott-1.4.1+ds/png-fnott.h000066400000000000000000000001171447512336000153420ustar00rootroot00000000000000#pragma once #include pixman_image_t *png_load(const char *path); fnott-1.4.1+ds/png.c000066400000000000000000000100521447512336000142040ustar00rootroot00000000000000#include "png-fnott.h" #include #include #include #include #include #include #include #define LOG_MODULE "png" #define LOG_ENABLE_DBG 0 #include "log.h" #include "stride.h" pixman_image_t * png_load(const char *path) { pixman_image_t *pix = NULL; FILE *fp = NULL; png_structp png_ptr = NULL; png_infop info_ptr = NULL; png_bytepp row_pointers = NULL; uint8_t *image_data = NULL; /* open file and test for it being a png */ if ((fp = fopen(path, "rb")) == NULL) { //LOG_ERRNO("%s: failed to open", path); goto err; } /* Verify PNG header */ uint8_t header[8] = {0}; if (fread(header, 1, 8, fp) != 8 || png_sig_cmp(header, 0, 8)) { // LOG_ERR("%s: not a PNG", path); goto err; } /* Prepare for reading the PNG */ if ((png_ptr = png_create_read_struct( PNG_LIBPNG_VER_STRING, NULL, NULL, NULL)) == NULL || (info_ptr = png_create_info_struct(png_ptr)) == NULL) { LOG_ERR("%s: failed to initialize libpng", path); goto err; } if (setjmp(png_jmpbuf(png_ptr))) { LOG_ERR("%s: libpng error", path); goto err; } png_init_io(png_ptr, fp); png_set_sig_bytes(png_ptr, 8); /* Get meta data */ png_read_info(png_ptr, info_ptr); int width = png_get_image_width(png_ptr, info_ptr); int height = png_get_image_height(png_ptr, info_ptr); png_byte color_type = png_get_color_type(png_ptr, info_ptr); png_byte bit_depth __attribute__((unused)) = png_get_bit_depth(png_ptr, info_ptr); int channels __attribute__((unused)) = png_get_channels(png_ptr, info_ptr); LOG_DBG("%s: %dx%d@%hhubpp, %d channels", path, width, height, bit_depth, channels); png_set_packing(png_ptr); png_set_interlace_handling(png_ptr); png_set_strip_16(png_ptr); /* "pack" 16-bit colors to 8-bit */ png_set_bgr(png_ptr); /* pixman expects pre-multiplied alpha */ png_set_alpha_mode(png_ptr, PNG_ALPHA_PREMULTIPLIED, 1.0); /* Tell libpng to expand to RGB(A) when necessary, and tell pixman * whether we have alpha or not */ pixman_format_code_t format; switch (color_type) { case PNG_COLOR_TYPE_GRAY: case PNG_COLOR_TYPE_GRAY_ALPHA: LOG_DBG("%d-bit gray%s", bit_depth, color_type == PNG_COLOR_TYPE_GRAY_ALPHA ? "+alpha" : ""); if (bit_depth < 8) png_set_expand_gray_1_2_4_to_8(png_ptr); png_set_gray_to_rgb(png_ptr); format = color_type == PNG_COLOR_TYPE_GRAY ? PIXMAN_r8g8b8 : PIXMAN_a8r8g8b8; break; case PNG_COLOR_TYPE_PALETTE: LOG_DBG("%d-bit colormap%s", bit_depth, png_get_valid(png_ptr, info_ptr, PNG_INFO_tRNS) ? "+tRNS" : ""); png_set_palette_to_rgb(png_ptr); if (png_get_valid(png_ptr, info_ptr, PNG_INFO_tRNS)) { png_set_tRNS_to_alpha(png_ptr); format = PIXMAN_a8r8g8b8; } else format = PIXMAN_r8g8b8; break; case PNG_COLOR_TYPE_RGB: LOG_DBG("RGB"); format = PIXMAN_r8g8b8; break; case PNG_COLOR_TYPE_RGBA: LOG_DBG("RGBA"); format = PIXMAN_a8r8g8b8; break; } png_read_update_info(png_ptr, info_ptr); size_t row_bytes __attribute__((unused)) = png_get_rowbytes(png_ptr, info_ptr); int stride = stride_for_format_and_width(format, width); image_data = malloc(height * stride); LOG_DBG("stride=%d, row-bytes=%zu", stride, row_bytes); assert(stride >= row_bytes); row_pointers = malloc(height * sizeof(png_bytep)); for (int i = 0; i < height; i++) row_pointers[i] = &image_data[i * stride]; png_read_image(png_ptr, row_pointers); pix = pixman_image_create_bits_no_clear( format, width, height, (uint32_t *)image_data, stride); err: if (pix == NULL) free(image_data); free(row_pointers); if (png_ptr != NULL) png_destroy_read_struct(&png_ptr, &info_ptr, NULL); if (fp != NULL) fclose(fp); return pix; } fnott-1.4.1+ds/screenshot-2.png000066400000000000000000002200161447512336000163010ustar00rootroot00000000000000PNG  IHDR'f6C IDATxy|-:,߱;$!p~P(J)GB ABH!w9J;o˲%[%"KZ~>3ggLP@ &EQmS~/ή$Kk1A^1IHHp… _B?/]qҥ۷?mݨCQDn 2g 3 YYYbؙ Oߤlܸ1*****jƍ/!ƞ>_#""RT*_|Ų>(<<|-EΤ'<<Ǐ?8lݺ>駟n {˗/w凉SO= H$vwԩuї~E@BBf\j.hjK;u:]kkP(|뭷?_W111Ȳ2Ǯc洴4kreȰ6\vxٗ}A0?@ܷo\.fkO?}ǜYx*ĉQQQ0gΜZ[zzzj7]u#8p#h"TdE_3Ʌŋ}yꩧX zu`Srɛo`>۶mo޽-innDcǎUVYG|s0 m5.QmKm…'n~\>o޼z 3116ZM~~~V[n+kҽoܸ1++qsRgٞ شiΜ9>ՕkX,n2CS݌\Tp ?Ul۶m@RRRܾ׊57Y^^K7ZmwwUh4͸>۶mFo_|˗/_|9I555eee'O''g޽/_>|0ݻw+J{9չ0R.XrYF3,YdZ`07o^`Fp==|0UE =W'oғ-ݻiӦn$[|Ͳq8}:S!"ۯVƻͶ.>g0td2^|EعKHH,Yw^; ?W(ܻ=d޼y_n5k_| O>ہësMnАgVNF3""">szmVSS3k֬۷|7t3((O&a|\cZ#t-G=zf͚3gNNN@ x'=י$9U\?Ξ= |>"jooqVIpGVXu⧟~o8~8_~[k/_|A$q#v;#Hx?yΥ6ltjt:3㦛nJӃ| /P(֯_W_4FòxD"{~}:u_w:z+q0IkY>>z}www```PP&:::;;@PXXeg߾}^{7י=l7㎄$_uXx\.(W֞dI6:ޠ z>|ZZ㯽N;::Ӫ'>O<,HV^m08 Fժ$FRm;cX /?S:{d\Υuo'}oxZ nKgeeq0 4y` n$رc=B]^^N?쳶4999;vK^~ƞ߿}GljѱgΜC'{*++`Jr…PXXRN?wI80yVܴi3^__f͚2۫\ ?6X'u*F7of1mdb=vVqIssΝ;o6v.v*p}Iv1F~_[E"ʕ+mNOtɖ,g}nRSS׭['H5vqӟtnImz ۰Cl߾d뮻-[,3b4o>l6777{s7co[=ztӦMeee'|2"v!O/q_z?нIvV}޼eH1Lws3(//(*''v9 4da׮]O?bj]ݐ˙L߶ΔbWWWBB mKKڵku9_1flٲ7R_Gyu>޺sNI={nrh<HSڵ7b>Lsrr˱/q)]~TAmtވ:##cD?Bzѳ Ndd-[/^Bj4]vM*%i<{ .: Ĥ/P^!b@Db̽m `7u1v@Lfńj8ub0j $p62\@ A Iipu1v 30>?6p}ڑm[9X5{U;U6c K#V]2 p_ĥ0G=\J5{,ؽzn=0챱$I<Ø^qSlK/v Op|]CMXZȱsK q$an{c{bYG4wTq&y*`h\@ &yOH{lc9F21 #MNN+'Aޑ4=3۬ccoҎ:Ԩ[TS Oz0=鑶f5_7X w'%״'e^'vqv:GZGe:a9x‘ /& cF&cݲ1Q̑^կ`F.E啶KS'B-pm=?UA &+ @CaD1コ 3 :v?*oayc c353H[uvOg1hÍA=Ӫ[ky2¢Gؽ4qc1?o.0V4c>0.ODpa3eA/3%y3[&:h`{ 0%(S;Hkexly>eY^י^F]Sf7yC1m=me,W'}Z\: a16Ϛu/O ~(5ކ mq)"M;A ؁@ d[u1vL6@LW.{~{x@ Ն:&[4q@"v+#@ F5GP[@ cEQ@ /òdjy6@ 3-Zςˍ~ ƥ /7嗲}>uƎ+znYyO"V~13;9qNr|{`bl`Ywk[(C]> >eq"b![ IGKD""V~vA Sp$}puKTrw8b)1Pd=cB9@y o#Og@ ㋛Ƶ'v'NS(Bխݚ1n>g<=ڏOk[3& q`|* 3Z/H%sRTJ_եrlQFzswץ3bҜ@5wt] Ξ|l0.ƓIF[1L$驫nƥ.y 3bÃ>RRR\YC8WJLtlxXb[ ++&G7t٥.moj\tthܗ* +u9/?1 G.aӌbba/xD$ѫPψ ޝ{'p%N?T},dЀ V J\.fH(0;f%%٫ RS6~ MK-ov\XXFB\Qe5íN:3b2YaEUqU5s̝>ѬĨH$]P{Q,!*R(V0VCMyE%CfD$Z5v@QUsR-9_QU\];=W,2YrK .mghI&(@eŲP>G$p˯ȎsC&3asS2$[ +bBTжnMbPRTٙ*qZ~8n0_E! •IH:Yzȸ0&EF1WqafRZ]K[ǐd.N /umMdecCf3 'JJ(;Ζ~:udMM^0[*3 I7n3=t7ڎ#Պ IDAT 02K~e$qhd +ذkX$EQ=~ 8}04=JN!K 8Av1UXaC&WV0Ib|\&k궕 X9X(ok+*x$Uɾ$q݀AӧR3|#vk[9oNDPrtu;{۸LIq`i2 P)5}1$;;g%+vsA#P\]dRƁŠSm^@Gd$.ĴCR0i4TppyHt˃gًeNzլ@y]``RB8qh߉S3->pZ.pII3㷼3G.%EB\&[0 |@ R2`ob8r|ǡssRc",-ݽ^J [:zcޞ@"=_fe `|'y9*%`hbIÂ5}^vI+ IQ=ڙ8. 9tqS莥 fGN$HA8Af3nr%H),+9Mˬ'!C.8tGnG^]5Cqga(i8d1X*}}bB7]\y Ϊ KuԌ#"Lfم3槥$yQm8aqK~-3n?s' FΞМXۓhˤbdEQ,2@\:&|:C.*=\`4 fIs)93E쿚|^RtԐ|rͤ,PaNX$\2h4*85 A\u۸ PV*<"=_Z]J(۶EBBPhf¶:dw'$bwj;T AwݰH.搜]} -t9|_B'&ӚˆQCs|T7$TZT礥0^5SIR٬omJScc hsΪo>D_@dpЦvvJ-|+ζ~͈H.s زА\oo e{XOu }RJ)n3w=vЅ@(*EKgg WrW*ݰxs[Y&CQ< 2f%I K27,ܰ4sqfBx^\W;zf'ǤG\m֊dĨoX|ύq}7/s#v\SRU23.fFx`PHD˭ ?4}bPTeg5utF/6#h2P}hsO,ʘaO[gΈ*otfD,@?;_գ$ld.6H(|ys|XUnavO^$+ATd&/H,EQ\l梇6NWTjlmi y*%MfRXʏUWa¾wR] ߰4hbذo[E$cݽ Ԙ6X1BLpXu CC˛73%XTLU啌shWVh2 E-]Ы>/^jrO!WVəϸ5~O7fZ!j`M+JJT+& ^vREUd P)É6Mwjl 1jX|r}N$BO\l梇 r1uhzbBT* Ξ֗c+?L2 u<$%YIhoXl )Uk@lxX$1U0 G1< sLj:{nLhSfz)G `-HK9\PXXt`6q/`Zh`x0)iֽm/6.Naw/|-+gV|{~{ʗ*^46 AL 'W,Ic ˥ K.,r8jX}S@U".zr8j@L;{ jM@V7l\eJjp3LL+L- T} orEoKty,BoRG<"0|Pn`W uA@?2:u'|} N/LwyX$lZJ)Q,˱=nShaQ_lSmVreDs!DXԩz}vU0}oM7V7(4(gd-ݜA &=Ð)*a$V߭]lPȋT1%A^PPmw OsL̶߹Kd%e&F"TYeޕGLIA &"%:4Ͽ?m+=yDlXց9[t) :鍱H#6 ^zv$o/1[9H L *(L$ ޵V(_mٓ{ƿPb2Zsۙ9bb(:iPô088ǀ/X%q? 1I/ĂaQYq8c%M"fOvFD_.-}'>2^ahAyYUSހa%dž]779_]#S5@uK7َc_A=^Ib`- fXh=XvnhhWad=auK~~Qv i35{vO6b:a,<o;^ xstOwսum)YxoQ[c/fQƽDAL0c!%Y{e޺wA3-<}&3}.5. P>en_G!bEMi,Q* 1=cF;?'xl߼ذo`3MJkcP;֨>;AÐkty4"x}8)Ք4|bA S:uXe36a>|p|k獈w޸4sFDdiUقsg.2iz|dQE$UԙX6a_!>)a(L~ Y v9Q_Oyʶ͇vO%֓νam!ע׺z + AL}0vg[{nymт}rzEat{G*-t=NW(( #i*>YRݣ2[p z`$3XZvF@o'k1soLeL+ͤ/}^}_s'][L'mVMfsÆ^`3&F|uSְ3i5Нr~UCH:?,, uÆP'qb # i l;QbnLfX$@x_\DG%HbD, B!_*r , ý_ |I*WX0S'p LJ X'1ym G m,&X$T+M]67y$0\ky1N&Xb`4\ >ĚOatVah!s0QGI]s;,6M/A\sj>1]:%UMvhu$60lݢ*_X ,8Pl\rs[~GdykV6vt\s|7xaˎɉsCG  vc*rkې6*~~rP)I(kPۜe^0W"ްإ0yk/T(3]y?@RÌ>Rr@(xulxL"Ƈϕ;Fon8?T\ LtTaT(m/ p O_Xbju-R0;-<:aqflx`KZ@V/oLZ~S,6kr *ZfD٭W=Ry`؊8?-Qr"EOkhl֢Xp%姳G[R]ffx:\7Men JJ*7 ۶nŤb3޼|f8N-?Q\>+1|Z^OWpύ=GL.09a\+z:˗-KBn \%)iY7'ݾ{~y{iWW,bŀ`k8:*|6HTFj1m2lv!0,@WɯeG_i5 ake8HQ; @#&yZ/H%sRTJ_ՙL(#zFq1PQxiNJb|dP ;. DgL>x@otiwqLɤAG& {ֵI\R_=P(=_<3.6<(P#5,--ŕ5yc~Ddž)-rЦ]beqѡ~r_lh*(Ą*|d _La؏'O 8K73*2wa~ 1 C.fA$ȡ *4Wze8 ($ R HХsDstT͹ŕ]z">Ȑa}b{>sd(2oZ9;9:96 mI]cS#ܸd\&3`_ZȅBhf Pc{լD"{AAjձbۥUJ?⦥KoְEuǚUF}o-mv R&ݾftʬH.,+31: N/J7^]?):2PZ='&,H+e&&Էk sZ~$)t0~;K3ބ$x>|V&1ayTfdED6lxZ?ӵ<1P68nU/d]>۩gZ  ش|RQ־;D*_y]kdFJb 4} 57rkܼN6"(pˊeA2eH0h=0nzO1TJMnxm NH쯴:h7k:Z'{ɒт/nq!<0}"Ӏ}! eQ\X؊2'jVRcc R$ZHCNna3MrKiCoOHDϔI/):jdP~,PaNX$\=/;-h4?U$6k j6H38V  sx_@eՃ:7:$Iaf4EշJ1Hbl9tgU7~{ 28huKT?pHTh;>sg[fH$[9lY~]thHGvw.'co1&,Q b=V(-J@*m] _ehLJ7,^rVwV~P8an!H>[<+#1*U|XUnavO^$+ATd&/H,EQ\l梇6NWTjlmi y*%MffL?,}b_J)h`)<>H:2 S DWVh2 E-]Ы>/^jrO!WVəl-2uLQ+9_TַFi,8~\aVRZ!7Yֶs*"CTJ7Niz8@gjl 1jX|r}N$B*E#j/)r: c~+,ĆT=M5-R~dNoz;CEf%dǶ&,9ӣ)xrA_vUscBCjgu\4˽NIqŒԸމ5K in3gp: QdKbX02 6}XNz561p\'wb|.UcJˍp ]vQmӇ @ toq Gfv=`q95Lgﱪv'dr0+cp&S9 ӣaK3gT]>E\n6jqtя{( BG c3 x  @lЗ^H1!^:ZXׯcb]p*l{?xmЕ!-KԠ. IDAT0g͝It"p OSq Azȏ0h9JQ101yx )CA?X,#Gǃ agG@mӁ̹avqLE 4A XY`:/u:lB_8>f H:ރ_ 4 `az "Æ@FRq{6p1pGTV?4  P@ A bS:2V7@AmĔgN;;s麷bJ@ ذ UB?N]h;C%8olA^1oT\pX!B$@L$iF^1šQc=`#va;[#~;X64@Oj0&W`3CH7mo:%`wځ i\F#ԑAĨ`m6ԇ,~)4{19*` %XKyNV Mgy`ZS AL0>s'Fc,f ~_88\m$07ױ۽tN1|;X  a,;& h?{@,o;Q(uJ: 3`6)Ԃ8LJ MALnXA^1$#^uԷ#`u a [F'-gG-NXr8[@x :vﲃaCLo$QTϨ🁱H#6 ÕmH?_bs3,ATJ31b# 'v@ h6@3 ›6@LA 1ռw-@ qJ -+<.[@+ <'?#K^nn/->uƎ+z./;.?bq#ѣEQ anpݹumus=1~16in~+^(̻u3qUF;r҉.`Bn 1]X~߸=<{A8w-P P+eMQJGd$"39_^qȠeL6@5wt]ScBCE"^TYc- Y%dNJrJ#/4T.-HotQ`f\ T78^r .-.aqta~qOYyμ!Y"?wFDx]k{C{;}5!*"28[s9$x|츰J&W$feŠjP+Q+;|YQ$ITq2XBTP/!^{ڌ9i术*Z%]&YSԥ\ 2d6WU56%Xnѣ=t`d0lnJRfbœdkouE m (%UMJk4IFIK s'U .ֵu LoZo+@Qԇ;kx .U6tjtARLKBF{llmrmoZ>gOۏ\0cVbdUC{nae{O_~0:ޛ䲟N9[-;+|$޾]큳*_,W+}.׷~ f1T;-a^pA!] 'JJo[|f\5AQԱ8IIQ'K/Ąą9z0'6ƅqKiu-VÉw߰QR.ΞT*F$Yx=]4/+Ι{hNt$qGn3=t7ڎ#CW[$e2>'nGı"ZEQUQ!!a!'K/%]W8p]{Z}? x" ٛWc~|WUVv9v\6j;(uiu]ZIW**۞F炯X*Z,DhR_.O~qu]k'{rWK=xED,0nؓ[tύрq觓e m geQcG%ܸ؋i7&\& W ]+l|ƾYwI\KOb`!^k ̚^}Zx$E9|\&k궕]+ myE%ܱW^'\˖nwy+͉ ܲbY▮ngu"I0,6sc8 S_*O7`6DQTsg笄x®s:h7kzؼ0C^ Jo_'uQ!pew_8#;7nֵ9|Vʾ;z>À)|5: V\=S+(O,)n^>/ްO_qiF.gŒ 5uN]$?qQڌH |"lh-+Rz?k[-ظlkib |!3-@u~?x7Xv:4"Ќ[1avC0h2$b (HCf3X\X؊2'jVRcc0<]v ơ}'NNNLLX0NF%%]$Iqm:yz_"P$eu s| b)%Hynm!Fo^X&>W,{6.THX(ܢ5-\-݊\~.&4*v`Yb i`Eյv GZHnѬ+x831#!0qi&Wޣb|[VԷv-L5/-.Sgt;Fa.1[,  ͸E"=_fe `|'y9*%`hbIÂ5}^vI+rJRT{v&B=]. 0a{ uyV.yF[f׬(1V;/Żñd{A [AY̼ yQHOP$ZajL]i'ANsE6rzj m{*VU榧_jh #[En\h&+޶ՖL@Z`b ˒pR MYy$"l_~{nM{' M30tVQ3j"s:CQG5>M7.Kμ /ŕƱY)BEqP vnm:,.ԩ^aՕ7E$$Xc3QDFybf!72V[^Xj=s;QYm}vcve?wй۵%99jm\1%zdپMQk}@L7ش'X/YTi1z4j,qV.'\1 j>YWdãhN.5˝DJ9pj֬M8&MzFgS"P8yxSc;+-7}xJ}NO*N٤wMخלQXHKIyvKVu@kZnefVd^^f ˝j}YG2Ѡվ{fp|;/3cBK5!:Ъ>umss97ÛŅPk@z'9椙e#pyotC,F1K0]B s A􎌾q _>0Vs$)s}{{а^-I5=~冦Iq߼|otaj(Ƌ nm(b\ ʴZ&SȸYB^R&u GVhO[w\2tЛWնt(91;:jd/8uZSˤa :}rCәI r?~_y`ش3bFI%32#S3#Sq_nw{$W߫7_,+} &E[Z*NK1LB͝_۳n_ʙ3?{"IBW8jcATy.Ns[w{FG]J^_?P:{D-9Ac{U16Z z;ݑݾbR%圾v#)^0;BmB$.?H~(An$);CsUE9izƝS)FLκA24,9Ǟ}-+Ugli˴ɧ߷0J˰~yݖ&JvgZGNn [-[7(s&;[zG/t/3Mx$ꤦQ/Ԭ&_8Y7.4l wcKV~ES1 ?(=jtgdrzlھznM?4nϒ *(sb9񈵷$`8O%^iL?KsGw_ow8LyƼcݕv^O{(CS&cˏXn%((n1~p _7/5vM̸B&vNQsG򶞑:z(h̵Q^RwroU^j֟mK_y(`0'|j'bE9`0 `a0 Db `0L2 ꈻ`0 >V`0lu0 d7` `0[ $xl`0 >`0 fX#ƻb0 f 0<`0 f <†`0`0 &׉ƆрY9!6 >`0 f%|j"5Aa`0`O} $p)u0 4BV';"d~ (zQ`ְB[^F:hTlY8'{ޅ(g}=v9{~5/& {:=iV`CzF5&IxkMsr&dW*?7ش^9w&1'& Kʁ-Ƈ&& :-Oh_#qky}5ˮ2*B$AcD ҌJŬ29YZJX녉 YFr\,c c0p#  VUq#z§,Z+ N+ghU]H@%ϏƬ3"NWBHnLJI@"DL H13Y{dz=H˔@=8+ϯՐLMLMqr,ى B`N^FFuŦk]^_v24==) Xcc ]^i;6M:vYRVs佞* z)L Z/ fiYϺBC+KyI9IؽaKE9iF仇ۻDi.S_eœ ÓSu>0պcl7)/ٲR z$k[#e/?YYF֨Z ޾~3"rs4++S @`EZׂ̆I3R:x>A qR I3.O՚n1_k $H<^#/#CǨUedtU{N~x KJŒXR[޾*\e\?006\@q$zt8"OΜ 2?3τlq^R(VIa_8ydVӞmQ_m-剒pF[~NZ`-/l>Qkn/Βz }cӳ▟eSsoRrP/%q`0f^Dxy;o"GMY#/Zaɑgt V=OSW=8>q)qj={gqnNXؘ|wC~n^Fٛ5$qt;c:NWРֵu4vth8s{[J$Iv'f y M5wvŬ<}HDtHV{zdtzoL)KIeV_cXZ\0yohNd)~ #7=csBd NϺ-smILerf&|!g\)F=IDnDSAtJH ]MB9i=d5FN]৞jzGFĒz }ȒzIDqV.NG+̯Tiz/[ &&Ԭj2Fim'W\^ocgׄ$5lyT Mz]avsGմ-/, ۍuI>IB^`q^hOLϜu{o NI 㜴r`~"*9YN_5iЅCY̼ yQHOokhjLLܑhANsE6 ߫܂򉽻ժPіr=ÉJW8KQ\S"2%jvd8/KN<NK13-/Y^S˱J9Y0:/0Gxg,:i=b;N]2xl"H8s{U[Y\S{Fvkժr}4I$ 3V,I 0EdBodLє7,{lv ޼tuHȡT)"@INe"-K/%b<<쬘X/YEjF*/qˉ@"ݭVo Uz۽ce/+? D ;jUײṛt[s."l\jo@Qr .-%Ǐ۝.ZmiAka˖YQTPzzecwpR;Xb2wo O4uuefRUY\rf<9DS1'}C6 xr_}2dEkkiɾUgnF)916tڲ2T]nhDQI~ Mtg8.j6r+k'(?03.wvf=nZFF+ZVWY#W(JYQDf<:m Bjm, A^&o$ ^xyWEY%j2 3YIKK5A6wOLk7SYgc{=OA.5L:wWYЯ=jL}#c^gݲR>Ѭ ^,CdX-^spnO$IRͰPg%rbr'h0tX0>m/J5H/ cԷu _r㼚r+H !!1@@ܖv&K.! "?(y^ w>Q +c MJGbjFkWuQrASt!$^JXY9ѻfzp_`0`0pyW>5R]ۍʒ=7䤙C>\m8wޗ?!,$X: ;>J'@d5!9_?NO*6ݾt{0Iexbl`0깚 Us s)"g/$ se&g;:8NeXVs-ޛ`ݠը&rbZؠU(+ ]Yv`0덯|ņ1^o)}PsuM@I^ǟ/[o#?34/,qj/ ?])'|˧*?-o2̧;jև_mN(1Kcӿ80jpVJOybOeYQv(Vo]iߙnQ Yդڐ re'z,bPYoxi^~vOߺPVU}Qپ{}}Ab[V/4n)ROjK$p۷,k{mt]@@ 夦@EI{{/< Not-Fc w?_hiJ|eh,/I.z|L5^9{r}l)WLyÆ`ӳ`ԩ?cfFgH]޺ ¯=s )a?(œ7y=/?+5z]L^FdHfwe1B_z$}[7(QʖJ/6F:_ p>v30: QL βə a(%Z`CvxV1{$WʍŹi)PI7(;2i54 $ih~tgY 8"9iT{hĞ ^o6l)34.5voy{F<}`@;9_O\t6oϰ 901pNϾdF]k H{6/tܾ@M)\")V+,CZ24n/+YϤu<<|٠OMm*N tzł[Cv͌œ1횋Iq^ZQ>|oSa_|ٷywx?xA pS3n/qe]v-݃:*;uavAՒ: f]14ϴwzr>%/*sy H ?4e n7w^QNچ cA,\@[Aa#m|╆lL52}ƌg1B!.9WUM, `0AFfla&apr]U[(>JJӔe͝C?ǦgR s$I[C㎜-&9)pi7 ΄/շ j'S : ,FTWN_m/zzܾ9 bu0atzF/7ndrSޫ4j|pաA2ۍV-kmȲ%i)˨"?9s#7249?<2Y^pcwz`~V.Љ 5b{K=m}fKiSXLsԪ΁ޡ ^cL{K Ln\PTyquk=C@R1}F}AGa="MSX,Yڣ^8YⰦAz=-+;r_G$`05]}$_n/c;`|sjh[ YCDQylyC?H+xu0 mO[7C0@V`֖^gmȵJ'X`0-I|#4E ܿ׬ӳ*&I^m%?ghZsҡCԋ'4''YScvҡ1Uƶo*ݱi#rȜg$䏰Y!#!D z4IVO 4sxr sj>@@ͪ6ڰQH417~zvlrㅠ~WmdcD+79z]쑃f!qAN^m UV`Еd52ewy}ٙ7Ul ȭ2 $+OQ^&쎦.x>X;\m70q6ABҪ淪t=@yq?{A!.հU>_аr!wl !ynujPˑP*LoP$3=DјiLMz܋%'!}P[_uen1]UrHn*Eyeldjzdjz&/9fOdx̘dIOٓaΠUMidjbMȤzZmir2Ro*(/hT oV:E63Jը؝e&&>v檡{=}w24==`^FFuŦk]^_KʡI +ei?806  FlS٤hgܮᑄz=DEQ@/%18lSY':w):{'З+ sӌ:?w7w~NV:-0Y73K굤z)Ӳ[aNvZɩv^`~ޤcfJ1IhmmdeuZ^gj x͈zvRLQkl'NC!33s'߭J=輀5'ܺҰ(\^tܾc5sG av~3w-(,bhEmK%$͸q)qj={gqnNXX(@CG''{*˟ܷ;/6 y#SgoH]ۋ7o(jhkРֵu4vth8sqch+fWj+/E[Z*Z;:%~,"EQ{ G×K%z)ӼR[޸q6YqkeB>umٙf ;ktjR}7ؘwh49-cAOߓ'Bri[.DY:)"$"*e I/1ȊE2S8kfacn(źg^0+uP"//c-!k#CNy ݞYלCFŖxSjV{zdtz{aF.74B@[ą$szӳtK.?kGnz_oR("jDAnzƕn1)ry-6hÓSe4 Cv"Y^oe[yh qǩ$S ^$KaMӳN'زj2FcjZB#.+֛%@5o[i2>P\"UQ*]*%NB p5S^ $ZNdzOTO!twO]ƠZSu\@R xfY^21$woLgS tqWyʢכ~r8[AkM:&Njc[W`jG|S,L۝vg{؟rg+uB@A$AdWFD'یumVfL8wBcDEvow[[,M,DV{WolTtÓ߽U;8c(ZF""b|A?Yb|Eӡnt//,~%եO`E-cIw+HԖWrDI42A}r"ibiZŲj*ZL9fWKA7ƛr,Dtzc)`G)W?%\m޺ƟEA$J/o@xm3ZͻgRg%' qx͖XK՗ ~t ]osu{58wn!tb}Ű}S|>)׾LdG&m'V+GCםwlz=uy8!b};ݽ%AW m5 \^^#NPbԪ !4fwT0GWYx!)#"by 8DqZzƖ1=뜘I2jGmJr %&9.7ec*b|9(S@VYVDQQ3n7 Z!1Wrl,Tݪtn^p y w}p}paXۡ$F-FEUtkN;u8kw^;ãUɹ#_9{o奶%L{~y:ڐɱeZiTgnt 5 _;_:VǫΜbuNw IDAT>oh˫{+9Q V cv赚wodY7;n^WܑC5w{cTerr6,$JcsRr!uRsZ%B 6XB< szcS'-}h߲peV'9dQkrQs?1=s핿)8e!1JqB̷>E"EH!V`F\/9*0WN'ˌ+H CYc KUZq wz|ڧZyro޼55bR|pkix<}po6wnvf`l7:灩ר?q^l_|7gʰmxބz P_٩Y!35vAHꑵ:lcNZjtnzZuEsxB۞Uc|tD b0j=eojjk&1(X( 0h5&~ܮ 'Jbj9>iDEn\('Y# GNJrl| 3,XL1h,9P;-TxehуX` ^(B$)"%`D`$% |4݆Ëy玓K"hQI8֤2/Oy8Y{/֯ʢq;.̸\kyƝەn]6g|#)=poν_+dz8\4du0'=|NA_SA!OꜨ,WvkL5? @ kիKȆIRZ n޹$^k$tp]޼tuHȡT).EplPKPVDNK}[Vq*UaZU]Y.wiSYIdg&$! t!2RR!72h j=s;9PfeMZ0A2SwYEjF"oz)@/yݭVo 95g{l- I,DNDVs)|7ͯ=jR. 80p`} CB)Դ_C{ՌOQ&SY 3*m[N?ʢOhIB͝C[#MM瑩#tԖxAvZ}vM^xxރ$eYsY&+ՔfN5礙sRr-fmA4MQ(:=wo vq65O;]>umss97CuZ@ZJK;]Zڨ\Kˆ El.C5vvշvD7-iefVd^^f ˝j}YG2Ѡվ{fp|;/3cBK5!Q$* ־x)`lp 3ZME8N-+sOe${x^FƓ.a&(JM][KKm:s6BH^J(KIMtg8.j6r+JPBaf\T3z*z[ځDoꇾ=rċoMU1A)R *X X ( h]'$.?^!" !]gP$D/gzRkoav?p8wna"2к mZ dxqNJ|RI1+G:4'649WsA.5L:wWYP὚mK-FCzGFk} ;"J5\^#BJx/^UQaIL`c=vsR&s 0ryڮMi)ajyrxstϴZ&SȘOVDsEPI1 :]rc`e‚ w;$IV6JR"G^ b]0;+lF Mg:W\2+QS1kor]YhJ}pwiKs4)*"h Ǟ N sy =t3_>)pRCp%ȉAkK:6e_Fʮ5JBgQ.i,9>&- uuBݫݸ& "fUIԒE(v(y{'wdevc,HVD@G8sKPkӠed*D++x$!־9m_5/w@EYe,a,PQArL>x/y-?#^_%޹ ~i^b&k<4jv+?GJoh[;0Կ~?8}?l cYϟE9 I ׿^`Be­Z[~Ïm)M=4oɟKm_xE'Ԯ@ӻhR-'jzÛxG~tbP@+MnWɪUwzŔz=r&'Y4L.@Nұ-a<,PQA@ÃG_@h@t`E`$٤  _"Zav)!1@ j#Ƕ$I~Duͽ^AEǷĹ;Ta+L>t+&|;8$-x`k1Ht﷮ʛvYhiٵQ?ܹgf'>肠NZSZ8o@!N!$D -Il Vl~q-$A$Q]QUTa>xvI A741PRF p3[Qxa^`0&0^ U~-UAhR &|SYtI(ZTTPWd6a˺d雞b$$ϋӑA7ЩW⺝b\XַnxjaFn"e.NEM;}Y:`0w$D ձ^-e Kq_BӷAσ^  S〞~4h4) KԴ_M&9^%rPOv-h4aJ\p2^n'Y--Z~738Bgn~囯uMa[ fW (@xOMY:4)~9?z0{FЖϣ㫉:P IV852 S Dռ%-˺p47.547z޺4?|iNL}\NҞʒv[o3򍟜 ]٠lІֱ9[?;225?7tZ~+~Ńb>l f='9' iIB[ @ 0tTs@P^Όm!jA$@ )^d'Kq,2$vgV_Q>k/%IppzKtymK]2h5AS3n_ӪYWX/QiU쑝ؿeCcj|.>{8*:e|,ǐA!]쏣On!%P*@$/6d~@_J[ IBIj/#0哓f'8 -j>n]jC6t(0;FĞ=KbשUQproe*? ծ`0kj8njEk7f*hGȟiXPCEGĄuWˀKn<,3~At [,o~pF­;=wGf\?Ij2*QfS>525;< wzKW(jUٱi~qhl78>yA`ڤT]zöDߒ\$<†C^䨨T8)BB/=@ b0"15ox:SLD)P @1duNdH>(R{`җY>}X.؇eeǼ,mm n}G0uqn+~Tt4Ӥlgf9ϲO!GGҊ@% 0EJ)Ҥ`6 !bu<"q6/q|_GwGə74,twH@xٱQ@-APFF$%TT߸FSsFs? af: fjhٔKjZ;̼ }(#2Ć#-lƈ@SD1VfC6ayuۓ1x Y@CR&hzOC1*8P+lv=ors(D ) 3JcB`0 QC^)&yUy撏jBʔ,>T3P(̝OJT$$ 8lIyVYoܾ!D6Fkrw:d\1rulZA vVeI!y Dz lu0uHB$ T)5`>uا>/o|h ЁBVi֑HsR ay`R`KDfb @-s@P@D!$Zy|.Dpi@)HԩB4]$Hˋh1\uTIba.PJi3Yog*:(EIR=7$_8 mF-|s@R4Glu0m\4&9< DY/,Ne"/ gdyL # +mx@q!!RiiQy "z;lu0:KG) Q a1})J$H !͋@#C f A!&L=9$$K/?P/ Z &_G2RZ 0uI<-!JB0<3¿TEd<@?#\gq7~Ue.S3,n~۠[9?B^(=z[cbő?&VYhlz[DH~ȱ'I~qʊt"| o"1K?DŽ @İ? P'ѯx+ @W:`'Z侎$ת^"9i 3*< Mfi>mr@$!24c9lC"4ǃQ /2㫕HWI@,\t҇or$FY` Djy#l:DWd Qxy'D@JD2$g@ti- e&C8a ^bRe1ƕnytVYXdR x~Kc,0# t@ځN 0Њx^B 1Hxwt~%VYڜO]~:jŤbh8˂ĉ,'@fL8| fS@AAԜ~|Er~~!|E&(fm!v`[u0u|D甸W{БZ ӂDUPP $^Ua:V/â#feಱb `3uPP{@AG @}|@p*( #E:RdB}kѼM;6mW]pyJIxkM-ɍOI^ުn׏ɉǎ?g? rHxr^NϪh$yA鷕~NWUԩ:u~&d}{IEٚN"p tHK%E/}2|R$?zr1Xlr>EbH.Q[7R>hX.yW^R%e>Js}D]@@76oA`}(dpl't;Ҝl :陘5;ܞTY@H(32]9\ ]bs?bjк|݋ucw'2K*|W/c'Wr R!@'ㅪbz,:nׄq$թzĬr]-=[khX 3W)K62"-|\^rm9rbbeXb ˭*{n9?!ԉ=W(1| x,0~w?M>J%.J()Fڧ^,ĜIKS+jB%ca򼜜4T. nwPJ.^U96;10柌1=כ.=5%7z\onf$'2/AvLMI&הcQr;gsiQ!S$lۃAƒXqA]eYNZX,x>?npU(dͥ%ZB&:Q Һc/)oGxI'8_g~y-IW;!5R(F dZ&D&)-NPc&7w@bhztuÆjLv'#Eنm"9# 23^Ԣ8Q goL̆DECrtfhF#؋Y]l,*(䧛ZK !DX\İJ3uڳͭ,oA^,y_R6RRh:\j ߐrE@%PȤ5lWnRtKLV\7bu#>1Z$ qnԿ˧5KҾsE,M{t9Le6 òo1,*e1,BG>/#Oܟ󝋗WwHu#|qucYO.7a\<2D =4x>\~˯tU)zX(SgĽHJT,XrIO٩ר&h#HJ*U^VM̚8whd&'=wWfrR=XYd7Y,&fL:-@N}2q IDATJѲ,Y>\Jrld]1_/),UZe7Y j%fHWJ. ;Hu#^?;"xYju4s4u,Mx?y&çqM5WwXGmc: ۻ /=zHxvfzYn.tǘ(w/^i镊Ňe089%EY/t:Ǯ^>D$2oɖް-lXU>|\P 눼d#ɑI%BL@ Ic^j# `IUwR>wgXnJbDnP՟{\Q Ah=8>CW_`L::>r~M[ϩ_%Nyardb:Ki0k5`Ng8N\bٍTte0AHaGF$"Q!ceA ? Z(a|V,EIT,еB!|9͵4UwR>zX.EG{ BkiWێGwmG=9u׳ܜB9 ɨyXfSQăCZ#QɗqlR+OYŨ]1evTV'Z(-]| nVen,ͮ,X6Y1Osb̛})9a-VH%MacY,PD(nu#^㮬P)9p構~|Zq׬7{?N^[־tϐ~y?:iZ:'3d/=AN'E J<eMLq<~<\]_Y"39\K/d=@ggA"ڸ!;-%N65Xlaos|ÓSyiz BoՔg$%䤺R=:c,P)d&XNKR4aɼG%t\X}4*yP/:1A?7.*n܎)^"G )Ot|mAbB($~+!߸cx{BP_8.(_J0Yt{?6ȸ#hixITxH.sOTqSqbm`Y[aXZ;q|צrٜMڲj77l yEeHI~jD&-R{d$t]HOJ|fi ΡaKMYDsZYTT}:f3ͭ 6TW "r`8=T*ҁuqTRWQNpc b/ĔR&+.H5 \P;* WF>Î3P VYznKU#e?|^Ww*tjqOͫwKPqr RSFV*XDl.D$kJ[))Ig;Q+RK7{\XΣ.XqxǶ\sY`;FLbꪢ,˲}7:{إv52-GNzR. Ec۳R+JϷS4mw_kd6p\lk7K*ΰD).,XRBsTV}fB!ǒXeg;nNOLgXδD[mND,i*ic[_!5e{ueE~'jPٽú n+xpelKHA?7O IK>8c'9G0,N1"RGI(ZLPRQYZgKҏTqs 8 *JX$rz<-ݽÓS,0>?T \*uzgn1,349ER!%dlsnz݆K1DIBMi1IQ{Fb9Bvj*˲3ܙ,J.o/h2ljBPtr bq8|w|r8~ZFԩU4N̞n!"qX,T*$G. ;>-:X$rzOM˥R\v܌tOvwR7U>Uu;:b!BX4 I%~JmF2?-Q2?/%Qr_+Ih:ʢvcvS@ q?_ "kQ ʷv::jr޽tetp!XDD >~NRKuEa0q&scؒP=9G,o_Y݈Wq ~EX,צL'gs[p$8\& +`Ɉ햤G$:/"}dB/qZ:A$p!׆4m[wD$PbNvu]vNʂ8ey:H nËu\C U Y[CN7AIO|lƤUmWoDmvz ٢q,6u ]n]6GUmVA+iq_10>Oz R%6`k~`!nF)6 Nґs@!$&(6ԣoiq@o߾StBnN޺; iO=3;UOR6wzo]=7jH~ⱆ4=I篟oV]@Z@?;41}CadmEޥ=CF#UN|=Z GNw MLpœ26{tE3Zgiq_p{`|`|duJDF-̼-M3kwyS5áU&b u}I?{K}Z؜xIHP+V%0,"@ +޿~CCC\hGP!B \:T  JcӖٛ{BV.}(RPH%rx\D?HUSn/U8aq쇐 yt9Odbw No8>cY? @ $baRjb61ke{Gls~t?E޽<:=W̕}$;S_}L@rۿylwa0 C6q_ĮM|լ')?r-oi)JMk\n8;I *e6rࡑKmsrU>̭eߘ2P"@{+ 7 dNO_X*%r1Xl$E EbH"ar3R q)'KJr\"@{+]KU;0!IJ`wO_s2wRSN[Ҝl W&f'7=D#wNG{(+cX߷=_\[XXz+ǘdF+AN&fM6–*khX bkP7\|#;!z@ܵC౎L")-3b\rlvc`~siQ!S$lۃARRKN\or=Q/)G *rbKLMt)2B&\Z(dr10>55<¥(Éc 1X,wW }s`Yp榧~f,˚N\ǯ6}>2[;-g('/ \@,M̘Ru[j7l61 ;2=-,7Ԟzzhr蹋f{p?K C/5vds$BM̚8whd&'=wWfrRr0R~ՑVa"FUrưL$^nwjI/lW"PdDW5 `&D{=Q1pe1Q*5ꠛ;I#gXBތ,lwFV*$'8;$"b?4<5[6۝7#XxP:޽xeSIцC_kc, #J0 s<@+<3.AHBFQXVrssp8d=Q^CDgpHy_BX$R凶 P  [q%iYm B 5B=Ttxo4MIbwNN.YVt{ƌ3zO a)(,/G*@;@N.XguyWe)&hF30##XU %ȥD^Vd8=)ydH,S Kz@[DZ!߱vR.;~1-I_Uow#̈́G_T䦧={gcg0?p)D@94To(eBkRr8ZhKJ( ƲX8h1rs0:ûSǭ|xt WFF {2(|9$Hz)Wo_\SZ;:6j46wXҬeO/d1vke[H%჻2~"tZ 2UEj*ʼ^4Qx ?J.Ө3ֈ3.x ?Љ ZI\t[y,xҩiz}Fb $ʊE"]G/KN$b3)A+. * @<` :7(EnLsS^ ]0Pm!%;O^o 0luUHݛ6*dit ǿܜn,+8k0L $hV+˲CS̲ܜΡa{k6AR72OCyKlH!e%j53+ UJ dRߵt=v9QwnM.q\Lvmn]n|=,q.G((ǯ5 M[m&c?8G]ܹ*?3m_:3?p;lwȥRB#7,6(lkoߍ6tۭV*bDqLKޱtI"Fˏ_k6RSWWVz VM a6)w(eTt* LfÑB=,-k\7& ))ՙmHרiiPT,ƒXĒXȔ| 3Ðbq:pVc,WW$qW4r>06gז zF;΄IIZ;h@ "t(:?|}Y@Z@ q>t i@ē@8ȇ @ k :'\ a]|؞}hP x5α8_kϟ}"39!s]W'MVPVJ+ 2 3kҕp䵎C*lӂ@LYT,^"v Nz 2 F x>"9I:u*ӽ)+FUʐA H0!IJ`ӄXGovrD&!7#?P #ha?tscIg8p *8LZ ) FZjq `HܝNUrD$"$$k IDATbVɥ*yx-2j4ͥ uKEz7|V]Uc3CK "dM\< |g|~Ŷv˲7:?/8mCa,NK .u N]:?6gdRR>޻4r`bƚSDXR@ VXG(ՏLP)<>;$$dWtVH% K7wS4i@ VFy xG$\A`91̲$H%HcӖTP0g8uvmN/Tdmٜ0IO@rd YΨ,iɺ!szG^FRaV w! t0;E%E'woNZzص%g&j\Ұ:=:"p>1JD-0Yי2Z復hrJP<{gE~.g3 &9H\2(fTB. kMV) $b%pC{߻BRt̔Ɋak \d$%poaHNcoMot7 Qʥ0ZhCu]e~V0F%KC)N[?'$: U 7^8xcצrf@rITcsinv}eʊs->zJ!s=aI:-Iʙ4Y)᛿\n؜HOd&'jp}2oxzh`sio4;2BE,tJfnlrw>72>m>)*¬BC,9IKL*_?441kH֍ZzFd΍E un[D949cq0 ^ z3JV0O^6 r '3/;V=>\FfC5vv+8jA$iPp7;ɦ=GԭW@ Kb#p&și!~_(:2ko)/թò,IQ7*'&;OtO8m9z!EޚuLZ@KDaX&l?049xg^=u0egfWՈumz ;U1@[pªe"4_p*@ܗ|h%ك:uUǑm xp M@ĝCJ!eS@ Jc%@ NE{"&Tp# ÐAüslfd)?I#;6 zO]g?%5^h=B@.d7gז@]Ž{u.ɤ_81, xD,y 9bR3YJR*UBmN 4NI"#> އ!Ms/(G{r{+,˕qn#N':!n9KB8%D. ?j_ַ3ћ._]O\i㢫nq00 uJ`IIUZ۳6H (Ngv/y #w\q?%uMUN=S/vX]=/Y𺊂?I Kds}RiĚ8*fHP C]q'Y)H]nrH!!t)D%LLPFkY_PB EY:R,\M2$ln- \bGWK;~vฦw8IE0cY{ȫ'&og,^zeVM!oh%1xc_oV]B "oFpOMsp_nDyZ'~K²;2 8b/CLJPq*acj1A, nwysVB Il[Z"3lҽJI:&j]p&IR J_֋l1B⸋db`)q{]>w-gGGlP]dG3yK~!'7j2c Ws;[ڭ֞Q&fԊT,/c4 6wM[ƌGwT'5>8ie|RZXݿ"%چ'M&S.s޻7b4 IZUMYC[ʅ8hMwfϨ0*%kJR_LSgbn'Z'8im kڜo N[&]5ŁWùۀlw΄Ȳ+'5wKQtOS(l$=T*r+P$$\6nX^Oj0 /|p+_CAW_upkX7:^>\lsms]8$T,zCR[m5Q6ni7&6f--_! :>"pfOz>% E1GwM[E嶾] @%&'wo la؜_c/>0>sĬq|P88l 8s'UG0alO\j<5e,44^V}0E}7?]d*IC 8fؕm W>"#IOrv|0gHTV_?O}\8*wآ*~sc`oiJΝarL߿Ԟ+4mv&쮵KIXƈ15<4iMOx|7jW†~gYI:զ̏_9=<~e/.%=\ъBQ̬թV,tV :ʲ߿S+3`9;峬77f 0f4u.tɲ36]O'EWGV/> gMCzulcAoeO$R|ʞ 1 ̘4 Tr[KwB ~ݫMZ4D"ߥS&^Cޅ;\nF2w4'+UB!/o?{l~eX8ܱ흱8JK6[&^R;w~s`9\>y#rR5{fϗLN4ι—˱@3Mon d';U'v~#O74v|]jb7K)<#r3 ~}CѧCIoXh;e7XY{yb/ٗ89)} ّ)eHsfʩ.2|@[,.v?ΗW Ը՗^~ر'Nݔg|$o[{X{j쿿r$螽eCS\wG/sk{ðIӹs}@;;0xjO\Tݏ^>54p{ o*SS͡mUCpu֤ ) \k*[ jĕ>X,stjd?sC^,(nM:&ad8)e/Y ls͎T'V53Uwa!UQ4Owȃu^(^&Uv5Y.sqNLkիy\?e#mq;-7O^Z]71}Go9J˥LFieada*_j0+%kwƁ]K]Q BCuhuB43Ք{TrYek5Vȏr X],4y[*R&-O>'Vl]E]Eޏ_9͝O*xd?.3Xn>C95`zZ!U+ oX&+gdʔP9 U˪o]>^| ֪}u u@V.W'&&:3Y<7?=[B*-y6Za৑YjTA'z˪ /:2/ xaJ$ɿ+xRɧ?ښϷ4w|g3-?yolr%j(;U[iU&(/ȈS,w^k>'3z|LbAYheBCm+[6-\%p.0Eϑh̶9SvXh4bQڊlhm$ܸeyAW#/coůAހw$jZ%97\.3 !k2}֖%i&u:]3n`*}Xn-V( 7ߑi^;cXk7C,l*si4\3ѱ?I4ƷDخ /}p(\&%3kuN&-^ͧu]w;7o!z0.r{2zOcUgk WWJnjjE_``Yܛs UFplM!ʂ-vnnԹ忬Z4S" C{ՃzM.qx-HS?630>csz|IQ%1;l.i )׷"[wz'c|őuißP*oⱂ峂ڰ,؜@_5Es\|e_ h]N"P_W9Vc"':)l?Zex_a\Q7mUIArKp?bSI΄rISLUǔſ|&ZiV%,$PuB~h{߿ xS\V|\= 밼Ɖ]YZ%Wf&[t-n*1K\{kܓaCZE˃ ?(Fs*fy]'<3Y+EgYiWg6wT?|xG# `HUC8NX[a,>:^tdFUɕ L"Jb@$9p ۪]ŏ1P%x}$΄C,OU< `-Nl4]^ƙ?j Ա7U-|XXwӇx.WO?Y|>?w?iacY_ty*5rWtIcfq[8y3gppvzr @3zpo냪0MKXXvTx败2?OǮ M88K'>nRtR&K܏ea|­`YW3@.3dH)|BkuQ^ۜ/ IDATV;R~jھJs37;'nṶq}/t"/3wфetT"^]xÐ0wCuKp@#EN> 4etyomC[ʣ.;$Et G>2Ho58a20lgܕYY}[Yotwd:{IZ%xn F6g]3-m1Y2_ þi׾%A&hEkB ee6 %@=ԕrQ ;v'ttaȩ[{?zkm|M`TӰ8/#)+={^E+>X0̩`S}qBH2 .哻+'ؽ7ϴp )pvs~@΁1D,2kꚘSDTWQpg=cCnFҔvZ8Dlа8֩(x\lwVps-Yڊ\~-ժ:ɟ~Uէt{n:'s8^|IP\6YY\{ȧvֿ`ۭեp9͍α5IP8r;ܬy//|( _P6QqqmIyOGL˙K=I&6j Pe&k/)^YnFi"FA-Ca\sw(Ѫz`uɫDciYµ||wh Pi5Jl>p^ؔpMzm}PcMߌwʽgqȗ?<9szGЄ*6κℒ,ޖ~^I~'y )?sxtp[=I#E̍''p+Fu1돵Oߚ5"yU?i/Wp)7(_ jõl$nyԷ\;nu'|~~JՌH:ߚ:\Ks`0Ϥ^.dƩǷ^>cRmpXNWuڼLJSd@z0Ohw9ꤗM8fUdR/ 0`|c$RM__SBm[ͱ^73 r+Nl#gDG r04˾FU>q˧x6sTļڀ^vZ#=r6Uqtlv6\mFS(sKFص83z PdI#mbN :p R^ zR_)-q)+Y7:7Zk͑-|y}AU U4CDTI:DQg?1jKb&bWgQ&6Cg'+֬ {ZUqś >}5eE4E24%Mm Țj07*ʋ:m ={76ܟӷDQcc&2שQE'L6뱝uYlj,wn:ilQ'e"n(LV٣ \dre5T73S(KUcOCCS%!ӌݿ ;@&q_qeMhh+뱟AdmC+7`P810 C/z^HQtKy)j閭Mt,]&CdBYߞ>*2aeN +6 2ydho9|3n~:, n (Y` O]bhb4ifppd}\9 ңOr7`oduV+x"4Admdnڞk`ҬVR1ջ#L !q ϔf#t~03̢fpz<._L+zA0Tbh" Y&|".Hיe)#$EYp1n2Zk2F Z&k3:5⢊FDzkKWwȨQ{re7Q-$NSTJ)l'=e%ѻq .3>#KٸUCyB3M tuw_[ #M[n FZoC57ȋ&? &Qqu57"=bbOHi[R^A 3o CK7Cʨg`d Ƨwlw_8kS lp~yYx'CϾ2; Y?Y%*pGQOpg1 =*3zAID3r@py}Kq9"NszZ}{-慿$I~K}}.1q7fulWSS7?yzᏇL +lde6)-'$i"\ADqbj `Νr= (aYWc]hp&1/g;:Sܝ]x].%Ku_.e~p?.mU  1ANlyK#\Lu{;=}##^0T |A7MP}V=5xEyTYİ䪸O{eGȖI%6V@ytUr=M:r9'T\A۬^;N⯔4xC:|P.\ b9k3ȸUWK fUXt74*V^AG:N{oĝuH6F%&WJ. ٖ;[o ,8A2R3ZhfZ9OۏFs"w3:L'O]*M"= Y <:f~Nu{a~&!wrv<s?.t}ȓmlMfrY &&\/4CW?v7N/.+)ADb)p}GUok^>5U`(S 2 $i<^Nz+Ĉ »|J?>WK]4\ar3!H#8&uW9$ Ӗ7,ԙ<X\&Cp%m:Ze:XF`,Dt2.SQZ'4';.;q{!yu1:Aur|` [꿝"Ȇ$:c"QϴzTnJt6)_@y܉uG 9oj=N(׳uGAŖ+g~VS=g޳qS\s_ZhsO[ZG<_\`=.zg~N/44_ѩ}pլgAIX%ɦyok`: [Kl^ E޿|f{3ZT[n߷J;%2j<+0.=y Ft}Ɨ'^7޽x,&=œ}##_:ouը{n1əըƦѭECn_oiZs`s`ldbOw Uj#íto> ?2^򨓰h!x)NY[#p[]y9Ha_?u|hluwxKw 7[_G"MrF޽x;/|O=oW:ZV@{otCn|ɭwǔKE_zSy䅛vTs0=,/)n'+SD4CMOoB54ѨVɊ+K+5IwT۝n_x A@#h/kX P{75W׼q |'bCs늏 Gv Aos" 1<3AQ!eR(M;W\}aNejUA}+u_}Ҙ"9z!ȣtT-u")a?bֳ608κTUR`3<91H}AN+"M4mr*rŏom>yoH}exD;;Ʀf~2C` Di;*ƜS.O~Q`9wT/ Wl8?~}=c+?yشKVm*^[|pl+f<לط0÷ߑ{65[>hժ"2 YAҾKo'Ϭ9$A߻۬ӫ4M;b==G.^J(ccoQ~9w7Ԟz}tzzJ{Ymg>t| c$۳,96m?>aб6mty=#Y4=WR9 '.*QьZ,EFZ t颐J{FJ{V#OtZͶ|Ieg<z=F]E/B/918vwOrxIVuagݱu_P|e{MYiy=/pC@Hrkݡ[/߾7ᜩ(--,1Ihڞ(+uQ3,A~|9Ⴌ4sY=+զ p f(0,7鮡 G&U}!0qYM-UM:̵ZʪRǞDk޲j-˷6ߔM9FsT[dC䑝6[θFa]f{.% K[:'Xz%f,ZwC+.c' W: gWm|@Jz۫{WXP]f?esU#G.7n(-){GJyfS;wOO yw|z-j}Ԭ+^^Z;^!87Ƨo˜Y61# 2!Jܼ7`P810 C/z^HQd4/Ghڻ|ӽ;=  q+kT#+zGGGc sܮ{v]oٝטYb6<9urGI8ckݾq}ōNթ(7ls[͎N oS/9jJP~U:j+J>Eu#GraU:ƦCa l\~[Mu,^`#SJn3NZ;<{&q@{w,Z/9eD{VM@ЁPI\FV#]#7Μk "q3nIYo`٧w7~ı(.d}v}Ŗkt0p@RwJ(^[}|NZ|r;A8| -wxR+a.-~t=E.R[^|@yWgj5q:]f1ʬ9rtUD mN^a&_0v;q $NϺ\^f$2,ݞjSpy}?yN#QR䜎D)vl* GrQ}#o0J|+ztv]^Ԭ;ߒmxrsǝ=Y'_D213#4S3$ ird]fMSذp|\R[MƄZ۸+nfg׸әtg32+bJ)H9#-i{^vҒ*)5nw[o鉧M*I\js{v~pXd9Z _(`֨qZrA*Dmf +kURwddW[Tϋӕra=[9)*Q1eEQ$MQ*VT+V{RO:g_ YfY6EV d9MS^la5 <>񉬾(<ФY| 3qD޺TF@;Qpˠ@XgDєՖmSWXJ<>0fu"K`lC4l\Ejݳv'Xƞkz$I?0:==0:ZTds:;T yeV̞jSdcENMt[_ت̴tur{ 7Oan*P8*kxfKȃkOjFE1l@@,0PP[^Fq7ub#uu馑2r!dGI{rH$EƧf޿ruwJ *2+fJ)C2gw! i.: tuw϶XHtSŖ!n?QNŠJ>ysZUѝ;vw.^Hzhf(c#4ZA?1v:@(壹9fA=MLEn(- =CG)U?"`db4ZnQcɄa,I#"tJN:d1'tT c)RT"X'h |Swk׶蒵 ܅Ya\Iz"H8}k-\ 'x@iaشMďˑ98j;kv"]$Avf(– 2F:}+JYwXWTQ-T~h ׈l19j1K!ql ;y A94Il-+,Th 40:z+̥zIK'x@p76U8\^/tu'?XO_kG[-=rKWB{vj{eQ,^>n |=E ^Qdsn.BwRRϻ*U?S ÖoϮ6(%oQfȑv߸"G6Ahnp=i.M`FͭNwH;u>.%G/9eY=+զ~I' k/G2bĵt$0=MQܷfSьh8Ҹ͞ppd}܅iW{82GmMͺ-)݆'3A'T~nJ)!IyLvEIENDB`fnott-1.4.1+ds/screenshot.png000066400000000000000000000617661447512336000161610ustar00rootroot00000000000000PNG  IHDRsÇ IDATxwxו z*ER޻.[rc;nqz6/q77ɦljc;q,˒%YU$R콣>1$8AIQ式GÙ;w ~s%`"ߥ݀ 9 @AD  % HP(A€B JA0P"A$ ( a@D  % HP(A€B JA0P"A$ ( a@D  % HP(A #7^.ɳ;Rc8uzIec RaAv)qh8[^wB"-QNzs@g\2IB_piJqųdRj6-P: &C*BDnG9e\ֲ_a@fJ܏!9"E_콫;6\"O(CZ2A2{u2Hylᐉ2qhkYy읷J@WaQnD.fܒ]Y l^dYmk[^uE@cG Ս}ضW;iFq9$9` ,zW gGcb x}lsF}wuR?z\yh??]֭sb )>f󒂼V;Sڥڝqܵ 5^p<{OշhVB2{Fڑ۶lŜ5L)أէ5>=?/OtY8(kgbѼinRu˛{~Kde$~~('('$>n#W> 2A]57\ylk,.wJl N=~å_x|fJZ߿wLW%&xIj\"fu8?>t0p8|mZ6^@e}ǻJ<^O@#:z)q1@=Z#cӧ3+{b!0ޘ \meYJ)EV Xث71|ʇG/ī أnb Fc1.VmNާҦ^AƗؗd*ےJK4 *IWuwǓ[`g~tJ)Wjg'H䊦%*]jҤū:56jnpY {friכ)JOP~7L+Rj}6,uBu{6[ 7-(ѣ!i(XrL$O^kq8͟h|xb!_VzVnZ:{X5ofK֪XlB&Rgڜ"UP ""G7vJMS-2('}"b@ٴD"Tַ5wɥǶ,͙nNۺZmŜ[6JgL _uڅyo\ݶEj'G/vkSmPtSe4MoXZ_[bnd ~ds m}}:tR ?sY\<|bwVZul_.8G_pPvU!羳LåIjE}{/ I3um:}=-4v.[{uzusuo]xvV#|+?T/e՚U\3ϟYo&O:I$*o/ڥ1X6ӟZtc?9~[o1\.#9Fʑ/楅ŢU X ! ޹wfN]id_1!_j78\nT(d1 LY)D($L~z>lڝ? !Bvj= יa- K*[֕nК~ ô8Yj2z\%E5@$دdPfEe?=h촄9y>ʿB*z5A,m:@&녪2 ܑA(u ٣ruDMC./יNaWyQCzh 8U2Vr{Y68.g'Olp>t!#/&aROkRAg0_KgY<S*d{oAX/;~$-oo7ZN؊rGi3 bY ~S9|׺a|HXn^=ᰉw!&f:_?3 ~!~,fBaFJ?칛~1NyM*(z%='/׬7#h}E֕E@k6ʠ8|({o %.fzjZ!QRP$ |>qòœCWٓ~ q8=RP'X1vFDv>k6'5b7+J~cVGls[phf/)rYJxUɉ,HLV[) o  >uK RRSq12T~Op8Z?Wʆhx;tmbroN oX7~x^ ,]`(=5]GY鶬?AB2V68G;d^Ƅ`E]{Q6= V̙1lYbX=-]MSNYLz%lç'.r0' Q;'Q('ٯӷt^]mK?*k0M$}kٹ/:7\JE"MC`M_g;@"y@,񚆽+Δ;SF}0ҵoq@y^f?FMsơ|sxz!%w^j7̲TJ*('M[9wAU6-(XiB+CC{sWN({hGGwz?;Qry(;XMSwcGkўtKcJOT+DzwܾbNfJ\xBdכyΣ[(X9w5?W {ߩ &RgpHr27|aA&U~\n_?9yiB&g+sU=s ?CkMM|G{rmƐ7`6B"X?S]6.)o8|*ӝ??;bdQozx^#ݢijآQ('ҫ̪jJiAWm0]n٩q;̭oeF:}{`/U7`ޤ8Tlζ~Aغ"8!V9n*pݹvՆ^/٩zΖd夏ȠX)kǹkq> }}*t~~XsXސXЗPogEIUQ#IA\b~*^="wZ3`L?ݟWHr/sJ'$RQZr{]no^k泋}uL万7.b$wԧKV b4P{z*u}Gӣ$qSBUӛ_&~M#Al^^8*r/}g6,)`c# {`n\Z0 K& Ν4Lg #gFXi_4<#iO]>hROk$^5w\pkIEEK$2&*;^s2=A6_?WqZxuY@qE{Ngx++5%ѽozx^#֣FAqԷ&)&0izM]G/my~dct);o:hq?F=OcUFyPȒ2UPޡC4@EXA?(w(2д -Dv^A3e^޴̔x.q9Q/v w(w(A" a@D  % HP(A€B JA0P"A$ ( a@D  % HP(A€B JA0P"A$ ( a@D  % HP(A€B JA0P"A$ ( a@D  % HP(A€B JA0P"A$ ܩnܼ]ox7^ŝ >{ Mym6 IDATu--]n2`ABPv{OSz=ԂB 48[q\MOuSJ-کnrsB WgJ){-BW ^jwz>S2(s+u;c\x>m f{}kOUCG3Y8#jw6uA|tl킼7-fӛ~=.ª\nEQqJb9]Qu^ew\a$}>zF=<.Us7,)G*r[wWG]V$ `w䒈@qUq9+ܴlB*=m 8 "5&"q L[kLUN F&$T/T6t @ũ 꼌9ybxn]HχvPN$D(k/lh(*5QyIAeU-F/CsVT$JKش =Qy|evwzBjѭ+Y57sYο}||Ӳ¢֞Sk{tFٞ~z*Lu9\O]n9] 1œTEl^|2.FcA"D1xϾeUJ!Pd]Kw}kW,)dH MûKJ*@);ݞ+5-mxxʮ JL n7s+oҼ٩u 󏜿J z?;U+".R,t;^R\وByǂB=a[{\U\0k}zås$BD2 Y!.FCw%*(>}㣥/~D0k UMo\RH|P>:sm^VxKsL, ~~sguJ]{ňwq9]*o㆞@^zCŕ=+c҂- *йxvBPOܽ")V$I]Wq/'#1+R% Oofי+w|HE0=-KylsJ|ޖEcK-oZr %N~HhITOѣ5Z4FHTg&ǥǰ uTrIVj<{g~V \k mCJ8+5pFacs!6FĘŴuYJ2l_9g9At]Ewk jEMSX:$9}kn7ZB #>!r=Y9w#ZL6;??z)r{.74^\8]xU0ba(>;s͌3 ïr4HBr?8lHDI 2f֯l[ȭ eyQ*ty\. bH i a> V 3"r(0F5A-\`wR&s;޷s_f UVZVۚVl]QpV&+Dw]m d X${#)|-G2TF&B(obT Tp{dul^┲ <K,AH/IǷj(h:433b-vk Wg3*ٹ: ;(ԥ1Xbdfsh܃y\Lܥ1-v9abχ0>KC ;-~P~^]E]۹zP AЙ3p )zS:ri`.ǿGkј",bNG\mHOHUD~Qy޳uzM.4um ך RuHk,׷E`.>_D~,vf슌ܖ8ףi%W_zuOL"fKD|~OKwg$(`hM=sw$eZ?۾rN~f򫟜4c$2 eD|w^bKT) g['dLM<.'V![(%",#~g+j[{&;77u Fa<G.mX\q^w z^bdOl_1zN a A0P"A$ ( a@D  % HP(A€B JA0P"A$ ( aA]jtMECGSDO0T+%d7QRrxXL(=iITjxhVjw s΅JXGx\#iּYIr) eGP҃0Bl )ϦێT c(?wy=n'VW2[Rl|AI eEK@ND8kMx֍ pZ|W:Q:])u&p|j/pa%ItF ʯl;h0NА)vP&E%  eV dPrFP⍰BShP4HߏcݓtE܁LPr ^0\Ɉ#[(ZP=QWsʾh- M?c76ׂ ȝd ﭒ) 828H6:Al&پosà xEC:`J!&Xըz* WmKn:fO ȝ a7EM[qx.=b#_35tYHv7I& LP^r Xߑaz^qΎ ʄ Gj+p;2Mp{$&POv IB$J&L>iĂEyꏜ% =2ׇDE/wOS W^ötN DO?X}ީy*ǔ* aD + G0!m K|w=65=\Aniƛ= N rw]غ@~6G76na8$ !L 9@{?H .erwlc]2J>8ګ3YNG{j[frT7 A +gK?j55sr@Cv=z?dz/7Ľ!bg7X=MfZy ]cm=Z. bgM_RMbԴ\޶nQ~vZ|'r=E)eFtMDw JEgGU,tt7ezwBd(!m}zi#su=u= ݿ$.[k,m7m<[wWԏq O1]wEYk^ 1ɠsqLlo|t~fꅳ2=Zӫ館iH4 0.o1K(BA½$_1$Kji+fUC!;^QYrWEqMVbHTUco_~S!1{^xm[7ilՙ~鉪Ƿ-W(oދI~ܫ[루D%yӓ.ϡWk[f\,JP+3W ,hx@1lX2 #3r[2.s;?L҇%Lo=qPGRDPbwȝ/AIp,=N9-3 D((덼 e(=3. $IvzM%Ƭ![T"y/)NKIb*T2f?~pïV:$€:;]v\REֵt׷v?}ŒlW>>'sJSUU\Y3𸜑F;腒CQASj:G^|Y|#Y#VqЗ~4C<H'Rln~M[.lqh/̈hOZ!k6"V嬙{R/_ۻ0;73)+-~=}ŧ'.9UsN^Z)o>Ζ}rcۖ1uF $+C=d[UȞoMz5vIcڦ3ZRf.ٶEQ%dRkܝ|n/KSb?PI謕T& ԋ{ctv(I|=Ӳ@U>)ݔh˒6JDh2 hC.BܰH&*zR͉K5Ad&H[0_^/AoZ֣=WQzAnZ &+HM:Pq%ֿ.u_ v>G$_/i8cT!B}V251#M.̛8Lz7i}zۖ3<>/pa e[5j[z!۽Hx}&꟟{u=vC=y% } }_^1gε 8 !Qmv%lG^n ^~眜t1!F32B(_6J.TH"g;)hz(Z!ݰd3ѱʆ&A`<=*2{\:'O4%wF@4{գ ZզΠ^e Kc`x|f[M%e-*`㝞[4̶kfy\&Tk~@NGch AL1Q*aQ=/ aiGL.%DPLQNKRJ5+ w<:owgei]t=ة(zױ/yWcG_Pss@gf$C(9=0=ZSRzu&/ӺfOFN]el`sW5@:8ӕ?u͈~#F@Ł/i'`MD>pV'F z Ƕ-{ӟXAcx<كKK?|P@v˓LN$.YY!ϛ/HQlњ2?z)q>\rW$"hz)D W7mƕkM/eWjdwk4MgN Aۛ2|u0o90%2FIC``P3u }6›l^/ܑ-Rv8SYop8dZY43c8J>G7=U^g04fX= k..g~~Fȓ.*ҙlj[y\NB~Q]Kf1G];?+5WU™iIE=A|둍.T׶5ws25>`Fe Y AnQʸ~, 8XWzQ1 e9@jS C}2#Ҍ^ɻWg93C;mI6^:oسYbgwڜchK X'5Bt4oBre"0Y;%0Г Ȕ3aB"?T&+?V}+5$ybW9HEgTʄPС5Z:+ޢ LJ"@(/#dƧr"٣TUmٻSm^DƵZc@cO>o\W>)޹vgL-(s1J!z"ju)/zުoxqMr:5Lɍ)v5vxӧEP.w ϱy[ @#gƧ::> ܩ.ؾhJ6Q]=>%ɦ,K7i9ʛE( .[ o ȈD/$In#=D"XWnxFad1ORq(!AdYPz) H"JF "/+'ǑA;qetGH3hRASpLEt$ ^qI(#2pqNބ|vUf\Bp h3Njyo1{G77C,{_5lk#+קF*|{`~]xvSspss3A&q冐ZN@wH32|?$}7XH?OeæđN ۶.qƌt}zf#-A=@Zj27=27c %n2rT9+m e#,{C=B8}mui 60sZȤpBuMK&dž^'rz?PmW(8~ 0d/S[#HrڱS:.ŵyI%0 ?i$H;o++֤n(J--ŕ-=Zl8ODQt(J(¶Anrƿt i3οWS0Ş|Po%, _@ᘯZgCvftBQtgAr4P@ boM2SE[ %lWߐ'VR+g٨U~.?naeZ"tݺxδ0;(Ǒ¬5Z/>sOWwj ^/\8+庹lMg#h.xiugAk@dݻnAJx2QLP/!tuJt:ClY1ӠzL@^ތ`ωZ!o:8%gЈ^ã }b!?%>p6v5iI Gʰax gki^\l85vu$r01BN +V?5GiLp%yQ@POZ9CZ hJЛm-=|.'/+Y"f)窊+RX3i~~w_,9}e?y)"hǞ-ݚ M`<9Na{s)MKP=q:^7wf輜rW/-G/h5+AVR,q±My*%j E}z\@77eE/<>396짂X(:D IDAT/=Qdv;DD~ F2,L'.]OS>u*2Ғs9d:9l{4sUj;n~<!r&L($| n4[Y34\^ /(|]%Wk fPV,5}qAH!G2%LP2:x+!`^-AB)#}_n;NWIA& 8"g)VVD::Ťtˎ4 DdjABɰI ,g9=7M+=.ҷ}N A*@CJCJ ,cVQEMz9% HhŽV˩|y)h<4*>/[ΗgʽRX -ʔ  nU4IA&[r* ȍA$ ( az%2!?2eo$778\nw oVo6B_o=_ٯWs2w,ly]Ym[GGbgg]Gs@:IQ;xՒ¬_r3By[r{=Plt]>ş}gK6˷{um=?xsG¯PeE!@IEip)mMBZY_q1^S{SysJxg٨k+Mg6fgݧX@[ >0g̬fW):Ʋ wܖPN=:+`YlsZDH0U=^ JؿxT$b彆4z)@(B~PN=2qr!7FEӍdm8+ETmm ԓLfY977(',~b(f8/9B&4Z>>zJYXT$9(:x]b@zzsrdbJ.ٲF] טAc\ |W!2SVld%j bi%"p=z~<#zo ۖeQaQz)>F^̞9ƪy)7^hpt@UW͟TߎyՙIZù/o^`/V{Ngj` Si  m$iL9#.c/ʯooٝnرf^j*DugJ6cԿ(\T0}+5-Fbw~q9^e AnB8b^[z5/7}ǚy&t%٩?^&NreE9\hr8dZ֝xiw<>X HU,*ޝGQyUwWwN"$$!  DQfFqԙpv]wf]םgYuFeyuQ9Qrww:ݝtϪiB_I-yCbU,{Ǭ~%yƈVF\>?9\ʞ?cy?vo0㈘k=y0 OXNd816nJJJJJJ{;-GFLݶ><ݛ28h [wƢs3DZ R`D R0>?t{wn8cx p]aD BJ"ra14|_nG᝽GkkL#qlDIx7:qzPXf.Ɉz?=^oL|@PNycw곛A{_oߺxM5Kf~]ju1 . ;Mgj~K5H^X0wU39غ%rKuVv{6+L1Hh$4j%ϓgم_=~_^GDhVeevn-)w":9'oih۴fqAnPptM]iYyRXƎe.жURV}3]`(ʛoW*3Ҧhժ~\*ava+d48xn03c%/_<.G` |\ΌqyMMKDbBxm鲰 +"Ff*ebI3|u} ċgLo L)͂BNop%1w~s\e)2Nbrf> !8h3Vpr1 =EjԑaՋr7Lo8lCQXZjq 8\kHDѾg MAR>zx[śU**/]>m{eMDdL`I% fy}8^m5PеGEr9{sgqGMt{wtk;Gr"D\Ivd ޛtyGh~{ᶂ]Ib~vʱ<~yS0/r|]ANP$DG2 snv rT7)=y:K.yjq™Kny}WD6DvY> Q*GE%D{$E9n%kj Dm8,-)\P06mEN?D7fػzDvU g&.ԵQ+O-7#6Dם؜cЇ9i}.k;c"Ê4n[_^fgRHgx}vG銎/3}}c"2xŴ`fQIS2iu􇆄NyhՂ3Z؃k^ D%_ 0(Of<_& eD7n^8F #JQ@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@PPLt_ؼ'7lULD ͼq/T,cH.RbތN?uqngJqqFJz^|.]z8D fQLDبgfU5̙1uܚ5` A9&?0L0Um{J7Lln #25ޘox'"lFz3$8l퍎*{D6Ym; [_T5;?~U g};-W$_ɘ [w)fl"{Ǝ>+10'3yH! ۏmcDHy;{]ڻE!(DŽa_CTlTxUJ隺V=V C56]LDaYk$zgo)#BUNw ZRV}3P/`,.ٗ|>gم_=~_^GD:*Dz8>2L'c.#jkJ';\[wi˽tnsUJ究4~H36ƈ?'5RjB{־~sZ/t%DƢ9#kv~yTsӵm ì,^ ;\y~۾#gj Mk䁣kɜ:FH黂Uf>QlLimἬv:]4?sJT)ۏmcd(SEm7 (Pjb4)䲟=R43=aHdW/5?'UȱDiba'Q&cLb;j.: <2_dNO :=#,)"|_a`,'=a\[Y"L Q2 cw[l}”0e&)F'6, '*\wGn:5wt S#zPXxt: 1 3c"rexsQLJY,(r"gѬgqQՋsy&!F#e3cꖧoyjwŭ A9&28isKh܌@^ ;puNK$kx3M5RmHO5c a-L2_)yHn&DGR {¯gMP׶uwɶOKc":{.ahsey}~éd<>u=8jw*7-LM.-mmųݿl0p^#j1U7g`5t)~)c%]N$>Kb'`mC0^gRJx#1\(YE!|ִSkBDn7xъKDem1YZػ r%Őd 1Dx[,B,фYJ4lɘs3]h,>u:,?i\)5t_"#A}ԍw{H70-F#xI A9&qpdtBj"u ou^!"k;B5gY= R(dz&D>ax@ϠU}܌Wi8S(R3F=a^#"BpS8 F#ݸ =&UjZM=–PQLp19$:-W]_|ꅿT^j nd~v {׌uQ #VSODo&h!59q|IUӯ:sGIH%}^SO7\䆠k#xt_:]DҳuiI1Qz"br{ǿ|kK>{㿝j!Eǽ&Lq=;嵝 ”X u}o]!FA/qyQ"w_j~>YuK8^0RIJQ L;ml\8m^WÃ'ZMDTrĹ:C.!5ۏtϩ ӎ_ig#Zĭ c3q٧ۣu~ ӆ9wF>?uU.+=wH\Vjq™Kny}WD6DvY> Q~s|ΌNUU^jyb"m6 kpy}D$]DE0 =s 7]TPc m{$gumI羿vЙ'DG2 snvT)yɟk5ۣZ&RH} N9VqUsqOW9IDAT厈_ѓ'd7wvϑ~ti Gk;,^GldxnFktɷ>xƺ.9#Kșzan)F"\V)FX .=N5[T*QVd5ȰMkm?uyP*~ݻ?.>]cy^ܑ9J{Vjc8zRΞ #include #include #include #include #include #include #define LOG_MODULE "shm" #include "log.h" #include "stride.h" static tll(struct buffer) buffers; static void buffer_release(void *data, struct wl_buffer *wl_buffer) { struct buffer *buffer = data; assert(buffer->wl_buf == wl_buffer); assert(buffer->busy); buffer->busy = false; } static const struct wl_buffer_listener buffer_listener = { .release = &buffer_release, }; struct buffer * shm_get_buffer(struct wl_shm *shm, int width, int height) { tll_foreach(buffers, it) { if (it->item.width != width || it->item.height != height) continue; if (!it->item.busy) { it->item.busy = true; return &it->item; } } /* * No existing buffer available. Create a new one by: * * 1. open a memory backed "file" with memfd_create() * 2. mmap() the memory file, to be used by the pixman image * 3. create a wayland shm buffer for the same memory file * * The pixman image and the wayland buffer are now sharing memory. */ int pool_fd = -1; void *mmapped = NULL; size_t size = 0; struct wl_shm_pool *pool = NULL; struct wl_buffer *buf = NULL; pixman_image_t *pix = NULL; /* Backing memory for SHM */ #if defined(MEMFD_CREATE) pool_fd = memfd_create("fnott-wayland-shm-buffer-pool", MFD_CLOEXEC); #elif defined(__FreeBSD__) // memfd_create on FreeBSD 13 is SHM_ANON without sealing support pool_fd = shm_open(SHM_ANON, O_RDWR | O_CLOEXEC, 0600); #else char name[] = "/tmp/fnott-wayland-shm-buffer-pool-XXXXXX"; pool_fd = mkostemp(name, O_CLOEXEC); unlink(name); #endif if (pool_fd == -1) { LOG_ERRNO("failed to create SHM backing memory file"); goto err; } const int stride = stride_for_format_and_width(PIXMAN_a8r8g8b8, width); /* Total size */ size = stride * height; if (ftruncate(pool_fd, size) == -1) { LOG_ERRNO("failed to truncate SHM pool"); goto err; } mmapped = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, pool_fd, 0); if (mmapped == MAP_FAILED) { LOG_ERRNO("failed to mmap SHM backing memory file"); goto err; } pool = wl_shm_create_pool(shm, pool_fd, size); if (pool == NULL) { LOG_ERR("failed to create SHM pool"); goto err; } buf = wl_shm_pool_create_buffer( pool, 0, width, height, stride, WL_SHM_FORMAT_ARGB8888); if (buf == NULL) { LOG_ERR("failed to create SHM buffer"); goto err; } /* We use the entire pool for our single buffer */ wl_shm_pool_destroy(pool); pool = NULL; close(pool_fd); pool_fd = -1; /* One pixman image for each worker thread (do we really need multiple?) */ pix = pixman_image_create_bits_no_clear( PIXMAN_a8r8g8b8, width, height, (uint32_t *)mmapped, stride); /* Push to list of available buffers, but marked as 'busy' */ tll_push_back( buffers, ((struct buffer){ .width = width, .height = height, .stride = stride, .busy = true, .size = size, .mmapped = mmapped, .wl_buf = buf, .pix = pix} ) ); struct buffer *ret = &tll_back(buffers); wl_buffer_add_listener(ret->wl_buf, &buffer_listener, ret); return ret; err: if (pix != NULL) pixman_image_unref(pix); if (buf != NULL) wl_buffer_destroy(buf); if (pool != NULL) wl_shm_pool_destroy(pool); if (pool_fd != -1) close(pool_fd); if (mmapped != NULL) munmap(mmapped, size); return NULL; } void shm_fini(void) { tll_foreach(buffers, it) { struct buffer *buf = &it->item; if (buf->pix != NULL) pixman_image_unref(buf->pix); wl_buffer_destroy(buf->wl_buf); munmap(buf->mmapped, buf->size); tll_remove(buffers, it); } } fnott-1.4.1+ds/shm.h000066400000000000000000000005671447512336000142260ustar00rootroot00000000000000#pragma once #include #include #include #include struct buffer { int width; int height; int stride; bool busy; size_t size; void *mmapped; struct wl_buffer *wl_buf; pixman_image_t *pix; }; struct buffer *shm_get_buffer(struct wl_shm *shm, int width, int height); void shm_fini(void); fnott-1.4.1+ds/spawn.c000066400000000000000000000132541447512336000145570ustar00rootroot00000000000000#include "spawn.h" #include #include #include #include #include #include #include #include #include #include #define LOG_MODULE "spawn" #define LOG_ENABLE_DBG 0 #include "log.h" bool spawn(const char *cwd, char *const argv[], int stdin_fd, int stdout_fd, int stderr_fd) { pid_t pid = fork(); if (pid < 0) { LOG_ERRNO("failed to fork"); goto err; } if (pid == 0) { int pipe_fds[2] = {-1, -1}; if (pipe2(pipe_fds, O_CLOEXEC) < 0) { LOG_ERRNO("failed to create pipe"); goto err; } pid_t inner_pid = fork(); if (inner_pid == 0) { /* Child */ close(pipe_fds[0]); /* Clear signal mask */ sigset_t mask; sigemptyset(&mask); if (sigprocmask(SIG_SETMASK, &mask, NULL) < 0) goto child_err; bool close_stderr = stderr_fd >= 0; bool close_stdout = stdout_fd >= 0 && stdout_fd != stderr_fd; bool close_stdin = stdin_fd >= 0 && stdin_fd != stdout_fd && stdin_fd != stderr_fd; if ((stdin_fd >= 0 && (dup2(stdin_fd, STDIN_FILENO) < 0 || (close_stdin && close(stdin_fd) < 0))) || (stdout_fd >= 0 && (dup2(stdout_fd, STDOUT_FILENO) < 0 || (close_stdout && close(stdout_fd) < 0))) || (stderr_fd >= 0 && (dup2(stderr_fd, STDERR_FILENO) < 0 || (close_stderr && close(stderr_fd) < 0))) || (cwd != NULL && chdir(cwd) < 0) || execvp(argv[0], argv) < 0) { goto child_err; } assert(false); _exit(errno); child_err: ; const int errno_copy = errno; (void)!write(pipe_fds[1], &errno_copy, sizeof(errno_copy)); _exit(errno_copy); } /* Inner parent */ close(pipe_fds[1]); int errno_copy; static_assert(sizeof(errno_copy) == sizeof(errno), "errno size mismatch"); ssize_t ret = read(pipe_fds[0], &errno_copy, sizeof(errno_copy)); close(pipe_fds[0]); if (ret == 0) _exit(0); else if (ret < 0) _exit(errno); else { waitpid(inner_pid, NULL, 0); _exit(errno_copy); } assert(false); _exit(123); } int status; while (true) { if (waitpid(pid, &status, 0) >= 0) break; if (errno != EINTR) { LOG_ERRNO("failed to wait for child process"); return false; } } if (WIFEXITED(status)) { int errno_copy = WEXITSTATUS(status); if (errno_copy != 0) { LOG_ERRNO_P("%s: failed to spawn", errno_copy, argv[0]); return false; } return true; } else if (WIFSIGNALED(status)) { LOG_ERR("%s: killed by signal=%d", argv[0], WTERMSIG(status)); return false; } else { LOG_ERR("%s: died of unknown reason", argv[0]); return false; } assert(false); err: return false; } bool spawn_expand_template(const struct config_spawn_template *template, size_t key_count, const char *key_names[static key_count], const char *key_values[static key_count], size_t *argc, char ***argv) { *argc = 0; *argv = NULL; for (; template->argv[*argc] != NULL; (*argc)++) ; #define append(s, n) \ do { \ expanded = realloc(expanded, len + (n) + 1); \ memcpy(&expanded[len], s, n); \ len += n; \ expanded[len] = '\0'; \ } while (0) *argv = malloc((*argc + 1) * sizeof((*argv)[0])); /* Expand the provided keys */ for (size_t i = 0; i < *argc; i++) { size_t len = 0; char *expanded = NULL; char *start = NULL; char *last_end = template->argv[i]; while ((start = strstr(last_end, "${")) != NULL) { /* Append everything from the last template's end to this * one's beginning */ append(last_end, start - last_end); /* Find end of template */ start += 2; char *end = strstr(start, "}"); if (end == NULL) { /* Ensure final append() copies the unclosed '${' */ last_end = start - 2; LOG_WARN("notify: unclosed template: %s", last_end); break; } /* Expand template */ bool valid_key = false; for (size_t j = 0; j < key_count; j++) { if (strncmp(start, key_names[j], end - start) != 0) continue; append(key_values[j], strlen(key_values[j])); valid_key = true; break; } if (!valid_key) { /* Unrecognized template - append it as-is */ start -= 2; append(start, end + 1 - start); LOG_WARN("notify: unrecognized template: %.*s", (int)(end + 1 - start), start); } last_end = end + 1; } append(last_end, template->argv[i] + strlen(template->argv[i]) - last_end); (*argv)[i] = expanded; } (*argv)[*argc] = NULL; #undef append return true; } fnott-1.4.1+ds/spawn.h000066400000000000000000000006151447512336000145610ustar00rootroot00000000000000#pragma once #include #include #include "config.h" bool spawn(const char *cwd, char *const argv[], int stdin_fd, int stdout_fd, int stderr_fd); bool spawn_expand_template( const struct config_spawn_template *template, size_t key_count, const char *key_names[static key_count], const char *key_values[static key_count], size_t *argc, char ***argv); fnott-1.4.1+ds/stride.h000066400000000000000000000003061447512336000147200ustar00rootroot00000000000000#pragma once #include static inline int stride_for_format_and_width(pixman_format_code_t format, int width) { return (((PIXMAN_FORMAT_BPP(format) * width + 7) / 8 + 4 - 1) & -4); } fnott-1.4.1+ds/subprojects/000077500000000000000000000000001447512336000156215ustar00rootroot00000000000000fnott-1.4.1+ds/subprojects/fcft.wrap000066400000000000000000000001061447512336000174330ustar00rootroot00000000000000[wrap-git] url = https://codeberg.org/dnkl/fcft.git revision = master fnott-1.4.1+ds/subprojects/tllist.wrap000066400000000000000000000001101447512336000200170ustar00rootroot00000000000000[wrap-git] url = https://codeberg.org/dnkl/tllist.git revision = master fnott-1.4.1+ds/svg.c000066400000000000000000000030761447512336000142270ustar00rootroot00000000000000#include "svg.h" #include #define LOG_MODULE "svg" #define LOG_ENABLE_DBG 0 #include "log.h" #include #include pixman_image_t * svg_load(const char *path, int size) { NSVGimage *svg = nsvgParseFromFile(path, "px", 96); if (svg == NULL) return NULL; if (svg->width == 0 || svg->height == 0) { nsvgDelete(svg); return NULL; } struct NSVGrasterizer *rast = nsvgCreateRasterizer(); const int w = size; const int h = size; float scale = w > h ? w / svg->width : h / svg->height; uint8_t *data = malloc(h * w * 4); nsvgRasterize(rast, svg, 0, 0, scale, data, w, h, w * 4); nsvgDeleteRasterizer(rast); nsvgDelete(svg); pixman_image_t *img = pixman_image_create_bits_no_clear( PIXMAN_a8b8g8r8, w, h, (uint32_t *)data, w * 4); /* Nanosvg produces non-premultiplied ABGR, while pixman expects * premultiplied */ for (uint32_t *abgr = (uint32_t *)data; abgr < (uint32_t *)(data + h * w * 4); abgr++) { uint8_t alpha = (*abgr >> 24) & 0xff; uint8_t blue = (*abgr >> 16) & 0xff; uint8_t green = (*abgr >> 8) & 0xff; uint8_t red = (*abgr >> 0) & 0xff; if (alpha == 0xff) continue; if (alpha == 0x00) blue = green = red = 0x00; else { blue = blue * alpha / 0xff; green = green * alpha / 0xff; red = red * alpha / 0xff; } *abgr = (uint32_t)alpha << 24 | blue << 16 | green << 8 | red; } return img; } fnott-1.4.1+ds/svg.h000066400000000000000000000001311447512336000142210ustar00rootroot00000000000000#pragma once #include pixman_image_t *svg_load(const char *path, int size); fnott-1.4.1+ds/tokenize.c000066400000000000000000000043621447512336000152570ustar00rootroot00000000000000#include "tokenize.h" #include #include #define LOG_MODULE "tokenize" #define LOG_ENABLE_DBG 0 #include "log.h" static bool push_argv(char ***argv, size_t *size, char *arg, size_t *argc) { if (arg != NULL && arg[0] == '%') return true; if (*argc >= *size) { size_t new_size = *size > 0 ? 2 * *size : 10; char **new_argv = realloc(*argv, new_size * sizeof(new_argv[0])); if (new_argv == NULL) return false; *argv = new_argv; *size = new_size; } (*argv)[(*argc)++] = arg; return true; } bool tokenize_cmdline(char *cmdline, char ***argv) { *argv = NULL; size_t argv_size = 0; bool first_token_is_quoted = cmdline[0] == '"' || cmdline[0] == '\''; char delim = first_token_is_quoted ? cmdline[0] : ' '; char *p = first_token_is_quoted ? &cmdline[1] : &cmdline[0]; char *search_start = p; size_t idx = 0; while (*p != '\0') { char *end = strchr(search_start, delim); if (end == NULL) { if (delim != ' ') { LOG_ERR("unterminated %s quote", delim == '"' ? "double" : "single"); free(*argv); *argv = NULL; return false; } if (!push_argv(argv, &argv_size, p, &idx) || !push_argv(argv, &argv_size, NULL, &idx)) { goto err; } else return true; } if (end > p && *(end - 1) == '\\') { /* Escaped quote, remove one level of escaping and * continue searching for "our" closing quote */ memmove(end - 1, end, strlen(end)); end[strlen(end) - 1] = '\0'; search_start = end; continue; } *end = '\0'; if (!push_argv(argv, &argv_size, p, &idx)) goto err; p = end + 1; while (*p == delim) p++; while (*p == ' ') p++; if (*p == '"' || *p == '\'') { delim = *p; p++; } else delim = ' '; search_start = p; } if (!push_argv(argv, &argv_size, NULL, &idx)) goto err; return true; err: free(*argv); return false; } fnott-1.4.1+ds/tokenize.h000066400000000000000000000001301447512336000152510ustar00rootroot00000000000000#pragma once #include bool tokenize_cmdline(char *cmdline, char ***argv); fnott-1.4.1+ds/uri.c000066400000000000000000000173421447512336000142300ustar00rootroot00000000000000#include "uri.h" #include #include #include #include #define LOG_MODULE "uri" #define LOG_ENABLE_DBG 0 #include "log.h" enum { HEX_DIGIT_INVALID = 16 }; static uint8_t hex2nibble(char c) { switch (c) { case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': return c - '0'; case 'a': case 'b': case 'c': case 'd': case 'e': case 'f': return c - 'a' + 10; case 'A': case 'B': case 'C': case 'D': case 'E': case 'F': return c - 'A' + 10; } return HEX_DIGIT_INVALID; } bool uri_parse(const char *uri, size_t len, char **scheme, char **user, char **password, char **host, uint16_t *port, char **path, char **query, char **fragment) { LOG_DBG("parse URI: \"%.*s\"", (int)len, uri); if (scheme != NULL) *scheme = NULL; if (user != NULL) *user = NULL; if (password != NULL) *password = NULL; if (host != NULL) *host = NULL; if (port != NULL) *port = 0; if (path != NULL) *path = NULL; if (query != NULL) *query = NULL; if (fragment != NULL) *fragment = NULL; size_t left = len; const char *start = uri; const char *end = NULL; if ((end = memchr(start, ':', left)) == NULL) goto err; size_t scheme_len = end - start; if (scheme_len == 0) goto err; if (scheme != NULL) *scheme = strndup(start, scheme_len); LOG_DBG("scheme: \"%.*s\"", (int)scheme_len, start); start = end + 1; left = len - (start - uri); /* Authinfo */ if (left >= 2 && start[0] == '/' && start[1] == '/') { start += 2; left -= 2; /* [user[:password]@]@host[:port] */ /* Find beginning of path segment (required component * following the authinfo) */ const char *path_segment = memchr(start, '/', left); if (path_segment == NULL) goto err; size_t auth_left = path_segment - start; /* Do we have a user (and optionally a password)? */ const char *user_pw_end = memchr(start, '@', auth_left); if (user_pw_end != NULL) { size_t user_pw_len = user_pw_end - start; /* Do we have a password? */ const char *user_end = memchr(start, ':', user_pw_end - start); if (user_end != NULL) { size_t user_len = user_end - start; if (user_len == 0) goto err; if (user != NULL) *user = strndup(start, user_len); const char *pw = user_end + 1; size_t pw_len = user_pw_end - pw; if (pw_len == 0) goto err; if (password != NULL) *password = strndup(pw, pw_len); LOG_DBG("user: \"%.*s\"", (int)user_len, start); LOG_DBG("password: \"%.*s\"", (int)pw_len, pw); } else { size_t user_len = user_pw_end - start; if (user_len == 0) goto err; if (user != NULL) *user = strndup(start, user_len); LOG_DBG("user: \"%.*s\"", (int)user_len, start); } start = user_pw_end + 1; left = len - (start - uri); auth_left -= user_pw_len + 1; } const char *host_end = memchr(start, ':', auth_left); if (host_end != NULL) { size_t host_len = host_end - start; if (host != NULL) *host = strndup(start, host_len); const char *port_str = host_end + 1; size_t port_len = path_segment - port_str; if (port_len == 0) goto err; uint16_t _port = 0; for (size_t i = 0; i < port_len; i++) { if (!(port_str[i] >= '0' && port_str[i] <= '9')) goto err; _port *= 10; _port += port_str[i] - '0'; } if (port != NULL) *port = _port; LOG_DBG("host: \"%.*s\"", (int)host_len, start); LOG_DBG("port: \"%.*s\" (%hu)", (int)port_len, port_str, _port); } else { size_t host_len = path_segment - start; if (host != NULL) *host = strndup(start, host_len); LOG_DBG("host: \"%.*s\"", (int)host_len, start); } start = path_segment; left = len - (start - uri); } /* Do we have a query? */ const char *query_start = memchr(start, '?', left); const char *fragment_start = memchr(start, '#', left); size_t path_len = query_start != NULL ? query_start - start : fragment_start != NULL ? fragment_start - start : left; if (path_len == 0) goto err; /* Path - decode %xx encoded characters */ if (path != NULL) { const char *encoded = start; char *decoded = malloc(path_len + 1); char *p = decoded; size_t encoded_len = path_len; size_t decoded_len __attribute__((unused)) = 0; while (true) { /* Find next '%' */ const char *next = memchr(encoded, '%', encoded_len); if (next == NULL) { strncpy(p, encoded, encoded_len); decoded_len += encoded_len; p += encoded_len; break; } /* Copy everything leading up to the '%' */ size_t prefix_len = next - encoded; memcpy(p, encoded, prefix_len); p += prefix_len; encoded_len -= prefix_len; decoded_len += prefix_len; if (hex2nibble(next[1]) <= 15 && hex2nibble(next[2]) <= 15) { *p++ = hex2nibble(next[1]) << 4 | hex2nibble(next[2]); decoded_len++; encoded_len -= 3; encoded = next + 3; } else { *p++ = *next; decoded_len++; encoded_len -= 1; encoded = next + 1; } } *p = '\0'; *path = decoded; LOG_DBG("path: encoded=\"%.*s\", decoded=\"%s\"", (int)path_len, start, decoded); } else LOG_DBG("path: encoded=\"%.*s\", decoded=", (int)path_len, start); start = query_start != NULL ? query_start + 1 : fragment_start != NULL ? fragment_start + 1 : uri + len; left = len - (start - uri); if (query_start != NULL) { size_t query_len = fragment_start != NULL ? fragment_start - start : left; if (query_len == 0) goto err; if (query != NULL) *query = strndup(start, query_len); LOG_DBG("query: \"%.*s\"", (int)query_len, start); start = fragment_start != NULL ? fragment_start + 1 : uri + len; left = len - (start - uri); } if (fragment_start != NULL) { if (left == 0) goto err; if (fragment != NULL) *fragment = strndup(start, left); LOG_DBG("fragment: \"%.*s\"", (int)left, start); } return true; err: if (scheme != NULL) free(*scheme); if (user != NULL) free(*user); if (password != NULL) free(*password); if (host != NULL) free(*host); if (path != NULL) free(*path); if (query != NULL) free(*query); if (fragment != NULL) free(*fragment); return false; } bool hostname_is_localhost(const char *hostname) { char this_host[_POSIX_HOST_NAME_MAX]; if (gethostname(this_host, sizeof(this_host)) < 0) this_host[0] = '\0'; return (hostname != NULL && ( strcmp(hostname, "") == 0 || strcmp(hostname, "localhost") == 0 || strcmp(hostname, this_host) == 0)); } fnott-1.4.1+ds/uri.h000066400000000000000000000005001447512336000142210ustar00rootroot00000000000000#pragma once #include #include #include bool uri_parse(const char *uri, size_t len, char **scheme, char **user, char **password, char **host, uint16_t *port, char **path, char **query, char **fragment); bool hostname_is_localhost(const char *hostname); fnott-1.4.1+ds/wayland.c000066400000000000000000000763651447512336000151020ustar00rootroot00000000000000#include "wayland.h" #include #include #include #include #include #include #include #include #include #include #include #include #if defined(FNOTT_HAVE_IDLE_NOTIFY) #include #endif #include #define LOG_MODULE "wayland" #define LOG_ENABLE_DBG 0 #include "log.h" #include "notification.h" struct idle_timer { struct notif_mgr *notif_mgr; struct org_kde_kwin_idle_timeout *kde_idle_timeout; #if defined(FNOTT_HAVE_IDLE_NOTIFY) struct ext_idle_notification_v1 *idle_notification; #endif enum urgency urgency; struct seat *seat; }; struct seat { struct wayland *wayl; struct wl_seat *wl_seat; uint32_t wl_name; char *name; /* One for each urgency level */ struct idle_timer idle_timer[3]; bool is_idle[3]; struct wl_pointer *wl_pointer; struct { uint32_t serial; /* Current location */ int x; int y; struct wl_surface *on_surface; /* Cursor image */ struct wl_surface *surface; struct wl_cursor_theme *theme; struct wl_cursor *cursor; /* Cursor theme info */ int scale; } pointer; }; struct wayland { const struct config *conf; struct fdm *fdm; struct org_kde_kwin_idle *kde_idle_manager; #if defined(FNOTT_HAVE_IDLE_NOTIFY) struct ext_idle_notifier_v1 *idle_notifier; #endif struct notif_mgr *notif_mgr; struct wl_display *display; struct wl_registry *registry; struct wl_compositor *compositor; struct zxdg_output_manager_v1 *xdg_output_manager; struct wl_shm *shm; struct zwlr_layer_shell_v1 *layer_shell; bool have_argb8888; tll(struct seat) seats; tll(struct monitor) monitors; const struct monitor *monitor; }; static void seat_destroy(struct seat *seat) { if (seat == NULL) return; for (int i = 0; i < 3; i++) { if (seat->idle_timer[i].kde_idle_timeout != NULL) org_kde_kwin_idle_timeout_release(seat->idle_timer[i].kde_idle_timeout); #if defined(FNOTT_HAVE_IDLE_NOTIFY) if (seat->idle_timer[i].idle_notification != NULL) ext_idle_notification_v1_destroy(seat->idle_timer[i].idle_notification); #endif } if (seat->pointer.theme != NULL) wl_cursor_theme_destroy(seat->pointer.theme); if (seat->pointer.surface != NULL) wl_surface_destroy(seat->pointer.surface); if (seat->wl_pointer != NULL) wl_pointer_release(seat->wl_pointer); if (seat->wl_seat != NULL) wl_seat_release(seat->wl_seat); free(seat->name); } static void update_cursor_surface(struct seat *seat) { if (seat->pointer.serial == 0) return; if (seat->pointer.cursor == NULL) return; if (seat->wl_pointer == NULL) return; int scale = seat->pointer.scale; wl_surface_set_buffer_scale(seat->pointer.surface, scale); struct wl_cursor_image *image = seat->pointer.cursor->images[0]; wl_surface_attach( seat->pointer.surface, wl_cursor_image_get_buffer(image), 0, 0); wl_pointer_set_cursor( seat->wl_pointer, seat->pointer.serial, seat->pointer.surface, image->hotspot_x / scale, image->hotspot_y / scale); wl_surface_damage_buffer( seat->pointer.surface, 0, 0, INT32_MAX, INT32_MAX); wl_surface_commit(seat->pointer.surface); } static bool reload_cursor_theme(struct seat *seat, int new_scale) { if (seat->pointer.theme != NULL && seat->pointer.scale == new_scale) return true; if (seat->pointer.theme != NULL) { wl_cursor_theme_destroy(seat->pointer.theme); seat->pointer.theme = NULL; seat->pointer.cursor = NULL; } /* Cursor */ unsigned cursor_size = 24; const char *cursor_theme = getenv("XCURSOR_THEME"); { const char *env_cursor_size = getenv("XCURSOR_SIZE"); if (env_cursor_size != NULL) { unsigned size; if (sscanf(env_cursor_size, "%u", &size) == 1) cursor_size = size; } } /* Note: theme is (re)loaded on scale and output changes */ LOG_INFO("cursor theme: %s, size: %u, scale: %d", cursor_theme, cursor_size, new_scale); struct wayland *wayl = seat->wayl; seat->pointer.theme = wl_cursor_theme_load( cursor_theme, cursor_size * new_scale, wayl->shm); if (seat->pointer.theme == NULL) { LOG_ERR("%s: failed to load cursor theme", cursor_theme); return false; } seat->pointer.cursor = wl_cursor_theme_get_cursor( seat->pointer.theme, "left_ptr"); if (seat->pointer.cursor == NULL) { LOG_ERR("%s: failed to load cursor 'left_ptr'", seat->name); return false; } seat->pointer.scale = new_scale; return true; } static void shm_format(void *data, struct wl_shm *wl_shm, uint32_t format) { struct wayland *wayl = data; if (format == WL_SHM_FORMAT_ARGB8888) wayl->have_argb8888 = true; } static void output_update_ppi(struct monitor *mon) { if (mon->dim.mm.width == 0 || mon->dim.mm.height == 0) return; int x_inches = mon->dim.mm.width * 0.03937008; int y_inches = mon->dim.mm.height * 0.03937008; mon->ppi.real.x = mon->dim.px_real.width / x_inches; mon->ppi.real.y = mon->dim.px_real.height / y_inches; /* The *logical* size is affected by the transform */ switch (mon->transform) { case WL_OUTPUT_TRANSFORM_90: case WL_OUTPUT_TRANSFORM_270: case WL_OUTPUT_TRANSFORM_FLIPPED_90: case WL_OUTPUT_TRANSFORM_FLIPPED_270: { int swap = x_inches; x_inches = y_inches; y_inches = swap; break; } case WL_OUTPUT_TRANSFORM_NORMAL: case WL_OUTPUT_TRANSFORM_180: case WL_OUTPUT_TRANSFORM_FLIPPED: case WL_OUTPUT_TRANSFORM_FLIPPED_180: break; } mon->ppi.scaled.x = mon->dim.px_scaled.width / x_inches; mon->ppi.scaled.y = mon->dim.px_scaled.height / y_inches; float px_diag = sqrt( pow(mon->dim.px_scaled.width, 2) + pow(mon->dim.px_scaled.height, 2)); mon->dpi = px_diag / mon->inch * mon->scale; } static void output_geometry(void *data, struct wl_output *wl_output, int32_t x, int32_t y, int32_t physical_width, int32_t physical_height, int32_t subpixel, const char *make, const char *model, int32_t transform) { struct monitor *mon = data; free(mon->make); free(mon->model); mon->dim.mm.width = physical_width; mon->dim.mm.height = physical_height; mon->inch = sqrt(pow(mon->dim.mm.width, 2) + pow(mon->dim.mm.height, 2)) * 0.03937008; mon->make = make != NULL ? strdup(make) : NULL; mon->model = model != NULL ? strdup(model) : NULL; mon->subpixel = subpixel; mon->transform = transform; output_update_ppi(mon); } static void output_mode(void *data, struct wl_output *wl_output, uint32_t flags, int32_t width, int32_t height, int32_t refresh) { if ((flags & WL_OUTPUT_MODE_CURRENT) == 0) return; struct monitor *mon = data; mon->refresh = (float)refresh / 1000; mon->dim.px_real.width = width; mon->dim.px_real.height = height; output_update_ppi(mon); } static void output_done(void *data, struct wl_output *wl_output) { struct monitor *mon = data; if (notif_mgr_monitor_updated(mon->wayl->notif_mgr, mon)) notif_mgr_refresh(mon->wayl->notif_mgr); } static void output_scale(void *data, struct wl_output *wl_output, int32_t factor) { struct monitor *mon = data; mon->scale = factor; } static void xdg_output_handle_logical_position( void *data, struct zxdg_output_v1 *xdg_output, int32_t x, int32_t y) { struct monitor *mon = data; mon->x = x; mon->y = y; } static void xdg_output_handle_logical_size(void *data, struct zxdg_output_v1 *xdg_output, int32_t width, int32_t height) { struct monitor *mon = data; mon->dim.px_scaled.width = width; mon->dim.px_scaled.height = height; output_update_ppi(mon); } static void xdg_output_handle_done(void *data, struct zxdg_output_v1 *xdg_output) { struct monitor *mon = data; if (notif_mgr_monitor_updated(mon->wayl->notif_mgr, mon)) notif_mgr_refresh(mon->wayl->notif_mgr); } static void xdg_output_handle_name(void *data, struct zxdg_output_v1 *xdg_output, const char *name) { struct monitor *mon = data; struct wayland *wayl = mon->wayl; free(mon->name); mon->name = name != NULL ? strdup(name) : NULL; if (wayl->conf->output != NULL && mon->name != NULL && strcmp(mon->name, wayl->conf->output) == 0) { wayl->monitor = mon; } } static void xdg_output_handle_description(void *data, struct zxdg_output_v1 *xdg_output, const char *description) { struct monitor *mon = data; free(mon->description); mon->description = description != NULL ? strdup(description) : NULL; } static void wl_pointer_enter(void *data, struct wl_pointer *wl_pointer, uint32_t serial, struct wl_surface *surface, wl_fixed_t surface_x, wl_fixed_t surface_y) { struct seat *seat = data; const struct notif *notif = notif_mgr_get_notif_for_surface(seat->wayl->notif_mgr, surface); if (notif == NULL) { /* * Seen on Sway-1.5 when cursor is hovering over the area * where a notification is later shown, and that notification * is then dismissed without moving the mouse (i.e. either * with fnottctl, or a timeout). */ return; } const struct monitor *mon = notif_monitor(notif); assert(mon != NULL); const int scale = mon->scale; seat->pointer.serial = serial; seat->pointer.x = wl_fixed_to_int(surface_x) * scale; seat->pointer.y = wl_fixed_to_int(surface_y) * scale; seat->pointer.on_surface = surface; reload_cursor_theme(seat, scale); update_cursor_surface(seat); } static void wl_pointer_leave(void *data, struct wl_pointer *wl_pointer, uint32_t serial, struct wl_surface *surface) { struct seat *seat = data; seat->pointer.serial = 0; seat->pointer.x = seat->pointer.y = 0; seat->pointer.on_surface = NULL; } static void wl_pointer_motion(void *data, struct wl_pointer *wl_pointer, uint32_t time, wl_fixed_t surface_x, wl_fixed_t surface_y) { struct seat *seat = data; const struct notif *notif = notif_mgr_get_notif_for_surface( seat->wayl->notif_mgr, seat->pointer.on_surface); if (notif == NULL) return; const struct monitor *mon = notif_monitor(notif); assert(mon != NULL); const int scale = mon->scale; seat->pointer.x = wl_fixed_to_int(surface_x) * scale; seat->pointer.y = wl_fixed_to_int(surface_y) * scale; } static void wl_pointer_button(void *data, struct wl_pointer *wl_pointer, uint32_t serial, uint32_t time, uint32_t button, uint32_t state) { LOG_DBG("BUTTON: button=%x, state=%u", button, state); struct seat *seat = data; struct wayland *wayl = seat->wayl; switch (state) { case WL_POINTER_BUTTON_STATE_PRESSED: { if (button == BTN_LEFT) { struct notif *notif = notif_mgr_get_notif_for_surface( wayl->notif_mgr, seat->pointer.on_surface); if (notif != NULL) notif_mgr_dismiss_id(wayl->notif_mgr, notif_id(notif)); } break; } case WL_POINTER_BUTTON_STATE_RELEASED: break; } } static void wl_pointer_axis(void *data, struct wl_pointer *wl_pointer, uint32_t time, uint32_t axis, wl_fixed_t value) { } static void wl_pointer_axis_discrete(void *data, struct wl_pointer *wl_pointer, uint32_t axis, int32_t discrete) { } static void wl_pointer_frame(void *data, struct wl_pointer *wl_pointer) { } static void wl_pointer_axis_source(void *data, struct wl_pointer *wl_pointer, uint32_t axis_source) { } static void wl_pointer_axis_stop(void *data, struct wl_pointer *wl_pointer, uint32_t time, uint32_t axis) { } const struct wl_pointer_listener pointer_listener = { .enter = wl_pointer_enter, .leave = wl_pointer_leave, .motion = wl_pointer_motion, .button = wl_pointer_button, .axis = wl_pointer_axis, .frame = wl_pointer_frame, .axis_source = wl_pointer_axis_source, .axis_stop = wl_pointer_axis_stop, .axis_discrete = wl_pointer_axis_discrete, }; static void seat_capabilities(void *data, struct wl_seat *wl_seat, enum wl_seat_capability caps) { struct seat *seat = data; if (caps & WL_SEAT_CAPABILITY_POINTER) { if (seat->wl_pointer == NULL) { assert(seat->pointer.surface == NULL); seat->pointer.surface = wl_compositor_create_surface( seat->wayl->compositor); if (seat->pointer.surface == NULL) { LOG_ERR("%s: failed to create cursor surface", seat->name); return; } seat->wl_pointer = wl_seat_get_pointer(wl_seat); wl_pointer_add_listener(seat->wl_pointer, &pointer_listener, seat); } } else { if (seat->wl_pointer != NULL) { wl_surface_destroy(seat->pointer.surface); wl_pointer_release(seat->wl_pointer); if (seat->pointer.theme != NULL) wl_cursor_theme_destroy(seat->pointer.theme); seat->wl_pointer = NULL; seat->pointer.surface = NULL; seat->pointer.theme = NULL; seat->pointer.cursor = NULL; } } } static void seat_name(void *data, struct wl_seat *wl_seat, const char *name) { struct seat *seat = data; free(seat->name); seat->name = strdup(name); } static void idle_idled(struct idle_timer *timer) { LOG_DBG("idle notify for urgency level %d", timer->urgency); timer->seat->is_idle[timer->urgency] = true; notif_mgr_notifs_reload_timeout(timer->notif_mgr); } static void idle_resumed(struct idle_timer *timer) { LOG_DBG("resume notify for urgency level %d", timer->urgency); timer->seat->is_idle[timer->urgency] = false; notif_mgr_notifs_reload_timeout(timer->notif_mgr); } #if defined(FNOTT_HAVE_IDLE_NOTIFY) static void idle_notify_idled(void *data, struct ext_idle_notification_v1 *notification) { struct idle_timer *timer = data; assert(timer->idle_notification == notification); idle_idled(timer); } static void idle_notify_resumed(void *data, struct ext_idle_notification_v1 *notification) { struct idle_timer *timer = data; assert(timer->idle_notification == notification); idle_resumed(timer); } static const struct ext_idle_notification_v1_listener idle_notify_listener = { .idled = &idle_notify_idled, .resumed = &idle_notify_resumed, }; #endif static void kde_idled(void *data, struct org_kde_kwin_idle_timeout *timeout) { struct idle_timer *timer = data; assert(timer->kde_idle_timeout == timeout); idle_idled(timer); } static void kde_resumed(void *data, struct org_kde_kwin_idle_timeout *timeout) { struct idle_timer *timer = data; assert(timer->kde_idle_timeout == timeout); idle_resumed(timer); } static const struct org_kde_kwin_idle_timeout_listener kde_idle_listener = { .idle = kde_idled, .resumed = kde_resumed, }; static const struct wl_shm_listener shm_listener = { .format = &shm_format, }; static const struct wl_output_listener output_listener = { .geometry = &output_geometry, .mode = &output_mode, .done = &output_done, .scale = &output_scale, }; static struct zxdg_output_v1_listener xdg_output_listener = { .logical_position = xdg_output_handle_logical_position, .logical_size = xdg_output_handle_logical_size, .done = xdg_output_handle_done, .name = xdg_output_handle_name, .description = xdg_output_handle_description, }; static const struct wl_seat_listener seat_listener = { .capabilities = seat_capabilities, .name = seat_name, }; static bool verify_iface_version(const char *iface, uint32_t version, uint32_t wanted) { if (version >= wanted) return true; LOG_ERR("%s: need interface version %u, but compositor only implements %u", iface, wanted, version); return false; } static void seat_register_idle(struct seat *seat) { struct wayland *wayl = seat->wayl; const struct config *conf = wayl->conf; if ( #if defined(FNOTT_HAVE_IDLE_NOTIFY) wayl->idle_notifier == NULL && #endif wayl->kde_idle_manager == NULL) { /* No idle notification interfaces available (yet) */ return; } for (int i = 0; i < 3; i++) { const struct urgency_config *urg_conf = &conf->by_urgency[i]; if (urg_conf->idle_timeout_secs <= 0) continue; struct idle_timer *timer = &seat->idle_timer[i]; LOG_DBG("registering a new idle timer for urgency level %d: %ds", i, urg_conf->idle_timeout_secs); timer->notif_mgr = wayl->notif_mgr; timer->urgency = i; timer->seat = seat; #if defined(FNOTT_HAVE_IDLE_NOTIFY) if (wayl->idle_notifier != NULL) { /* We prefer the newer ext-idle-notify interface */ if (timer->kde_idle_timeout != NULL) { org_kde_kwin_idle_timeout_release(timer->kde_idle_timeout); timer->kde_idle_timeout = NULL; } assert(timer->kde_idle_timeout == NULL); timer->idle_notification = ext_idle_notifier_v1_get_idle_notification( wayl->idle_notifier, urg_conf->idle_timeout_secs * 1000, seat->wl_seat); ext_idle_notification_v1_add_listener( timer->idle_notification, &idle_notify_listener, timer); } else #endif if (wayl->kde_idle_manager != NULL) { #if defined(FNOTT_HAVE_IDLE_NOTIFY) assert(timer->idle_notification == NULL); #endif timer->kde_idle_timeout = org_kde_kwin_idle_get_idle_timeout( wayl->kde_idle_manager, seat->wl_seat, urg_conf->idle_timeout_secs * 1000); org_kde_kwin_idle_timeout_add_listener( timer->kde_idle_timeout, &kde_idle_listener, timer); } } } static void wayl_register_idle_for_all_seats(struct wayland *wayl) { assert( #if defined(FNOTT_HAVE_IDLE_NOTIFY) wayl->idle_notifier != NULL || #endif wayl->kde_idle_manager != NULL); tll_foreach(wayl->seats, it) seat_register_idle(&it->item); } static void handle_global(void *data, struct wl_registry *registry, uint32_t name, const char *interface, uint32_t version) { LOG_DBG("global: 0x%08x, interface=%s, version=%u", name, interface, version); struct wayland *wayl = data; if (strcmp(interface, wl_compositor_interface.name) == 0) { const uint32_t required = 4; if (!verify_iface_version(interface, version, required)) return; wayl->compositor = wl_registry_bind( wayl->registry, name, &wl_compositor_interface, required); } else if (strcmp(interface, wl_shm_interface.name) == 0) { const uint32_t required = 1; if (!verify_iface_version(interface, version, required)) return; wayl->shm = wl_registry_bind( wayl->registry, name, &wl_shm_interface, required); wl_shm_add_listener(wayl->shm, &shm_listener, wayl); } else if (strcmp(interface, zwlr_layer_shell_v1_interface.name) == 0) { const uint32_t required = 1; if (!verify_iface_version(interface, version, required)) return; wayl->layer_shell = wl_registry_bind( wayl->registry, name, &zwlr_layer_shell_v1_interface, required); } else if (strcmp(interface, wl_output_interface.name) == 0) { const uint32_t required = 3; if (!verify_iface_version(interface, version, required)) return; struct wl_output *output = wl_registry_bind( wayl->registry, name, &wl_output_interface, required); tll_push_back(wayl->monitors, ((struct monitor){ .wayl = wayl, .output = output, .wl_name = name,} )); struct monitor *mon = &tll_back(wayl->monitors); wl_output_add_listener(output, &output_listener, mon); /* * The "output" interface doesn't give us the monitors' * identifiers (e.g. "LVDS-1"). Use the XDG output interface * for that. */ assert(wayl->xdg_output_manager != NULL); if (wayl->xdg_output_manager != NULL) { mon->xdg = zxdg_output_manager_v1_get_xdg_output( wayl->xdg_output_manager, mon->output); zxdg_output_v1_add_listener(mon->xdg, &xdg_output_listener, mon); } } else if (strcmp(interface, zxdg_output_manager_v1_interface.name) == 0) { const uint32_t required = 2; if (!verify_iface_version(interface, version, required)) return; wayl->xdg_output_manager = wl_registry_bind( wayl->registry, name, &zxdg_output_manager_v1_interface, required); } else if (strcmp(interface, wl_seat_interface.name) == 0) { const uint32_t required = 4; if (!verify_iface_version(interface, version, required)) return; struct wl_seat *wl_seat = wl_registry_bind( wayl->registry, name, &wl_seat_interface, required); tll_push_back(wayl->seats, ((struct seat){ .wayl = wayl, .wl_seat = wl_seat, .wl_name = name})); struct seat *seat = &tll_back(wayl->seats); wl_seat_add_listener(wl_seat, &seat_listener, seat); seat_register_idle(seat); } else if (strcmp(interface, org_kde_kwin_idle_interface.name) == 0) { const uint32_t required = 1; if (!verify_iface_version(interface, version, required)) return; wayl->kde_idle_manager = wl_registry_bind(wayl->registry, name, &org_kde_kwin_idle_interface, required); wayl_register_idle_for_all_seats(wayl); } #if defined(FNOTT_HAVE_IDLE_NOTIFY) else if (strcmp(interface, ext_idle_notifier_v1_interface.name) == 0) { const uint32_t required = 1; if (!verify_iface_version(interface, version, required)) return; wayl->idle_notifier = wl_registry_bind(wayl->registry, name, &ext_idle_notifier_v1_interface, required); wayl_register_idle_for_all_seats(wayl); } #endif } static void monitor_destroy(struct monitor *mon) { free(mon->name); if (mon->xdg != NULL) zxdg_output_v1_destroy(mon->xdg); if (mon->output != NULL) wl_output_release(mon->output); free(mon->make); free(mon->model); free(mon->description); } static void handle_global_remove(void *data, struct wl_registry *registry, uint32_t name) { LOG_DBG("global removed: 0x%08x", name); struct wayland *wayl = data; tll_foreach(wayl->monitors, it) { struct monitor *mon = &it->item; if (mon->wl_name != name) continue; LOG_INFO("monitor disabled: %s", mon->name); if (wayl->monitor == mon) wayl->monitor = NULL; notif_mgr_monitor_removed(wayl->notif_mgr, mon); monitor_destroy(mon); tll_remove(wayl->monitors, it); return; } tll_foreach(wayl->seats, it) { struct seat *seat = &it->item; if (seat->wl_name != name) continue; LOG_INFO("seat removed: %s", seat->name); seat_destroy(seat); tll_remove(wayl->seats, it); } LOG_WARN("unknown global removed: 0x%08x", name); } static const struct wl_registry_listener registry_listener = { .global = &handle_global, .global_remove = &handle_global_remove, }; static bool fdm_handler(struct fdm *fdm, int fd, int events, void *data) { struct wayland *wayl = data; int event_count = 0; if (events & EPOLLIN) { if (wl_display_read_events(wayl->display) < 0) { LOG_ERRNO("failed to read events from the Wayland socket"); return false; } while (wl_display_prepare_read(wayl->display) != 0) { if (wl_display_dispatch_pending(wayl->display) < 0) { LOG_ERRNO("failed to dispatch pending Wayland events"); return false; } } } if (events & EPOLLHUP) { LOG_WARN("disconnected from Wayland"); // wl_display_cancel_read(wayl->display); return false; } wl_display_flush(wayl->display); return event_count != -1; } struct wayland * wayl_init(const struct config *conf, struct fdm *fdm, struct notif_mgr *notif_mgr) { struct wayland *wayl = calloc(1, sizeof(*wayl)); wayl->conf = conf; wayl->notif_mgr = notif_mgr; wayl->display = wl_display_connect(NULL); if (wayl->display == NULL) { LOG_ERR("failed to connect to wayland; no compositor running?"); goto err; } wayl->registry = wl_display_get_registry(wayl->display); if (wayl->registry == NULL) { LOG_ERR("failed to get wayland registry"); goto err; } wl_registry_add_listener(wayl->registry, ®istry_listener, wayl); wl_display_roundtrip(wayl->display); if (wayl->compositor == NULL) { LOG_ERR("no compositor"); goto err; } if (wayl->shm == NULL) { LOG_ERR("no shared memory buffers interface"); goto err; } if (wayl->layer_shell == NULL) { LOG_ERR("compositor does not support layer shells"); goto err; } if (( #if defined(FNOTT_HAVE_IDLE_NOTIFY) wayl->idle_notifier == NULL && #endif wayl->kde_idle_manager == NULL) && (conf->by_urgency[0].idle_timeout_secs > 0 || conf->by_urgency[1].idle_timeout_secs > 0 || conf->by_urgency[2].idle_timeout_secs > 0)) { LOG_WARN("compositor does not support idle protocol, ignoring 'idle-timeout' setting"); } wl_display_roundtrip(wayl->display); if (!wayl->have_argb8888) { LOG_ERR("compositor does not support ARGB surfaces"); goto err; } if (tll_length(wayl->monitors) == 0) { LOG_ERR("no outputs found"); goto err; } tll_foreach(wayl->monitors, it) { const struct monitor *mon = &it->item; LOG_INFO( "%s: %dx%d+%dx%d@%dHz %s %.2f\" scale=%d PPI=%dx%d (physical) PPI=%dx%d (logical), DPI=%.2f", mon->name, mon->dim.px_real.width, mon->dim.px_real.height, mon->x, mon->y, (int)round(mon->refresh), mon->model != NULL ? mon->model : mon->description, mon->inch, mon->scale, mon->ppi.real.x, mon->ppi.real.y, mon->ppi.scaled.x, mon->ppi.scaled.y, mon->dpi); } if (wl_display_prepare_read(wayl->display) != 0) { LOG_ERRNO("failed to prepare for reading wayland events"); goto err; } if (!fdm_add(fdm, wl_display_get_fd(wayl->display), EPOLLIN, &fdm_handler, wayl)) { LOG_ERR("failed to register with FDM"); goto err; } wayl->fdm = fdm; return wayl; err: wayl_destroy(wayl); return NULL; } void wayl_destroy(struct wayland *wayl) { if (wayl == NULL) return; if (wayl->fdm != NULL) fdm_del_no_close(wayl->fdm, wl_display_get_fd(wayl->display)); tll_foreach(wayl->monitors, it) monitor_destroy(&it->item); tll_free(wayl->monitors); tll_foreach(wayl->seats, it) seat_destroy(&it->item); tll_free(wayl->seats); #if defined(FNOTT_HAVE_IDLE_NOTIFY) if (wayl->idle_notifier != NULL) ext_idle_notifier_v1_destroy(wayl->idle_notifier); #endif if (wayl->kde_idle_manager != NULL) org_kde_kwin_idle_destroy(wayl->kde_idle_manager); if (wayl->layer_shell != NULL) zwlr_layer_shell_v1_destroy(wayl->layer_shell); if (wayl->xdg_output_manager != NULL) zxdg_output_manager_v1_destroy(wayl->xdg_output_manager); if (wayl->shm != NULL) wl_shm_destroy(wayl->shm); if (wayl->compositor != NULL) wl_compositor_destroy(wayl->compositor); if (wayl->registry != NULL) wl_registry_destroy(wayl->registry); if (wayl->display != NULL) { wayl_flush(wayl); wl_display_disconnect(wayl->display); } free(wayl); } bool wayl_is_idle_for_urgency(const struct wayland *wayl, const enum urgency urgency) { bool idle = true; assert(urgency >= 0 && urgency < 3); tll_foreach(wayl->seats, it) { struct seat *seat = &it->item; idle &= seat->is_idle[urgency]; } return idle; } struct wl_compositor * wayl_compositor(const struct wayland *wayl) { return wayl->compositor; } struct zwlr_layer_shell_v1 * wayl_layer_shell(const struct wayland *wayl) { return wayl->layer_shell; } struct buffer * wayl_get_buffer(const struct wayland *wayl, int width, int height) { return shm_get_buffer(wayl->shm, width, height); } const struct monitor * wayl_preferred_monitor(const struct wayland *wayl) { return wayl->monitor; } const struct monitor * wayl_monitor_get(const struct wayland *wayl, struct wl_output *output) { tll_foreach(wayl->monitors, it) { if (it->item.output == output) return &it->item; } return NULL; } int wayl_guess_scale(const struct wayland *wayl) { if (wayl->monitor != NULL) return wayl->monitor->scale; if (tll_length(wayl->monitors) == 0) return 1; bool all_have_same_scale = true; int last_scale = -1; tll_foreach(wayl->monitors, it) { if (last_scale == -1) last_scale = it->item.scale; else if (last_scale != it->item.scale) { all_have_same_scale = false; break; } } if (all_have_same_scale) { assert(last_scale >= 1); return last_scale; } return 1; } bool wayl_all_monitors_have_scale_one(const struct wayland *wayl) { tll_foreach(wayl->monitors, it) { if (it->item.scale > 1) return false; } return true; } enum fcft_subpixel wayl_guess_subpixel(const struct wayland *wayl) { if (wayl->monitor != NULL) return (enum fcft_subpixel)wayl->monitor->subpixel; if (tll_length(wayl->monitors) == 0) return FCFT_SUBPIXEL_DEFAULT; return (enum fcft_subpixel)tll_front(wayl->monitors).subpixel; } float wayl_dpi_guess(const struct wayland *wayl) { const struct monitor *mon = NULL; if (wayl->monitor != NULL) mon = wayl->monitor; else if (tll_length(wayl->monitors) > 0) mon = &tll_front(wayl->monitors); if (mon != NULL && mon->dpi > 0) return mon->dpi; return 96.; } int wayl_poll_fd(const struct wayland *wayl) { return wl_display_get_fd(wayl->display); } void wayl_flush(struct wayland *wayl) { wl_display_flush(wayl->display); } void wayl_roundtrip(struct wayland *wayl) { wl_display_cancel_read(wayl->display); if (wl_display_roundtrip(wayl->display) < 0) { LOG_ERRNO("failed to roundtrip Wayland display"); return; } while (wl_display_prepare_read(wayl->display) != 0) { if (wl_display_dispatch_pending(wayl->display) < 0) { LOG_ERRNO("failed to dispatch pending Wayland events"); return; } } wl_display_flush(wayl->display); } fnott-1.4.1+ds/wayland.h000066400000000000000000000043271447512336000150740ustar00rootroot00000000000000#pragma once #include #include #include #include "config.h" #include "fdm.h" #include "notification.h" #include "shm.h" struct wayland; struct monitor { struct wayland *wayl; struct wl_output *output; struct zxdg_output_v1 *xdg; uint32_t wl_name; int x; int y; struct { /* Physical size, in mm */ struct { int width; int height; } mm; /* Physical size, in pixels */ struct { int width; int height; } px_real; /* Scaled size, in pixels */ struct { int width; int height; } px_scaled; } dim; struct { /* PPI, based on physical size */ struct { int x; int y; } real; /* PPI, logical, based on scaled size */ struct { int x; int y; } scaled; } ppi; int scale; float dpi; float refresh; enum wl_output_subpixel subpixel; enum wl_output_transform transform; /* From wl_output */ char *make; char *model; /* From xdg_output */ char *name; char *description; float inch; /* e.g. 24" */ }; struct wayland *wayl_init(const struct config *conf, struct fdm *fdm, struct notif_mgr *notif_mgr); void wayl_destroy(struct wayland *wayl); struct wl_compositor *wayl_compositor(const struct wayland *wayl); struct zwlr_layer_shell_v1 *wayl_layer_shell(const struct wayland *wayl); bool wayl_is_idle_for_urgency(const struct wayland *wayl, const enum urgency urgency); struct buffer *wayl_get_buffer(const struct wayland *wayl, int width, int height); const struct monitor *wayl_preferred_monitor(const struct wayland *wayl); const struct monitor *wayl_monitor_get( const struct wayland *wayl, struct wl_output *output); int wayl_guess_scale(const struct wayland *wayl); float wayl_dpi_guess(const struct wayland *wayl); enum fcft_subpixel wayl_guess_subpixel(const struct wayland *wayl); bool wayl_all_monitors_have_scale_one(const struct wayland *wayl); int wayl_poll_fd(const struct wayland *wayl); void wayl_flush(struct wayland *wayl); void wayl_roundtrip(struct wayland *wayl); fnott-1.4.1+ds/xdg.c000066400000000000000000000052441447512336000142110ustar00rootroot00000000000000#include "xdg.h" #include #include #include #include #include #include #include #include #include #include #include #define LOG_MODULE "xdg" #define LOG_ENABLE_DBG 0 #include "log.h" xdg_data_dirs_t xdg_data_dirs(void) { xdg_data_dirs_t ret = tll_init(); const char *xdg_data_home = getenv("XDG_DATA_HOME"); if (xdg_data_home != NULL && xdg_data_home[0] != '\0') { int fd = open(xdg_data_home, O_RDONLY | O_DIRECTORY); if (fd >= 0) { struct xdg_data_dir d = {.fd = fd, .path = strdup(xdg_data_home)}; tll_push_back(ret, d); } } else { static const char *const local = ".local/share"; const struct passwd *pw = getpwuid(getuid()); char *path = malloc(strlen(pw->pw_dir) + 1 + strlen(local) + 1); sprintf(path, "%s/%s", pw->pw_dir, local); int fd = open(path, O_RDONLY | O_DIRECTORY); if (fd >= 0) { struct xdg_data_dir d = {.fd = fd, .path = path}; tll_push_back(ret, d); } else free(path); } const char *_xdg_data_dirs = getenv("XDG_DATA_DIRS"); if (_xdg_data_dirs != NULL) { char *ctx = NULL; char *copy = strdup(_xdg_data_dirs); for (const char *tok = strtok_r(copy, ":", &ctx); tok != NULL; tok = strtok_r(NULL, ":", &ctx)) { int fd = open(tok, O_RDONLY | O_DIRECTORY); if (fd >= 0) { struct xdg_data_dir d = {.fd = fd, .path = strdup(tok)}; tll_push_back(ret, d); } } free(copy); } else { int fd1 = open("/usr/local/share", O_RDONLY | O_DIRECTORY); int fd2 = open("/usr/share", O_RDONLY | O_DIRECTORY); if (fd1 >= 0) { struct xdg_data_dir d = {.fd = fd1, .path = strdup("/usr/local/share")}; tll_push_back(ret, d); } if (fd2 >= 0) { struct xdg_data_dir d = {.fd = fd2, .path = strdup("/usr/share")}; tll_push_back(ret, d);; } } return ret; } void xdg_data_dirs_destroy(xdg_data_dirs_t dirs) { tll_foreach(dirs, it) { close(it->item.fd); free(it->item.path); tll_remove(dirs, it); } } const char * xdg_cache_dir(void) { const char *xdg_cache_home = getenv("XDG_CACHE_HOME"); if (xdg_cache_home != NULL && xdg_cache_home[0] != '\0') return xdg_cache_home; static char path[PATH_MAX]; const struct passwd *pw = getpwuid(getuid()); snprintf(path, sizeof(path), "%s/.cache", pw->pw_dir); return path; } fnott-1.4.1+ds/xdg.h000066400000000000000000000004051447512336000142100ustar00rootroot00000000000000#pragma once #include "tllist.h" struct xdg_data_dir { char *path; int fd; }; typedef tll(struct xdg_data_dir) xdg_data_dirs_t; xdg_data_dirs_t xdg_data_dirs(void); void xdg_data_dirs_destroy(xdg_data_dirs_t dirs); const char *xdg_cache_dir(void);