pax_global_header00006660000000000000000000000064146662510710014522gustar00rootroot0000000000000052 comment=b9ded6027ae287cd44bf93caebc682ef0d7583bc fyi-1.0.4/000077500000000000000000000000001466625107100123135ustar00rootroot00000000000000fyi-1.0.4/CHANGELOG.md000066400000000000000000000021221466625107100141210ustar00rootroot00000000000000# Changelog * [1.0.4](#1-0-4) * [1.0.3](#1-0-3) * [1.0.2](#1-0-2) * [1.0.1](#1-0-1) * [1.0.0](#1-0-0) ## 1.0.4 ### Added * FreeBSD support ### Fixed * Leading space added to the "body" string ([#5][5]). [5]: https://codeberg.org/dnkl/fyi/issues/5 ### Contributors * antenore * Baptiste Daroussin ## 1.0.3 ### Fixed * Missing include, causing compilation errors. * Missing fish completions for `--image-data` and `--image-size` ### Contributors * q66 ## 1.0.2 ### Added * Bash completions * Support for inline image data via `--image-data` and `--image-size`. `--image-data` points to a file containing raw RGBA data, and `--image-size` specifies its dimensions (e.g. `128x128`). The image data is sent to the notification daemon using the `image-data` hint. ### Changed * `image-path` hint is no longer set automatically. This aligns `fyi` with `notify-send`. * Ignore `--category` if set to the empty string. * Ignore empty string `--hint` values. ### Contributors * Leon Henrik Plickat ## 1.0.1 ### Fixed * Git tag issue on codeberg ## 1.0.0 First release fyi-1.0.4/LICENSE000066400000000000000000000020561466625107100133230ustar00rootroot00000000000000MIT License Copyright (c) 2024 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. fyi-1.0.4/PKGBUILD000066400000000000000000000011461466625107100134410ustar00rootroot00000000000000pkgname=fyi pkgver=1.0.4 pkgrel=1 pkgdesc="Command line utility to create desktop notifications" arch=('x86_64' 'aarch64') url=https://codeberg.org/dnkl/fyi license=(mit) makedepends=('meson' 'ninja') depends=('dbus') 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() { export CFLAGS="${CFLAGS} -fno-exceptions" meson --prefix=/usr --buildtype=minsize --wrap-mode=nofallback .. ninja } package() { DESTDIR="${pkgdir}/" ninja install } fyi-1.0.4/README.md000066400000000000000000000057171466625107100136040ustar00rootroot00000000000000# FYI [![Packaging status](https://repology.org/badge/vertical-allrepos/fyi.svg?columns=4)](https://repology.org/project/fyi/versions) FYI (for your information) is a command line utility to send desktop notifications to the user via a notification daemon implementing XDG desktop notifications. It is a almost a `notify-send` clone, with the following differences: * `notify-send` does not implement `--close`. * `notify-send` does not expose activation tokens (needed for window focus/activation) in any meaningful way. It prints it as a debug message when `G_MESSAGES_DEBUG=all`; `fyi` prints it when you use `--print-token`. * `fyi` has consistent syntax in its `--action` and `--hint`options. * `fyi` can print the _reason_ a notification was closed, with `--print-reason`. * `fyi` can query the notification daemon for its name and version information. * `fyi` can query the notification daemon for its capabilities. * `fyi` has shell completions (bash and [fish](https://fishshell.com/)). * `fyi` has a single run-time dependency: [dbus](https://www.freedesktop.org/wiki/Software/dbus/) (the original D-Bus implementation). ## Examples Display a notification: ```sh fyi 'This is the title' this is the notification message ``` Display a notification with a custom app-name (typically displays an icon too): ```sh fyi --app-name firefox notification is this from firefox... ``` Display a notification with a custom app-name and _another_ icon: ```sh fyi --app-name firefox -i chromium notification firefox or chromium... ``` Print notification ID, and wait for notification to close: ```sh fyi --print-id --wait title message ``` prints ``` id=7 ``` Close the notification sent above: ```sh fyi --close 7 ``` or, press `ctrl+c`; this will send `SIGINT` to `fyi`, which will close the notification. Add two actions (then trigger one of them): ```sh fyi --action default:'Click to activate' --action no:Nope title message ``` prints ``` action=default (or no) ``` Print ID, close reason and activation token: ```sh fyi --print-id --print-reason --print-token --action default:'Click to activate' \ title message ``` prints ``` id=5 xdgtoken=24c1d76038357e75ec04f27e30ef46bb action=default reason=dismissed ``` Set a custom image, in addition to the application icon (note: not all notification daemons support showing _both_ an icon and an image, and will select one of them): ```sh fyi --icon firefox --hint string:image-path /path/to/image.png title message ``` Set a custom image in-band. Same limitations to icon vs. image as above. The image file **must** be raw RGBA pixels. I.e. you **cannot** use a plain PNG, or SVG etc. You can use `convert` (from ImageMagick) to convert e.g. a PNG. Since the file contains no metadata, you also need to tell `fyi` the image's dimensions: ```sh # Get image dimensions file /path/to/image.png # Convert to raw RGBA convert /path/to/image.png /path/to/raw.rgba # Notify! fyi --image-data /path/to/raw.rgba --image-size WxH title message ``` fyi-1.0.4/completions/000077500000000000000000000000001466625107100146475ustar00rootroot00000000000000fyi-1.0.4/completions/bash/000077500000000000000000000000001466625107100155645ustar00rootroot00000000000000fyi-1.0.4/completions/bash/fyi000066400000000000000000000005311466625107100162750ustar00rootroot00000000000000 OPTS="\ -a --app-name= \ -i --icon= \ -u --urgency= \ -c --category= \ -A --action= \ -H --hint= \ -r --replaces= \ -t --expire-time= \ --transient \ -C --close= \ -p --print-id \ -R --print-reason \ -T --print-token \ -w --wait \ --image-data= \ --image-size= \ --server-info \ --server-capabilities \ -v --version" complete -W "${OPTS}" fyi fyi-1.0.4/completions/fish/000077500000000000000000000000001466625107100156005ustar00rootroot00000000000000fyi-1.0.4/completions/fish/fyi.fish000066400000000000000000000037451466625107100172530ustar00rootroot00000000000000complete -c fyi -x -s a -l app-name -d "app-name, typically used for the icon, unless --icon is used" complete -c fyi -r -s i -l icon -d "notification icon, either a symbolic name or a file" complete -c fyi -x -s u -l urgency -a "low normal critical" -d "notification urgency" complete -c fyi -x -s c -l category -d "notification category" complete -c fyi -x -s r -l replaces -d "update an existing notification" complete -c fyi -x -s t -l expire-time -d "notification timeout, in milliseconds" complete -c fyi -x -s C -l close -d "close an existing notification" complete -c fyi -x -s A -l action -d "add an action to display (NAME:LABEL)" complete -c fyi -x -s H -l hint -d "add a custom hint (TYPE:NAME:VALUE)" complete -c fyi -l transient -d "show a transient notification" complete -c fyi -s p -l print-id -d "print the notification ID" complete -c fyi -s T -l print-token -d "print the activation token" complete -c fyi -s R -l print-reason -d "print the reason the notification was closed" complete -c fyi -s w -l wait -d "wait for notification to close" complete -c fyi -x -l image-data -d "raw RGBA image data, sets the 'image-data' hint" complete -c fyi -x -l image-size -d "dimensions of the image loaded by --image-data" complete -c fyi -l server-info -d "print server name and version" complete -c fyi -l server-capabilities -d "print server capabilities" complete -c fyi -s v -l version -d "show the version number and quit" complete -c fyi -s h -l help -d "show help message and quit" fyi-1.0.4/completions/meson.build000066400000000000000000000006501466625107100170120ustar00rootroot00000000000000#zsh_install_dir = join_paths(get_option('datadir'), 'zsh', 'site-functions') fish_install_dir = join_paths(get_option('datadir'), 'fish', 'vendor_completions.d') bash_install_dir = join_paths(get_option('datadir'), 'bash-completion', 'completions') #install_data('zsh/_fyi', install_dir: zsh_install_dir) install_data('fish/fyi.fish', install_dir: fish_install_dir) install_data('bash/fyi', install_dir: bash_install_dir) fyi-1.0.4/doc/000077500000000000000000000000001466625107100130605ustar00rootroot00000000000000fyi-1.0.4/doc/fyi.1.scd000066400000000000000000000122111466625107100144760ustar00rootroot00000000000000fyi(1) # NAME fyi - send desktop notifications # SYNOPSIS *fyi* [_OPTION_]... _TITLE_ [_MESSAGE_]++ *fyi --close*=_ID_++ *fyi --server-info*++ *fyi --server-capabilities* # DESCRIPTION *fyi* is a command line utility to send desktop notifications to the user via a notification daemon implementing XDG desktop notifications. It is similar to the well-known *notify-send*(1) utility. Indeed, most of the options are identical. When used without any options, *fyi* sends the notification and immediately exits, without printing anything on stdout. Use *--print-id* to have *fyi* print the daemon assigned notification ID on stdout, in the format: *id=*_ID_ To see why the notification was closed, use *--print-reason*. The reason is printed on stdout, in the format: *reason=*_REASON_ *fyi* can also block until the notification has been closed, using the *--wait* option. Sending SIGINT to the *fyi* process will force-close the notification. *fyi* will also block if the notification has any actions. When an action is triggered, *fyi* prints the name of the action on stdout, in the format: *action=*_NAME_ Some notification daemons can send an "activation token". This is typically done when the user clicks the notification, or triggers the default action. The token can be used to focus (activate) a window. To see the token, use *--print-token*. The token is printed on stdout, in the format: *xdgtoken=*_TOKEN_ # OPTIONS *-a*,*--app-name*=_NAME_ Application name. Notification daemons will either display it as text, or use it to select an icon to show (unless *--icon* is used). Default: _fyi_ *-i*,*--icon*=_ICON_ Icon to display, either as a symbolic icon name (e.g. _firefox_) or a filename. Default: _none_ *-u*,*--urgency*=*low|normal|critical* Notification urgency. Shortcut for *--hint=byte:urgency:0|1|2*. Default: _normal_. *-c*,*--category*=_CATEGORY_ Notification category. Shortcut for *--hint=string:category:CATEGORY*. Default: _none_. *-A*,*--action*=_NAME_:_LABEL_ Defines an action to display (e.g. as a button, or in a list, depending on notification daemon). _LABEL_ is what the notification daemon will display for the user. When the user triggers an action, *fyi* will print the corresponding _NAME_, in the format: *action=*_NAME_ This option can be specified multiple times, to define multiple actions. Using this option implies *--wait*. *-H*,*--hint*=_TYPE_:_NAME_:_VALUE_ Defines a custom hint. How these are interpreted depends on the notification daemon. One common use case is to display a progress bar of some kind; most notification daemon recognizes *int:value:*. Another common hint is *string:x-canonical-private-synchronous:*. Many notification daemons will replace any existing notification with the same _name_. This is similar to *--replaces*, except you do not need a notification ID. *fyi* recognizes the following types: - boolean - byte - int - double - string *-r*,*--replaces*=_ID_ If there is an existing notification with the specified ID, replace it. Otherwise, create a new notification. *-t*,*--expire-time*=_TIME_ The notification will be closed automatically after _TIME_ milliseconds. *--transient* By-pass the server's persistence capability, if any. Shortcut for *--hint=boolean:transient=true*. *-C*,*--close*=_ID_ If there is an existing notification with the specified ID, close it. *-p*,*--print-id* Print the daemon assigned notification ID, in the format *id=*_ID_. *-R*,*--print-reason* Print the reason the notification was closed, in the format: - *reason=expired* - *reason=dismissed* - *reason=force-closed* - *reason=unknown* Using this option implies *--wait*. *-T*,*--print-token* Print the activation token, if any, in the format: *xdgtoken=*_TOKEN_ Some notification daemons send an activation token when the notification is dismissed; either when the notification is clicked, or the default action is invoked. Others will send it regardless of which action was invoked. Some will only send it when an action is invoked, while others will send it when the notification is dismissed, regardless of how. The token can be used to focus (activate, raise) a window. It is not directly useable by *fyi*, but programs using *fyi* as a helper to display notifications can use it. Using this option implies *--wait*. *-w*,*--wait* Wait for the notification to be closed before exiting. If the user triggered an action, the name of the action will be printed (see *-A*,*--action*). This option is implied when the any of the following options are used: - *-A*,*--action* - *-R*,*--print-reason* - *-T*,*--print-token* *--image-data*=_FILE_ Sets the *image-data* hint in the notification, with the raw pixel data from _FILE_. The data is assumed to be raw RGBA data. You must also provide the image size, see *--image-size*. *--image-size=WIDTHxHEIGHT* The dimensions of the image loaded by *--image-data*. *--server-info* Display notification daemon name and version. *--server-capabilities* Display notification daemon capabilities. *-v*,*--version* Show the version number and quit. # SEE ALSO - *notify-send*(1) - *gdbus*(1) fyi-1.0.4/doc/meson.build000066400000000000000000000007471466625107100152320ustar00rootroot00000000000000scdoc_prog = find_program(scdoc.get_variable('scdoc'), native: true) foreach man_src : [{'name': 'fyi', '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: scdoc_prog.full_path(), capture: true, feed: true, install: true, install_dir: join_paths(get_option('mandir'), 'man@0@'.format(section))) endforeach fyi-1.0.4/generate-version.sh000077500000000000000000000030741466625107100161330ustar00rootroot00000000000000#!/bin/sh set -e if [ ${#} -ne 3 ]; then echo "Usage: ${0} " exit 1 fi 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}" extra="" fi major=$(echo "${new_version}" | sed -r 's/([0-9]+)\.([0-9]+)\.([0-9]+).*/\1/') minor=$(echo "${new_version}" | sed -r 's/([0-9]+)\.([0-9]+)\.([0-9]+).*/\2/') patch=$(echo "${new_version}" | sed -r 's/([0-9]+)\.([0-9]+)\.([0-9]+).*/\3/') extra=$(echo "${new_version}" | sed -r 's/([0-9]+)\.([0-9]+)\.([0-9]+)(-([0-9]+-g[a-z0-9]+) .*)?.*/\5/') new_version="#define FYI_VERSION \"${new_version}\" #define FYI_MAJOR ${major} #define FYI_MINOR ${minor} #define FYI_PATCH ${patch} #define FYI_EXTRA \"${extra}\"" 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 fyi-1.0.4/main.c000066400000000000000000001016231466625107100134060ustar00rootroot00000000000000#define _GNU_SOURCE #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "version.h" #define IFACE "org.freedesktop.Notifications" enum urgency { URGENCY_LOW, URGENCY_NORMAL, URGENCY_CRITICAL, }; struct action { const char *name; const char *label; }; struct hint { int type; /* DBUS_TYPE_* */ const char *type_as_string; const char *name; union { uint8_t byte; int32_t integer; uint32_t boolean; double floating; const char *string; } u; }; static volatile sig_atomic_t got_sigint = 0; static void sigint_handler(int signo) { assert(signo == SIGINT); got_sigint = 1; } static void usage(const char *progname) { printf( "Usage: %s [OPTIONS…] TITLE [MESSAGE]\n" " %s --close=ID\n" " %s --server-info\n" " %s --server-capabilities\n\n" "" "Options:\n" " -a,--app-name=NAME app-name, typically used for the icon, unless\n" " --icon is used\n" " -i,--icon=ICON notification icon. Can be either a symbolic name,\n" " or a file (default: none)\n" " --image-data=FILE file content (must be raw RGBA pixel data) is sent\n" " as a image-data hint\n" " --image-size=WIDTHxHEIGHT width and height, in pixels, of --image-data\n" " -u,--urgency=URGENCY notification urgency; low, normal or critical\n" " (default: normal)\n" " -c,--category=CATEGORY notification category (default: none)\n" " -A,--action=NAME:LABEL defines an action to display. Implies --wait. May\n" " be set multiple times. The name of the\n" " triggered action is output to stdout\n" " -H,--hint=TYPE:NAME:VALUE specifies additional hints. Valid types are\n" " boolean, byte, int, double and string\n" " -r,--replaces=ID update an existing notification\n" " -t,--expire-time=TIME_MS notification timeout, in milliseconds (default:\n" " server defined)\n" " --transient show a transient notification. Transient\n" " notifications by-pass the server's persistence\n" " capability, if any.\n" " -C,--close=ID close an existing notification\n" " -p,--print-id print the notification ID on stdout\n" " -R,--print-reason print the reason the notification was closed\n" " -T,--print-token print the activation token, if present\n" " -w,--wait wait for the notification to be closed before\n" " exiting\n" " --server-info display server name and version\n" " --server-capabilities display server capabilities\n" " -v,--version show the version number and quit\n" , progname, progname, progname, progname); } static const char * version_and_features(void) { static char buf[256]; snprintf(buf, sizeof(buf), "version: %s", FYI_VERSION); return buf; } static bool send_close_notification(DBusConnection *conn, uint32_t notification_id) { DBusMessage *msg = dbus_message_new_method_call( IFACE, "/org/freedesktop/Notifications", IFACE, "CloseNotification"); /* Mimic notify-send: close the notification */ DBusMessageIter args; dbus_message_iter_init_append(msg, &args); dbus_message_iter_append_basic(&args, DBUS_TYPE_UINT32, ¬ification_id); dbus_connection_send(conn, msg, 0); dbus_connection_flush(conn); dbus_message_unref(msg); return true; } static bool send_server_information(DBusConnection *conn) { DBusError err = DBUS_ERROR_INIT; bool ret = false; DBusMessage *msg = dbus_message_new_method_call( IFACE, "/org/freedesktop/Notifications", IFACE, "GetServerInformation"); DBusMessage *reply = dbus_connection_send_with_reply_and_block( conn, msg, DBUS_TIMEOUT_USE_DEFAULT, &err); if (dbus_error_is_set(&err)) { fprintf( stderr, "error: failed to get server information: %s\n", err.message); goto out; } const char *name; const char *vendor; const char *version; const char *spec_version; if (!dbus_message_get_args(reply, &err, DBUS_TYPE_STRING, &name, DBUS_TYPE_STRING, &vendor, DBUS_TYPE_STRING, &version, DBUS_TYPE_STRING, &spec_version, DBUS_TYPE_INVALID)) { fprintf(stderr, "error: failed to parse reply: %s\n", err.message); goto out; } printf("name: %s\n", name); printf("vendor: %s\n", vendor); printf("version: %s\n", version); printf("spec-version: %s\n", spec_version); ret = true; out: dbus_message_unref(msg); dbus_message_unref(reply); dbus_error_free(&err); return ret; } static bool send_server_capabilities(DBusConnection *conn) { DBusError err = DBUS_ERROR_INIT; bool ret = false; DBusMessage *msg = dbus_message_new_method_call( IFACE, "/org/freedesktop/Notifications", IFACE, "GetCapabilities"); DBusMessage *reply = dbus_connection_send_with_reply_and_block( conn, msg, DBUS_TIMEOUT_USE_DEFAULT, &err); if (dbus_error_is_set(&err)) { fprintf( stderr, "error: failed to get server capabilities: %s\n", err.message); goto out; } DBusMessageIter vals; dbus_message_iter_init(reply, &vals); if (dbus_message_iter_get_arg_type(&vals) != DBUS_TYPE_ARRAY) { fprintf(stderr, "error: expected an array of strings\n"); goto out; } DBusMessageIter caps; dbus_message_iter_recurse(&vals, &caps); while (dbus_message_iter_get_arg_type(&caps) != DBUS_TYPE_INVALID) { if (dbus_message_iter_get_arg_type(&caps) != DBUS_TYPE_STRING) { fprintf(stderr, "error: non-string capability\n"); goto out; } const char *cap; dbus_message_iter_get_basic(&caps, &cap); printf("%s\n", cap); dbus_message_iter_next(&caps); } dbus_message_iter_next(&vals); assert(dbus_message_iter_get_arg_type(&vals) == DBUS_TYPE_INVALID); ret = true; out: dbus_message_unref(reply); dbus_message_unref(msg); dbus_error_free(&err); return ret; } static bool notification_closed_handler(DBusMessage *signal, uint32_t *id, uint32_t *reason) { DBusError err = DBUS_ERROR_INIT; bool ret = false; if (!dbus_message_get_args(signal, &err, DBUS_TYPE_UINT32, id, DBUS_TYPE_UINT32, reason, DBUS_TYPE_INVALID)) { fprintf( stderr, "error: NotificationClosed: failed to parse signal arguments: %s\n", err.message); goto out; } ret = true; out: dbus_error_free(&err); dbus_message_unref(signal); return ret; } static bool action_invoked_handler(DBusMessage *signal, uint32_t *id, char **action_name) { DBusError err = DBUS_ERROR_INIT; bool ret = false; if (action_name != NULL) *action_name = NULL; const char *action; if (!dbus_message_get_args(signal, &err, DBUS_TYPE_UINT32, id, DBUS_TYPE_STRING, &action, DBUS_TYPE_INVALID)) { fprintf( stderr, "error: ActionInvoked: failed to parse signal arguments: %s\n", err.message); goto out; } if (action_name != NULL) *action_name = strdup(action); ret = true; out: dbus_error_free(&err); dbus_message_unref(signal); return ret; } static bool activation_token_handler(DBusMessage *signal, uint32_t *id, char **token) { DBusError err = DBUS_ERROR_INIT; bool ret = false; if (token != NULL) *token = NULL; const char *tok; if (!dbus_message_get_args(signal, &err, DBUS_TYPE_UINT32, id, DBUS_TYPE_STRING, &tok, DBUS_TYPE_INVALID)) { fprintf( stderr, "error: ActivationToken: failed to parse signal arguments: %s\n", err.message); goto out; } if (token != NULL) *token = strdup(tok); ret = true; out: dbus_error_free(&err); dbus_message_unref(signal); return ret; } static void add_urgency_hint(DBusMessageIter *hints, enum urgency urgency) { static const char *name = "urgency"; DBusMessageIter hint; DBusMessageIter value; dbus_message_iter_open_container(hints, DBUS_TYPE_DICT_ENTRY, 0, &hint); dbus_message_iter_append_basic(&hint, DBUS_TYPE_STRING, &name); dbus_message_iter_open_container(&hint, DBUS_TYPE_VARIANT, DBUS_TYPE_BYTE_AS_STRING, &value); dbus_message_iter_append_basic(&value, DBUS_TYPE_BYTE, &(uint8_t){urgency}); dbus_message_iter_close_container(&hint, &value); dbus_message_iter_close_container(hints, &hint); } static void add_category_hint(DBusMessageIter *hints, const char *category) { static const char *name = "category"; if (category == NULL || category[0] == '\0') return; DBusMessageIter hint; DBusMessageIter value; dbus_message_iter_open_container(hints, DBUS_TYPE_DICT_ENTRY, 0, &hint); dbus_message_iter_append_basic(&hint, DBUS_TYPE_STRING, &name); dbus_message_iter_open_container(&hint, DBUS_TYPE_VARIANT, DBUS_TYPE_STRING_AS_STRING, &value); dbus_message_iter_append_basic(&value, DBUS_TYPE_STRING, &category); dbus_message_iter_close_container(&hint, &value); dbus_message_iter_close_container(hints, &hint); } static void add_transient_hint(DBusMessageIter *hints, bool transient) { static const char *name = "transient"; if (!transient) return; DBusMessageIter hint; DBusMessageIter value; dbus_message_iter_open_container(hints, DBUS_TYPE_DICT_ENTRY, 0, &hint); dbus_message_iter_append_basic(&hint, DBUS_TYPE_STRING, &name); dbus_message_iter_open_container(&hint, DBUS_TYPE_VARIANT, DBUS_TYPE_BOOLEAN_AS_STRING, &value); dbus_message_iter_append_basic(&value, DBUS_TYPE_BOOLEAN, &(dbus_bool_t){transient}); dbus_message_iter_close_container(&hint, &value); dbus_message_iter_close_container(hints, &hint); } static void add_image_data_hint(DBusMessageIter *hints, const uint8_t *image, int32_t width, int32_t height) { static const char *name = "image-data"; if (width == 0 || height == 0) return; DBusMessageIter hint; DBusMessageIter value; dbus_message_iter_open_container(hints, DBUS_TYPE_DICT_ENTRY, 0, &hint); dbus_message_iter_append_basic(&hint, DBUS_TYPE_STRING, &name); dbus_message_iter_open_container( &hint, DBUS_TYPE_VARIANT, DBUS_STRUCT_BEGIN_CHAR_AS_STRING DBUS_TYPE_INT32_AS_STRING DBUS_TYPE_INT32_AS_STRING DBUS_TYPE_INT32_AS_STRING DBUS_TYPE_BOOLEAN_AS_STRING DBUS_TYPE_INT32_AS_STRING DBUS_TYPE_INT32_AS_STRING DBUS_TYPE_ARRAY_AS_STRING DBUS_TYPE_BYTE_AS_STRING DBUS_STRUCT_END_CHAR_AS_STRING, &value); /* We only support RGBA image data */ const int32_t stride = width * 4; DBusMessageIter data; dbus_message_iter_open_container(&value, DBUS_TYPE_STRUCT, NULL, &data); dbus_message_iter_append_basic(&data, DBUS_TYPE_INT32, &width); dbus_message_iter_append_basic(&data, DBUS_TYPE_INT32, &height); dbus_message_iter_append_basic(&data, DBUS_TYPE_INT32, &stride); dbus_message_iter_append_basic(&data, DBUS_TYPE_BOOLEAN, &(dbus_bool_t){true}); dbus_message_iter_append_basic(&data, DBUS_TYPE_INT32, &(int32_t){8}); dbus_message_iter_append_basic(&data, DBUS_TYPE_INT32, &(int32_t){4}); DBusMessageIter pixels; dbus_message_iter_open_container(&data, DBUS_TYPE_ARRAY,DBUS_TYPE_BYTE_AS_STRING, &pixels); for (size_t i = 0; i < height * stride; i++) dbus_message_iter_append_basic(&pixels, DBUS_TYPE_BYTE, &image[i]); dbus_message_iter_close_container(&data, &pixels); dbus_message_iter_close_container(&value, &data); dbus_message_iter_close_container(&hint, &value); dbus_message_iter_close_container(hints, &hint); } static void add_user_hint(DBusMessageIter *hints, const struct hint *h) { DBusMessageIter hint; DBusMessageIter value; dbus_message_iter_open_container(hints, DBUS_TYPE_DICT_ENTRY, 0, &hint); dbus_message_iter_append_basic(&hint, DBUS_TYPE_STRING, &h->name); dbus_message_iter_open_container(&hint, DBUS_TYPE_VARIANT, h->type_as_string, &value); dbus_message_iter_append_basic(&value, h->type, &h->u); dbus_message_iter_close_container(&hint, &value); dbus_message_iter_close_container(hints, &hint); } int main(int argc, char *const *argv) { int ret = EXIT_FAILURE; /* Disable buffering. This ensures e.g. the ID is flushed immediately */ setbuf(stdout, NULL); #define OPT_SERVER_INFO 256 #define OPT_SERVER_CAPABILITIES 257 #define OPT_TRANSIENT 258 #define OPT_IMAGE_DATA 259 #define OPT_IMAGE_SIZE 260 const struct option longopts[] = { {"app-name", required_argument, NULL, 'a'}, {"icon", required_argument, NULL, 'i'}, {"image-data", required_argument, NULL, OPT_IMAGE_DATA}, {"image-size", required_argument, NULL, OPT_IMAGE_SIZE}, {"urgency", required_argument, NULL, 'u'}, {"category", required_argument, NULL, 'c'}, {"action", required_argument, NULL, 'A'}, {"hint", required_argument, NULL, 'H'}, {"replaces", required_argument, NULL, 'r'}, {"expire-time", required_argument, NULL, 't'}, {"close", required_argument, NULL, 'C'}, {"transient", no_argument, NULL, OPT_TRANSIENT}, {"print-id", no_argument, NULL, 'p'}, {"print-reason", no_argument, NULL, 'R'}, {"print-token", no_argument, NULL, 'T'}, {"wait", no_argument, NULL, 'w'}, {"server-info", no_argument, NULL, OPT_SERVER_INFO}, {"server-capabilities", no_argument, NULL, OPT_SERVER_CAPABILITIES}, {"version", no_argument, NULL, 'v'}, {"help", no_argument, NULL, 'h'}, {NULL, no_argument, NULL, 0}, }; const char *progname = argv[0]; const char *app_id = progname; const char *icon = NULL; const char *image_data_file = NULL; const char *category = NULL; uint32_t replaces_id = 0; uint32_t close_id = 0; int32_t expire_time = -1; enum urgency urgency = URGENCY_NORMAL; struct action *actions = NULL; size_t action_count = 0; struct hint *hints = NULL; size_t hint_count = 0; bool print_id = false; bool print_reason = false; bool print_token = false; bool wait = false; bool transient = false; bool server_info = false; bool server_capabilities = false; char *body = NULL; char *icon_uri = NULL; uint8_t *image_data = NULL; int32_t image_width = 0; int32_t image_height = 0; DBusError err = DBUS_ERROR_INIT; DBusMessage *msg = NULL; DBusMessage *reply = NULL; DBusConnection *conn = NULL; while (true) { int c = getopt_long(argc, argv, "+a:i:u:c:A:H:r:t:C:pRTwhv", longopts, NULL); if (c < 0) break; switch (c) { case 'a': app_id = optarg; break; case 'i': icon = optarg; break; case 'c': category = optarg; break; case 'p': print_id = true; break; case 'R': print_reason = true; break; case 'T': print_token = true; break; case 'w': wait = true; break; case OPT_TRANSIENT: transient = true; break; case 'A': { char *split = strchr(optarg, ':'); if (split == NULL) { fprintf(stderr, "error: invalid action: %s\n", optarg); goto out; } *split = '\0'; const char *name = optarg; const char *label = split + 1; actions = realloc(actions, (action_count + 1) * sizeof(actions[0])); actions[action_count++] = (struct action){name, label}; break; } case 'H': { const char *type_str = optarg; const char *name = NULL; const char *value = NULL; char *split = strchr(type_str, ':'); if (split != NULL) { *split = '\0'; name = split + 1; split = strchr(name, ':'); if (split != NULL) { *split = '\0'; value = split + 1; } } if (name == NULL || value == NULL) { fprintf(stderr, "error: invalid hint: %s\n", optarg); goto out; } struct hint hint = {.name = name}; errno = 0; char *end = NULL; if (strcmp(type_str, "boolean") == 0) { hint.type = DBUS_TYPE_BOOLEAN; hint.type_as_string = DBUS_TYPE_BOOLEAN_AS_STRING; hint.u.boolean = strcmp(value, "1") == 0 || strcasecmp(value, "on") == 0 || strcasecmp(value, "true") == 0; } else if (strcmp(type_str, "byte") == 0) { hint.type = DBUS_TYPE_BYTE; hint.type_as_string = DBUS_TYPE_BYTE_AS_STRING; hint.u.byte = strtoul(value, &end, 10); } else if (strcmp(type_str, "int") == 0) { hint.type = DBUS_TYPE_INT32; hint.type_as_string = DBUS_TYPE_INT32_AS_STRING; hint.u.integer = strtol(value, &end, 10); } else if (strcmp(type_str, "double") == 0) { hint.type = DBUS_TYPE_DOUBLE; hint.type_as_string = DBUS_TYPE_DOUBLE_AS_STRING; hint.u.floating = strtod(value, &end); } else if (strcmp(type_str, "string") == 0) { hint.type = DBUS_TYPE_STRING; hint.type_as_string = DBUS_TYPE_STRING_AS_STRING; hint.u.string = value; /* Ignore empty string values */ if (value[0] == '\0') break; } else { fprintf(stderr, "error: invalid hint type: %s\n", type_str); goto out; } if ((hint.type == DBUS_TYPE_BYTE || hint.type == DBUS_TYPE_INT32 || hint.type == DBUS_TYPE_DOUBLE) && (errno != 0 || end == NULL || *end != '\0')) { fprintf(stderr, "error: invalid hint value: %s\n", value); goto out; } hints = realloc(hints, (hint_count + 1) * sizeof(hints[0])); hints[hint_count++] = hint; break; } case 'r': { errno = 0; char *end = NULL; unsigned long id = strtoul(optarg, &end, 10); if (errno == 0 && end != NULL && *end == '\0') replaces_id = (uint32_t)id; else { fprintf(stderr, "error: %s: invalid ID\n", optarg); goto out; } break; } case 't': { errno = 0; char *end = NULL; long timeout = strtol(optarg, &end, 10); if (errno == 0 && end != NULL && *end == '\0') expire_time = (int32_t)timeout; else { fprintf(stderr, "error: %s: invalid expire time\n", optarg); goto out; } break; } case 'u': if (strcmp(optarg, "low") == 0) urgency = URGENCY_LOW; else if (strcmp(optarg, "normal") == 0) urgency = URGENCY_NORMAL; else if (strcmp(optarg, "critical") == 0) urgency = URGENCY_CRITICAL; else { fprintf(stderr, "error: %s: invalid urgency level\n", optarg); goto out; } break; case 'C': { errno = 0; char *end = NULL; unsigned long id = strtoul(optarg, &end, 10); if (errno == 0 && end != NULL && *end == '\0') close_id = (uint32_t)id; else { fprintf(stderr, "error: %s: invalid ID\n", optarg); goto out; } break; } case OPT_IMAGE_DATA: image_data_file = optarg; break; case OPT_IMAGE_SIZE: { errno = 0; char *end = NULL; const unsigned long w = strtoul(optarg, &end, 10); if (errno == 0 && end != NULL && *end == 'x' && *(end + 1) != '\0') { const unsigned long h = strtoul(end + 1, &end, 10); if (errno == 0 && end != NULL && *end == '\0') { image_width = w; image_height = h; } else { fprintf(stderr, "error: %s: invalid image size\n", optarg); goto out; } } else { fprintf(stderr, "error: %s: invalid image size\n", optarg); goto out; } break; } case OPT_SERVER_INFO: server_info = true; break; case OPT_SERVER_CAPABILITIES: server_capabilities = true; break; case 'v': printf("fyi %s\n", version_and_features()); ret = EXIT_SUCCESS; goto out; case 'h': usage(progname); ret = EXIT_SUCCESS; goto out; case '?': ret = EXIT_FAILURE; goto out; } } if (argc > 0) { argc -= optind; argv += optind; } if (argc == 0 && close_id == 0 && !server_info && !server_capabilities) { usage(progname); goto out; } const char *title = argv[0]; for (size_t i = 1, len = 0; i < argc; i++) { const char *word = argv[i]; size_t word_length = strlen(word); bool add_space = i > 1; body = realloc(body, len + add_space + word_length + 1); body[len] = ' '; memcpy(&body[len + add_space], word, word_length); len += add_space + word_length; body[len] = '\0'; } if (body == NULL) body = strdup(""); if (action_count > 0 || print_reason || print_token) { /* Actions implies wait */ wait = true; } /* If 'icon' exists on disk, treat it as a filename, otherwise as a symbolic icon name */ if (icon != NULL) { char *path = realpath(icon, NULL); if (path != NULL) { icon_uri = malloc(strlen("file://") + strlen(path) + 1); strcpy(icon_uri, "file://"); strcat(icon_uri, path); free(path); } } conn = dbus_bus_get(DBUS_BUS_SESSION, &err); if (dbus_error_is_set(&err)) { fprintf(stderr, "error: failed to connect: %s\n", err.message); goto out; } if (server_info) { ret = send_server_information(conn) ? EXIT_SUCCESS : EXIT_FAILURE; goto out; } if (server_capabilities) { ret = send_server_capabilities(conn) ? EXIT_SUCCESS : EXIT_FAILURE; goto out; } if (close_id > 0) { ret = send_close_notification(conn, close_id) ? EXIT_SUCCESS : EXIT_FAILURE; goto out; } if (image_data_file != NULL) { struct stat st; int fd = open(image_data_file, O_RDONLY); if (fd < 0 || fstat(fd, &st) < 0) { fprintf(stderr, "error: %s: failed to open+stat: %s\n", image_data_file, strerror(errno)); if (fd >= 0) close(fd); goto out; } /* 4 bytes per pixel (RGBA) */ size_t expected_size = image_width * image_height * 4; if (st.st_size != expected_size) { fprintf( stderr, "error: %s: file size (%ld) does not match image dimensions (%dx%d)\n", image_data_file, (long)st.st_size, image_width, image_height); close(fd); goto out; } //printf("allocating %lu image data bytes\n", st.st_size); image_data = malloc(st.st_size); if (image_data == NULL || read(fd, image_data, st.st_size) != (ssize_t)st.st_size) { fprintf(stderr, "error: %s: failed to read: %s\n", image_data_file, strerror(errno)); close(fd); goto out; } close(fd); } msg = dbus_message_new_method_call( IFACE, "/org/freedesktop/Notifications", IFACE, "Notify"); const char *icon_arg = icon != NULL ? icon : ""; DBusMessageIter args; dbus_message_iter_init_append(msg, &args); dbus_message_iter_append_basic(&args, DBUS_TYPE_STRING, &app_id); dbus_message_iter_append_basic(&args, DBUS_TYPE_UINT32, &replaces_id); dbus_message_iter_append_basic(&args, DBUS_TYPE_STRING, &icon_arg); dbus_message_iter_append_basic(&args, DBUS_TYPE_STRING, &title); dbus_message_iter_append_basic(&args, DBUS_TYPE_STRING, &body); /* Actions: array of strings. Every even item is the action name, every odd item is the action label */ DBusMessageIter args_actions; dbus_message_iter_open_container( &args, DBUS_TYPE_ARRAY, DBUS_TYPE_STRING_AS_STRING, &args_actions); for (size_t i = 0; i < action_count; i++) { const struct action *a = &actions[i]; dbus_message_iter_append_basic(&args_actions, DBUS_TYPE_STRING, &a->name); dbus_message_iter_append_basic(&args_actions, DBUS_TYPE_STRING, &a->label); } dbus_message_iter_close_container(&args, &args_actions); DBusMessageIter args_hints; dbus_message_iter_open_container( &args, DBUS_TYPE_ARRAY, DBUS_DICT_ENTRY_BEGIN_CHAR_AS_STRING DBUS_TYPE_STRING_AS_STRING DBUS_TYPE_VARIANT_AS_STRING DBUS_DICT_ENTRY_END_CHAR_AS_STRING, &args_hints); { add_urgency_hint(&args_hints, urgency); add_category_hint(&args_hints, category); add_transient_hint(&args_hints, transient); add_image_data_hint(&args_hints, image_data, image_width, image_height); /* User specified hints */ for (size_t i = 0; i < hint_count; i++) add_user_hint(&args_hints, &hints[i]); } dbus_message_iter_close_container(&args, &args_hints); /* Expire timeout */ dbus_message_iter_append_basic(&args, DBUS_TYPE_INT32, &expire_time); /* Sign up for signals *before* we send the notification, to avoid race */ if (wait) { dbus_bus_add_match(conn, "type='signal',interface='" IFACE "'", &err); if (dbus_error_is_set(&err)) { fprintf(stderr, "error: failed to register for notification signals: %s", err.message); goto out; } } /* Send notification, and wait for reply */ reply = dbus_connection_send_with_reply_and_block( conn, msg, DBUS_TIMEOUT_USE_DEFAULT, &err); if (dbus_error_is_set(&err)) { fprintf(stderr, "error: failed to send notification: %s\n", err.message); goto out; } dbus_message_unref(msg); msg = NULL; uint32_t notification_id; if (!dbus_message_get_args(reply, &err, DBUS_TYPE_UINT32, ¬ification_id, DBUS_TYPE_INVALID)) { fprintf(stderr, "error: failed to parse reply: %s\n", err.message); goto out; } dbus_message_unref(reply); reply = NULL; if (print_id) printf("id=%u\n", notification_id); if (!wait) { ret = EXIT_SUCCESS; } else { bool connected = true; struct sigaction act = {.sa_handler = &sigint_handler}; sigemptyset(&act.sa_mask); sigaction(SIGINT, &act, NULL); int dbus_fd = -1; if (!dbus_connection_get_unix_fd(conn, &dbus_fd)) { fprintf(stderr, "error: failed to get dbus socket\n"); goto out; } while (true) { DBusMessage *signal = dbus_connection_pop_message(conn); if (signal == NULL) { /* * No more queued up messages. Need to first wait for * the connection to become readable, then pull in * more messages into the queue. */ if (!connected) break; /* Wait for D-Bus connection to become readable again */ while (true) { struct pollfd fds[] = {{.fd = dbus_fd, .events = POLLIN}}; int poll_ret = poll(fds, 1, -1); if (poll_ret < 0) { if (errno == EINTR) { if (got_sigint) { send_close_notification(conn, notification_id); /* Continue processing messages. * * We'll exit when we get the * NotificationClosed signal. */ } /* Poll again */ continue; } else { fprintf(stderr, "error: failed to poll: %s\n", strerror(errno)); goto out; } } if (fds[0].revents & POLLHUP) { fprintf(stderr, "error: disconnected\n"); connected = false; } /* Messages to read - exit the poll loop */ break; } if (connected && !dbus_connection_read_write(conn, 0)) { fprintf(stderr, "error: disconnected\n"); connected = false; /* Continue popping messages until local queue is empty */ } continue; } if (dbus_message_is_signal(signal, IFACE, "NotificationClosed")) { uint32_t id; uint32_t reason; if (!notification_closed_handler(signal, &id, &reason)) break; if (id == notification_id) { if (print_reason) { switch (reason) { case 1: printf("reason=expired\n"); break; case 2: printf("reason=dismissed\n"); break; case 3: printf("reason=force-closed\n"); break; default: printf("reason=unknown\n"); break; } } ret = EXIT_SUCCESS; break; } } else if (dbus_message_is_signal(signal, IFACE, "ActionInvoked")) { uint32_t id; char *action_name = NULL; if (!action_invoked_handler(signal, &id, &action_name)) break; if (id == notification_id) { printf("action=%s\n", action_name); send_close_notification(conn, notification_id); } free(action_name); } else if (dbus_message_is_signal(signal, IFACE, "ActivationToken")) { uint32_t id; char *token = NULL; if (!activation_token_handler(signal, &id, &token)) break; if (id == notification_id && print_token) printf("xdgtoken=%s\n", token); free(token); } else dbus_message_unref(signal); } } out: if (reply != NULL) dbus_message_unref(reply); if (msg != NULL) dbus_message_unref(msg); if (conn != NULL) dbus_connection_unref(conn); dbus_error_free(&err); free(image_data); free(icon_uri); free(body); free(hints); free(actions); return ret; } fyi-1.0.4/meson.build000066400000000000000000000021421466625107100144540ustar00rootroot00000000000000project('fyi', 'c', version: '1.0.4', license: 'MIT', default_options: [ 'c_std=c11', 'warning_level=1', 'werror=true', 'b_ndebug=if-release']) is_debug_build = get_option('buildtype').startswith('debug') cc = meson.get_compiler('c') add_project_arguments( (is_debug_build ? ['-D_DEBUG'] : [cc.get_supported_arguments('-fno-asynchronous-unwind-tables')]), language: 'c') scdoc = dependency('scdoc', native: true, required: get_option('docs')) if scdoc.found() install_data( 'LICENSE', 'README.md', 'CHANGELOG.md', install_dir: join_paths(get_option('datadir'), 'doc', 'fyi')) subdir('doc') endif 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@']) dbus = dependency('dbus-1') executable('fyi', 'main.c', version, dependencies: [dbus], install: true) subdir('completions') fyi-1.0.4/meson_options.txt000066400000000000000000000002261466625107100157500ustar00rootroot00000000000000option('docs', type: 'feature', description: 'Build and install documentation (man pages, example foot.ini, readme, changelog, license etc).')