diff --git a/pack.csv b/pack.csv index 1ba12aa..d9bd132 100644 --- a/pack.csv +++ b/pack.csv @@ -61,7 +61,7 @@ A,P,bspwm,windowmanagement A,P,sxhkd,Keyboard shortcuts A,P,compton,Required for st to get transparent background (xcomp graphic glitches) A,P,libnotify,notifications -A,P,dunst,Notification Server +A,M,dunst,Notification Server (with centering support) # A,A,polybar,status bar A,A,lemonbar-xft-git,status bar A,A,xtitle,helper to get window titles diff --git a/packages/dunst/.github/ISSUE_TEMPLATE.md b/packages/dunst/.github/ISSUE_TEMPLATE.md new file mode 100644 index 0000000..ea22ec0 --- /dev/null +++ b/packages/dunst/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,24 @@ + + + + + +### Installation info + + + +- Version: `` +- Install type: `` +- Distro and version: ` ` diff --git a/packages/dunst/.gitignore b/packages/dunst/.gitignore new file mode 100644 index 0000000..c3a8c84 --- /dev/null +++ b/packages/dunst/.gitignore @@ -0,0 +1,9 @@ +dunst +*.o +core +vgcore.* +dunst.1 +org.knopwob.dunst.service +dunst.systemd.service +dunstify +test/test diff --git a/packages/dunst/.travis.yml b/packages/dunst/.travis.yml new file mode 100644 index 0000000..ba198d0 --- /dev/null +++ b/packages/dunst/.travis.yml @@ -0,0 +1,27 @@ +addons: + apt: + packages: + - libdbus-1-dev + - libx11-dev + - libxrandr-dev + - libxinerama-dev + - libxss-dev + - libxdg-basedir-dev + - libglib2.0-dev + - libpango1.0-dev + - libcairo2-dev + - libnotify-dev + - libgtk-3-dev +dist: trusty +sudo: false +language: c +script: CFLAGS=-Werror make all dunstify test +compiler: + - gcc + - clang +notifications: + irc: + channels: + - "chat.freenode.net#dunst" + on_success: change + on_failure: always diff --git a/packages/dunst/AUTHORS b/packages/dunst/AUTHORS new file mode 100644 index 0000000..f95fd0c --- /dev/null +++ b/packages/dunst/AUTHORS @@ -0,0 +1,4 @@ +Sascha Kruse (http://github.com/knopwob) + +contributors: +See `git shortlog` for a list of contributors and their contributions diff --git a/packages/dunst/CHANGELOG.md b/packages/dunst/CHANGELOG.md new file mode 100644 index 0000000..ed4cef6 --- /dev/null +++ b/packages/dunst/CHANGELOG.md @@ -0,0 +1,115 @@ +# Dunst changelog + +## Unreleased + +### Added +- `ellipsize` option to control how long lines should be ellipsized when `word_wrap` is set to `false` + +### Fixed +- `new_icon` rule being ignored on notifications that had a raw icon +- Do not replace format strings, which are in notification content +- DBus related memory leaks closed: + On the DBus connection, all hints never got freed. For raw icons, dunst saved them three times. + +## Changed +- transient hints are now handled + An additional rule option (`match_transient` and `set_transient`) is added + to optionally reset the transient setting + +## 1.2.0 - 2017-07-12 + +### Added +- `always_run_script` option to run script even if a notification is suppressed +- Support for more icon file types +- Support for raw icons +- `hide_duplicate_count` option to hide the number of duplicate notifications +- Support for per-urgency frame colours +- `markup` setting for more fine-grained control over how markup is handled +- `history_ignore` rule action to exclude a notification from being added to the history +- Support for setting the dpi value dunst will use for font rendering via the `Xft.dpi` X resource +- Experimental support for per-monitor dpi calculation +- `max_icon_size` option to scale down icons if they exceed a certain size +- Middle click on notifications can be used to trigger actions +- Systemd service file, installed by default +- `%n` format flag for getting progress value without any extra characters + +### Changed +- Text and icons are now centred vertically +- Notifications aren't considered duplicate if urgency or icons differ +- The maximum length of a notification is limited to 5000 characters +- The frame width and color settings were moved to the global section as `frame_width` and `frame_color` respectively +- Dropped Xinerama in favour of RandR, Xinerama can be enabled with the `-force_xinerama` option if needed + +### Deprecated +- `allow_markup` is deprecated with `markup` as its replacement +- The urgency specific command line flags have been deprecated with no replacement, respond to issue #328 on the bug tracker if you depend on them + +### Fixed +- Infinite loop if there are 2 configuration file sections with the same name +- URLs with dashes and underscores in them are now parsed properly +- Many memory leaks +- Category based rules were applied without actually matching +- dmenu command not parsing quoted arguments correctly +- Icon alignment with dynamic width +- Issue when loading configuration files with very long lines +- '\n' is no longer expanded to a newline inside notification text +- Notification window wasn't redrawn if obscured on systems without a compositor +- `ignore_newline` now works regardless of the markup setting +- dmenu process being left as a zombie if no option was selected +- Crash when opening urls parsed from `` tags + +## 1.1.0 - 2014-07-29 +- fix nasty memory leak +- icon support (still work in progress) +- fix issue where keybindings aren't working when numlock is activated + +## 1.0.0 - 2013-04-15 +- use pango/cairo as drawing backend +- make use of pangos ability to parse markup +- support for actions via context menu +- indicator for actions/urls found +- use blocking I/O. No more waking up the CPU multiple times per second to check for new dbus messages + +## 0.5.0 - 2013-01-26 +- new default dunstrc +- frames for window +- trigger scripts on matching notifications +- context menu for urls (using dmenu) +- pause and resume function +- use own code for ini parsing (this removes inih) +- progress hints + +## 0.4.0 - 2012-09-27 +- separator between notifications +- word wrap long lines +- real transparance +- bouncing text (alternative to word_wrap) +- new option for line height +- better multihead support +- don't die when keybindings can't be grabbed +- bugfix: forgetting geometry +- (optional) static configuration + +## 0.3.1 - 2012-08-02 +- fix -mon option + +## 0.3.0 - 2012-07-30 +- full support for Desktop Notification Specification (mandatory parts) +- option to select monitor on which notifications are shown +- follow focus +- oneline mode +- text alignment +- show age of notifications +- sticky history +- filter duplicate messages +- keybinding to close all notifications +- new way to specify keybindings +- cleanup / bugfixes etc. +- added dunst.service + +## 0.2.0 - 2012-06-26 +- introduction of dunstrc +- removed static configuration via config.h +- don't timeout when user is idle +- xft-support +- history (a.k.a. redisplay old notifications) diff --git a/packages/dunst/LICENSE b/packages/dunst/LICENSE new file mode 100644 index 0000000..c7e4862 --- /dev/null +++ b/packages/dunst/LICENSE @@ -0,0 +1,29 @@ +Copyright © 2013, Sascha Kruse and contributors +All rights reserved. + +All files (unless otherwise noted) are licensed under the BSD license: + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + * Neither the name of Sascha Kruse nor the + names of contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY Sascha Kruse ''AS IS'' AND ANY +EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL Sascha Kruse BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/dunst/Makefile b/packages/dunst/Makefile new file mode 100644 index 0000000..e37a978 --- /dev/null +++ b/packages/dunst/Makefile @@ -0,0 +1,105 @@ +# dunst - Notification-daemon +# See LICENSE file for copyright and license details. + +include config.mk + +VERSION := "1.2.0-non-git" +ifneq ($(wildcard ./.git/.),) +VERSION := $(shell git describe --tags) +endif + +LIBS := $(shell pkg-config --libs ${pkg_config_packs}) +INCS := $(shell pkg-config --cflags ${pkg_config_packs}) + +ifneq (clean, $(MAKECMDGOALS)) +ifeq ($(and $(INCS),$(LIBS)),) +$(error "pkg-config failed!") +endif +endif + +CFLAGS += -I. ${INCS} +LDFLAGS+= -L. ${LIBS} + +SRC := $(sort $(shell find src/ -name '*.c')) +OBJ := ${SRC:.c=.o} +TEST_SRC := $(sort $(shell find test/ -name '*.c')) +TEST_OBJ := $(TEST_SRC:.c=.o) + +.PHONY: all debug +all: doc dunst service + +debug: CFLAGS += ${CFLAGS_DEBUG} +debug: LDFLAGS += ${LDFLAGS_DEBUG} +debug: CPPFLAGS += ${CPPFLAGS_DEBUG} +debug: all + +.c.o: + ${CC} -o $@ -c $< ${CFLAGS} + +${OBJ}: config.mk + +dunst: ${OBJ} main.o + ${CC} ${CFLAGS} -o $@ ${OBJ} main.o ${LDFLAGS} + +dunstify: dunstify.o + ${CC} ${CFLAGS} -o $@ dunstify.o ${LDFLAGS} + +.PHONY: test +test: test/test + cd test && ./test + +test/test: ${OBJ} ${TEST_OBJ} + ${CC} ${CFLAGS} -o $@ ${TEST_OBJ} ${OBJ} ${LDFLAGS} + +.PHONY: doc +doc: docs/dunst.1 +docs/dunst.1: docs/dunst.pod + pod2man --name=dunst -c "Dunst Reference" --section=1 --release=${VERSION} $< > $@ + +.PHONY: service +service: + @sed "s|##PREFIX##|$(PREFIX)|" org.knopwob.dunst.service.in > org.knopwob.dunst.service + @sed "s|##PREFIX##|$(PREFIX)|" dunst.systemd.service.in > dunst.systemd.service + +.PHONY: clean clean-dunst clean-dunstify clean-doc clean-tests +clean: clean-dunst clean-dunstify clean-doc clean-tests + +clean-dunst: + rm -f dunst ${OBJ} main.o + rm -f org.knopwob.dunst.service + rm -f dunst.systemd.service + +clean-dunstify: + rm -f dunstify.o + rm -f dunstify + +clean-doc: + rm -f docs/dunst.1 + +clean-tests: + rm -f test/test test/*.o + +.PHONY: install install-dunst install-doc install-service uninstall +install: install-dunst install-doc install-service + +install-dunst: dunst doc + mkdir -p ${DESTDIR}${PREFIX}/bin + install -m755 dunst ${DESTDIR}${PREFIX}/bin + mkdir -p ${DESTDIR}${MANPREFIX}/man1 + install -m644 docs/dunst.1 ${DESTDIR}${MANPREFIX}/man1 + +install-doc: + mkdir -p ${DESTDIR}${PREFIX}/share/dunst + install -m644 dunstrc ${DESTDIR}${PREFIX}/share/dunst + +install-service: service + mkdir -p ${DESTDIR}${PREFIX}/share/dbus-1/services/ + install -m644 org.knopwob.dunst.service ${DESTDIR}${PREFIX}/share/dbus-1/services + install -Dm644 dunst.systemd.service ${DESTDIR}${PREFIX}/lib/systemd/user/dunst.service + +uninstall: + rm -f ${DESTDIR}${PREFIX}/bin/dunst + rm -f ${DESTDIR}${MANPREFIX}/man1/dunst.1 + rm -f ${DESTDIR}${PREFIX}/share/dbus-1/services/org.knopwob.dunst.service + rm -f ${DESTDIR}${PREFIX}/lib/systemd/user/dunst.service + rm -rf ${DESTDIR}${PREFIX}/share/dunst diff --git a/packages/dunst/README.md b/packages/dunst/README.md new file mode 100644 index 0000000..9f65591 --- /dev/null +++ b/packages/dunst/README.md @@ -0,0 +1,51 @@ +[![Build Status](https://travis-ci.org/dunst-project/dunst.svg?branch=master)](https://travis-ci.org/dunst-project/dunst) + +## Dunst + +* [Wiki][wiki] +* [Description](#description) +* [Compiling](#compiling) +* [Copyright](#copyright) + +## Description + +Dunst is a highly configurable and lightweight notification daemon. + + +## Compiling + +Dunst has a number of build dependencies that must be present before attempting configuration. The names are different depending on [distribution](https://github.com/dunst-project/dunst/wiki/Dependencies): + +- dbus +- libxinerama +- libxrandr +- libxss +- libxdg-basedir +- glib +- pango/cairo +- libgtk-3-dev + +Checkout the [wiki][wiki] for more information. + +## Bug reports + +Please use the [issue tracker][issue-tracker] provided by GitHub to send us bug reports or feature requests. You can also join us on the IRC channel `#dunst` on Freenode. + +## Mantainers + +Nikos Tsipinakis + +Jonathan Lusso + +## Author + +written by Sascha Kruse + +## Copyright + +copyright 2013 Sascha Kruse and contributors (see LICENSE for licensing information) + +If you feel that copyrights are violated, please send me an email. + +[issue-tracker]: https://github.com/dunst-project/dunst/issues +[wiki]: https://github.com/dunst-project/dunst/wiki diff --git a/packages/dunst/RELEASE_NOTES b/packages/dunst/RELEASE_NOTES new file mode 100644 index 0000000..72879a1 --- /dev/null +++ b/packages/dunst/RELEASE_NOTES @@ -0,0 +1,146 @@ +=================================================================================== +Release Notes For v1.2.0 +=================================================================================== + +After about 3 years of inactivity, dunst is back under active development. + +Version 1.2 is supposed to be fully backwards compatible with 1.1 but due to +the number of changes and the time since the last release there may be some +overlooked breakages. If one is found it should be reported to the bug tracker. + +For users: + +* Markup + The `allow_markup` setting has been deprecated in favour of `markup` which + is a multi-value setting that can be used to control much more accurately + how markup in notifications should be handled. Currently it only supports + `no`, `strip` and `full` as values but it is planned to be expanded soon. + + To preserve backwards compatibility, `allow_markup` is still supported but + users are encouraged to update their configuration files since it will be + removed after a few major releases. + +* DPI handling + The DPI value used is now retrieved from the `Xft.dpi` X resource if + available. If not, the default value 96 will be used. + + Additionally, as an experiment a per-monitor dpi setting, which tries to + calculate an acceptable dpi values for each monitor, has been added to the + experimental section of the configuration file. + +* RandR and Xinerama + Dunst switched from using the Xinerama extension to provide multi-monitor + support to using the more modern RandR extension. While this change won't + affect the majority of users, some legacy drivers do not support RandR. In + that case, the `force_xinerama` option was added as a way to fall back to + the old method. + + The downside of forcing Xinerama to be used is that dunst won't be able to + detect when a monitor is added or removed meaning that follow mode might + break if the screen layout changes. + +* Frame settings + All the settings in the frame section of the configuration file have been + deprecated and have been moved into the global section. The `color` and `size` + settings became `frame_color` and `frame_size` respectively. As with + `allow_markup`, the old format still works but it'll be removed in one of the + next major releases. + +* Deprecation of urgency-specific command line flags + The urgency specific command line flags (`-li, -ni, -ci, -lf, -nf, -cf, -lb, + -nb, -cb, -lfr, -nfr, -cfr, -lto, -nto, -cto`) have been deprecated with no + plan for a replacement. If you rely on them please respond to issue #328 on + the bug tracker with your use case. + +For maintainers: + +* The project homepage has been changed to https://dunst-project.org +* The main repository has been changed to https://github.com/dunst-project/dunst + +* Dependency changes: + - Dependency on libraries that were unused in the code but were mentioned as + dependencies has been dropped. Dunst no longer depends on: libfreetype, + libxft and libxext. + - Added dependency on libxradnr and libgtk2.0. + +For a full list of changes see CHANGELOG.md. + +=================================================================================== +Release Notes For v1.0.0 +=================================================================================== + +PACKAGE MAINTAINERS: + There are new dependencies introduced with this version: + *glib + *pango/cairo + +* The drawing backend was moved from Xlib to Cairo/Pango. + This change requires some user intervention since Pango + uses different font strings. For example "Monospace-12" + becomes "Monospace 12". Font sizes might also get interpreted + differently. + +* Markup + Markup within the notification can be interpreted by pango. + Examples are italic and bold. If the Markup + can't be parsed by pango, the tags are stripped from the + notification and the text is displayed as plain text. An error + message gets printed to stdout describing why the markup could + not be parsed. + + To make use of markup the option allow_markup must be set in dunstrc. + If this option is not set, dunst falls back to the old behaviour + (stripping all tags from the text and display plain text). + +* Actions are now supported. + If a client adds an action to a notification this gets indicated + by an (A) infront of the notification. In this case the + context menu shortcut can be used to invoke this action. + +* Indicator for URLs. + If dunst detects an URL within the notification this gets indicated + by an (U) infront of the notification. As with actions the URL can + be opened with the context menu shortcut. (This requires the browser + option to be set in the dunstrc). + +* dunstify ( a drop-in replacement for notify-send) + Since notify-send lacks some features I need to for testing, I've + written dunstify. It supports the same option as notify-send + The + abillity to print the id of the notification to stdout and to replace + or close existing notifications. + + example: + id=$(dunstify -p "Replace" "this should get replaced after the sleep") + sleep 5 + dunstify -r $id "replacement" + + see dunstify --help for additional info. + + Since dunstify depends on non-public parts of libnotify it may break + on every other libnotify version than that version that I use. + Because of this it does not get build and installed by default. + It can be build with "make dunstify". An installation target does + not exist. + + Please don't open bug reports when dunstify doesn't work with your + version of libnotify + +=================================================================================== +Release Notes For v0.4.0 +=================================================================================== + +Since dunst has lost its ability to show notifications independend of +dbus/libnotify a long time ago I think it is time that the describtion reflects +that. Even though this breaks the acronym. So if you're a packager please update +the package description to read something like: + +short: +"Dunst - a dmenu-ish notification-daemon" + +long: +"Dunst is a highly configurable and lightweight notification-daemon" + +Release Tarballs are now available at: +http://www.knopwob.org/public/dunst-release/ + +For more information have a look at the CHANGELOG and the new options in dunstrc. diff --git a/packages/dunst/config.h b/packages/dunst/config.h new file mode 100644 index 0000000..ec85694 --- /dev/null +++ b/packages/dunst/config.h @@ -0,0 +1,148 @@ +/* see example dunstrc for additional explanations about these options */ + +settings_t defaults = { + +.font = "-*-terminus-medium-r-*-*-16-*-*-*-*-*-*-*", +.markup = MARKUP_NO, +.normbgcolor = "#1793D1", +.normfgcolor = "#DDDDDD", +.critbgcolor = "#ffaaaa", +.critfgcolor = "#000000", +.lowbgcolor = "#aaaaff", +.lowfgcolor = "#000000", +.format = "%s %b", /* default format */ + +.timeouts = { 10*G_USEC_PER_SEC, 10*G_USEC_PER_SEC, 0 }, /* low, normal, critical */ +.icons = { "dialog-information", "dialog-information", "dialog-warning" }, /* low, normal, critical */ + +.transparency = 0, /* transparency */ +.geom = "0x0", /* geometry */ +.title = "Dunst", /* the title of dunst notification windows */ +.class = "Dunst", /* the class of dunst notification windows */ +.shrink = false, /* shrinking */ +.sort = true, /* sort messages by urgency */ +.indicate_hidden = true, /* show count of hidden messages */ +.idle_threshold = 0, /* don't timeout notifications when idle for x seconds */ +.show_age_threshold = -1, /* show age of notification, when notification is older than x seconds */ +.align = left, /* text alignment [left/center/right] */ +.sticky_history = true, +.history_length = 20, /* max amount of notifications kept in history */ +.show_indicators = true, +.word_wrap = false, +.ellipsize = middle, +.ignore_newline = false, +.line_height = 0, /* if line height < font height, it will be raised to font height */ +.notification_height = 0, /* if notification height < font height and padding, it will be raised */ + +.separator_height = 2, /* height of the separator line between two notifications */ +.padding = 0, +.h_padding = 0, /* horizontal padding */ +.sep_color = AUTO, /* AUTO, FOREGROUND, FRAME, CUSTOM */ +.sep_custom_color_str = NULL,/* custom color if sep_color is set to CUSTOM */ + +.frame_width = 0, +.frame_color = "#888888", + +/* show a notification on startup + * This is mainly for crash detection since dbus restarts dunst + * automatically after a crash, so crashes might get unnotices otherwise + * */ +.startup_notification = false, + +/* monitor to display notifications on */ +.monitor = 0, + +/* path to dmenu */ +.dmenu = "/usr/bin/dmenu", + +.browser = "/usr/bin/firefox", + +.max_icon_size = 0, + +/* paths to default icons */ +.icon_path = "/usr/share/icons/gnome/16x16/status/:/usr/share/icons/gnome/16x16/devices/", + + +/* follow focus to different monitor and display notifications there? + * possible values: + * FOLLOW_NONE + * FOLLOW_MOUSE + * FOLLOW_KEYBOARD + * + * everything else than FOLLOW_NONE overrides 'monitor' + */ +.f_mode = FOLLOW_NONE, + +/* keyboard shortcuts + * use for example "ctrl+shift+space" + * use "none" to disable + */ +.close_ks = {.str = "none", + .code = 0,.sym = NoSymbol,.is_valid = false +}, /* ignore this */ + +.close_all_ks = {.str = "none", + .code = 0,.sym = NoSymbol,.is_valid = false +}, /* ignore this */ + +.history_ks = {.str = "none", + .code = 0,.sym = NoSymbol,.is_valid = false +}, /* ignore this */ + +.context_ks = {.str = "none", + .code = 0,.sym = NoSymbol,.is_valid = false +}, /* ignore this */ + +}; + +rule_t default_rules[] = { + /* name can be any unique string. It is used to identify + * the rule in dunstrc to override it there + */ + + /* an empty rule with no effect */ + { + .name = "empty", + .appname = NULL, + .summary = NULL, + .body = NULL, + .icon = NULL, + .category = NULL, + .msg_urgency = -1, + .timeout = -1, + .urgency = -1, + .markup = MARKUP_NULL, + .history_ignore = 1, + .match_transient = 1, + .set_transient = -1, + .new_icon = NULL, + .fg = NULL, + .bg = NULL, + .format = NULL, + .script = NULL, + }, + + /* ignore transient hints in history by default */ + { + .name = "ignore_transient_in_history", + .appname = NULL, + .summary = NULL, + .body = NULL, + .icon = NULL, + .category = NULL, + .msg_urgency = -1, + .timeout = -1, + .urgency = -1, + .markup = MARKUP_NULL, + .history_ignore = 1, + .match_transient = 1, + .set_transient = -1, + .new_icon = NULL, + .fg = NULL, + .bg = NULL, + .format = NULL, + .script = NULL, + }, +}; + +/* vim: set tabstop=8 shiftwidth=8 expandtab textwidth=0: */ diff --git a/packages/dunst/config.mk b/packages/dunst/config.mk new file mode 100644 index 0000000..7b26a48 --- /dev/null +++ b/packages/dunst/config.mk @@ -0,0 +1,38 @@ +# paths +PREFIX ?= /usr/local +MANPREFIX = ${PREFIX}/share/man + +# uncomment to disable parsing of dunstrc +# or use "CFLAGS=-DSTATIC_CONFIG make" to build +#STATIC= -DSTATIC_CONFIG # Warning: This is deprecated behavior + +# flags +CPPFLAGS += -D_DEFAULT_SOURCE -DVERSION=\"${VERSION}\" +CFLAGS += -g --std=gnu99 -pedantic -Wall -Wno-overlength-strings -Os ${STATIC} ${CPPFLAGS} +LDFLAGS += -lm -L${X11LIB} + +CPPFLAGS_DEBUG := -DDEBUG_BUILD +CFLAGS_DEBUG := -O0 +LDFLAGS_DEBUG := + +pkg_config_packs := dbus-1 \ + gio-2.0 \ + gdk-3.0 \ + "glib-2.0 >= 2.36" \ + pangocairo \ + x11 \ + xinerama \ + "xrandr >= 1.5" \ + xscrnsaver + +# check if we need libxdg-basedir +ifeq (,$(findstring STATIC_CONFIG,$(CFLAGS))) + pkg_config_packs += libxdg-basedir +else +$(warning STATIC_CONFIG is deprecated behavior. It will get removed in future releases) +endif + +# dunstify also needs libnotify +ifneq (,$(findstring dunstify,${MAKECMDGOALS})) + pkg_config_packs += libnotify +endif diff --git a/packages/dunst/contrib/dunst_espeak.sh b/packages/dunst/contrib/dunst_espeak.sh new file mode 100755 index 0000000..4d88c01 --- /dev/null +++ b/packages/dunst/contrib/dunst_espeak.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +summary="$2" +body="$3" + +echo "$summary $body" | espeak diff --git a/packages/dunst/docs/dunst.pod b/packages/dunst/docs/dunst.pod new file mode 100644 index 0000000..0973fa8 --- /dev/null +++ b/packages/dunst/docs/dunst.pod @@ -0,0 +1,720 @@ +=head1 NAME + +dunst - A customizable and lightweight notification-daemon + +=head1 SYNOPSIS + +dunst [-conf file] [-font font] [-geometry geom] [-format fmt] [-follow mode] [-monitor n] [-history_length n] ... + +=head1 DESCRIPTION + +Dunst is a highly configurable and lightweight notification daemon. + +=head1 COMMAND LINE OPTIONS + +=over 4 + +=item B<-h/--help> + +List all command line flags + +=item B<-conf/-config file> + +Use alternative config file. + +=item B<-v/--version> + +Print version information. + +=item B<-print> + +Print notifications to stdout. This might be useful for logging, setting up +rules or using the output in other scripts. + +=back + +=head1 CONFIGURATION + +An example configuration file is included (usually /usr/share/dunst/dunstrc). +To change the configuration, copy this file to ~/.config/dunst/dunstrc and edit +it accordingly. + +The configuration is divided into sections in an ini-like format. The 'global' +section contains most general settings while the 'shortcuts' sections contains +all keyboard configuration and the 'experimental' section all the features that +have not yet been tested thoroughly. + +Any section that is not one of the above is assumed to be a rule, see RULES for +more details. + +For backwards compatibility reasons the section name 'frame' is considered bound +and can't be used as a rule. + +=head2 Command line + +Each configuration option in the global section can be overridden from the +command line by adding a single dash in front of it's name. +For example the font option can be overridden by running + + $ dunst -font "LiberationSans Mono 4" + +Configuration options that take boolean values can only currently be set to +"true" through the command line via the same method. e.g. + + $ dunst -shrink + +This is a known limitation of the way command line parameters are parsed and +will be changed in the future. + +Available settings per section: + +=head2 Global section + +=over 4 + +=item B (default: 0) + +Specifies on which monitor the notifications should be displayed in, count +starts at 0. See the B setting. + +=item B (values: [none/mouse/keyboard] default: none) + +Defines where the notifications should be placed in a multi-monitor setup. All +values except I override the B setting. + +=over 4 + +=item B + +The notifications will be placed on the monitor specified by the B +setting. + +=item B + +The notifications will be placed on the monitor that the mouse is currently in. + +=item B + +The notifications will be placed on the monitor that contains the window with +keyboard focus. + +=back + +=item B (format: [{width}][x{height}][+/-{x}[+/-{y}]], default: "0x0+0-0") + +The geometry of the window the notifications will be displayed in. + +=over 4 + +=item B + +The width of the notification window in pixels. A negative value sets the width +to the screen width B. If the width is +omitted then the window expands to cover the whole screen. If it's 0 the window +expands to the width of the longest message being displayed. + +=item B + +The number of notifications that can appear at one time. When this +limit is reached any additional notifications will be queued and displayed when +the currently displayed ones either time out or are manually dismissed. If +B is true, then the specified limit is reduced by 1 and the +last notification is a message informing how many hidden notifications are +waiting to be displayed. See the B entry for more information. + +The physical(pixel) height of the notifications vary depending on the number of +lines that need to be displayed. + +See B for changing the physical height. + +=item B + +Respectively the horizontal and vertical offset in pixels from the corner +of the screen that the notification should be drawn at. For the horizontal(x) +offset, a positive value is measured from the left of the screen while a +negative one from the right. For the vertical(y) offset, a positive value is +measured from the top while a negative from the bottom. + +It's important to note that the positive and negative sign B affect the +position even if the offset is 0. For example, a horizontal offset of +0 puts +the notification on the left border of the screen while a horizontal offset of +-0 at the right border. The same goes for the vertical offset. + +=back + +=item B (values: [true/false], default: true) + +If this is set to true, a notification indicating how many notifications are +not being displayed due to the notification limit (see B) will be +shown B. + +Meaning that if this is enabled the number of visible notifications will be 1 +less than what is specified in geometry, the last slot will be taken by the +hidden count. + +=item B (values: [true/false], default: false) + +Shrink window if it's smaller than the width. Will be ignored if width is 0. + +This is used mainly in order to have the shrinking benefit of dynamic width (see +geometry) while also having an upper bound on how long a notification can get +before wrapping. + +=item B (default: 0) + +A 0-100 range on how transparent the notification window should be, with 0 +being fully opaque and 100 invisible. + +This setting will only work if a compositor is running. + +=item B (default: 0) + +The minimum height of the notification window in pixels. If the text and +padding cannot fit in within the height specified by this value, the height +will be increased as needed. + +=item B (default: 2) + +The height in pixels of the separator between notifications, if set to 0 there +will be no separating line between notifications. + +=item B (default: 0) + +The distance in pixels from the content to the separator/border of the window +in the vertical axis + +=item B (default: 0) + +The distance in pixels from the content to the border of the window +in the horizontal axis + +=item B (default: 0) + +Defines width in pixels of frame around the notification window. Set to 0 to +disable. + +=item B (default: #888888) + +Defines color of the frame around the notification window. See COLORS. + +=item B (values: [auto/foreground/frame/#RRGGBB] default: auto) + +Sets the color of the separator line between two notifications. + +=over 4 + +=item B + +Dunst tries to find a color that fits the rest of the notification color +scheme automatically. + +=item B + +The color will be set to the same as the foreground color of the topmost +notification that's being separated. + +=item B + +The color will be set to the frame color of the notification with the highest +urgency between the 2 notifications that are being separated. + +=item B + +Any other value is interpreted as a color, see COLORS + +=back + +=item B (values: [true/false], default: true) + +If set to true, display notifications with higher urgency above the others. + +=item B (default: 0) + +Don't timeout notifications if user is idle longer than this time. +See TIME FORMAT for valid times. + +Set to 0 to disable. + +Transient notifications will ignore this setting and timeout anyway. +Use a rule overwriting with 'set_transient = no' to disable this behavior. + +=item B (default: "Monospace 8") + +Defines the font or font set used. Optionally set the size as a decimal number +after the font name and space. +Multiple font options can be separated with commas. + +This options is parsed as a Pango font description. + +=item B (default: 0) + +The amount of extra spacing between text lines in pixels. Set to 0 to +disable. + +=item B (values: [full/strip/no], default: no) + +Defines how markup in notifications is handled. + +It's important to note that markup in the format option will be parsed +regardless of what this is set to. + +Possible values: + +=over 4 + +=item B + +Allow a small subset of html markup in notifications + + bold + italic + strikethrough + underline + +For a complete reference see + + +=item B + +This setting is provided for compatibility with some broken +clients that send markup even though it's not enabled on the +server. + +Dunst will try to strip the markup but the parsing is simplistic so using this +option outside of matching rules for specific applications B. + +See RULES + +=item B + +Disable markup parsing, incoming notifications will be treated as +plain text. Dunst will not advertise that it can parse markup if this is set as +a global setting. + +=back + +=item B (default: "%s %b") + +Specifies how the various attributes of the notification should be formatted on +the notification window. + +Regardless of the status of the B setting, any markup tags that are +present in the format will be parsed. Note that because of that, if a literal +ampersand (&) is needed it needs to be escaped as '&' + +If '\n' is present anywhere in the format, it will be replaced with +a literal newline. + +If any of the following strings are present, they will be replaced with the +equivalent notification attribute. + +=over 4 + +=item B<%a> appname + +=item B<%s> summary + +=item B<%b> body + +=item B<%i> iconname (including its path) + +=item B<%I> iconname (without its path) + +=item B<%p> progress value ([ 0%] to [100%]) + +=item B<%n> progress value without any extra characters + +=item B<%%> Literal % + +=back + +If any of these exists in the format but hasn't been specified in the +notification (e.g. no icon has been set), the placeholders will simply be +removed from the format. + +=item B (values: [left/center/right], default: left) + +Defines how the text should be aligned within the notification. + +=item B (default: -1) + +Show age of message if message is older than this time. +See TIME FORMAT for valid times. + +Set to -1 to disable. + +=item B (values: [true/false], default: false) + +Specifies how very long lines should be handled + +If it's set to false, long lines will be truncated an ellipsised. + +If it's set to true, long lines will be broken into multiple lines expanding +the notification window height as necessary for them to fit. + +=item B (values: [start/middle/end], default: middle) + +If word_wrap is set to false, specifies where truncated lines should be +ellipsized. + +=item B (values: [true/false], default: false) + +If set to true, replace newline characters in notifications with whitespace. + +=item B (values: [true/false], default: true) + +If set to true, duplicate notifications will be stacked together instead of +being displayed separately. + +Two notifications are considered duplicate if the name of the program that sent +it, summary, body, icon and urgency are all identical. + +=item B (values: [true/false], default: false) + +Hide the count of stacked duplicate notifications. + +=item B (values: [true/false], default: true) + +Show an indicator if a notification contains actions and/or open-able URLs. See +ACTIONS below for further details. + +=item B (values: [left/right/off], default: off) + +Defines the position of the icon in the notification window. Setting it to off +disables icons. + +=item B (default: 0) + +Defines the maximum size in pixels for the icons. +If the icon is smaller than the specified value it won't be affected. +If it's larger then it will be scaled down so that the larger axis is equivalent +to the specified size. + +Set to 0 to disable icon scaling. (default) + +If B is set to off, this setting is ignored. + +=item B (default: "/usr/share/icons/gnome/16x16/status/:/usr/share/icons/gnome/16x16/devices/") + +Can be set to a colon-separated list of paths to search for icons to use with +notifications. + +Dunst doesn't currently do any type of icon lookup outside of these +directories. + +=item B (values: [true/false], default: true) + +If set to true, notifications that have been recalled from history will not +time out automatically. + +=item B (default: 20) + +Maximum number of notifications that will be kept in history. After that limit +is reached, older notifications will be deleted once a new one arrives. See +HISTORY. + +=item B (default: "/usr/bin/dmenu") + +The command that will be run when opening the context menu. Should be either +a dmenu command or a dmenu-compatible menu. + +=item B (default: "/usr/bin/firefox") + +The command that will be run when opening a URL. The URL to be opened will be +appended to the end of the value of this setting. + +=item B (values: [true/false] default: true] + +Always run rule-defined scripts, even if the notification is suppressed with +format = "". See SCRIPTING. + +=item B (default: "Dunst") + +Defines the title of notification windows spawned by dunst. (_NET_WM_NAME +property). There should be no need to modify this setting for regular use. + +=item B<class> (default: "Dunst") + +Defines the class of notification windows spawned by dunst. (First part of +WM_CLASS). There should be no need to modify this setting for regular use. + +=item B<startup_notification> (values: [true/false], default: false) + +Display a notification on startup. This is usually used for debugging and there +shouldn't be any need to use this option. + +=item B<force_xinerama> (values: [true/false], default: false) + +Use the Xinerama extension instead of RandR for multi-monitor support. This +setting is provided for compatibility with older nVidia drivers that do not +support RandR and using it on systems that support RandR is highly discouraged. + +By enabling this setting dunst will not be able to detect when a monitor is +connected or disconnected which might break follow mode if the screen layout +changes. + +=back + +=head2 Shortcut section + +Keyboard shortcuts are defined in the following format: "Modifier+key" where the +modifier is one of ctrl,mod1,mod2,mod3,mod4 and key is any keyboard key. + +=over 4 + +=item B<close> + +B<command line flag>: -key <key> + +Specifies the keyboard shortcut for closing a notification. + +=item B<close_all> + +B<command line flag>: -all_key <key> + +Specifies the keyboard shortcut for closing all currently displayed notifications. + +=item B<history> + +B<command line flag>: -history_key <key> + +Specifies the keyboard shortcut for recalling a single notification from history. + +=item B<context> + +B<command line flag>: -context_key <key> + +Specifies the keyboard shortcut that opens the context menu. + +=back + +=head2 Urgency sections + +The urgency sections work in a similar way to rules and can be used to specify +attributes for the different urgency levels of notifications (low, normal, +critical). Currently only the background, foreground, timeout, frame_color and +icon attributes can be modified. + +The urgency sections are urgency_low, urgency_normal, urgency_critical for low, +normal and critical urgency respectively. + +See the example configuration file for examples. + +Additionally, you can override these settings via the following command line +flags: + +Please note these flags may be removed in the future. See issue #328 in the bug +tracker for discussions (See REPORTING BUGS). + +=over 4 + +=item B<-li/ni/ci icon> + +Defines the icon for low, normal and critical notifications respectively. + +Where I<icon> is a path to an image file containing the icon. + +=item B<-lf/nf/cf color> + +Defines the foreground color for low, normal and critical notifications respectively. + +See COLORS for the value format. + +=item B<-lb/nb/cb color> + +Defines the background color for low, normal and critical notifications respectively. + +See COLORS for the value format. + +=item B<-lfr/nfr/cfr color> + +Defines the frame color for low, normal and critical notifications respectively. + +See COLORS for more information + +=item B<-lto/nto/cto secs> + +Defines the timeout time for low, normal and critical notifications +respectively. +See TIME FORMAT for valid times. + +=back + +=head1 HISTORY + +Dunst saves a number of notifications (specified by B<history_length>) in memory. +These notifications can be recalled (i.e. redesiplayed) by pressing the +B<history_key> (see the shortcuts section), whether these notifications will +time out like if they have been just send depends on the value of the +B<sticky_history> setting. + +Past notifications are redisplayed in a first-in-last-out order, meaning that +pressing the history key once will bring up the most recent notification that +had been closed/timed out. + +=head1 RULES + +Rules allow the conditional modification of notifications. They are defined by +creating a section in the configuration file that has any name that is not +already used internally (i.e. any name other than 'global', 'experimental', +'frame', 'shortcuts', 'urgency_low', 'urgency_normal' and 'urgency_critical'). + +There are 2 parts in configuring a rule: Defining the filters that control when +a rule should apply and then the actions that should be taken when the rule is +matched. + +=over 4 + +=item B<filtering> + +Notifications can be matched for any of the following attributes: appname, +summary, body, icon, category, match_transient and msg_urgency where each is +the respective notification attribute to be matched and 'msg_urgency' is the +urgency of the notification, it is named so to not conflict with trying to +modify the urgency. + +To define a matching rule simply assign the specified value to the value that +should be matched, for example: + + appname="notify-send" + +Matches only messages that were send via notify-send. If multiple filter +expressions are present, all of them have to match for the rule to be applied +(logical AND). + +Shell-like globing is supported. + +=item B<modifying> + +The following attributes can be overridden: timeout, urgency, foreground, +background, new_icon, set_transient, format where, as with the filtering attributes, +each one corresponds to the respective notification attribute to be modified. + +As with filtering, to make a rule modify an attribute simply assign it in the +rule definition. + +If the format is set to an empty string, the notification will not be +suppressed. + +=back + +=head2 SCRIPTING + +Within rules you can specify a script to be run every time the rule is matched +by assigning the 'script' option to the name of the script to be run. + +When the script is called details of the notification that triggered it will be +passed via command line parameters in the following order: appname, summary, +body, icon, urgency. + +Where icon is the absolute path to the icon file if there is one and urgency is +one of "LOW", "NORMAL" or "CRITICAL". + +If the notification is suppressed, the script will not be run unless +B<always_run_scripts> is set to true. + +If '~/' occurs at the beginning of the script parameter, it will get replaced by the +users' home directory. If the value is not an absolute path, the directories in the +PATH variable will be searched for an executable of the same name. + +=head1 COLORS + +Colors are interpreted as X11 color values. This includes both verbatim +color names such as "Yellow", "Blue", "White", etc as well as #RGB and #RRGGBB +values. + +B<NOTE>: '#' is interpreted as a comment, to use it the entire value needs to +be in quotes like so: separator_color="#123456" + +=head2 NOTIFY-SEND + +dunst is able to get different colors for a message via notify-send. +In order to do that you have to add a hint via the -h option. +The progress value can be set with a hint, too. + +=over 4 + +=item notify-send -h string:fgcolor:#ff4444 + +=item notify-send -h string:bgcolor:#4444ff -h string:fgcolor:#ff4444 + +=item notify-send -h int:value:42 "Working ..." + +=back + +=head1 ACTIONS + +Dunst allows notifiers (i.e.: programs that send the notifications) to specify +actions. Dunst has support for both displaying indicators for these, and +interacting with these actions. + +If "show_indicators" is true and a notification has an action, an "(A)" will be +prepended to the notification format. Likewise, an "(U)" is preneded to +notifications with URLs. It is possible to interact with notifications that +have actions regardless of this setting, though it may not be obvious which +notifications HAVE actions. + +The "context" keybinding is used to interact with these actions, by showing a +menu of possible actions. This feature requires "dmenu" or a dmenu drop-in +replacement present. + +Alternatively, you can invoke an action with a middle click on the notification. +If there is exactly one associated action, or one is marked as default, that one +is invoked. If there are multiple, the context menu is shown. The same applies +to URLs when there are no actions. + +=head1 TIME FORMAT + +A time can be any decimal integer value suffixed with a time unit. If no unit +given, seconds ("s") is taken as default. + +Time units understood by dunst are "ms", "s", "m", "h" and "d". + +Example time: "1000ms" "10m" + +=head1 MISCELLANEOUS + +Dunst can be paused by sending a notification with a summary of +"DUNST_COMMAND_PAUSE" and resumed with a summary of "DUNST_COMMAND_RESUME". +Alternatively you can send SIGUSR1 and SIGUSR2 to pause and unpause +respectively. For Example: + +=over 4 + +=item killall -SIGUSR1 dunst # pause + +=item killall -SIGUSR2 dunst # resume + +=back + +When paused dunst will not display any notifications but keep all notifications +in a queue. This can for example be wrapped around a screen locker (i3lock, +slock) to prevent flickering of notifications through the lock and to read all +missed notifications after returning to the computer. + +=head1 FILES + +$XDG_CONFIG_HOME/dunst/dunstrc + +-or- + +$HOME/.config/dunst/dunstrc + +=head1 AUTHORS + +Written by Sascha Kruse <knopwob@googlemail.com> + +=head1 REPORTING BUGS + +Bugs and suggestions should be reported on GitHub at https://github.com/dunst-project/dunst/issues + +=head1 COPYRIGHT + +Copyright 2013 Sascha Kruse and contributors (see LICENSE for licensing information) + +If you feel that copyrights are violated, please send me an email. + +=head1 SEE ALSO + +dwm(1), dmenu(1), twmn(1), notify-send(1) diff --git a/packages/dunst/docs/dunst_layout.png b/packages/dunst/docs/dunst_layout.png new file mode 100644 index 0000000..6f971a4 Binary files /dev/null and b/packages/dunst/docs/dunst_layout.png differ diff --git a/packages/dunst/docs/dunst_layout.xcf b/packages/dunst/docs/dunst_layout.xcf new file mode 100644 index 0000000..f94d1c7 Binary files /dev/null and b/packages/dunst/docs/dunst_layout.xcf differ diff --git a/packages/dunst/dunst.systemd.service.in b/packages/dunst/dunst.systemd.service.in new file mode 100644 index 0000000..11df700 --- /dev/null +++ b/packages/dunst/dunst.systemd.service.in @@ -0,0 +1,13 @@ +[Unit] +Description=Dunst notification daemon +Documentation=man:dunst(1) +PartOf=graphical-session.target + +[Service] +Type=dbus +BusName=org.freedesktop.Notifications +ExecStart=##PREFIX##/bin/dunst + +[Install] +WantedBy=default.target + diff --git a/packages/dunst/dunstify.c b/packages/dunst/dunstify.c new file mode 100644 index 0000000..af03430 --- /dev/null +++ b/packages/dunst/dunstify.c @@ -0,0 +1,334 @@ +#include <glib.h> +#include <libnotify/notify.h> +#include <locale.h> +#include <stdbool.h> +#include <stdlib.h> +#include <string.h> +#include <gdk-pixbuf/gdk-pixbuf.h> + +static gchar *appname = "dunstify"; +static gchar *summary = NULL; +static gchar *body = NULL; +static NotifyUrgency urgency = NOTIFY_URGENCY_NORMAL; +static gchar *urgency_str = NULL; +static gchar **hint_strs = NULL; +static gchar **action_strs = NULL; +static gint timeout = NOTIFY_EXPIRES_DEFAULT; +static gchar *icon = NULL; +static gchar *raw_icon_path = NULL; +static gboolean capabilities = false; +static gboolean serverinfo = false; +static gboolean printid = false; +static guint32 replace_id = 0; +static guint32 close_id = 0; +static gboolean block = false; + +static GOptionEntry entries[] = +{ + { "appname", 'a', 0, G_OPTION_ARG_STRING, &appname, "Name of your application", "NAME" }, + { "urgency", 'u', 0, G_OPTION_ARG_STRING, &urgency_str, "The urgency of this notification", "URG" }, + { "hints", 'h', 0, G_OPTION_ARG_STRING_ARRAY, &hint_strs, "User specified hints", "HINT" }, + { "action", 'A', 0, G_OPTION_ARG_STRING_ARRAY, &action_strs, "Actions the user can invoke", "ACTION" }, + { "timeout", 't', 0, G_OPTION_ARG_INT, &timeout, "The time until the notification expires", "TIMEOUT" }, + { "icon", 'i', 0, G_OPTION_ARG_STRING, &icon, "An Icon that should be displayed with the notification", "ICON" }, + { "raw_icon", 'I', 0, G_OPTION_ARG_STRING, &raw_icon_path, "Path to the icon to be sent as raw image data", "RAW_ICON"}, + { "capabilities", 'c', 0, G_OPTION_ARG_NONE, &capabilities, "Print the server capabilities and exit", NULL}, + { "serverinfo", 's', 0, G_OPTION_ARG_NONE, &serverinfo, "Print server information and exit", NULL}, + { "printid", 'p', 0, G_OPTION_ARG_NONE, &printid, "Print id, which can be used to update/replace this notification", NULL}, + { "replace", 'r', 0, G_OPTION_ARG_INT, &replace_id, "Set id of this notification.", "ID"}, + { "close", 'C', 0, G_OPTION_ARG_INT, &close_id, "Set id of this notification.", "ID"}, + { "block", 'b', 0, G_OPTION_ARG_NONE, &block, "Block until notification is closed and print close reason", NULL}, + { NULL } +}; + +void die(int exit_value) +{ + if (notify_is_initted()) + notify_uninit(); + exit(exit_value); +} + +void print_capabilities(void) +{ + GList *caps = notify_get_server_caps(); + for (GList *iter = caps; iter; iter = iter->next) { + if (strlen(iter->data) > 0) { + g_print("%s\n", (char *)iter->data); + } + } +} + +void print_serverinfo(void) +{ + char *name; + char *vendor; + char *version; + char *spec_version; + + if (!notify_get_server_info(&name, &vendor, &version, &spec_version)) { + g_printerr("Unable to get server information"); + exit(1); + } + + g_print("name:%s\nvendor:%s\nversion:%s\nspec_version:%s\n", name, + vendor, + version, + spec_version); +} + +void parse_commandline(int argc, char *argv[]) +{ + GError *error = NULL; + GOptionContext *context; + + context = g_option_context_new("- Dunstify"); + g_option_context_add_main_entries(context, entries, NULL); + if (!g_option_context_parse(context, &argc, &argv, &error)){ + g_printerr("Invalid commandline: %s\n", error->message); + exit(1); + } + + g_option_context_free(context); + + if (capabilities) { + print_capabilities(); + die(0); + } + + if (serverinfo) { + print_serverinfo(); + die(0); + } + + if (argc < 2 && close_id < 1) { + g_printerr("I need at least a summary\n"); + die(1); + } else if (argc < 2) { + summary = g_strdup("These are not the summaries you are looking for"); + } else { + summary = g_strdup(argv[1]); + } + + if (argc > 2) { + body = g_strdup(argv[2]); + } + + if (urgency_str) { + switch (urgency_str[0]) { + case 'l': + case 'L': + case '0': + urgency = NOTIFY_URGENCY_LOW; + break; + case 'n': + case 'N': + case '1': + urgency = NOTIFY_URGENCY_NORMAL; + break; + case 'c': + case 'C': + case '2': + urgency = NOTIFY_URGENCY_CRITICAL; + break; + default: + g_printerr("Unknown urgency: %s\n", urgency_str); + g_printerr("Assuming normal urgency\n"); + break; + } + } +} + +typedef struct _NotifyNotificationPrivate +{ + guint32 id; + char *app_name; + char *summary; + char *body; + + /* NULL to use icon data. Anything else to have server lookup icon */ + char *icon_name; + + /* + * -1 = use server default + * 0 = never timeout + * > 0 = Number of milliseconds before we timeout + */ + gint timeout; + + GSList *actions; + GHashTable *action_map; + GHashTable *hints; + + gboolean has_nondefault_actions; + gboolean updates_pending; + + gulong proxy_signal_handler; + + gint closed_reason; +} knickers; + +int get_id(NotifyNotification *n) +{ + knickers *kn = n->priv; + + /* I'm sorry for taking a peek */ + return kn->id; +} + +void put_id(NotifyNotification *n, guint32 id) +{ + knickers *kn = n->priv; + + /* And know I'm putting stuff into + * your knickers. I'm sorry. + * I'm so sorry. + * */ + + kn->id = id; +} + +void actioned(NotifyNotification *n, char *a, gpointer foo) +{ + notify_notification_close(n, NULL); + g_print("%s\n", a); + die(0); +} + +void closed(NotifyNotification *n, gpointer foo) +{ + g_print("%d\n", notify_notification_get_closed_reason(n)); + die(0); +} + +void add_action(NotifyNotification *n, char *str) +{ + char *action = str; + char *label = strchr(str, ','); + + if (!label || *(label+1) == '\0') { + g_printerr("Malformed action. Excpected \"action,label\", got \"%s\"", str); + return; + } + + *label = '\0'; + label++; + + notify_notification_add_action(n, action, label, actioned, NULL, NULL); +} + +void add_hint(NotifyNotification *n, char *str) +{ + char *type = str; + char *name = strchr(str, ':'); + if (!name || *(name+1) == '\0') { + g_printerr("Malformed hint. Expected \"type:name:value\", got \"%s\"", str); + return; + } + *name = '\0'; + name++; + char *value = strchr(name, ':'); + if (!value || *(value+1) == '\0') { + g_printerr("Malformed hint. Expected \"type:name:value\", got \"%s\"", str); + return; + } + *value = '\0'; + value++; + + if (strcmp(type, "int") == 0) + notify_notification_set_hint_int32(n, name, atoi(value)); + else if (strcmp(type, "double") == 0) + notify_notification_set_hint_double(n, name, atof(value)); + else if (strcmp(type, "string") == 0) + notify_notification_set_hint_string(n, name, value); + else if (strcmp(type, "byte") == 0) { + gint h_byte = g_ascii_strtoull(value, NULL, 10); + if (h_byte < 0 || h_byte > 0xFF) + g_printerr("Not a byte: \"%s\"", value); + else + notify_notification_set_hint_byte(n, name, (guchar) h_byte); + } else + g_printerr("Malformed hint. Expected a type of int, double, string or byte, got %s\n", type); + +} + +int main(int argc, char *argv[]) +{ + setlocale(LC_ALL, ""); + #if !GLIB_CHECK_VERSION(2,35,0) + g_type_init(); + #endif + parse_commandline(argc, argv); + + if (!notify_init(appname)) { + g_printerr("Unable to initialize libnotify\n"); + die(1); + } + + NotifyNotification *n; + n = notify_notification_new(summary, body, icon); + notify_notification_set_timeout(n, timeout); + notify_notification_set_urgency(n, urgency); + + GError *err = NULL; + + if (raw_icon_path) { + GdkPixbuf *raw_icon = gdk_pixbuf_new_from_file(raw_icon_path, &err); + + if(err) { + g_printerr("Unable to get raw icon: %s\n", err->message); + die(1); + } + + notify_notification_set_image_from_pixbuf(n, raw_icon); + } + + if (close_id > 0) { + put_id(n, close_id); + notify_notification_close(n, &err); + if (err) { + g_printerr("Unable to close notification: %s\n", err->message); + die(1); + } + die(0); + } + + if (replace_id > 0) { + put_id(n, replace_id); + } + + GMainLoop *l = NULL; + + if (block || action_strs) { + l = g_main_loop_new(NULL, false); + g_signal_connect(n, "closed", G_CALLBACK(closed), NULL); + } + + if (action_strs) + for (int i = 0; action_strs[i]; i++) { + add_action(n, action_strs[i]); + } + + if (hint_strs) + for (int i = 0; hint_strs[i]; i++) { + add_hint(n, hint_strs[i]); + } + + + notify_notification_show(n, &err); + if (err) { + g_printerr("Unable to send notification: %s\n", err->message); + die(1); + } + + if (printid) + g_print("%d\n", get_id(n)); + + if (block || action_strs) + g_main_loop_run(l); + + g_object_unref(G_OBJECT (n)); + + die(0); +} + +/* vim: set tabstop=8 shiftwidth=8 expandtab textwidth=0: */ diff --git a/packages/dunst/dunstrc b/packages/dunst/dunstrc new file mode 100644 index 0000000..dbe7eab --- /dev/null +++ b/packages/dunst/dunstrc @@ -0,0 +1,326 @@ +[global] + ### Display ### + + # Which monitor should the notifications be displayed on. + monitor = 0 + + # Display notification on focused monitor. Possible modes are: + # mouse: follow mouse pointer + # keyboard: follow window with keyboard focus + # none: don't follow anything + # + # "keyboard" needs a window manager that exports the + # _NET_ACTIVE_WINDOW property. + # This should be the case for almost all modern window managers. + # + # If this option is set to mouse or keyboard, the monitor option + # will be ignored. + follow = mouse + + # The geometry of the window: + # [{width}]x{height}[+/-{x}+/-{y}] + # The geometry of the message window. + # The height is measured in number of notifications everything else + # in pixels. If the width is omitted but the height is given + # ("-geometry x2"), the message window expands over the whole screen + # (dmenu-like). If width is 0, the window expands to the longest + # message displayed. A positive x is measured from the left, a + # negative from the right side of the screen. Y is measured from + # the top and down respectively. + # The width can be negative. In this case the actual width is the + # screen width minus the width defined in within the geometry option. + geometry = "300x5-30+20" + + # Show how many messages are currently hidden (because of geometry). + indicate_hidden = yes + + # Shrink window if it's smaller than the width. Will be ignored if + # width is 0. + shrink = no + + # The transparency of the window. Range: [0; 100]. + # This option will only work if a compositing window manager is + # present (e.g. xcompmgr, compiz, etc.). + transparency = 0 + + # The height of the entire notification. If the height is smaller + # than the font height and padding combined, it will be raised + # to the font height and padding. + notification_height = 0 + + # Draw a line of "separator_height" pixel height between two + # notifications. + # Set to 0 to disable. + separator_height = 2 + + # Padding between text and separator. + padding = 8 + + # Horizontal padding. + horizontal_padding = 8 + + # Defines width in pixels of frame around the notification window. + # Set to 0 to disable. + frame_width = 3 + + # Defines color of the frame around the notification window. + frame_color = "#aaaaaa" + + # Define a color for the separator. + # possible values are: + # * auto: dunst tries to find a color fitting to the background; + # * foreground: use the same color as the foreground; + # * frame: use the same color as the frame; + # * anything else will be interpreted as a X color. + separator_color = frame + + # Sort messages by urgency. + sort = yes + + # Don't remove messages, if the user is idle (no mouse or keyboard input) + # for longer than idle_threshold seconds. + # Set to 0 to disable. + # Transient notifications ignore this setting. + idle_threshold = 120 + + ### Text ### + + font = Monospace 8 + + # The spacing between lines. If the height is smaller than the + # font height, it will get raised to the font height. + line_height = 0 + + # Possible values are: + # full: Allow a small subset of html markup in notifications: + # <b>bold</b> + # <i>italic</i> + # <s>strikethrough</s> + # <u>underline</u> + # + # For a complete reference see + # <http://developer.gnome.org/pango/stable/PangoMarkupFormat.html>. + # + # strip: This setting is provided for compatibility with some broken + # clients that send markup even though it's not enabled on the + # server. Dunst will try to strip the markup but the parsing is + # simplistic so using this option outside of matching rules for + # specific applications *IS GREATLY DISCOURAGED*. + # + # no: Disable markup parsing, incoming notifications will be treated as + # plain text. Dunst will not advertise that it has the body-markup + # capability if this is set as a global setting. + # + # It's important to note that markup inside the format option will be parsed + # regardless of what this is set to. + markup = full + + # The format of the message. Possible variables are: + # %a appname + # %s summary + # %b body + # %i iconname (including its path) + # %I iconname (without its path) + # %p progress value if set ([ 0%] to [100%]) or nothing + # %n progress value if set without any extra characters + # %% Literal % + # Markup is allowed + format = "<b>%s</b>\n%b" + + # Alignment of message text. + # Possible values are "left", "center" and "right". + alignment = left + + # Show age of message if message is older than show_age_threshold + # seconds. + # Set to -1 to disable. + show_age_threshold = 60 + + # Split notifications into multiple lines if they don't fit into + # geometry. + word_wrap = yes + + # When word_wrap is set to no, specify where to ellipsize long lines. + # Possible values are "start", "middle" and "end". + ellipsize = middle + + # Ignore newlines '\n' in notifications. + ignore_newline = no + + # Merge multiple notifications with the same content + stack_duplicates = true + + # Hide the count of merged notifications with the same content + hide_duplicate_count = false + + # Display indicators for URLs (U) and actions (A). + show_indicators = yes + + ### Icons ### + + # Align icons left/right/off + icon_position = off + + # Scale larger icons down to this size, set to 0 to disable + max_icon_size = 32 + + # Paths to default icons. + icon_path = /usr/share/icons/gnome/16x16/status/:/usr/share/icons/gnome/16x16/devices/ + + ### History ### + + # Should a notification popped up from history be sticky or timeout + # as if it would normally do. + sticky_history = yes + + # Maximum amount of notifications kept in history + history_length = 20 + + ### Misc/Advanced ### + + # dmenu path. + dmenu = /usr/bin/dmenu -p dunst: + + # Browser for opening urls in context menu. + browser = /usr/bin/firefox -new-tab + + # Always run rule-defined scripts, even if the notification is suppressed + always_run_script = true + + # Define the title of the windows spawned by dunst + title = Dunst + + # Define the class of the windows spawned by dunst + class = Dunst + + # Print a notification on startup. + # This is mainly for error detection, since dbus (re-)starts dunst + # automatically after a crash. + startup_notification = false + + ### Legacy + + # Use the Xinerama extension instead of RandR for multi-monitor support. + # This setting is provided for compatibility with older nVidia drivers that + # do not support RandR and using it on systems that support RandR is highly + # discouraged. + # + # By enabling this setting dunst will not be able to detect when a monitor + # is connected or disconnected which might break follow mode if the screen + # layout changes. + force_xinerama = false + +# Experimental features that may or may not work correctly. Do not expect them +# to have a consistent behaviour across releases. +[experimental] + # Calculate the dpi to use on a per-monitor basis. + # If this setting is enabled the Xft.dpi value will be ignored and instead + # dunst will attempt to calculate an appropriate dpi value for each monitor + # using the resolution and physical size. This might be useful in setups + # where there are multiple screens with very different dpi values. + per_monitor_dpi = false + +[shortcuts] + + # Shortcuts are specified as [modifier+][modifier+]...key + # Available modifiers are "ctrl", "mod1" (the alt-key), "mod2", + # "mod3" and "mod4" (windows-key). + # Xev might be helpful to find names for keys. + + # Close notification. + close = ctrl+space + + # Close all notifications. + close_all = ctrl+shift+space + + # Redisplay last message(s). + # On the US keyboard layout "grave" is normally above TAB and left + # of "1". Make sure this key actually exists on your keyboard layout, + # e.g. check output of 'xmodmap -pke' + history = ctrl+grave + + # Context menu. + context = ctrl+shift+period + +[urgency_low] + # IMPORTANT: colors have to be defined in quotation marks. + # Otherwise the "#" and following would be interpreted as a comment. + background = "#222222" + foreground = "#888888" + timeout = 10 + # Icon for notifications with low urgency, uncomment to enable + #icon = /path/to/icon + +[urgency_normal] + background = "#285577" + foreground = "#ffffff" + timeout = 10 + # Icon for notifications with normal urgency, uncomment to enable + #icon = /path/to/icon + +[urgency_critical] + background = "#900000" + foreground = "#ffffff" + frame_color = "#ff0000" + timeout = 0 + # Icon for notifications with critical urgency, uncomment to enable + #icon = /path/to/icon + +# Every section that isn't one of the above is interpreted as a rules to +# override settings for certain messages. +# Messages can be matched by "appname", "summary", "body", "icon", "category", +# "msg_urgency" and you can override the "timeout", "urgency", "foreground", +# "background", "new_icon" and "format". +# Shell-like globbing will get expanded. +# +# SCRIPTING +# You can specify a script that gets run when the rule matches by +# setting the "script" option. +# The script will be called as follows: +# script appname summary body icon urgency +# where urgency can be "LOW", "NORMAL" or "CRITICAL". +# +# NOTE: if you don't want a notification to be displayed, set the format +# to "". +# NOTE: It might be helpful to run dunst -print in a terminal in order +# to find fitting options for rules. + +#[espeak] +# summary = "*" +# script = dunst_espeak.sh + +#[script-test] +# summary = "*script*" +# script = dunst_test.sh + +#[ignore] +# # This notification will not be displayed +# summary = "foobar" +# format = "" + +#[history-ignore] +# # This notification will not be saved in history +# summary = "foobar" +# history_ignore = yes + +#[signed_on] +# appname = Pidgin +# summary = "*signed on*" +# urgency = low +# +#[signed_off] +# appname = Pidgin +# summary = *signed off* +# urgency = low +# +#[says] +# appname = Pidgin +# summary = *says* +# urgency = critical +# +#[twitter] +# appname = Pidgin +# summary = *twitter.com* +# urgency = normal +# +# vim: ft=cfg diff --git a/packages/dunst/main.c b/packages/dunst/main.c new file mode 100644 index 0000000..e31b420 --- /dev/null +++ b/packages/dunst/main.c @@ -0,0 +1,7 @@ +#include "src/dunst.h" + +int main(int argc, char *argv[]) +{ + return dunst_main(argc, argv); +} +/* vim: set tabstop=8 shiftwidth=8 expandtab textwidth=0: */ diff --git a/packages/dunst/org.knopwob.dunst.service.in b/packages/dunst/org.knopwob.dunst.service.in new file mode 100644 index 0000000..a8e8ac1 --- /dev/null +++ b/packages/dunst/org.knopwob.dunst.service.in @@ -0,0 +1,4 @@ +[D-BUS Service] +Name=org.freedesktop.Notifications +Exec=##PREFIX##/bin/dunst +SystemdService=dunst.service diff --git a/packages/dunst/src/dbus.c b/packages/dunst/src/dbus.c new file mode 100644 index 0000000..bf9a815 --- /dev/null +++ b/packages/dunst/src/dbus.c @@ -0,0 +1,492 @@ +/* copyright 2013 Sascha Kruse and contributors (see LICENSE for licensing information) */ +#include "dbus.h" + +#include <gio/gio.h> +#include <glib.h> +#include <stdio.h> +#include <stdlib.h> + +#include "dunst.h" +#include "notification.h" +#include "queues.h" +#include "settings.h" +#include "utils.h" + +GDBusConnection *dbus_conn; + +static GDBusNodeInfo *introspection_data = NULL; + +static const char *introspection_xml = + "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" + "<node name=\"/org/freedesktop/Notifications\">" + " <interface name=\"org.freedesktop.Notifications\">" + + " <method name=\"GetCapabilities\">" + " <arg direction=\"out\" name=\"capabilities\" type=\"as\"/>" + " </method>" + + " <method name=\"Notify\">" + " <arg direction=\"in\" name=\"app_name\" type=\"s\"/>" + " <arg direction=\"in\" name=\"replaces_id\" type=\"u\"/>" + " <arg direction=\"in\" name=\"app_icon\" type=\"s\"/>" + " <arg direction=\"in\" name=\"summary\" type=\"s\"/>" + " <arg direction=\"in\" name=\"body\" type=\"s\"/>" + " <arg direction=\"in\" name=\"actions\" type=\"as\"/>" + " <arg direction=\"in\" name=\"hints\" type=\"a{sv}\"/>" + " <arg direction=\"in\" name=\"expire_timeout\" type=\"i\"/>" + " <arg direction=\"out\" name=\"id\" type=\"u\"/>" + " </method>" + + " <method name=\"CloseNotification\">" + " <arg direction=\"in\" name=\"id\" type=\"u\"/>" + " </method>" + + " <method name=\"GetServerInformation\">" + " <arg direction=\"out\" name=\"name\" type=\"s\"/>" + " <arg direction=\"out\" name=\"vendor\" type=\"s\"/>" + " <arg direction=\"out\" name=\"version\" type=\"s\"/>" + " <arg direction=\"out\" name=\"spec_version\" type=\"s\"/>" + " </method>" + + " <signal name=\"NotificationClosed\">" + " <arg name=\"id\" type=\"u\"/>" + " <arg name=\"reason\" type=\"u\"/>" + " </signal>" + + " <signal name=\"ActionInvoked\">" + " <arg name=\"id\" type=\"u\"/>" + " <arg name=\"action_key\" type=\"s\"/>" + " </signal>" + " </interface>" + "</node>"; + +static void on_get_capabilities(GDBusConnection *connection, + const gchar *sender, + const GVariant *parameters, + GDBusMethodInvocation *invocation); +static void on_notify(GDBusConnection *connection, + const gchar *sender, + GVariant *parameters, + GDBusMethodInvocation *invocation); +static void on_close_notification(GDBusConnection *connection, + const gchar *sender, + GVariant *parameters, + GDBusMethodInvocation *invocation); +static void on_get_server_information(GDBusConnection *connection, + const gchar *sender, + const GVariant *parameters, + GDBusMethodInvocation *invocation); +static RawImage *get_raw_image_from_data_hint(GVariant *icon_data); + +void handle_method_call(GDBusConnection *connection, + const gchar *sender, + const gchar *object_path, + const gchar *interface_name, + const gchar *method_name, + GVariant *parameters, + GDBusMethodInvocation *invocation, + gpointer user_data) +{ + if (g_strcmp0(method_name, "GetCapabilities") == 0) { + on_get_capabilities(connection, sender, parameters, invocation); + } else if (g_strcmp0(method_name, "Notify") == 0) { + on_notify(connection, sender, parameters, invocation); + } else if (g_strcmp0(method_name, "CloseNotification") == 0) { + on_close_notification(connection, sender, parameters, invocation); + } else if (g_strcmp0(method_name, "GetServerInformation") == 0) { + on_get_server_information(connection, sender, parameters, invocation); + } else { + fprintf(stderr, "WARNING: sender: %s; unknown method_name: %s\n", sender, + method_name); + } +} + +static void on_get_capabilities(GDBusConnection *connection, + const gchar *sender, + const GVariant *parameters, + GDBusMethodInvocation *invocation) +{ + GVariantBuilder *builder; + GVariant *value; + + builder = g_variant_builder_new(G_VARIANT_TYPE("as")); + g_variant_builder_add(builder, "s", "actions"); + g_variant_builder_add(builder, "s", "body"); + g_variant_builder_add(builder, "s", "body-hyperlinks"); + + if (settings.markup != MARKUP_NO) + g_variant_builder_add(builder, "s", "body-markup"); + + value = g_variant_new("(as)", builder); + g_variant_builder_unref(builder); + g_dbus_method_invocation_return_value(invocation, value); + + g_dbus_connection_flush(connection, NULL, NULL, NULL); +} + +static void on_notify(GDBusConnection *connection, + const gchar *sender, + GVariant *parameters, + GDBusMethodInvocation *invocation) +{ + + gchar *appname = NULL; + guint replaces_id = 0; + gchar *icon = NULL; + gchar *summary = NULL; + gchar *body = NULL; + Actions *actions = g_malloc0(sizeof(Actions)); + gint timeout = -1; + + /* hints */ + gint urgency = 1; + gint progress = -1; + gboolean transient = 0; + gchar *fgcolor = NULL; + gchar *bgcolor = NULL; + gchar *category = NULL; + RawImage *raw_icon = NULL; + + { + GVariantIter *iter = g_variant_iter_new(parameters); + GVariant *content; + GVariant *dict_value; + int idx = 0; + while ((content = g_variant_iter_next_value(iter))) { + + switch (idx) { + case 0: + if (g_variant_is_of_type(content, G_VARIANT_TYPE_STRING)) + appname = g_variant_dup_string(content, NULL); + break; + case 1: + if (g_variant_is_of_type(content, G_VARIANT_TYPE_UINT32)) + replaces_id = g_variant_get_uint32(content); + break; + case 2: + if (g_variant_is_of_type(content, G_VARIANT_TYPE_STRING)) + icon = g_variant_dup_string(content, NULL); + break; + case 3: + if (g_variant_is_of_type(content, G_VARIANT_TYPE_STRING)) + summary = g_variant_dup_string(content, NULL); + break; + case 4: + if (g_variant_is_of_type(content, G_VARIANT_TYPE_STRING)) + body = g_variant_dup_string(content, NULL); + break; + case 5: + if (g_variant_is_of_type(content, G_VARIANT_TYPE_STRING_ARRAY)) + actions->actions = g_variant_dup_strv(content, &(actions->count)); + break; + case 6: + if (g_variant_is_of_type(content, G_VARIANT_TYPE_DICTIONARY)) { + + dict_value = g_variant_lookup_value(content, "urgency", G_VARIANT_TYPE_BYTE); + if (dict_value) { + urgency = g_variant_get_byte(dict_value); + g_variant_unref(dict_value); + } + + dict_value = g_variant_lookup_value(content, "fgcolor", G_VARIANT_TYPE_STRING); + if (dict_value) { + fgcolor = g_variant_dup_string(dict_value, NULL); + g_variant_unref(dict_value); + } + + dict_value = g_variant_lookup_value(content, "bgcolor", G_VARIANT_TYPE_STRING); + if (dict_value) { + bgcolor = g_variant_dup_string(dict_value, NULL); + g_variant_unref(dict_value); + } + + dict_value = g_variant_lookup_value(content, "category", G_VARIANT_TYPE_STRING); + if (dict_value) { + category = g_variant_dup_string(dict_value, NULL); + g_variant_unref(dict_value); + } + + dict_value = g_variant_lookup_value(content, "image-path", G_VARIANT_TYPE_STRING); + if (dict_value) { + g_free(icon); + icon = g_variant_dup_string(dict_value, NULL); + g_variant_unref(dict_value); + } + + dict_value = g_variant_lookup_value(content, "image-data", G_VARIANT_TYPE("(iiibiiay)")); + if (!dict_value) + dict_value = g_variant_lookup_value(content, "image_data", G_VARIANT_TYPE("(iiibiiay)")); + if (!dict_value) + dict_value = g_variant_lookup_value(content, "icon_data", G_VARIANT_TYPE("(iiibiiay)")); + if (dict_value) { + raw_icon = get_raw_image_from_data_hint(dict_value); + g_variant_unref(dict_value); + } + + /* Check for transient hints + * + * According to the spec, the transient hint should be boolean. + * But notify-send does not support hints of type 'boolean'. + * So let's check for int and boolean until notify-send is fixed. + */ + if((dict_value = g_variant_lookup_value(content, "transient", G_VARIANT_TYPE_BOOLEAN))) { + transient = g_variant_get_boolean(dict_value); + g_variant_unref(dict_value); + } else if((dict_value = g_variant_lookup_value(content, "transient", G_VARIANT_TYPE_UINT32))) { + transient = g_variant_get_uint32(dict_value) > 0; + g_variant_unref(dict_value); + } else if((dict_value = g_variant_lookup_value(content, "transient", G_VARIANT_TYPE_INT32))) { + transient = g_variant_get_int32(dict_value) > 0; + g_variant_unref(dict_value); + } + + if((dict_value = g_variant_lookup_value(content, "value", G_VARIANT_TYPE_INT32))) { + progress = g_variant_get_int32(dict_value); + g_variant_unref(dict_value); + } else if((dict_value = g_variant_lookup_value(content, "value", G_VARIANT_TYPE_UINT32))) { + progress = g_variant_get_uint32(dict_value); + g_variant_unref(dict_value); + } + } + break; + case 7: + if (g_variant_is_of_type(content, G_VARIANT_TYPE_INT32)) + timeout = g_variant_get_int32(content); + break; + } + g_variant_unref(content); + idx++; + } + + g_variant_iter_free(iter); + } + + fflush(stdout); + + notification *n = notification_create(); + n->appname = appname; + n->summary = summary; + n->body = body; + n->icon = icon; + n->raw_icon = raw_icon; + n->timeout = timeout < 0 ? -1 : timeout * 1000; + n->markup = settings.markup; + n->progress = (progress < 0 || progress > 100) ? -1 : progress; + n->urgency = urgency; + n->category = category; + n->dbus_client = g_strdup(sender); + n->transient = transient; + + if (actions->count < 1) { + actions_free(actions); + actions = NULL; + } + n->actions = actions; + + for (int i = 0; i < ColLast; i++) { + n->color_strings[i] = NULL; + } + n->color_strings[ColFG] = fgcolor; + n->color_strings[ColBG] = bgcolor; + + notification_init(n); + int id = queues_notification_insert(n, replaces_id); + + GVariant *reply = g_variant_new("(u)", id); + g_dbus_method_invocation_return_value(invocation, reply); + g_dbus_connection_flush(connection, NULL, NULL, NULL); + + // The message got discarded + if (id == 0) { + notification_closed(n, 2); + notification_free(n); + } + + wake_up(); +} + +static void on_close_notification(GDBusConnection *connection, + const gchar *sender, + GVariant *parameters, + GDBusMethodInvocation *invocation) +{ + guint32 id; + g_variant_get(parameters, "(u)", &id); + queues_notification_close_id(id, REASON_SIG); + wake_up(); + g_dbus_method_invocation_return_value(invocation, NULL); + g_dbus_connection_flush(connection, NULL, NULL, NULL); +} + +static void on_get_server_information(GDBusConnection *connection, + const gchar *sender, + const GVariant *parameters, + GDBusMethodInvocation *invocation) +{ + GVariant *value; + + value = g_variant_new("(ssss)", "dunst", "knopwob", VERSION, "1.2"); + g_dbus_method_invocation_return_value(invocation, value); + + g_dbus_connection_flush(connection, NULL, NULL, NULL); +} + +void notification_closed(notification *n, enum reason reason) +{ + if (reason < REASON_MIN || REASON_MAX < reason) { + fprintf(stderr, "ERROR: Closing notification with reason '%d' not supported. " + "Closing it with reason '%d'.\n", reason, REASON_UNDEF); + reason = REASON_UNDEF; + } + + if (!dbus_conn) { + fprintf(stderr, "ERROR: Tried to close notification but dbus connection not set!\n"); + return; + } + + GVariant *body = g_variant_new("(uu)", n->id, reason); + GError *err = NULL; + + g_dbus_connection_emit_signal(dbus_conn, + n->dbus_client, + "/org/freedesktop/Notifications", + "org.freedesktop.Notifications", + "NotificationClosed", + body, + &err); + + if (err) { + fprintf(stderr, "Unable to close notification: %s\n", err->message); + g_error_free(err); + } + +} + +void action_invoked(notification *n, const char *identifier) +{ + GVariant *body = g_variant_new("(us)", n->id, identifier); + GError *err = NULL; + + g_dbus_connection_emit_signal(dbus_conn, + n->dbus_client, + "/org/freedesktop/Notifications", + "org.freedesktop.Notifications", + "ActionInvoked", + body, + &err); + + if (err) { + fprintf(stderr, "Unable to invoke action: %s\n", err->message); + g_error_free(err); + } +} + +static const GDBusInterfaceVTable interface_vtable = { + handle_method_call +}; + +static void on_bus_acquired(GDBusConnection *connection, + const gchar *name, + gpointer user_data) +{ + guint registration_id; + + GError *err = NULL; + + registration_id = g_dbus_connection_register_object(connection, + "/org/freedesktop/Notifications", + introspection_data->interfaces[0], + &interface_vtable, + NULL, + NULL, + &err); + + if (registration_id == 0) { + fprintf(stderr, "Unable to register dbus connection: %s\n", err->message); + exit(1); + } +} + +static void on_name_acquired(GDBusConnection *connection, + const gchar *name, + gpointer user_data) +{ + dbus_conn = connection; +} + +static void on_name_lost(GDBusConnection *connection, + const gchar *name, + gpointer user_data) +{ + fprintf(stderr, "Name Lost. Is Another notification daemon running?\n"); + exit(1); +} + +static RawImage *get_raw_image_from_data_hint(GVariant *icon_data) +{ + RawImage *image = g_malloc(sizeof(RawImage)); + GVariant *data_variant; + gsize expected_len; + + g_variant_get(icon_data, + "(iiibii@ay)", + &image->width, + &image->height, + &image->rowstride, + &image->has_alpha, + &image->bits_per_sample, + &image->n_channels, + &data_variant); + + expected_len = (image->height - 1) * image->rowstride + image->width + * ((image->n_channels * image->bits_per_sample + 7) / 8); + + if (expected_len != g_variant_get_size (data_variant)) { + fprintf(stderr, "Expected image data to be of length %" G_GSIZE_FORMAT + " but got a " "length of %" G_GSIZE_FORMAT, + expected_len, + g_variant_get_size (data_variant)); + g_free(image); + g_variant_unref(data_variant); + return NULL; + } + + image->data = (guchar *) g_memdup(g_variant_get_data(data_variant), + g_variant_get_size(data_variant)); + g_variant_unref(data_variant); + + return image; +} + +int initdbus(void) +{ + guint owner_id; + + #if !GLIB_CHECK_VERSION(2,35,0) + g_type_init(); + #endif + + introspection_data = g_dbus_node_info_new_for_xml(introspection_xml, + NULL); + + owner_id = g_bus_own_name(G_BUS_TYPE_SESSION, + "org.freedesktop.Notifications", + G_BUS_NAME_OWNER_FLAGS_NONE, + on_bus_acquired, + on_name_acquired, + on_name_lost, + NULL, + NULL); + + return owner_id; +} + +void dbus_tear_down(int owner_id) +{ + if (introspection_data) + g_dbus_node_info_unref(introspection_data); + + g_bus_unown_name(owner_id); +} + +/* vim: set tabstop=8 shiftwidth=8 expandtab textwidth=0: */ diff --git a/packages/dunst/src/dbus.h b/packages/dunst/src/dbus.h new file mode 100644 index 0000000..51fd323 --- /dev/null +++ b/packages/dunst/src/dbus.h @@ -0,0 +1,24 @@ +/* copyright 2013 Sascha Kruse and contributors (see LICENSE for licensing information) */ + +#ifndef DUNST_DBUS_H +#define DUNST_DBUS_H + +#include "notification.h" + +enum reason { + REASON_MIN = 1, + REASON_TIME = 1, + REASON_USER = 2, + REASON_SIG = 3, + REASON_UNDEF = 4, + REASON_MAX = 4, +}; + +int initdbus(void); +void dbus_tear_down(int id); +/* void dbus_poll(int timeout); */ +void notification_closed(notification *n, enum reason reason); +void action_invoked(notification *n, const char *identifier); + +#endif +/* vim: set tabstop=8 shiftwidth=8 expandtab textwidth=0: */ diff --git a/packages/dunst/src/dunst.c b/packages/dunst/src/dunst.c new file mode 100644 index 0000000..5d55467 --- /dev/null +++ b/packages/dunst/src/dunst.c @@ -0,0 +1,232 @@ +/* copyright 2012 - 2013 Sascha Kruse and contributors (see LICENSE for licensing information) */ + +#define XLIB_ILLEGAL_ACCESS + +#include "dunst.h" + +#include <X11/Xlib.h> +#include <glib-unix.h> +#include <glib.h> +#include <signal.h> +#include <stdbool.h> +#include <stdio.h> +#include <stdlib.h> + +#include "dbus.h" +#include "menu.h" +#include "notification.h" +#include "option_parser.h" +#include "queues.h" +#include "settings.h" +#include "x11/screen.h" +#include "x11/x.h" + +#ifndef VERSION +#define VERSION "version info needed" +#endif + +#define MSG 1 +#define INFO 2 +#define DEBUG 3 + +typedef struct _x11_source { + GSource source; + Display *dpy; + Window w; +} x11_source_t; + +/* index of colors fit to urgency level */ + +GMainLoop *mainloop = NULL; + +GSList *rules = NULL; + +/* misc funtions */ + +void wake_up(void) +{ + run(NULL); +} + +gboolean run(void *data) +{ + queues_check_timeouts(x_is_idle()); + queues_update(); + + static int timeout_cnt = 0; + static gint64 next_timeout = 0; + + if (data && timeout_cnt > 0) { + timeout_cnt--; + } + + if (queues_length_displayed() > 0 && !xctx.visible) { + x_win_show(); + } + + if (xctx.visible && queues_length_displayed() == 0) { + x_win_hide(); + } + + if (xctx.visible) { + x_win_draw(); + } + + if (xctx.visible) { + gint64 now = g_get_monotonic_time(); + gint64 sleep = queues_get_next_datachange(now); + gint64 timeout_at = now + sleep; + + if (sleep >= 0) { + if (timeout_cnt == 0 || timeout_at < next_timeout) { + g_timeout_add(sleep/1000, run, mainloop); + next_timeout = timeout_at; + timeout_cnt++; + } + } + } + + /* always return false to delete timers */ + return false; +} + +gboolean pause_signal(gpointer data) +{ + queues_pause_on(); + wake_up(); + + return G_SOURCE_CONTINUE; +} + +gboolean unpause_signal(gpointer data) +{ + queues_pause_off(); + wake_up(); + + return G_SOURCE_CONTINUE; +} + +gboolean quit_signal(gpointer data) +{ + g_main_loop_quit(mainloop); + + return G_SOURCE_CONTINUE; +} + +static void teardown(void) +{ + regex_teardown(); + + teardown_queues(); + + x_free(); +} + +int dunst_main(int argc, char *argv[]) +{ + + queues_init(); + + cmdline_load(argc, argv); + + if (cmdline_get_bool("-v/-version", false, "Print version") + || cmdline_get_bool("--version", false, "Print version")) { + print_version(); + } + + char *cmdline_config_path; + cmdline_config_path = + cmdline_get_string("-conf/-config", NULL, + "Path to configuration file"); + load_settings(cmdline_config_path); + + if (cmdline_get_bool("-h/-help", false, "Print help") + || cmdline_get_bool("--help", false, "Print help")) { + usage(EXIT_SUCCESS); + } + + int owner_id = initdbus(); + + x_setup(); + + if (settings.startup_notification) { + notification *n = notification_create(); + n->appname = g_strdup("dunst"); + n->summary = g_strdup("startup"); + n->body = g_strdup("dunst is up and running"); + n->progress = -1; + n->timeout = 10 * G_USEC_PER_SEC; + n->markup = MARKUP_NO; + n->urgency = URG_LOW; + notification_init(n); + queues_notification_insert(n, 0); + // we do not call wakeup now, wake_up does not work here yet + } + + mainloop = g_main_loop_new(NULL, FALSE); + + GPollFD dpy_pollfd = { xctx.dpy->fd, + G_IO_IN | G_IO_HUP | G_IO_ERR, 0 + }; + + GSourceFuncs x11_source_funcs = { + x_mainloop_fd_prepare, + x_mainloop_fd_check, + x_mainloop_fd_dispatch, + NULL, + NULL, + NULL + }; + + GSource *x11_source = + g_source_new(&x11_source_funcs, sizeof(x11_source_t)); + ((x11_source_t *) x11_source)->dpy = xctx.dpy; + ((x11_source_t *) x11_source)->w = xctx.win; + g_source_add_poll(x11_source, &dpy_pollfd); + + g_source_attach(x11_source, NULL); + + guint pause_src = g_unix_signal_add(SIGUSR1, pause_signal, NULL); + guint unpause_src = g_unix_signal_add(SIGUSR2, unpause_signal, NULL); + + /* register SIGINT/SIGTERM handler for + * graceful termination */ + guint term_src = g_unix_signal_add(SIGTERM, quit_signal, NULL); + guint int_src = g_unix_signal_add(SIGINT, quit_signal, NULL); + + run(NULL); + g_main_loop_run(mainloop); + g_main_loop_unref(mainloop); + + /* remove signal handler watches */ + g_source_remove(pause_src); + g_source_remove(unpause_src); + g_source_remove(term_src); + g_source_remove(int_src); + + g_source_destroy(x11_source); + + dbus_tear_down(owner_id); + + teardown(); + + return 0; +} + +void usage(int exit_status) +{ + puts("usage:\n"); + const char *us = cmdline_create_usage(); + puts(us); + exit(exit_status); +} + +void print_version(void) +{ + printf + ("Dunst - A customizable and lightweight notification-daemon %s\n", + VERSION); + exit(EXIT_SUCCESS); +} + +/* vim: set tabstop=8 shiftwidth=8 expandtab textwidth=0: */ diff --git a/packages/dunst/src/dunst.h b/packages/dunst/src/dunst.h new file mode 100644 index 0000000..657cfdd --- /dev/null +++ b/packages/dunst/src/dunst.h @@ -0,0 +1,37 @@ +/* copyright 2013 Sascha Kruse and contributors (see LICENSE for licensing information) */ + +#ifndef DUNST_DUNST_H +#define DUNST_DUNST_H + +#include <glib.h> +#include <stdbool.h> +#include <stdio.h> + +#include "notification.h" + +#define PERR(msg, errnum) printf("(%d) %s : %s\n", __LINE__, (msg), (strerror(errnum))) + +#define ColLast 3 +#define ColFrame 2 +#define ColFG 1 +#define ColBG 0 + +extern GSList *rules; +extern const char *color_strings[3][3]; + +/* return id of notification */ +gboolean run(void *data); +void wake_up(void); + +int dunst_main(int argc, char *argv[]); + +void check_timeouts(void); +void usage(int exit_status); +void print_version(void); +char *extract_urls(const char *str); +void context_menu(void); +void wake_up(void); +void pause_signal_handler(int sig); + +#endif +/* vim: set tabstop=8 shiftwidth=8 expandtab textwidth=0: */ diff --git a/packages/dunst/src/markup.c b/packages/dunst/src/markup.c new file mode 100644 index 0000000..cd91ff8 --- /dev/null +++ b/packages/dunst/src/markup.c @@ -0,0 +1,109 @@ +/* copyright 2013 Sascha Kruse and contributors (see LICENSE for licensing information) */ + +#include "markup.h" + +#include <assert.h> +#include <stdbool.h> + +#include "settings.h" +#include "utils.h" + +static char *markup_quote(char *str) +{ + assert(str != NULL); + + str = string_replace_all("&", "&", str); + str = string_replace_all("\"", """, str); + str = string_replace_all("'", "'", str); + str = string_replace_all("<", "<", str); + str = string_replace_all(">", ">", str); + + return str; +} + +static char *markup_unquote(char *str) +{ + assert(str != NULL); + + str = string_replace_all(""", "\"", str); + str = string_replace_all("'", "'", str); + str = string_replace_all("<", "<", str); + str = string_replace_all(">", ">", str); + str = string_replace_all("&", "&", str); + + return str; +} + +static char *markup_br2nl(char *str) +{ + assert(str != NULL); + + str = string_replace_all("<br>", "\n", str); + str = string_replace_all("<br/>", "\n", str); + str = string_replace_all("<br />", "\n", str); + return str; +} + +/* + * Strip any markup from text; turn it in to plain text. + * + * For well-formed markup, the following two commands should be + * roughly equivalent: + * + * out = markup_strip(in); + * pango_parse_markup(in, -1, 0, NULL, &out, NULL, NULL); + * + * However, `pango_parse_markup()` balks at invalid markup; + * `markup_strip()` shouldn't care if there is invalid markup. + */ +char *markup_strip(char *str) +{ + if (str == NULL) { + return NULL; + } + + /* strip all tags */ + string_strip_delimited(str, '<', '>'); + + /* unquote the remainder */ + str = markup_unquote(str); + + return str; +} + +/* + * Transform the string in accordance with `markup_mode` and + * `settings.ignore_newline` + */ +char *markup_transform(char *str, enum markup_mode markup_mode) +{ + if (str == NULL) { + return NULL; + } + + switch (markup_mode) { + case MARKUP_NULL: + /* `assert(false)`, but with a meaningful error message */ + assert(markup_mode != MARKUP_NULL); + break; + case MARKUP_NO: + str = markup_quote(str); + break; + case MARKUP_STRIP: + str = markup_br2nl(str); + str = markup_strip(str); + str = markup_quote(str); + break; + case MARKUP_FULL: + str = markup_br2nl(str); + break; + } + + if (settings.ignore_newline) { + str = string_replace_all("\n", " ", str); + } + + return str; +} + +/* vim: set tabstop=8 shiftwidth=8 expandtab textwidth=0: */ diff --git a/packages/dunst/src/markup.h b/packages/dunst/src/markup.h new file mode 100644 index 0000000..8304e2d --- /dev/null +++ b/packages/dunst/src/markup.h @@ -0,0 +1,11 @@ +/* copyright 2013 Sascha Kruse and contributors (see LICENSE for licensing information) */ +#ifndef DUNST_MARKUP_H +#define DUNST_MARKUP_H + +#include "settings.h" + +char *markup_strip(char *str); +char *markup_transform(char *str, enum markup_mode markup_mode); + +#endif +/* vim: set tabstop=8 shiftwidth=8 expandtab textwidth=0: */ diff --git a/packages/dunst/src/menu.c b/packages/dunst/src/menu.c new file mode 100644 index 0000000..b45cdb5 --- /dev/null +++ b/packages/dunst/src/menu.c @@ -0,0 +1,262 @@ +/* copyright 2013 Sascha Kruse and contributors (see LICENSE for licensing information) */ + +#include "menu.h" + +#include <errno.h> +#include <glib.h> +#include <regex.h> +#include <stdbool.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/wait.h> +#include <unistd.h> + +#include "dbus.h" +#include "dunst.h" +#include "notification.h" +#include "queues.h" +#include "settings.h" +#include "utils.h" + +static bool is_initialized = false; +static regex_t cregex; + +static int regex_init(void) +{ + if (is_initialized) + return 1; + + char *regex = + "\\b(https?://|ftps?://|news://|mailto:|file://|www\\.)" + "[-[:alnum:]_\\@;/?:&=%$.+!*\x27,~#]*" + "(\\([-[:alnum:]_\\@;/?:&=%$.+!*\x27,~#]*\\)|[-[:alnum:]_\\@;/?:&=%$+*~])+"; + int ret = regcomp(&cregex, regex, REG_EXTENDED | REG_ICASE); + if (ret != 0) { + fputs("failed to compile regex", stderr); + return 0; + } else { + is_initialized = true; + return 1; + } +} + +void regex_teardown(void) +{ + if (is_initialized) { + regfree(&cregex); + is_initialized = false; + } +} + +/* + * Exctract all urls from a given string. + * + * Return: a string of urls separated by \n + * + */ +char *extract_urls(const char *to_match) +{ + char *urls = NULL; + + if (!regex_init()) + return NULL; + + const char *p = to_match; + regmatch_t m; + + while (1) { + int nomatch = regexec(&cregex, p, 1, &m, 0); + if (nomatch) { + return urls; + } + int start; + int finish; + if (m.rm_so == -1) { + break; + } + start = m.rm_so + (p - to_match); + finish = m.rm_eo + (p - to_match); + + char *match = g_strndup(to_match + start, finish - start); + + urls = string_append(urls, match, "\n"); + + g_free(match); + + p += m.rm_eo; + } + return urls; +} + +/* + * Open url in browser. + * + */ +void open_browser(const char *in) +{ + // remove prefix and test url + char *url = extract_urls(in); + if (!url) + return; + + int browser_pid1 = fork(); + + if (browser_pid1) { + g_free(url); + int status; + waitpid(browser_pid1, &status, 0); + } else { + int browser_pid2 = fork(); + if (browser_pid2) { + exit(0); + } else { + char *browser_cmd = + string_append(settings.browser, url, " "); + char **cmd = g_strsplit(browser_cmd, " ", 0); + execvp(cmd[0], cmd); + } + } +} + +/* + * Notify the corresponding client + * that an action has been invoked + */ +void invoke_action(const char *action) +{ + notification *invoked = NULL; + char *action_identifier = NULL; + + char *appname_begin = strchr(action, '['); + if (!appname_begin) { + printf("invalid action: %s\n", action); + return; + } + appname_begin++; + int appname_len = strlen(appname_begin) - 1; // remove ] + int action_len = strlen(action) - appname_len - 3; // remove space, [, ] + + for (const GList *iter = queues_get_displayed(); iter; + iter = iter->next) { + notification *n = iter->data; + if (g_str_has_prefix(appname_begin, n->appname) && strlen(n->appname) == appname_len) { + if (!n->actions) + continue; + + for (int i = 0; i < n->actions->count; i += 2) { + char *a_identifier = n->actions->actions[i]; + char *name = n->actions->actions[i + 1]; + if (g_str_has_prefix(action, name) && strlen(name) == action_len) { + invoked = n; + action_identifier = a_identifier; + break; + } + } + } + } + + if (invoked && action_identifier) { + action_invoked(invoked, action_identifier); + } +} + +/* + * Dispatch whatever has been returned + * by the menu. + */ +void dispatch_menu_result(const char *input) +{ + char *in = g_strdup(input); + g_strstrip(in); + if (in[0] == '#') { + invoke_action(in + 1); + } else { + open_browser(in); + } + g_free(in); +} + +/* + * Open the context menu that let's the user + * select urls/actions/etc + */ +void context_menu(void) +{ + if (settings.dmenu_cmd == NULL) { + fprintf(stderr, "dmenu command not set properly. Cowardly refusing to open the context menu.\n"); + return; + } + char *dmenu_input = NULL; + + for (const GList *iter = queues_get_displayed(); iter; + iter = iter->next) { + notification *n = iter->data; + + if (n->urls) + dmenu_input = string_append(dmenu_input, n->urls, "\n"); + + if (n->actions) + dmenu_input = + string_append(dmenu_input, n->actions->dmenu_str, + "\n"); + } + + if (!dmenu_input) + return; + + char buf[1024] = {0}; + int child_io[2]; + int parent_io[2]; + if (pipe(child_io) != 0) { + PERR("pipe()", errno); + g_free(dmenu_input); + return; + } + if (pipe(parent_io) != 0) { + PERR("pipe()", errno); + g_free(dmenu_input); + return; + } + int pid = fork(); + + if (pid == 0) { + close(child_io[1]); + close(parent_io[0]); + close(0); + if (dup(child_io[0]) == -1) { + PERR("dup()", errno); + exit(EXIT_FAILURE); + } + close(1); + if (dup(parent_io[1]) == -1) { + PERR("dup()", errno); + exit(EXIT_FAILURE); + } + execvp(settings.dmenu_cmd[0], settings.dmenu_cmd); + } else { + close(child_io[0]); + close(parent_io[1]); + size_t wlen = strlen(dmenu_input); + if (write(child_io[1], dmenu_input, wlen) != wlen) { + PERR("write()", errno); + } + close(child_io[1]); + + size_t len = read(parent_io[0], buf, 1023); + + waitpid(pid, NULL, 0); + + if (len == 0) { + g_free(dmenu_input); + return; + } + } + + close(parent_io[0]); + + dispatch_menu_result(buf); + + g_free(dmenu_input); +} +/* vim: set tabstop=8 shiftwidth=8 expandtab textwidth=0: */ diff --git a/packages/dunst/src/menu.h b/packages/dunst/src/menu.h new file mode 100644 index 0000000..298fd85 --- /dev/null +++ b/packages/dunst/src/menu.h @@ -0,0 +1,11 @@ +/* copyright 2013 Sascha Kruse and contributors (see LICENSE for licensing information) */ +#ifndef DUNST_MENU_H +#define DUNST_MENU_H + +char *extract_urls(const char *to_match); +void open_browser(const char *in); +void invoke_action(const char *action); +void regex_teardown(void); + +#endif +/* vim: set tabstop=8 shiftwidth=8 expandtab textwidth=0: */ diff --git a/packages/dunst/src/notification.c b/packages/dunst/src/notification.c new file mode 100644 index 0000000..4459c58 --- /dev/null +++ b/packages/dunst/src/notification.c @@ -0,0 +1,580 @@ +/* copyright 2013 Sascha Kruse and contributors (see LICENSE for licensing information) */ + +#include "notification.h" + +#include <assert.h> +#include <errno.h> +#include <glib.h> +#include <libgen.h> +#include <stdbool.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/wait.h> +#include <unistd.h> + +#include "dbus.h" +#include "dunst.h" +#include "markup.h" +#include "menu.h" +#include "queues.h" +#include "rules.h" +#include "settings.h" +#include "utils.h" +#include "x11/x.h" + +/* + * print a human readable representation + * of the given notification to stdout. + */ +void notification_print(notification *n) +{ + printf("{\n"); + printf("\tappname: '%s'\n", n->appname); + printf("\tsummary: '%s'\n", n->summary); + printf("\tbody: '%s'\n", n->body); + printf("\ticon: '%s'\n", n->icon); + printf("\traw_icon set: %s\n", (n->raw_icon ? "true" : "false")); + printf("\tcategory: %s\n", n->category); + printf("\ttimeout: %ld\n", n->timeout/1000); + printf("\turgency: %s\n", notification_urgency_to_string(n->urgency)); + printf("\ttransient: %d\n", n->transient); + printf("\tformatted: '%s'\n", n->msg); + printf("\tfg: %s\n", n->color_strings[ColFG]); + printf("\tbg: %s\n", n->color_strings[ColBG]); + printf("\tframe: %s\n", n->color_strings[ColFrame]); + printf("\tid: %d\n", n->id); + if (n->urls) { + printf("\turls:\n"); + printf("\t{\n"); + printf("\t\t%s\n", n->urls); + printf("\t}\n"); + } + + if (n->actions) { + printf("\tactions:\n"); + printf("\t{\n"); + for (int i = 0; i < n->actions->count; i += 2) { + printf("\t\t[%s,%s]\n", n->actions->actions[i], + n->actions->actions[i + 1]); + } + printf("\t}\n"); + printf("\tactions_dmenu: %s\n", n->actions->dmenu_str); + } + printf("\tscript: %s\n", n->script); + printf("}\n"); +} + +/* + * Run the script associated with the + * given notification. + */ +void notification_run_script(notification *n) +{ + if (!n->script || strlen(n->script) < 1) + return; + + char *appname = n->appname ? n->appname : ""; + char *summary = n->summary ? n->summary : ""; + char *body = n->body ? n->body : ""; + char *icon = n->icon ? n->icon : ""; + + const char *urgency = notification_urgency_to_string(n->urgency); + + int pid1 = fork(); + + if (pid1) { + int status; + waitpid(pid1, &status, 0); + } else { + int pid2 = fork(); + if (pid2) { + exit(0); + } else { + int ret = execlp(n->script, + n->script, + appname, + summary, + body, + icon, + urgency, + (char *)NULL); + if (ret != 0) { + PERR("Unable to run script", errno); + exit(EXIT_FAILURE); + } + } + } +} + +/* + * Helper function to convert an urgency to a string + */ +const char *notification_urgency_to_string(enum urgency urgency) +{ + switch (urgency) { + case URG_NONE: + return "NONE"; + case URG_LOW: + return "LOW"; + case URG_NORM: + return "NORMAL"; + case URG_CRIT: + return "CRITICAL"; + default: + return "UNDEF"; + } +} + +/* + * Helper function to compare to given + * notifications. + */ +int notification_cmp(const void *va, const void *vb) +{ + notification *a = (notification *) va; + notification *b = (notification *) vb; + + if (!settings.sort) + return 1; + + if (a->urgency != b->urgency) { + return b->urgency - a->urgency; + } else { + return a->id - b->id; + } +} + +/* + * Wrapper for notification_cmp to match glib's + * compare functions signature. + */ +int notification_cmp_data(const void *va, const void *vb, void *data) +{ + return notification_cmp(va, vb); +} + +int notification_is_duplicate(const notification *a, const notification *b) +{ + //Comparing raw icons is not supported, assume they are not identical + if (settings.icon_position != icons_off + && (a->raw_icon != NULL || b->raw_icon != NULL)) + return false; + + return strcmp(a->appname, b->appname) == 0 + && strcmp(a->summary, b->summary) == 0 + && strcmp(a->body, b->body) == 0 + && (settings.icon_position != icons_off ? strcmp(a->icon, b->icon) == 0 : 1) + && a->urgency == b->urgency; +} + +/* + * Free the actions element + * @a: (nullable): Pointer to #Actions + */ +void actions_free(Actions *a) +{ + if (!a) + return; + + g_strfreev(a->actions); + g_free(a->dmenu_str); + g_free(a); +} + +/* + * Free a #RawImage + * @i: (nullable): pointer to #RawImage + */ +void rawimage_free(RawImage *i) +{ + if (!i) + return; + + g_free(i->data); + g_free(i); +} + +/* + * Free the memory used by the given notification. + */ +void notification_free(notification *n) +{ + assert(n != NULL); + g_free(n->appname); + g_free(n->summary); + g_free(n->body); + g_free(n->icon); + g_free(n->msg); + g_free(n->dbus_client); + g_free(n->category); + g_free(n->text_to_render); + g_free(n->urls); + + actions_free(n->actions); + rawimage_free(n->raw_icon); + + g_free(n); +} + +/* + * Replace the two chars where **needle points + * with a quoted "replacement", according to the markup settings. + * + * The needle is a double pointer and gets updated upon return + * to point to the first char, which occurs after replacement. + * + */ +void notification_replace_single_field(char **haystack, + char **needle, + const char *replacement, + enum markup_mode markup_mode) +{ + + assert(*needle[0] == '%'); + // needle has to point into haystack (but not on the last char) + assert(*needle >= *haystack); + assert(*needle - *haystack < strlen(*haystack) - 1); + + int pos = *needle - *haystack; + + char *input = markup_transform(g_strdup(replacement), markup_mode); + *haystack = string_replace_at(*haystack, pos, 2, input); + + // point the needle to the next char + // which was originally in haystack + *needle = *haystack + pos + strlen(input); + + g_free(input); +} + +char *notification_extract_markup_urls(char **str_ptr) +{ + char *start, *end, *replace_buf, *str, *urls = NULL, *url, *index_buf; + int linkno = 1; + + str = *str_ptr; + while ((start = strstr(str, "<a href")) != NULL) { + end = strstr(start, ">"); + if (end != NULL) { + replace_buf = g_strndup(start, end - start + 1); + url = extract_urls(replace_buf); + if (url != NULL) { + str = string_replace(replace_buf, "[", str); + + index_buf = g_strdup_printf("[#%d]", linkno++); + if (urls == NULL) { + urls = g_strconcat(index_buf, " ", url, NULL); + } else { + char *tmp = urls; + urls = g_strconcat(tmp, "\n", index_buf, " ", url, NULL); + g_free(tmp); + } + + index_buf[0] = ' '; + str = string_replace("</a>", index_buf, str); + g_free(index_buf); + g_free(url); + } else { + str = string_replace(replace_buf, "", str); + str = string_replace("</a>", "", str); + } + g_free(replace_buf); + } else { + break; + } + } + *str_ptr = str; + return urls; +} + +/* + * Create notification struct and initialise everything to NULL, + * this function is guaranteed to return a valid pointer. + */ +notification *notification_create(void) +{ + return g_malloc0(sizeof(notification)); +} + +void notification_init_defaults(notification *n) +{ + assert(n != NULL); + if(n->appname == NULL) n->appname = g_strdup("unknown"); + if(n->summary == NULL) n->summary = g_strdup(""); + if(n->body == NULL) n->body = g_strdup(""); + if(n->category == NULL) n->category = g_strdup(""); +} + +/* + * Initialize the given notification + * + * n should be a pointer to a notification allocated with + * notification_create, it is undefined behaviour to pass a notification + * allocated some other way. + */ +void notification_init(notification *n) +{ + assert(n != NULL); + + //Prevent undefined behaviour by initialising required fields + notification_init_defaults(n); + + n->script = NULL; + n->text_to_render = NULL; + + n->format = settings.format; + + rule_apply_all(n); + + if (n->icon != NULL && strlen(n->icon) <= 0) { + g_free(n->icon); + n->icon = NULL; + } + + if (n->raw_icon == NULL && n->icon == NULL) { + n->icon = g_strdup(settings.icons[n->urgency]); + } + + n->urls = notification_extract_markup_urls(&(n->body)); + + n->msg = string_replace_all("\\n", "\n", g_strdup(n->format)); + + /* replace all formatter */ + for(char *substr = strchr(n->msg, '%'); + substr; + substr = strchr(substr, '%')) { + + char pg[16]; + char *icon_tmp; + + switch(substr[1]) { + case 'a': + notification_replace_single_field( + &n->msg, + &substr, + n->appname, + MARKUP_NO); + break; + case 's': + notification_replace_single_field( + &n->msg, + &substr, + n->summary, + n->markup); + break; + case 'b': + notification_replace_single_field( + &n->msg, + &substr, + n->body, + n->markup); + break; + case 'I': + icon_tmp = g_strdup(n->icon); + notification_replace_single_field( + &n->msg, + &substr, + icon_tmp ? basename(icon_tmp) : "", + MARKUP_NO); + g_free(icon_tmp); + break; + case 'i': + notification_replace_single_field( + &n->msg, + &substr, + n->icon ? n->icon : "", + MARKUP_NO); + break; + case 'p': + if (n->progress != -1) + sprintf(pg, "[%3d%%]", n->progress); + + notification_replace_single_field( + &n->msg, + &substr, + n->progress != -1 ? pg : "", + MARKUP_NO); + break; + case 'n': + if (n->progress != -1) + sprintf(pg, "%d", n->progress); + + notification_replace_single_field( + &n->msg, + &substr, + n->progress != -1 ? pg : "", + MARKUP_NO); + break; + case '%': + notification_replace_single_field( + &n->msg, + &substr, + "%", + MARKUP_NO); + break; + case '\0': + fprintf(stderr, "WARNING: format_string has trailing %% character." + "To escape it use %%%%."); + break; + default: + fprintf(stderr, "WARNING: format_string %%%c" + " is unknown\n", substr[1]); + // shift substr pointer forward, + // as we can't interpret the format string + substr++; + break; + } + } + + n->msg = g_strchomp(n->msg); + + /* truncate overlong messages */ + if (strlen(n->msg) > DUNST_NOTIF_MAX_CHARS) { + char *buffer = g_malloc(DUNST_NOTIF_MAX_CHARS); + strncpy(buffer, n->msg, DUNST_NOTIF_MAX_CHARS); + buffer[DUNST_NOTIF_MAX_CHARS-1] = '\0'; + + g_free(n->msg); + n->msg = buffer; + } + + n->dup_count = 0; + + /* urgency > URG_CRIT -> array out of range */ + if (n->urgency < URG_MIN) + n->urgency = URG_LOW; + if (n->urgency > URG_MAX) + n->urgency = URG_CRIT; + + if (!n->color_strings[ColFG]) { + n->color_strings[ColFG] = xctx.color_strings[ColFG][n->urgency]; + } + + if (!n->color_strings[ColBG]) { + n->color_strings[ColBG] = xctx.color_strings[ColBG][n->urgency]; + } + + if (!n->color_strings[ColFrame]) { + n->color_strings[ColFrame] = xctx.color_strings[ColFrame][n->urgency]; + } + + n->timeout = + n->timeout < 0 ? settings.timeouts[n->urgency] : n->timeout; + n->start = 0; + + n->timestamp = g_get_monotonic_time(); + + n->redisplayed = false; + + n->first_render = true; + + char *tmp = g_strconcat(n->summary, " ", n->body, NULL); + + char *tmp_urls = extract_urls(tmp); + n->urls = string_append(n->urls, tmp_urls, "\n"); + g_free(tmp_urls); + + if (n->actions) { + n->actions->dmenu_str = NULL; + for (int i = 0; i < n->actions->count; i += 2) { + char *human_readable = n->actions->actions[i + 1]; + string_replace_char('[', '(', human_readable); // kill square brackets + string_replace_char(']', ')', human_readable); + + char *act_str = g_strdup_printf("#%s [%s]", human_readable, n->appname); + if (act_str) { + n->actions->dmenu_str = string_append(n->actions->dmenu_str, act_str, "\n"); + g_free(act_str); + } + } + } + + g_free(tmp); +} + +void notification_update_text_to_render(notification *n) +{ + g_free(n->text_to_render); + n->text_to_render = NULL; + + char *buf = NULL; + + char *msg = g_strchomp(n->msg); + + /* print dup_count and msg */ + if ((n->dup_count > 0 && !settings.hide_duplicate_count) + && (n->actions || n->urls) && settings.show_indicators) { + buf = g_strdup_printf("(%d%s%s) %s", + n->dup_count, + n->actions ? "A" : "", + n->urls ? "U" : "", msg); + } else if ((n->actions || n->urls) && settings.show_indicators) { + buf = g_strdup_printf("(%s%s) %s", + n->actions ? "A" : "", + n->urls ? "U" : "", msg); + } else if (n->dup_count > 0 && !settings.hide_duplicate_count) { + buf = g_strdup_printf("(%d) %s", n->dup_count, msg); + } else { + buf = g_strdup(msg); + } + + /* print age */ + gint64 hours, minutes, seconds; + gint64 t_delta = g_get_monotonic_time() - n->timestamp; + + if (settings.show_age_threshold >= 0 + && t_delta >= settings.show_age_threshold) { + hours = t_delta / G_USEC_PER_SEC / 3600; + minutes = t_delta / G_USEC_PER_SEC / 60 % 60; + seconds = t_delta / G_USEC_PER_SEC % 60; + + char *new_buf; + if (hours > 0) { + new_buf = + g_strdup_printf("%s (%ldh %ldm %lds old)", buf, hours, + minutes, seconds); + } else if (minutes > 0) { + new_buf = + g_strdup_printf("%s (%ldm %lds old)", buf, minutes, + seconds); + } else { + new_buf = g_strdup_printf("%s (%lds old)", buf, seconds); + } + + g_free(buf); + buf = new_buf; + } + + n->text_to_render = buf; +} + +/* + * If the notification has exactly one action, or one is marked as default, + * invoke it. If there are multiple and no default, open the context menu. If + * there are no actions, proceed similarly with urls. + */ +void notification_do_action(notification *n) +{ + if (n->actions) { + if (n->actions->count == 2) { + action_invoked(n, n->actions->actions[0]); + return; + } + for (int i = 0; i < n->actions->count; i += 2) { + if (strcmp(n->actions->actions[i], "default") == 0) { + action_invoked(n, n->actions->actions[i]); + return; + } + } + context_menu(); + + } else if (n->urls) { + if (strstr(n->urls, "\n") == NULL) + open_browser(n->urls); + else + context_menu(); + } +} + +/* vim: set tabstop=8 shiftwidth=8 expandtab textwidth=0: */ diff --git a/packages/dunst/src/notification.h b/packages/dunst/src/notification.h new file mode 100644 index 0000000..c7da691 --- /dev/null +++ b/packages/dunst/src/notification.h @@ -0,0 +1,87 @@ +/* copyright 2013 Sascha Kruse and contributors (see LICENSE for licensing information) */ +#ifndef DUNST_NOTIFICATION_H +#define DUNST_NOTIFICATION_H + +#include <glib.h> +#include <stdbool.h> + +#include "settings.h" + +#define DUNST_NOTIF_MAX_CHARS 5000 + +enum urgency { + URG_NONE = -1, + URG_MIN = 0, + URG_LOW = 0, + URG_NORM = 1, + URG_CRIT = 2, + URG_MAX = 2, +}; + +typedef struct _raw_image { + int width; + int height; + int rowstride; + int has_alpha; + int bits_per_sample; + int n_channels; + unsigned char *data; +} RawImage; + +typedef struct _actions { + char **actions; + char *dmenu_str; + gsize count; +} Actions; + +typedef struct _notification { + char *appname; + char *summary; + char *body; + char *icon; + RawImage *raw_icon; + char *msg; /* formatted message */ + char *category; + char *text_to_render; + const char *format; + char *dbus_client; + gint64 start; + gint64 timestamp; + gint64 timeout; + enum urgency urgency; + enum markup_mode markup; + bool redisplayed; /* has been displayed before? */ + int id; + int dup_count; + int displayed_height; + const char *color_strings[3]; + bool first_render; + bool transient; + + int progress; /* percentage (-1: undefined) */ + int history_ignore; + const char *script; + char *urls; + Actions *actions; +} notification; + +notification *notification_create(void); +void notification_init(notification *n); +void actions_free(Actions *a); +void rawimage_free(RawImage *i); +void notification_free(notification *n); +int notification_cmp(const void *a, const void *b); +int notification_cmp_data(const void *a, const void *b, void *data); +int notification_is_duplicate(const notification *a, const notification *b); +void notification_run_script(notification *n); +void notification_print(notification *n); +void notification_replace_single_field(char **haystack, + char **needle, + const char *replacement, + enum markup_mode markup_mode); +void notification_update_text_to_render(notification *n); +void notification_do_action(notification *n); + +const char *notification_urgency_to_string(enum urgency urgency); +#endif +/* vim: set tabstop=8 shiftwidth=8 expandtab textwidth=0: */ diff --git a/packages/dunst/src/option_parser.c b/packages/dunst/src/option_parser.c new file mode 100644 index 0000000..7641ccb --- /dev/null +++ b/packages/dunst/src/option_parser.c @@ -0,0 +1,565 @@ +/* copyright 2013 Sascha Kruse and contributors (see LICENSE for licensing information) */ + +#include "option_parser.h" + +#include <glib.h> +#include <stdbool.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#include "utils.h" + +typedef struct _entry_t { + char *key; + char *value; +} entry_t; + +typedef struct _section_t { + char *name; + int entry_count; + entry_t *entries; +} section_t; + +static int section_count = 0; +static section_t *sections; + +static section_t *new_section(const char *name); +static section_t *get_section(const char *name); +static void add_entry(const char *section_name, const char *key, const char *value); +static const char *get_value(const char *section, const char *key); +static char *clean_value(const char *value); + +static int cmdline_argc; +static char **cmdline_argv; + +static char *usage_str = NULL; +static void cmdline_usage_append(const char *key, const char *type, const char *description); + +static int cmdline_find_option(const char *key); + +section_t *new_section(const char *name) +{ + for (int i = 0; i < section_count; i++) { + if (!strcmp(name, sections[i].name)) { + die("Duplicated section in dunstrc detected.\n", -1); + } + } + + section_count++; + sections = g_realloc(sections, sizeof(section_t) * section_count); + sections[section_count - 1].name = g_strdup(name); + sections[section_count - 1].entries = NULL; + sections[section_count - 1].entry_count = 0; + return §ions[section_count - 1]; +} + +void free_ini(void) +{ + for (int i = 0; i < section_count; i++) { + for (int j = 0; j < sections[i].entry_count; j++) { + g_free(sections[i].entries[j].key); + g_free(sections[i].entries[j].value); + } + g_free(sections[i].entries); + g_free(sections[i].name); + } + g_free(sections); + section_count = 0; + sections = NULL; +} + +section_t *get_section(const char *name) +{ + for (int i = 0; i < section_count; i++) { + if (strcmp(sections[i].name, name) == 0) + return §ions[i]; + } + + return NULL; +} + +void add_entry(const char *section_name, const char *key, const char *value) +{ + section_t *s = get_section(section_name); + if (s == NULL) { + s = new_section(section_name); + } + + s->entry_count++; + int len = s->entry_count; + s->entries = g_realloc(s->entries, sizeof(entry_t) * len); + s->entries[s->entry_count - 1].key = g_strdup(key); + s->entries[s->entry_count - 1].value = clean_value(value); +} + +const char *get_value(const char *section, const char *key) +{ + section_t *s = get_section(section); + if (!s) { + return NULL; + } + + for (int i = 0; i < s->entry_count; i++) { + if (strcmp(s->entries[i].key, key) == 0) { + return s->entries[i].value; + } + } + return NULL; +} + +char *ini_get_path(const char *section, const char *key, const char *def) +{ + return string_to_path(ini_get_string(section, key, def)); +} + +char *ini_get_string(const char *section, const char *key, const char *def) +{ + const char *value = get_value(section, key); + if (value) + return g_strdup(value); + + return def ? g_strdup(def) : NULL; +} + +gint64 ini_get_time(const char *section, const char *key, gint64 def) +{ + const char *timestring = get_value(section, key); + gint64 val = def; + + if (timestring) { + val = string_to_time(timestring); + } + + return val; +} + +int ini_get_int(const char *section, const char *key, int def) +{ + const char *value = get_value(section, key); + if (value == NULL) + return def; + else + return atoi(value); +} + +double ini_get_double(const char *section, const char *key, double def) +{ + const char *value = get_value(section, key); + if (value == NULL) + return def; + else + return atof(value); +} + +bool ini_is_set(const char *ini_section, const char *ini_key) +{ + return get_value(ini_section, ini_key) != NULL; +} + +const char *next_section(const char *section) +{ + if (section_count == 0) + return NULL; + + if (section == NULL) { + return sections[0].name; + } + + for (int i = 0; i < section_count; i++) { + if (strcmp(section, sections[i].name) == 0) { + if (i + 1 >= section_count) + return NULL; + else + return sections[i + 1].name; + } + } + return NULL; +} + +int ini_get_bool(const char *section, const char *key, int def) +{ + const char *value = get_value(section, key); + if (value == NULL) + return def; + else { + switch (value[0]) { + case 'y': + case 'Y': + case 't': + case 'T': + case '1': + return true; + case 'n': + case 'N': + case 'f': + case 'F': + case '0': + return false; + default: + return def; + } + } +} + +char *clean_value(const char *value) +{ + char *s; + + if (value[0] == '"') + s = g_strdup(value + 1); + else + s = g_strdup(value); + + if (s[strlen(s) - 1] == '"') + s[strlen(s) - 1] = '\0'; + + return s; +} + +int load_ini_file(FILE *fp) +{ + if (!fp) + return 1; + + char *line = NULL; + size_t line_len = 0; + + int line_num = 0; + char *current_section = NULL; + while (getline(&line, &line_len, fp) != -1) { + line_num++; + + char *start = g_strstrip(line); + + if (*start == ';' || *start == '#' || strlen(start) == 0) + continue; + + if (*start == '[') { + char *end = strchr(start + 1, ']'); + if (!end) { + fprintf(stderr, + "Warning: invalid config file at line %d\n", + line_num); + fprintf(stderr, "Missing ']'\n"); + continue; + } + + *end = '\0'; + + g_free(current_section); + current_section = (g_strdup(start + 1)); + new_section(current_section); + continue; + } + + char *equal = strchr(start + 1, '='); + if (!equal) { + fprintf(stderr, + "Warning: invalid config file at line %d\n", + line_num); + fprintf(stderr, "Missing '='\n"); + continue; + } + + *equal = '\0'; + char *key = g_strstrip(start); + char *value = g_strstrip(equal + 1); + + char *quote = strchr(value, '"'); + if (quote) { + char *closing_quote = strchr(quote + 1, '"'); + if (!closing_quote) { + fprintf(stderr, + "Warning: invalid config file at line %d\n", + line_num); + fprintf(stderr, "Missing '\"'\n"); + continue; + } + } else { + char *comment = strpbrk(value, "#;"); + if (comment) + *comment = '\0'; + } + value = g_strstrip(value); + + if (!current_section) { + fprintf(stderr, + "Warning: invalid config file at line %d\n", + line_num); + fprintf(stderr, "Key value pair without a section\n"); + continue; + } + + add_entry(current_section, key, value); + } + free(line); + g_free(current_section); + return 0; +} + +void cmdline_load(int argc, char *argv[]) +{ + cmdline_argc = argc; + cmdline_argv = argv; +} + +int cmdline_find_option(const char *key) +{ + if (!key) { + return -1; + } + char *key1 = g_strdup(key); + char *key2 = strchr(key1, '/'); + + if (key2) { + *key2 = '\0'; + key2++; + } + + /* look for first key */ + for (int i = 0; i < cmdline_argc; i++) { + if (strcmp(key1, cmdline_argv[i]) == 0) { + g_free(key1); + return i; + } + } + + /* look for second key if one was specified */ + if (key2) { + for (int i = 0; i < cmdline_argc; i++) { + if (strcmp(key2, cmdline_argv[i]) == 0) { + g_free(key1); + return i; + } + } + } + + g_free(key1); + return -1; +} + +static const char *cmdline_get_value(const char *key) +{ + int idx = cmdline_find_option(key); + if (idx < 0) { + return NULL; + } + + if (idx + 1 >= cmdline_argc) { + /* the argument is missing */ + fprintf(stderr, "Warning: %s, missing argument. Ignoring\n", + key); + return NULL; + } + return cmdline_argv[idx + 1]; +} + +char *cmdline_get_string(const char *key, const char *def, const char *description) +{ + cmdline_usage_append(key, "string", description); + const char *str = cmdline_get_value(key); + + if (str) + return g_strdup(str); + if (def == NULL) + return NULL; + else + return g_strdup(def); +} + +char *cmdline_get_path(const char *key, const char *def, const char *description) +{ + cmdline_usage_append(key, "string", description); + const char *str = cmdline_get_value(key); + + if (str) + return string_to_path(g_strdup(str)); + else + return string_to_path(g_strdup(def)); +} + +gint64 cmdline_get_time(const char *key, gint64 def, const char *description) +{ + cmdline_usage_append(key, "time", description); + const char *timestring = cmdline_get_value(key); + gint64 val = def; + + if (timestring) { + val = string_to_time(timestring); + } + + return val; +} + +int cmdline_get_int(const char *key, int def, const char *description) +{ + cmdline_usage_append(key, "int", description); + const char *str = cmdline_get_value(key); + + if (str == NULL) + return def; + else + return atoi(str); +} + +double cmdline_get_double(const char *key, double def, const char *description) +{ + cmdline_usage_append(key, "double", description); + const char *str = cmdline_get_value(key); + + if (str == NULL) + return def; + else + return atof(str); +} + +int cmdline_get_bool(const char *key, int def, const char *description) +{ + cmdline_usage_append(key, "", description); + int idx = cmdline_find_option(key); + + if (idx > 0) + return true; + else + return def; +} + +bool cmdline_is_set(const char *key) +{ + return cmdline_get_value(key) != NULL; +} + +char *option_get_path(const char *ini_section, + const char *ini_key, + const char *cmdline_key, + const char *def, + const char *description) +{ + char *val = NULL; + + if (cmdline_key) { + val = cmdline_get_path(cmdline_key, NULL, description); + } + + if (val) { + return val; + } else { + return ini_get_path(ini_section, ini_key, def); + } +} + +char *option_get_string(const char *ini_section, + const char *ini_key, + const char *cmdline_key, + const char *def, + const char *description) +{ + char *val = NULL; + + if (cmdline_key) { + val = cmdline_get_string(cmdline_key, NULL, description); + } + + if (val) { + return val; + } else { + return ini_get_string(ini_section, ini_key, def); + } +} + +gint64 option_get_time(const char *ini_section, + const char *ini_key, + const char *cmdline_key, + gint64 def, + const char *description) +{ + gint64 ini_val = ini_get_time(ini_section, ini_key, def); + return cmdline_get_time(cmdline_key, ini_val, description); +} + +int option_get_int(const char *ini_section, + const char *ini_key, + const char *cmdline_key, + int def, + const char *description) +{ + /* *str is only used to check wether the cmdline option is actually set. */ + const char *str = cmdline_get_value(cmdline_key); + + /* we call cmdline_get_int even when the option isn't set in order to + * add the usage info */ + int val = cmdline_get_int(cmdline_key, def, description); + + if (!str) + return ini_get_int(ini_section, ini_key, def); + else + return val; +} + +double option_get_double(const char *ini_section, + const char *ini_key, + const char *cmdline_key, + double def, + const char *description) +{ + const char *str = cmdline_get_value(cmdline_key); + double val = cmdline_get_double(cmdline_key, def, description); + + if (!str) + return ini_get_double(ini_section, ini_key, def); + else + return val; +} + +int option_get_bool(const char *ini_section, + const char *ini_key, + const char *cmdline_key, + int def, + const char *description) +{ + int val = false; + + if (cmdline_key) + val = cmdline_get_bool(cmdline_key, false, description); + + if (cmdline_key && val) { + /* this can only be true if the value has been set, + * so we can return */ + return true; + } + + return ini_get_bool(ini_section, ini_key, def); +} + +void cmdline_usage_append(const char *key, const char *type, const char *description) +{ + char *key_type; + if (type && strlen(type) > 0) + key_type = g_strdup_printf("%s (%s)", key, type); + else + key_type = g_strdup(key); + + if (!usage_str) { + usage_str = + g_strdup_printf("%-40s - %s\n", key_type, description); + g_free(key_type); + return; + } + + char *tmp; + tmp = + g_strdup_printf("%s%-40s - %s\n", usage_str, key_type, description); + g_free(key_type); + + g_free(usage_str); + usage_str = tmp; + +} + +const char *cmdline_create_usage(void) +{ + return usage_str; +} + +/* vim: set tabstop=8 shiftwidth=8 expandtab textwidth=0: */ diff --git a/packages/dunst/src/option_parser.h b/packages/dunst/src/option_parser.h new file mode 100644 index 0000000..e816b7e --- /dev/null +++ b/packages/dunst/src/option_parser.h @@ -0,0 +1,67 @@ +/* copyright 2013 Sascha Kruse and contributors (see LICENSE for licensing information) */ +#ifndef DUNST_OPTION_PARSER_H +#define DUNST_OPTION_PARSER_H + +#include <glib.h> +#include <stdbool.h> +#include <stdio.h> + +int load_ini_file(FILE *); +char *ini_get_path(const char *section, const char *key, const char *def); +char *ini_get_string(const char *section, const char *key, const char *def); +gint64 ini_get_time(const char *section, const char *key, gint64 def); +int ini_get_int(const char *section, const char *key, int def); +double ini_get_double(const char *section, const char *key, double def); +int ini_get_bool(const char *section, const char *key, int def); +bool ini_is_set(const char *ini_section, const char *ini_key); +void free_ini(void); + +void cmdline_load(int argc, char *argv[]); +/* for all cmdline_get_* key can be either "-key" or "-key/-longkey" */ +char *cmdline_get_string(const char *key, const char *def, const char *description); +char *cmdline_get_path(const char *key, const char *def, const char *description); +int cmdline_get_int(const char *key, int def, const char *description); +double cmdline_get_double(const char *key, double def, const char *description); +int cmdline_get_bool(const char *key, int def, const char *description); +bool cmdline_is_set(const char *key); +const char *cmdline_create_usage(void); + +char *option_get_string(const char *ini_section, + const char *ini_key, + const char *cmdline_key, + const char *def, + const char *description); +char *option_get_path(const char *ini_section, + const char *ini_key, + const char *cmdline_key, + const char *def, + const char *description); +gint64 option_get_time(const char *ini_section, + const char *ini_key, + const char *cmdline_key, + gint64 def, + const char *description); +int option_get_int(const char *ini_section, + const char *ini_key, + const char *cmdline_key, + int def, + const char *description); +double option_get_double(const char *ini_section, + const char *ini_key, + const char *cmdline_key, + double def, + const char *description); +int option_get_bool(const char *ini_section, + const char *ini_key, + const char *cmdline_key, + int def, + const char *description); + +/* returns the next known section. + * if section == NULL returns first section. + * returns NULL if no more sections are available + */ +const char *next_section(const char *section); + +#endif +/* vim: set tabstop=8 shiftwidth=8 expandtab textwidth=0: */ diff --git a/packages/dunst/src/queues.c b/packages/dunst/src/queues.c new file mode 100644 index 0000000..fa9da51 --- /dev/null +++ b/packages/dunst/src/queues.c @@ -0,0 +1,384 @@ +/* copyright 2013 Sascha Kruse and contributors (see LICENSE for licensing information) */ + +#include "queues.h" + +#include <assert.h> +#include <glib.h> +#include <stdio.h> +#include <string.h> + +#include "notification.h" +#include "settings.h" + +/* notification lists */ +static GQueue *waiting = NULL; /* all new notifications get into here */ +static GQueue *displayed = NULL; /* currently displayed notifications */ +static GQueue *history = NULL; /* history of displayed notifications */ + +unsigned int displayed_limit = 0; +int next_notification_id = 1; +bool pause_displayed = false; + +static bool queues_stack_duplicate(notification *n); + +void queues_init(void) +{ + history = g_queue_new(); + displayed = g_queue_new(); + waiting = g_queue_new(); +} + +void queues_displayed_limit(unsigned int limit) +{ + displayed_limit = limit; +} + +/* misc getter functions */ +const GList *queues_get_displayed() +{ + return g_queue_peek_head_link(displayed); +} +unsigned int queues_length_waiting() +{ + return waiting->length; +} +unsigned int queues_length_displayed() +{ + return displayed->length; +} +unsigned int queues_length_history() +{ + return history->length; +} + +int queues_notification_insert(notification *n, int replaces_id) +{ + + /* do not display the message, if the message is empty */ + if (strlen(n->msg) == 0) { + if (settings.always_run_script) { + notification_run_script(n); + } + printf("skipping notification: %s %s\n", n->body, n->summary); + return 0; + } + /* Do not insert the message if it's a command */ + if (strcmp("DUNST_COMMAND_PAUSE", n->summary) == 0) { + pause_displayed = true; + return 0; + } + if (strcmp("DUNST_COMMAND_RESUME", n->summary) == 0) { + pause_displayed = false; + return 0; + } + + if (replaces_id == 0) { + n->id = ++next_notification_id; + if (!settings.stack_duplicates || !queues_stack_duplicate(n)) + g_queue_insert_sorted(waiting, n, notification_cmp_data, NULL); + } else { + n->id = replaces_id; + if (!queues_notification_replace_id(n)) + g_queue_insert_sorted(waiting, n, notification_cmp_data, NULL); + } + + if (settings.print_notifications) + notification_print(n); + + return n->id; +} + +/* + * Replaces duplicate notification and stacks it + * + * Returns %true, if notification got stacked + * Returns %false, if notification did not get stacked + */ +static bool queues_stack_duplicate(notification *n) +{ + for (GList *iter = g_queue_peek_head_link(displayed); iter; + iter = iter->next) { + notification *orig = iter->data; + if (notification_is_duplicate(orig, n)) { + /* If the progress differs, probably notify-send was used to update the notification + * So only count it as a duplicate, if the progress was not the same. + * */ + if (orig->progress == n->progress) { + orig->dup_count++; + } else { + orig->progress = n->progress; + } + + iter->data = n; + + n->start = g_get_monotonic_time(); + + n->dup_count = orig->dup_count; + + notification_closed(orig, 1); + + notification_free(orig); + return true; + } + } + + for (GList *iter = g_queue_peek_head_link(waiting); iter; + iter = iter->next) { + notification *orig = iter->data; + if (notification_is_duplicate(orig, n)) { + /* If the progress differs, probably notify-send was used to update the notification + * So only count it as a duplicate, if the progress was not the same. + * */ + if (orig->progress == n->progress) { + orig->dup_count++; + } else { + orig->progress = n->progress; + } + iter->data = n; + + n->dup_count = orig->dup_count; + + notification_closed(orig, 1); + + notification_free(orig); + return true; + } + } + + return false; +} + +bool queues_notification_replace_id(notification *new) +{ + + for (GList *iter = g_queue_peek_head_link(displayed); + iter; + iter = iter->next) { + notification *old = iter->data; + if (old->id == new->id) { + iter->data = new; + new->start = g_get_monotonic_time(); + new->dup_count = old->dup_count; + notification_run_script(new); + notification_free(old); + return true; + } + } + + for (GList *iter = g_queue_peek_head_link(waiting); + iter; + iter = iter->next) { + notification *old = iter->data; + if (old->id == new->id) { + iter->data = new; + new->dup_count = old->dup_count; + notification_free(old); + return true; + } + } + return false; +} + +int queues_notification_close_id(int id, enum reason reason) +{ + notification *target = NULL; + + for (GList *iter = g_queue_peek_head_link(displayed); iter; + iter = iter->next) { + notification *n = iter->data; + if (n->id == id) { + g_queue_remove(displayed, n); + target = n; + break; + } + } + + for (GList *iter = g_queue_peek_head_link(waiting); iter; + iter = iter->next) { + notification *n = iter->data; + if (n->id == id) { + assert(target == NULL); + g_queue_remove(waiting, n); + target = n; + break; + } + } + + if (target) { + notification_closed(target, reason); + queues_history_push(target); + } + + return reason; +} + +int queues_notification_close(notification *n, enum reason reason) +{ + assert(n != NULL); + return queues_notification_close_id(n->id, reason); +} + +void queues_history_pop(void) +{ + if (g_queue_is_empty(history)) + return; + + notification *n = g_queue_pop_tail(history); + n->redisplayed = true; + n->start = 0; + n->timeout = settings.sticky_history ? 0 : n->timeout; + g_queue_push_head(waiting, n); +} + +void queues_history_push(notification *n) +{ + if (!n->history_ignore) { + if (settings.history_length > 0 && history->length >= settings.history_length) { + notification *to_free = g_queue_pop_head(history); + notification_free(to_free); + } + + g_queue_push_tail(history, n); + } else { + notification_free(n); + } +} + +void queues_history_push_all(void) +{ + while (displayed->length > 0) { + queues_notification_close(g_queue_peek_head_link(displayed)->data, REASON_USER); + } + + while (waiting->length > 0) { + queues_notification_close(g_queue_peek_head_link(waiting)->data, REASON_USER); + } +} + +void queues_check_timeouts(bool idle) +{ + /* nothing to do */ + if (displayed->length == 0) + return; + + GList *iter = g_queue_peek_head_link(displayed); + while (iter) { + notification *n = iter->data; + + /* + * Update iter to the next item before we either exit the + * current iteration of the loop or potentially delete the + * notification which would invalidate the pointer. + */ + iter = iter->next; + + /* don't timeout when user is idle */ + if (idle && !n->transient) { + n->start = g_get_monotonic_time(); + continue; + } + + /* skip hidden and sticky messages */ + if (n->start == 0 || n->timeout == 0) { + continue; + } + + /* remove old message */ + if (g_get_monotonic_time() - n->start > n->timeout) { + queues_notification_close(n, REASON_TIME); + } + } +} + +void queues_update() +{ + if (pause_displayed) { + while (displayed->length > 0) { + g_queue_insert_sorted( + waiting, g_queue_pop_head(displayed), notification_cmp_data, NULL); + } + return; + } + + /* move notifications from queue to displayed */ + while (waiting->length > 0) { + + if (displayed_limit > 0 && displayed->length >= displayed_limit) { + /* the list is full */ + break; + } + + notification *n = g_queue_pop_head(waiting); + + if (!n) + return; + + n->start = g_get_monotonic_time(); + + if (!n->redisplayed && n->script) { + notification_run_script(n); + } + + g_queue_insert_sorted(displayed, n, notification_cmp_data, NULL); + } +} + +gint64 queues_get_next_datachange(gint64 time) +{ + gint64 sleep = G_MAXINT64; + + for (GList *iter = g_queue_peek_head_link(displayed); iter; + iter = iter->next) { + notification *n = iter->data; + gint64 ttl = n->timeout - (time - n->start); + + if (n->timeout > 0) { + if (ttl > 0) + sleep = MIN(sleep, ttl); + else + // while we're processing, the notification already timed out + return 0; + } + + if (settings.show_age_threshold >= 0) { + gint64 age = time - n->timestamp; + + if (age > settings.show_age_threshold) + // sleep exactly until the next shift of the second happens + sleep = MIN(sleep, ((G_USEC_PER_SEC) - (age % (G_USEC_PER_SEC)))); + else if (ttl > settings.show_age_threshold) + sleep = MIN(sleep, settings.show_age_threshold); + } + } + + return sleep != G_MAXINT64 ? sleep : -1; +} + +void queues_pause_on(void) +{ + pause_displayed = true; +} + +void queues_pause_off(void) +{ + pause_displayed = false; +} + +bool queues_pause_status(void) +{ + return pause_displayed; +} + +static void teardown_notification(gpointer data) +{ + notification *n = data; + notification_free(n); +} + +void teardown_queues(void) +{ + g_queue_free_full(history, teardown_notification); + g_queue_free_full(displayed, teardown_notification); + g_queue_free_full(waiting, teardown_notification); +} +/* vim: set tabstop=8 shiftwidth=8 expandtab textwidth=0: */ diff --git a/packages/dunst/src/queues.h b/packages/dunst/src/queues.h new file mode 100644 index 0000000..f280cd5 --- /dev/null +++ b/packages/dunst/src/queues.h @@ -0,0 +1,133 @@ +/* copyright 2013 Sascha Kruse and contributors (see LICENSE for licensing information) */ + +#ifndef DUNST_QUEUE_H +#define DUNST_QUEUE_H + +#include "dbus.h" +#include "notification.h" + +/* + * Initialise neccessary queues + */ +void queues_init(void); + +/* + * Set maximum notification count to display + * and store in displayed queue + */ +void queues_displayed_limit(unsigned int limit); + +/* + * Return read only list of notifications + */ +const GList *queues_get_displayed(); + +/* + * Returns the current amount of notifications, + * which are shown, waiting or already in history + */ +unsigned int queues_length_waiting(); +unsigned int queues_length_displayed(); +unsigned int queues_length_history(); + +/* + * Insert a fully initialized notification into queues + * Respects stack_duplicates, and notification replacement + * + * If replaces_id != 0, n replaces notification with id replaces_id + * If replaces_id == 0, n gets occupies a new position + * + * Returns the assigned notification id + * If returned id == 0, the message was dismissed + */ +int queues_notification_insert(notification *n, int replaces_id); + +/* + * Replace the notification which matches the id field of + * the new notification. The given notification is inserted + * right in the same position as the old notification. + * + * Returns true, if a matching notification has been found + * and is replaced. Else false. + */ +bool queues_notification_replace_id(notification *new); + +/* + * Close the notification that has n->id == id + * + * Sends a signal and pushes it automatically to history. + * + * After closing, call wake_up to synchronize the queues with the UI + * (which closes the notification on screen) + */ +int queues_notification_close_id(int id, enum reason reason); + +/* Close the given notification. SEE queues_notification_close_id. + * + * @n: (transfer full): The notification to close + * @reason: The reason to close + * */ +int queues_notification_close(notification *n, enum reason reason); + +/* + * Pushed the latest notification of history to the displayed queue + * and removes it from history + */ +void queues_history_pop(void); + +/* + * Push a single notification to history + * The given notification has to be removed its queue + * + * @n: (transfer full): The notification to push to history + */ +void queues_history_push(notification *n); + +/* + * Push all waiting and displayed notifications to history + */ +void queues_history_push_all(void); + +/* + * Check timeout of each notification and close it, if neccessary + */ +void queues_check_timeouts(bool idle); + +/* + * Move inserted notifications from waiting queue to displayed queue + * and show them. In displayed queue, the amount of elements is limited + * to the amount set via queues_displayed_limit + */ +void queues_update(); + +/* + * Return the distance to the next event in the queue, + * which forces an update visible to the user + * + * This may be: + * + * - notification hits timeout + * - notification's age second changes + * - notification's age threshold is hit + */ +gint64 queues_get_next_datachange(gint64 time); + +/* + * Pause queue-management of dunst + * pause_on = paused (no notifications displayed) + * pause_off = running + * + * Calling update_lists is neccessary + */ +void queues_pause_on(void); +void queues_pause_off(void); +bool queues_pause_status(void); + +/* + * Remove all notifications from all lists + * and free the notifications + */ +void teardown_queues(void); + +#endif +/* vim: set tabstop=8 shiftwidth=8 expandtab textwidth=0: */ diff --git a/packages/dunst/src/rules.c b/packages/dunst/src/rules.c new file mode 100644 index 0000000..086c627 --- /dev/null +++ b/packages/dunst/src/rules.c @@ -0,0 +1,91 @@ +/* copyright 2013 Sascha Kruse and contributors (see LICENSE for licensing information) */ + +#include "rules.h" + +#include <fnmatch.h> +#include <glib.h> + +#include "dunst.h" + +/* + * Apply rule to notification. + */ +void rule_apply(rule_t *r, notification *n) +{ + if (r->timeout != -1) + n->timeout = r->timeout; + if (r->urgency != URG_NONE) + n->urgency = r->urgency; + if (r->history_ignore != -1) + n->history_ignore = r->history_ignore; + if (r->set_transient != -1) + n->transient = r->set_transient; + if (r->markup != MARKUP_NULL) + n->markup = r->markup; + if (r->new_icon) { + g_free(n->icon); + n->icon = g_strdup(r->new_icon); + rawimage_free(n->raw_icon); + n->raw_icon = NULL; + } + if (r->fg) + n->color_strings[ColFG] = r->fg; + if (r->bg) + n->color_strings[ColBG] = r->bg; + if (r->format) + n->format = r->format; + if (r->script) + n->script = r->script; +} + +/* + * Check all rules if they match n and apply. + */ +void rule_apply_all(notification *n) +{ + for (GSList *iter = rules; iter; iter = iter->next) { + rule_t *r = iter->data; + if (rule_matches_notification(r, n)) { + rule_apply(r, n); + } + } +} + +/* + * Initialize rule with default values. + */ +void rule_init(rule_t *r) +{ + r->name = NULL; + r->appname = NULL; + r->summary = NULL; + r->body = NULL; + r->icon = NULL; + r->category = NULL; + r->msg_urgency = URG_NONE; + r->timeout = -1; + r->urgency = URG_NONE; + r->markup = MARKUP_NULL; + r->new_icon = NULL; + r->history_ignore = false; + r->match_transient = -1; + r->set_transient = -1; + r->fg = NULL; + r->bg = NULL; + r->format = NULL; +} + +/* + * Check whether rule should be applied to n. + */ +bool rule_matches_notification(rule_t *r, notification *n) +{ + return ((!r->appname || !fnmatch(r->appname, n->appname, 0)) + && (!r->summary || !fnmatch(r->summary, n->summary, 0)) + && (!r->body || !fnmatch(r->body, n->body, 0)) + && (!r->icon || !fnmatch(r->icon, n->icon, 0)) + && (!r->category || !fnmatch(r->category, n->category, 0)) + && (r->match_transient == -1 || (r->match_transient == n->transient)) + && (r->msg_urgency == URG_NONE || r->msg_urgency == n->urgency)); +} +/* vim: set tabstop=8 shiftwidth=8 expandtab textwidth=0: */ diff --git a/packages/dunst/src/rules.h b/packages/dunst/src/rules.h new file mode 100644 index 0000000..b8d1d87 --- /dev/null +++ b/packages/dunst/src/rules.h @@ -0,0 +1,43 @@ +/* copyright 2013 Sascha Kruse and contributors (see LICENSE for licensing information) */ +#ifndef DUNST_RULES_H +#define DUNST_RULES_H + +#include <glib.h> +#include <stdbool.h> + +#include "notification.h" +#include "settings.h" + +typedef struct _rule_t { + char *name; + /* filters */ + char *appname; + char *summary; + char *body; + char *icon; + char *category; + int msg_urgency; + + /* actions */ + gint64 timeout; + enum urgency urgency; + enum markup_mode markup; + int history_ignore; + int match_transient; + int set_transient; + char *new_icon; + char *fg; + char *bg; + const char *format; + const char *script; +} rule_t; + +extern GSList *rules; + +void rule_init(rule_t *r); +void rule_apply(rule_t *r, notification *n); +void rule_apply_all(notification *n); +bool rule_matches_notification(rule_t *r, notification *n); + +#endif +/* vim: set tabstop=8 shiftwidth=8 expandtab textwidth=0: */ diff --git a/packages/dunst/src/settings.c b/packages/dunst/src/settings.c new file mode 100644 index 0000000..5b61507 --- /dev/null +++ b/packages/dunst/src/settings.c @@ -0,0 +1,697 @@ +/* copyright 2013 Sascha Kruse and contributors (see LICENSE for licensing information) */ + +#include "settings.h" + +#include <glib.h> +#include <stdio.h> +#include <string.h> +#ifndef STATIC_CONFIG +#include <basedir.h> +#include <basedir_fs.h> +#endif + +#include "rules.h" // put before config.h to fix missing include +#include "config.h" +#include "dunst.h" +#include "notification.h" +#include "option_parser.h" +#include "utils.h" + +settings_t settings; + +static void parse_follow_mode(const char *mode) +{ + if (strcmp(mode, "mouse") == 0) + settings.f_mode = FOLLOW_MOUSE; + else if (strcmp(mode, "keyboard") == 0) + settings.f_mode = FOLLOW_KEYBOARD; + else if (strcmp(mode, "none") == 0) + settings.f_mode = FOLLOW_NONE; + else { + fprintf(stderr, "Warning: unknown follow mode: \"%s\"\n", mode); + settings.f_mode = FOLLOW_NONE; + } +} + +static enum markup_mode parse_markup_mode(const char *mode) +{ + if (strcmp(mode, "strip") == 0) { + return MARKUP_STRIP; + } else if (strcmp(mode, "no") == 0) { + return MARKUP_NO; + } else if (strcmp(mode, "full") == 0 || strcmp(mode, "yes") == 0) { + return MARKUP_FULL; + } else { + fprintf(stderr, "Warning: unknown markup mode: \"%s\"\n", mode); + return MARKUP_NO; + } +} + +static enum urgency ini_get_urgency(const char *section, const char *key, const int def) +{ + int ret = def; + char *urg = ini_get_string(section, key, ""); + + if (strlen(urg) > 0) { + if (strcmp(urg, "low") == 0) + ret = URG_LOW; + else if (strcmp(urg, "normal") == 0) + ret = URG_NORM; + else if (strcmp(urg, "critical") == 0) + ret = URG_CRIT; + else + fprintf(stderr, + "unknown urgency: %s, ignoring\n", + urg); + } + g_free(urg); + return ret; +} + +void load_settings(char *cmdline_config_path) +{ + +#ifndef STATIC_CONFIG + xdgHandle xdg; + FILE *config_file = NULL; + + xdgInitHandle(&xdg); + + if (cmdline_config_path != NULL) { + config_file = fopen(cmdline_config_path, "r"); + } + if (config_file == NULL) { + config_file = xdgConfigOpen("dunst/dunstrc", "r", &xdg); + } + if (config_file == NULL) { + /* Fall back to just "dunstrc", which was used before 2013-06-23 + * (before v0.2). */ + config_file = xdgConfigOpen("dunstrc", "r", &xdg); + if (config_file == NULL) { + puts("no dunstrc found -> skipping\n"); + xdgWipeHandle(&xdg); + } + } + + load_ini_file(config_file); +#else + fprintf(stderr, "Warning: dunstrc parsing disabled. " + "Using STATIC_CONFIG is deprecated behavior.\n"); +#endif + + settings.per_monitor_dpi = option_get_bool( + "experimental", + "per_monitor_dpi", NULL, false, + "" + ); + + settings.force_xinerama = option_get_bool( + "global", + "force_xinerama", "-force_xinerama", false, + "Force the use of the Xinerama extension" + ); + + settings.font = option_get_string( + "global", + "font", "-font/-fn", defaults.font, + "The font dunst should use." + ); + + { + // Check if allow_markup set + if (ini_is_set("global", "allow_markup")) { + bool allow_markup = option_get_bool( + "global", + "allow_markup", NULL, false, + "Allow markup in notifications" + ); + + settings.markup = (allow_markup ? MARKUP_FULL : MARKUP_STRIP); + fprintf(stderr, "Warning: 'allow_markup' is deprecated, please use 'markup' instead.\n"); + } + + char *c = option_get_string( + "global", + "markup", "-markup", NULL, + "Specify how markup should be handled" + ); + + //Use markup if set + //Use default if settings.markup not set yet + // (=>c empty&&!allow_markup) + if (c) { + settings.markup = parse_markup_mode(c); + } else if (!settings.markup) { + settings.markup = defaults.markup; + } + g_free(c); + } + + settings.format = option_get_string( + "global", + "format", "-format", defaults.format, + "The format template for the notifications" + ); + + settings.sort = option_get_bool( + "global", + "sort", "-sort", defaults.sort, + "Sort notifications by urgency and date?" + ); + + settings.indicate_hidden = option_get_bool( + "global", + "indicate_hidden", "-indicate_hidden", defaults.indicate_hidden, + "Show how many notificaitons are hidden?" + ); + + settings.word_wrap = option_get_bool( + "global", + "word_wrap", "-word_wrap", defaults.word_wrap, + "Truncating long lines or do word wrap" + ); + + { + char *c = option_get_string( + "global", + "ellipsize", "-ellipsize", "", + "Ellipsize truncated lines on the start/middle/end" + ); + + if (strlen(c) == 0) { + settings.ellipsize = defaults.ellipsize; + } else if (strcmp(c, "start") == 0) { + settings.ellipsize = start; + } else if (strcmp(c, "middle") == 0) { + settings.ellipsize = middle; + } else if (strcmp(c, "end") == 0) { + settings.ellipsize = end; + } else { + fprintf(stderr, "Warning: unknown ellipsize value: \"%s\"\n", c); + settings.ellipsize = defaults.ellipsize; + } + g_free(c); + } + + settings.ignore_newline = option_get_bool( + "global", + "ignore_newline", "-ignore_newline", defaults.ignore_newline, + "Ignore newline characters in notifications" + ); + + settings.idle_threshold = option_get_time( + "global", + "idle_threshold", "-idle_threshold", defaults.idle_threshold, + "Don't timeout notifications if user is longer idle than threshold" + ); + + settings.monitor = option_get_int( + "global", + "monitor", "-mon/-monitor", defaults.monitor, + "On which monitor should the notifications be displayed" + ); + + { + char *c = option_get_string( + "global", + "follow", "-follow", "", + "Follow mouse, keyboard or none?" + ); + + if (strlen(c) > 0) { + parse_follow_mode(c); + g_free(c); + } + } + + settings.title = option_get_string( + "global", + "title", "-t/-title", defaults.title, + "Define the title of windows spawned by dunst." + ); + + settings.class = option_get_string( + "global", + "class", "-c/-class", defaults.class, + "Define the class of windows spawned by dunst." + ); + + settings.geom = option_get_string( + "global", + "geometry", "-geom/-geometry", defaults.geom, + "Geometry for the window" + ); + + settings.shrink = option_get_bool( + "global", + "shrink", "-shrink", defaults.shrink, + "Shrink window if it's smaller than the width" + ); + + settings.line_height = option_get_int( + "global", + "line_height", "-lh/-line_height", defaults.line_height, + "Add spacing between lines of text" + ); + + settings.notification_height = option_get_int( + "global", + "notification_height", "-nh/-notification_height", defaults.notification_height, + "Define height of the window" + ); + + { + char *c = option_get_string( + "global", + "alignment", "-align/-alignment", "", + "Text alignment left/center/right" + ); + + if (strlen(c) > 0) { + if (strcmp(c, "left") == 0) + settings.align = left; + else if (strcmp(c, "center") == 0) + settings.align = center; + else if (strcmp(c, "right") == 0) + settings.align = right; + else + fprintf(stderr, + "Warning: unknown alignment\n"); + g_free(c); + } + } + + settings.show_age_threshold = option_get_time( + "global", + "show_age_threshold", "-show_age_threshold", defaults.show_age_threshold, + "When should the age of the notification be displayed?" + ); + + settings.hide_duplicate_count = option_get_bool( + "global", + "hide_duplicate_count", "-hide_duplicate_count", false, + "Hide the count of merged notifications with the same content" + ); + + settings.sticky_history = option_get_bool( + "global", + "sticky_history", "-sticky_history", defaults.sticky_history, + "Don't timeout notifications popped up from history" + ); + + settings.history_length = option_get_int( + "global", + "history_length", "-history_length", defaults.history_length, + "Max amount of notifications kept in history" + ); + + settings.show_indicators = option_get_bool( + "global", + "show_indicators", "-show_indicators", defaults.show_indicators, + "Show indicators for actions \"(A)\" and URLs \"(U)\"" + ); + + settings.separator_height = option_get_int( + "global", + "separator_height", "-sep_height/-separator_height", defaults.separator_height, + "height of the separator line" + ); + + settings.padding = option_get_int( + "global", + "padding", "-padding", defaults.padding, + "Padding between text and separator" + ); + + settings.h_padding = option_get_int( + "global", + "horizontal_padding", "-horizontal_padding", defaults.h_padding, + "horizontal padding" + ); + + settings.transparency = option_get_int( + "global", + "transparency", "-transparency", defaults.transparency, + "Transparency. range 0-100" + ); + + { + char *c = option_get_string( + "global", + "separator_color", "-sep_color/-separator_color", "", + "Color of the separator line (or 'auto')" + ); + + if (strlen(c) > 0) { + if (strcmp(c, "auto") == 0) + settings.sep_color = AUTO; + else if (strcmp(c, "foreground") == 0) + settings.sep_color = FOREGROUND; + else if (strcmp(c, "frame") == 0) + settings.sep_color = FRAME; + else { + settings.sep_color = CUSTOM; + settings.sep_custom_color_str = g_strdup(c); + } + } + g_free(c); + } + + settings.stack_duplicates = option_get_bool( + "global", + "stack_duplicates", "-stack_duplicates", true, + "Merge multiple notifications with the same content" + ); + + settings.startup_notification = option_get_bool( + "global", + "startup_notification", "-startup_notification", false, + "print notification on startup" + ); + + settings.dmenu = option_get_path( + "global", + "dmenu", "-dmenu", defaults.dmenu, + "path to dmenu" + ); + + { + GError *error = NULL; + if (!g_shell_parse_argv(settings.dmenu, NULL, &settings.dmenu_cmd, &error)) { + fprintf(stderr, "Unable to parse dmenu command: %s\n", error->message); + fprintf(stderr, "dmenu functionality will be disabled.\n"); + g_error_free(error); + settings.dmenu_cmd = NULL; + } + } + + + settings.browser = option_get_path( + "global", + "browser", "-browser", defaults.browser, + "path to browser" + ); + + { + char *c = option_get_string( + "global", + "icon_position", "-icon_position", "off", + "Align icons left/right/off" + ); + + if (strlen(c) > 0) { + if (strcmp(c, "left") == 0) + settings.icon_position = icons_left; + else if (strcmp(c, "right") == 0) + settings.icon_position = icons_right; + else if (strcmp(c, "off") == 0) + settings.icon_position = icons_off; + else + fprintf(stderr, + "Warning: unknown icon position: %s\n", c); + g_free(c); + } + } + + settings.max_icon_size = option_get_int( + "global", + "max_icon_size", "-max_icon_size", defaults.max_icon_size, + "Scale larger icons down to this size, set to 0 to disable" + ); + + // If the deprecated icon_folders option is used, + // read it and generate its usage string. + if (ini_is_set("global", "icon_folders") || cmdline_is_set("-icon_folders")) { + settings.icon_path = option_get_string( + "global", + "icon_folders", "-icon_folders", defaults.icon_path, + "folders to default icons (deprecated, please use 'icon_path' instead)" + ); + fprintf(stderr, "Warning: 'icon_folders' is deprecated, please use 'icon_path' instead.\n"); + } + // Read value and generate usage string for icon_path. + // If icon_path is set, override icon_folder. + // if not, but icon_folder is set, use that instead of the compile time default. + settings.icon_path = option_get_string( + "global", + "icon_path", "-icon_path", + settings.icon_path ? settings.icon_path : defaults.icon_path, + "paths to default icons" + ); + + { + // Backwards compatibility with the legacy 'frame' section. + if (ini_is_set("frame", "width")) { + settings.frame_width = option_get_int( + "frame", + "width", NULL, defaults.frame_width, + "Width of frame around the window" + ); + fprintf(stderr, "Warning: The frame section is deprecated, width has been renamed to frame_width and moved to the global section.\n"); + } + + settings.frame_width = option_get_int( + "global", + "frame_width", "-frame_width", + settings.frame_width ? settings.frame_width : defaults.frame_width, + "Width of frame around the window" + ); + + if (ini_is_set("frame", "color")) { + settings.frame_color = option_get_string( + "frame", + "color", NULL, defaults.frame_color, + "Color of the frame around the window" + ); + fprintf(stderr, "Warning: The frame section is deprecated, color has been renamed to frame_color and moved to the global section.\n"); + } + + settings.frame_color = option_get_string( + "global", + "frame_color", "-frame_color", + settings.frame_color ? settings.frame_color : defaults.frame_color, + "Color of the frame around the window" + ); + + } + settings.lowbgcolor = option_get_string( + "urgency_low", + "background", "-lb", defaults.lowbgcolor, + "Background color for notifications with low urgency" + ); + + settings.lowfgcolor = option_get_string( + "urgency_low", + "foreground", "-lf", defaults.lowfgcolor, + "Foreground color for notifications with low urgency" + ); + + settings.lowframecolor = option_get_string( + "urgency_low", + "frame_color", "-lfr", NULL, + "Frame color for notifications with low urgency" + ); + + settings.timeouts[URG_LOW] = option_get_time( + "urgency_low", + "timeout", "-lto", defaults.timeouts[URG_LOW], + "Timeout for notifications with low urgency" + ); + + settings.icons[URG_LOW] = option_get_string( + "urgency_low", + "icon", "-li", defaults.icons[URG_LOW], + "Icon for notifications with low urgency" + ); + + settings.normbgcolor = option_get_string( + "urgency_normal", + "background", "-nb", defaults.normbgcolor, + "Background color for notifications with normal urgency" + ); + + settings.normfgcolor = option_get_string( + "urgency_normal", + "foreground", "-nf", defaults.normfgcolor, + "Foreground color for notifications with normal urgency" + ); + + settings.normframecolor = option_get_string( + "urgency_normal", + "frame_color", "-nfr", NULL, + "Frame color for notifications with normal urgency" + ); + + settings.timeouts[URG_NORM] = option_get_time( + "urgency_normal", + "timeout", "-nto", defaults.timeouts[URG_NORM], + "Timeout for notifications with normal urgency" + ); + + settings.icons[URG_NORM] = option_get_string( + "urgency_normal", + "icon", "-ni", defaults.icons[URG_NORM], + "Icon for notifications with normal urgency" + ); + + settings.critbgcolor = option_get_string( + "urgency_critical", + "background", "-cb", defaults.critbgcolor, + "Background color for notifications with critical urgency" + ); + + settings.critfgcolor = option_get_string( + "urgency_critical", + "foreground", "-cf", defaults.critfgcolor, + "Foreground color for notifications with ciritical urgency" + ); + + settings.critframecolor = option_get_string( + "urgency_critical", + "frame_color", "-cfr", NULL, + "Frame color for notifications with critical urgency" + ); + + settings.timeouts[URG_CRIT] = option_get_time( + "urgency_critical", + "timeout", "-cto", defaults.timeouts[URG_CRIT], + "Timeout for notifications with critical urgency" + ); + + settings.icons[URG_CRIT] = option_get_string( + "urgency_critical", + "icon", "-ci", defaults.icons[URG_CRIT], + "Icon for notifications with critical urgency" + ); + + settings.close_ks.str = option_get_string( + "shortcuts", + "close", "-key", defaults.close_ks.str, + "Shortcut for closing one notification" + ); + + settings.close_all_ks.str = option_get_string( + "shortcuts", + "close_all", "-all_key", defaults.close_all_ks.str, + "Shortcut for closing all notifications" + ); + + settings.history_ks.str = option_get_string( + "shortcuts", + "history", "-history_key", defaults.history_ks.str, + "Shortcut to pop the last notification from history" + ); + + settings.context_ks.str = option_get_string( + "shortcuts", + "context", "-context_key", defaults.context_ks.str, + "Shortcut for context menu" + ); + + settings.print_notifications = cmdline_get_bool( + "-print", false, + "Print notifications to cmdline (DEBUG)" + ); + + settings.always_run_script = option_get_bool( + "global", + "always_run_script", "-always_run_script", true, + "Always run rule-defined scripts, even if the notification is suppressed with format = \"\"." + ); + + { + char *c = option_get_string( + "global", + "centering", "-centering", "off", + "Align notifications on screen off/horizontal/vertical/both" + ); + + if (strcmp(c, "off") == 0) + settings.centering = CENTERING_OFF; + else if (strcmp(c, "horizontal") == 0) + settings.centering = CENTERING_HORIZONTAL; + else if (strcmp(c, "vertical") == 0) + settings.centering = CENTERING_VERTICAL; + else if (strcmp(c, "both") == 0) + settings.centering = CENTERING_BOTH; + else + fprintf(stderr, + "Warning: unknown centering option: %s\n", c); + g_free(c); + } + + /* push hardcoded default rules into rules list */ + for (int i = 0; i < G_N_ELEMENTS(default_rules); i++) { + rules = g_slist_insert(rules, &(default_rules[i]), -1); + } + + const char *cur_section = NULL; + for (;;) { + cur_section = next_section(cur_section); + if (!cur_section) + break; + if (strcmp(cur_section, "global") == 0 + || strcmp(cur_section, "frame") == 0 + || strcmp(cur_section, "experimental") == 0 + || strcmp(cur_section, "shortcuts") == 0 + || strcmp(cur_section, "urgency_low") == 0 + || strcmp(cur_section, "urgency_normal") == 0 + || strcmp(cur_section, "urgency_critical") == 0) + continue; + + /* check for existing rule with same name */ + rule_t *r = NULL; + for (GSList *iter = rules; iter; iter = iter->next) { + rule_t *match = iter->data; + if (match->name && + strcmp(match->name, cur_section) == 0) + r = match; + } + + if (r == NULL) { + r = g_malloc(sizeof(rule_t)); + rule_init(r); + rules = g_slist_insert(rules, r, -1); + } + + r->name = g_strdup(cur_section); + r->appname = ini_get_string(cur_section, "appname", r->appname); + r->summary = ini_get_string(cur_section, "summary", r->summary); + r->body = ini_get_string(cur_section, "body", r->body); + r->icon = ini_get_string(cur_section, "icon", r->icon); + r->category = ini_get_string(cur_section, "category", r->category); + r->timeout = ini_get_time(cur_section, "timeout", r->timeout); + + { + char *c = ini_get_string( + cur_section, + "markup", NULL + ); + + if (c != NULL) { + r->markup = parse_markup_mode(c); + g_free(c); + } + } + + r->urgency = ini_get_urgency(cur_section, "urgency", r->urgency); + r->msg_urgency = ini_get_urgency(cur_section, "msg_urgency", r->msg_urgency); + r->fg = ini_get_string(cur_section, "foreground", r->fg); + r->bg = ini_get_string(cur_section, "background", r->bg); + r->format = ini_get_string(cur_section, "format", r->format); + r->new_icon = ini_get_string(cur_section, "new_icon", r->new_icon); + r->history_ignore = ini_get_bool(cur_section, "history_ignore", r->history_ignore); + r->match_transient = ini_get_bool(cur_section, "match_transient", r->match_transient); + r->set_transient = ini_get_bool(cur_section, "set_transient", r->set_transient); + r->script = ini_get_path(cur_section, "script", NULL); + } + +#ifndef STATIC_CONFIG + if (config_file) { + fclose(config_file); + free_ini(); + xdgWipeHandle(&xdg); + } +#endif +} +/* vim: set tabstop=8 shiftwidth=8 expandtab textwidth=0: */ diff --git a/packages/dunst/src/settings.h b/packages/dunst/src/settings.h new file mode 100644 index 0000000..62e6f15 --- /dev/null +++ b/packages/dunst/src/settings.h @@ -0,0 +1,84 @@ +/* copyright 2013 Sascha Kruse and contributors (see LICENSE for licensing information) */ +#ifndef DUNST_SETTINGS_H +#define DUNST_SETTINGS_H + +#include <stdbool.h> + +#include "x11/x.h" + +enum alignment { left, center, right }; +enum ellipsize { start, middle, end }; +enum icon_position_t { icons_left, icons_right, icons_off }; +enum separator_color { FOREGROUND, AUTO, FRAME, CUSTOM }; +enum follow_mode { FOLLOW_NONE, FOLLOW_MOUSE, FOLLOW_KEYBOARD }; +enum markup_mode { MARKUP_NULL, MARKUP_NO, MARKUP_STRIP, MARKUP_FULL }; +enum centering { CENTERING_OFF, CENTERING_HORIZONTAL, CENTERING_VERTICAL, CENTERING_BOTH }; + +typedef struct _settings { + bool print_notifications; + bool per_monitor_dpi; + enum markup_mode markup; + bool stack_duplicates; + bool hide_duplicate_count; + char *font; + char *normbgcolor; + char *normfgcolor; + char *normframecolor; + char *critbgcolor; + char *critfgcolor; + char *critframecolor; + char *lowbgcolor; + char *lowfgcolor; + char *lowframecolor; + char *format; + gint64 timeouts[3]; + char *icons[3]; + unsigned int transparency; + char *geom; + enum centering centering; + char *title; + char *class; + int shrink; + int sort; + int indicate_hidden; + gint64 idle_threshold; + gint64 show_age_threshold; + enum alignment align; + int sticky_history; + int history_length; + int show_indicators; + int word_wrap; + enum ellipsize ellipsize; + int ignore_newline; + int line_height; + int notification_height; + int separator_height; + int padding; + int h_padding; + enum separator_color sep_color; + char *sep_custom_color_str; + int frame_width; + char *frame_color; + int startup_notification; + int monitor; + char *dmenu; + char **dmenu_cmd; + char *browser; + enum icon_position_t icon_position; + int max_icon_size; + char *icon_path; + enum follow_mode f_mode; + bool always_run_script; + keyboard_shortcut close_ks; + keyboard_shortcut close_all_ks; + keyboard_shortcut history_ks; + keyboard_shortcut context_ks; + bool force_xinerama; +} settings_t; + +extern settings_t settings; + +void load_settings(char *cmdline_config_path); + +#endif +/* vim: set tabstop=8 shiftwidth=8 expandtab textwidth=0: */ diff --git a/packages/dunst/src/utils.c b/packages/dunst/src/utils.c new file mode 100644 index 0000000..c79d06d --- /dev/null +++ b/packages/dunst/src/utils.c @@ -0,0 +1,173 @@ +/* copyright 2013 Sascha Kruse and contributors (see LICENSE for licensing information) */ +#include "utils.h" + +#include <assert.h> +#include <errno.h> +#include <glib.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +char *string_replace_char(char needle, char replacement, char *haystack) +{ + char *current = haystack; + while ((current = strchr(current, needle)) != NULL) + *current++ = replacement; + return haystack; +} + +char *string_replace_at(char *buf, int pos, int len, const char *repl) +{ + char *tmp; + int size, buf_len, repl_len; + + buf_len = strlen(buf); + repl_len = strlen(repl); + size = (buf_len - len) + repl_len + 1; + + if (repl_len <= len) { + tmp = buf; + } else { + tmp = g_malloc(size); + } + + memcpy(tmp, buf, pos); + memcpy(tmp + pos, repl, repl_len); + memmove(tmp + pos + repl_len, buf + pos + len, buf_len - (pos + len) + 1); + + if (tmp != buf) { + g_free(buf); + } + + return tmp; +} + +char *string_replace(const char *needle, const char *replacement, char *haystack) +{ + char *start; + start = strstr(haystack, needle); + if (start == NULL) { + return haystack; + } + + return string_replace_at(haystack, (start - haystack), strlen(needle), replacement); +} + +char *string_replace_all(const char *needle, const char *replacement, char *haystack) +{ + char *start; + int needle_pos; + int needle_len, repl_len; + + needle_len = strlen(needle); + if (needle_len == 0) { + return haystack; + } + + start = strstr(haystack, needle); + repl_len = strlen(replacement); + + while (start != NULL) { + needle_pos = start - haystack; + haystack = string_replace_at(haystack, needle_pos, needle_len, replacement); + start = strstr(haystack + needle_pos + repl_len, needle); + } + return haystack; +} + +char *string_append(char *a, const char *b, const char *sep) +{ + if (!a || *a == '\0') { + g_free(a); + return g_strdup(b); + } + if (!b || *b == '\0') + return a; + + char *new; + if (!sep) + new = g_strconcat(a, b, NULL); + else + new = g_strconcat(a, sep, b, NULL); + g_free(a); + + return new; + +} + +void string_strip_delimited(char *str, char a, char b) +{ + int iread=-1, iwrite=0, copen=0; + while (str[++iread] != 0) { + if (str[iread] == a) { + ++copen; + } else if (str[iread] == b && copen > 0) { + --copen; + } else if (copen == 0) { + str[iwrite++] = str[iread]; + } + } + str[iwrite] = 0; +} + +char *string_to_path(char *string) +{ + + if (string && 0 == strncmp(string, "~/", 2)) { + char *home = g_strconcat(getenv("HOME"), "/", NULL); + + string = string_replace("~/", home, string); + + g_free(home); + } + + return string; +} + +gint64 string_to_time(const char *string) +{ + + assert(string); + + errno = 0; + char *endptr; + gint64 val = strtoll(string, &endptr, 10); + + if (errno != 0) { + fprintf(stderr, "ERROR: Time: '%s': %s.\n", string, strerror(errno)); + return 0; + } else if (string == endptr) { + fprintf(stderr, "ERROR: Time: No digits found.\n"); + return 0; + } else if (errno != 0 && val == 0) { + fprintf(stderr, "ERROR: Time: '%s' unknown error.\n", string); + return 0; + } else if (errno == 0 && !*endptr) { + return val * G_USEC_PER_SEC; + } + + // endptr may point to a separating space + while (*endptr == ' ') + endptr++; + + if (0 == strncmp(endptr, "ms", 2)) + return val * 1000; + else if (0 == strncmp(endptr, "s", 1)) + return val * G_USEC_PER_SEC; + else if (0 == strncmp(endptr, "m", 1)) + return val * G_USEC_PER_SEC * 60; + else if (0 == strncmp(endptr, "h", 1)) + return val * G_USEC_PER_SEC * 60 * 60; + else if (0 == strncmp(endptr, "d", 1)) + return val * G_USEC_PER_SEC * 60 * 60 * 24; + else + return 0; +} + +void die(char *text, int exit_value) +{ + fputs(text, stderr); + exit(exit_value); +} + +/* vim: set tabstop=8 shiftwidth=8 expandtab textwidth=0: */ diff --git a/packages/dunst/src/utils.h b/packages/dunst/src/utils.h new file mode 100644 index 0000000..18333d9 --- /dev/null +++ b/packages/dunst/src/utils.h @@ -0,0 +1,34 @@ +/* copyright 2013 Sascha Kruse and contributors (see LICENSE for licensing information) */ +#ifndef DUNST_UTILS_H +#define DUNST_UTILS_H + +#include <glib.h> + +/* replace all occurrences of the character needle with the character replacement in haystack */ +char *string_replace_char(char needle, char replacement, char *haystack); + +/* replace all occurrences of needle with replacement in haystack */ +char *string_replace_all(const char *needle, const char *replacement, char *haystack); + +/* replace <len> characters with <repl> at position <pos> of the string <buf> */ +char *string_replace_at(char *buf, int pos, int len, const char *repl); + +/* replace needle with replacement in haystack */ +char *string_replace(const char *needle, const char *replacement, char *haystack); + +char *string_append(char *a, const char *b, const char *sep); + +/* strip content between two delimiter characters (inplace) */ +void string_strip_delimited(char *str, char a, char b); + +/* exit with an error message */ +void die(char *msg, int exit_value); + +/* replace tilde and path-specific values with its equivalents */ +char *string_to_path(char *string); + +/* convert time units (ms, s, m) to internal gint64 microseconds */ +gint64 string_to_time(const char *string); + +#endif +/* vim: set tabstop=8 shiftwidth=8 expandtab textwidth=0: */ diff --git a/packages/dunst/src/x11/screen.c b/packages/dunst/src/x11/screen.c new file mode 100644 index 0000000..bce8c54 --- /dev/null +++ b/packages/dunst/src/x11/screen.c @@ -0,0 +1,332 @@ +#include "screen.h" + +#include <X11/X.h> +#include <X11/Xatom.h> +#include <X11/Xlib.h> +#include <X11/Xresource.h> +#include <X11/extensions/Xinerama.h> +#include <X11/extensions/Xrandr.h> +#include <X11/extensions/randr.h> +#include <assert.h> +#include <glib.h> +#include <locale.h> +#include <stdbool.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#include "src/settings.h" +#include "x.h" + +screen_info *screens; +int screens_len; + +bool dunst_follow_errored = false; + +int randr_event_base = 0; + +static int randr_major_version = 0; +static int randr_minor_version = 0; + +void randr_init(); +void randr_update(); +void xinerama_update(); +void screen_update_fallback(); +static void x_follow_setup_error_handler(void); +static int x_follow_tear_down_error_handler(void); +static int FollowXErrorHandler(Display *display, XErrorEvent *e); +static Window get_focused_window(void); + +static double get_xft_dpi_value() +{ + static double dpi = -1; + //Only run this once, we don't expect dpi changes during runtime + if (dpi <= -1) { + XrmInitialize(); + char *xRMS = XResourceManagerString(xctx.dpy); + + if (xRMS == NULL) { + dpi = 0; + return 0; + } + + XrmDatabase xDB = XrmGetStringDatabase(xRMS); + char *xrmType; + XrmValue xrmValue; + + if (XrmGetResource(xDB, "Xft.dpi", "Xft.dpi", &xrmType, &xrmValue)) { + dpi = strtod(xrmValue.addr, NULL); + } else { + dpi = 0; + } + XrmDestroyDatabase(xDB); + } + return dpi; +} + +void init_screens() +{ + if (!settings.force_xinerama) { + randr_init(); + randr_update(); + } else { + xinerama_update(); + } +} + +void alloc_screen_ar(int n) +{ + assert(n > 0); + if (n <= screens_len) return; + + screens = g_realloc(screens, n * sizeof(screen_info)); + + memset(screens, 0, n * sizeof(screen_info)); + + screens_len = n; +} + +void randr_init() +{ + int randr_error_base = 0; + if (!XRRQueryExtension(xctx.dpy, &randr_event_base, &randr_error_base)) { + fprintf(stderr, "Could not initialize the RandR extension, falling back to single monitor mode.\n"); + return; + } + XRRQueryVersion(xctx.dpy, &randr_major_version, &randr_minor_version); + XRRSelectInput(xctx.dpy, RootWindow(xctx.dpy, DefaultScreen(xctx.dpy)), RRScreenChangeNotifyMask); +} + +void randr_update() +{ + if (randr_major_version < 1 + || (randr_major_version == 1 && randr_minor_version < 5)) { + fprintf(stderr, "Server RandR version too low (%i.%i). Falling back to single monitor mode\n", + randr_major_version, + randr_minor_version); + screen_update_fallback(); + return; + } + + int n = 0; + XRRMonitorInfo *m = XRRGetMonitors(xctx.dpy, RootWindow(xctx.dpy, DefaultScreen(xctx.dpy)), true, &n); + + if (n < 1) { + fprintf(stderr, "Get monitors reported %i monitors, falling back to single monitor mode\n", n); + screen_update_fallback(); + return; + } + + assert(m); + + alloc_screen_ar(n); + + for (int i = 0; i < n; i++) { + screens[i].dim.x = m[i].x; + screens[i].dim.y = m[i].y; + screens[i].dim.w = m[i].width; + screens[i].dim.h = m[i].height; + screens[i].dim.mmh = m[i].mheight; + } + + XRRFreeMonitors(m); +} + +static int autodetect_dpi(screen_info *scr) +{ + return (double)scr->dim.h * 25.4 / (double)scr->dim.mmh; +} + +void screen_check_event(XEvent event) +{ + if (event.type == randr_event_base + RRScreenChangeNotify) + randr_update(); +} + +void xinerama_update() +{ + int n; + XineramaScreenInfo *info = XineramaQueryScreens(xctx.dpy, &n); + + if (!info) { + fprintf(stderr, "(Xinerama) Could not get screen info, falling back to single monitor mode\n"); + screen_update_fallback(); + return; + } + + alloc_screen_ar(n); + + for (int i = 0; i < n; i++) { + screens[i].dim.x = info[i].x_org; + screens[i].dim.y = info[i].y_org; + screens[i].dim.h = info[i].height; + screens[i].dim.w = info[i].width; + } + XFree(info); +} + +void screen_update_fallback() +{ + alloc_screen_ar(1); + + int screen; + if (settings.monitor >= 0) + screen = settings.monitor; + else + screen = DefaultScreen(xctx.dpy); + + screens[0].dim.w = DisplayWidth(xctx.dpy, screen); + screens[0].dim.h = DisplayHeight(xctx.dpy, screen); +} + +/* + * Select the screen on which the Window + * should be displayed. + */ +screen_info *get_active_screen() +{ + int ret = 0; + if (settings.monitor > 0 && settings.monitor < screens_len) { + ret = settings.monitor; + goto sc_cleanup; + } + + x_follow_setup_error_handler(); + + if (settings.f_mode == FOLLOW_NONE) { + ret = XDefaultScreen(xctx.dpy); + goto sc_cleanup; + + } else { + int x, y; + assert(settings.f_mode == FOLLOW_MOUSE + || settings.f_mode == FOLLOW_KEYBOARD); + Window root = + RootWindow(xctx.dpy, DefaultScreen(xctx.dpy)); + + if (settings.f_mode == FOLLOW_MOUSE) { + int dummy; + unsigned int dummy_ui; + Window dummy_win; + + XQueryPointer(xctx.dpy, + root, + &dummy_win, + &dummy_win, + &x, + &y, + &dummy, + &dummy, + &dummy_ui); + } + + if (settings.f_mode == FOLLOW_KEYBOARD) { + + Window focused = get_focused_window(); + + if (focused == 0) { + /* something went wrong. Fallback to default */ + ret = XDefaultScreen(xctx.dpy); + goto sc_cleanup; + } + + Window child_return; + XTranslateCoordinates(xctx.dpy, focused, root, + 0, 0, &x, &y, &child_return); + } + + for (int i = 0; i < screens_len; i++) { + if (INRECT(x, y, screens[i].dim.x, screens[i].dim.y, + screens[i].dim.w, screens[i].dim.h)) { + ret = i; + } + } + + if (ret > 0) + goto sc_cleanup; + + /* something seems to be wrong. Fallback to default */ + ret = XDefaultScreen(xctx.dpy); + goto sc_cleanup; + } +sc_cleanup: + x_follow_tear_down_error_handler(); + assert(screens); + assert(ret >= 0 && ret < screens_len); + return &screens[ret]; +} + +double get_dpi_for_screen(screen_info *scr) +{ + double dpi = 0; + if ((!settings.force_xinerama && settings.per_monitor_dpi && + (dpi = autodetect_dpi(scr)))) + return dpi; + else if ((dpi = get_xft_dpi_value())) + return dpi; + else + return 96; +} + +/* + * Return the window that currently has + * the keyboard focus. + */ +static Window get_focused_window(void) +{ + Window focused = 0; + Atom type; + int format; + unsigned long nitems, bytes_after; + unsigned char *prop_return = NULL; + Window root = RootWindow(xctx.dpy, DefaultScreen(xctx.dpy)); + Atom netactivewindow = + XInternAtom(xctx.dpy, "_NET_ACTIVE_WINDOW", false); + + XGetWindowProperty(xctx.dpy, + root, + netactivewindow, + 0L, + sizeof(Window), + false, + XA_WINDOW, + &type, + &format, + &nitems, + &bytes_after, + &prop_return); + if (prop_return) { + focused = *(Window *)prop_return; + XFree(prop_return); + } + + return focused; +} + +static void x_follow_setup_error_handler(void) +{ + dunst_follow_errored = false; + + XFlush(xctx.dpy); + XSetErrorHandler(FollowXErrorHandler); +} + +static int x_follow_tear_down_error_handler(void) +{ + XFlush(xctx.dpy); + XSync(xctx.dpy, false); + XSetErrorHandler(NULL); + return dunst_follow_errored; +} + +static int FollowXErrorHandler(Display *display, XErrorEvent *e) +{ + dunst_follow_errored = true; + char err_buf[BUFSIZ]; + XGetErrorText(display, e->error_code, err_buf, BUFSIZ); + fputs(err_buf, stderr); + fputs("\n", stderr); + + return 0; +} +/* vim: set tabstop=8 shiftwidth=8 expandtab textwidth=0: */ diff --git a/packages/dunst/src/x11/screen.h b/packages/dunst/src/x11/screen.h new file mode 100644 index 0000000..8b47142 --- /dev/null +++ b/packages/dunst/src/x11/screen.h @@ -0,0 +1,31 @@ +/* copyright 2013 Sascha Kruse and contributors (see LICENSE for licensing information) */ +#ifndef DUNST_SCREEN_H +#define DUNST_SCREEN_H + +#include <X11/Xlib.h> + +#define INRECT(x,y,rx,ry,rw,rh) ((x) >= (rx) && (x) < (rx)+(rw) && (y) >= (ry) && (y) < (ry)+(rh)) + +typedef struct _dimension_t { + int x; + int y; + unsigned int h; + unsigned int mmh; + unsigned int w; + int mask; + int negative_width; +} dimension_t; + +typedef struct _screen_info { + int scr; + dimension_t dim; +} screen_info; + +void init_screens(); +void screen_check_event(XEvent event); + +screen_info *get_active_screen(); +double get_dpi_for_screen(screen_info *scr); + +#endif +/* vim: set tabstop=8 shiftwidth=8 expandtab textwidth=0: */ diff --git a/packages/dunst/src/x11/x.c b/packages/dunst/src/x11/x.c new file mode 100644 index 0000000..552f5d5 --- /dev/null +++ b/packages/dunst/src/x11/x.c @@ -0,0 +1,1364 @@ +/* copyright 2013 Sascha Kruse and contributors (see LICENSE for licensing information) */ +#include "x.h" + +#include <X11/X.h> +#include <X11/XKBlib.h> +#include <X11/Xatom.h> +#include <X11/Xlib.h> +#include <X11/Xutil.h> +#include <assert.h> +#include <cairo-xlib.h> +#include <cairo.h> +#include <gdk-pixbuf/gdk-pixbuf.h> +#include <gdk/gdk.h> +#include <glib-object.h> +#include <locale.h> +#include <math.h> +#include <pango/pango-attributes.h> +#include <pango/pango-font.h> +#include <pango/pango-layout.h> +#include <pango/pango-types.h> +#include <pango/pangocairo.h> +#include <stdbool.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +#include "src/dbus.h" +#include "src/dunst.h" +#include "src/markup.h" +#include "src/notification.h" +#include "src/queues.h" +#include "src/settings.h" +#include "src/utils.h" + +#include "screen.h" + +#define WIDTH 400 +#define HEIGHT 400 + +xctx_t xctx; +bool dunst_grab_errored = false; + +typedef struct _cairo_ctx { + cairo_status_t status; + cairo_surface_t *surface; + cairo_t *context; + PangoFontDescription *desc; +} cairo_ctx_t; + +typedef struct _colored_layout { + PangoLayout *l; + color_t fg; + color_t bg; + color_t frame; + char *text; + PangoAttrList *attr; + cairo_surface_t *icon; + notification *n; +} colored_layout; + +cairo_ctx_t cairo_ctx; + +/* FIXME refactor setup teardown handlers into one setup and one teardown */ +static void x_shortcut_setup_error_handler(void); +static int x_shortcut_tear_down_error_handler(void); +static void x_win_move(int width, int height); +static void setopacity(Window win, unsigned long opacity); +static void x_handle_click(XEvent ev); +static void x_win_setup(void); + +static color_t x_color_hex_to_double(int hexValue) +{ + color_t color; + color.r = ((hexValue >> 16) & 0xFF) / 255.0; + color.g = ((hexValue >> 8) & 0xFF) / 255.0; + color.b = ((hexValue) & 0xFF) / 255.0; + + return color; +} + +static color_t x_string_to_color_t(const char *str) +{ + char *end; + long int val = strtol(str+1, &end, 16); + if (*end != '\0' && *(end+1) != '\0') { + printf("WARNING: Invalid color string: \"%s\"\n", str); + } + + return x_color_hex_to_double(val); +} + +static double _apply_delta(double base, double delta) +{ + base += delta; + if (base > 1) + base = 1; + if (base < 0) + base = 0; + + return base; +} + +static color_t calculate_foreground_color(color_t bg) +{ + double c_delta = 0.1; + color_t color = bg; + + /* do we need to darken or brighten the colors? */ + bool darken = (bg.r + bg.g + bg.b) / 3 > 0.5; + + int signedness = darken ? -1 : 1; + + color.r = _apply_delta(color.r, c_delta * signedness); + color.g = _apply_delta(color.g, c_delta * signedness); + color.b = _apply_delta(color.b, c_delta * signedness); + + return color; +} + +static color_t x_get_separator_color(colored_layout *cl, colored_layout *cl_next) +{ + switch (settings.sep_color) { + case FRAME: + if (cl_next->n->urgency > cl->n->urgency) + return cl_next->frame; + else + return cl->frame; + case CUSTOM: + return x_string_to_color_t(settings.sep_custom_color_str); + case FOREGROUND: + return cl->fg; + case AUTO: + return calculate_foreground_color(cl->bg); + default: + printf("Unknown separator color type. Please file a Bugreport.\n"); + return cl->fg; + + } +} + +static void x_cairo_setup(void) +{ + cairo_ctx.surface = cairo_xlib_surface_create(xctx.dpy, + xctx.win, DefaultVisual(xctx.dpy, 0), WIDTH, HEIGHT); + + cairo_ctx.context = cairo_create(cairo_ctx.surface); + + cairo_ctx.desc = pango_font_description_from_string(settings.font); +} + +static void r_setup_pango_layout(PangoLayout *layout, int width) +{ + pango_layout_set_wrap(layout, PANGO_WRAP_WORD_CHAR); + pango_layout_set_width(layout, width * PANGO_SCALE); + pango_layout_set_font_description(layout, cairo_ctx.desc); + pango_layout_set_spacing(layout, settings.line_height * PANGO_SCALE); + + PangoAlignment align; + switch (settings.align) { + case left: + default: + align = PANGO_ALIGN_LEFT; + break; + case center: + align = PANGO_ALIGN_CENTER; + break; + case right: + align = PANGO_ALIGN_RIGHT; + break; + } + pango_layout_set_alignment(layout, align); + +} + +static void free_colored_layout(void *data) +{ + colored_layout *cl = data; + g_object_unref(cl->l); + pango_attr_list_unref(cl->attr); + g_free(cl->text); + if (cl->icon) cairo_surface_destroy(cl->icon); + g_free(cl); +} + +static bool have_dynamic_width(void) +{ + return (xctx.geometry.mask & WidthValue && xctx.geometry.w == 0); +} + +static bool does_file_exist(const char *filename) +{ + return (access(filename, F_OK) != -1); +} + +static bool is_readable_file(const char *filename) +{ + return (access(filename, R_OK) != -1); +} + +const char *get_filename_ext(const char *filename) +{ + const char *dot = strrchr(filename, '.'); + if (!dot || dot == filename) return ""; + return dot + 1; +} + +static dimension_t calculate_dimensions(GSList *layouts) +{ + dimension_t dim; + dim.w = 0; + dim.h = 0; + dim.x = 0; + dim.y = 0; + dim.mask = xctx.geometry.mask; + + screen_info *scr = get_active_screen(); + if (have_dynamic_width()) { + /* dynamic width */ + dim.w = 0; + } else if (xctx.geometry.mask & WidthValue) { + /* fixed width */ + if (xctx.geometry.negative_width) { + dim.w = scr->dim.w - xctx.geometry.w; + } else { + dim.w = xctx.geometry.w; + } + } else { + /* across the screen */ + dim.w = scr->dim.w; + } + + dim.h += 2 * settings.frame_width; + dim.h += (g_slist_length(layouts) - 1) * settings.separator_height; + + int text_width = 0, total_width = 0; + for (GSList *iter = layouts; iter; iter = iter->next) { + colored_layout *cl = iter->data; + int w=0,h=0; + pango_layout_get_pixel_size(cl->l, &w, &h); + if (cl->icon) { + h = MAX(cairo_image_surface_get_height(cl->icon), h); + w += cairo_image_surface_get_width(cl->icon) + settings.h_padding; + } + h = MAX(settings.notification_height, h + settings.padding * 2); + dim.h += h; + text_width = MAX(w, text_width); + + if (have_dynamic_width() || settings.shrink) { + /* dynamic width */ + total_width = MAX(text_width + 2 * settings.h_padding, total_width); + + /* subtract height from the unwrapped text */ + dim.h -= h; + + if (total_width > scr->dim.w) { + /* set width to screen width */ + dim.w = scr->dim.w - xctx.geometry.x * 2; + } else if (have_dynamic_width() || (total_width < xctx.geometry.w && settings.shrink)) { + /* set width to text width */ + dim.w = total_width + 2 * settings.frame_width; + } + + /* re-setup the layout */ + w = dim.w; + w -= 2 * settings.h_padding; + w -= 2 * settings.frame_width; + if (cl->icon) w -= cairo_image_surface_get_width(cl->icon) + settings.h_padding; + r_setup_pango_layout(cl->l, w); + + /* re-read information */ + pango_layout_get_pixel_size(cl->l, &w, &h); + if (cl->icon) { + h = MAX(cairo_image_surface_get_height(cl->icon), h); + w += cairo_image_surface_get_width(cl->icon) + settings.h_padding; + } + h = MAX(settings.notification_height, h + settings.padding * 2); + dim.h += h; + text_width = MAX(w, text_width); + } + } + + if (dim.w <= 0) { + dim.w = text_width + 2 * settings.h_padding; + dim.w += 2 * settings.frame_width; + } + + return dim; +} + +static cairo_surface_t *gdk_pixbuf_to_cairo_surface(const GdkPixbuf *pixbuf) +{ + cairo_surface_t *icon_surface = NULL; + cairo_t *cr; + cairo_format_t format; + double width, height; + + format = gdk_pixbuf_get_has_alpha(pixbuf) ? CAIRO_FORMAT_ARGB32 : CAIRO_FORMAT_RGB24; + width = gdk_pixbuf_get_width(pixbuf); + height = gdk_pixbuf_get_height(pixbuf); + icon_surface = cairo_image_surface_create(format, width, height); + cr = cairo_create(icon_surface); + gdk_cairo_set_source_pixbuf(cr, pixbuf, 0, 0); + cairo_paint(cr); + cairo_destroy(cr); + return icon_surface; +} + +static GdkPixbuf *get_pixbuf_from_file(const char *icon_path) +{ + GdkPixbuf *pixbuf = NULL; + if (is_readable_file(icon_path)) { + GError *error = NULL; + pixbuf = gdk_pixbuf_new_from_file(icon_path, &error); + if (pixbuf == NULL) + g_free(error); + } + return pixbuf; +} + +static GdkPixbuf *get_pixbuf_from_path(char *icon_path) +{ + GdkPixbuf *pixbuf = NULL; + gchar *uri_path = NULL; + if (strlen(icon_path) > 0) { + if (g_str_has_prefix(icon_path, "file://")) { + uri_path = g_filename_from_uri(icon_path, NULL, NULL); + if (uri_path != NULL) { + icon_path = uri_path; + } + } + /* absolute path? */ + if (icon_path[0] == '/' || icon_path[0] == '~') { + pixbuf = get_pixbuf_from_file(icon_path); + } + /* search in icon_path */ + if (pixbuf == NULL) { + char *start = settings.icon_path, + *end, *current_folder, *maybe_icon_path; + do { + end = strchr(start, ':'); + if (end == NULL) end = strchr(settings.icon_path, '\0'); /* end = end of string */ + + current_folder = g_strndup(start, end - start); + /* try svg */ + maybe_icon_path = g_strconcat(current_folder, "/", icon_path, ".svg", NULL); + if (!does_file_exist(maybe_icon_path)) { + g_free(maybe_icon_path); + /* fallback to png */ + maybe_icon_path = g_strconcat(current_folder, "/", icon_path, ".png", NULL); + } + g_free(current_folder); + + pixbuf = get_pixbuf_from_file(maybe_icon_path); + g_free(maybe_icon_path); + if (pixbuf != NULL) { + return pixbuf; + } + + start = end + 1; + } while (*(end) != '\0'); + } + if (pixbuf == NULL) { + fprintf(stderr, + "Could not load icon: '%s'\n", icon_path); + } + if (uri_path != NULL) { + g_free(uri_path); + } + } + return pixbuf; +} + +static GdkPixbuf *get_pixbuf_from_raw_image(const RawImage *raw_image) +{ + GdkPixbuf *pixbuf = NULL; + + pixbuf = gdk_pixbuf_new_from_data(raw_image->data, + GDK_COLORSPACE_RGB, + raw_image->has_alpha, + raw_image->bits_per_sample, + raw_image->width, + raw_image->height, + raw_image->rowstride, + NULL, + NULL); + + return pixbuf; +} + +static PangoLayout *create_layout(cairo_t *c) +{ + screen_info *screen = get_active_screen(); + + PangoContext *context = pango_cairo_create_context(c); + pango_cairo_context_set_resolution(context, get_dpi_for_screen(screen)); + + PangoLayout *layout = pango_layout_new(context); + + g_object_unref(context); + + return layout; +} + +static colored_layout *r_init_shared(cairo_t *c, notification *n) +{ + colored_layout *cl = g_malloc(sizeof(colored_layout)); + cl->l = create_layout(c); + + if (!settings.word_wrap) { + PangoEllipsizeMode ellipsize; + switch (settings.ellipsize) { + case start: + ellipsize = PANGO_ELLIPSIZE_START; + break; + case middle: + ellipsize = PANGO_ELLIPSIZE_MIDDLE; + break; + case end: + ellipsize = PANGO_ELLIPSIZE_END; + break; + default: + assert(false); + } + pango_layout_set_ellipsize(cl->l, ellipsize); + } + + GdkPixbuf *pixbuf = NULL; + + if (n->raw_icon && + settings.icon_position != icons_off) { + + pixbuf = get_pixbuf_from_raw_image(n->raw_icon); + + } else if (n->icon && settings.icon_position != icons_off) { + pixbuf = get_pixbuf_from_path(n->icon); + } + + if (pixbuf != NULL) { + int w = gdk_pixbuf_get_width(pixbuf); + int h = gdk_pixbuf_get_height(pixbuf); + int larger = w > h ? w : h; + if (settings.max_icon_size && larger > settings.max_icon_size) { + GdkPixbuf *scaled; + if (w >= h) { + scaled = gdk_pixbuf_scale_simple(pixbuf, + settings.max_icon_size, + (int) ((double) settings.max_icon_size / w * h), + GDK_INTERP_BILINEAR); + } else { + scaled = gdk_pixbuf_scale_simple(pixbuf, + (int) ((double) settings.max_icon_size / h * w), + settings.max_icon_size, + GDK_INTERP_BILINEAR); + } + g_object_unref(pixbuf); + pixbuf = scaled; + } + + cl->icon = gdk_pixbuf_to_cairo_surface(pixbuf); + g_object_unref(pixbuf); + } else { + cl->icon = NULL; + } + + if (cl->icon && cairo_surface_status(cl->icon) != CAIRO_STATUS_SUCCESS) { + cairo_surface_destroy(cl->icon); + cl->icon = NULL; + } + + cl->fg = x_string_to_color_t(n->color_strings[ColFG]); + cl->bg = x_string_to_color_t(n->color_strings[ColBG]); + cl->frame = x_string_to_color_t(n->color_strings[ColFrame]); + + cl->n = n; + + dimension_t dim = calculate_dimensions(NULL); + int width = dim.w; + + if (have_dynamic_width()) { + r_setup_pango_layout(cl->l, -1); + } else { + width -= 2 * settings.h_padding; + width -= 2 * settings.frame_width; + if (cl->icon) width -= cairo_image_surface_get_width(cl->icon) + settings.h_padding; + r_setup_pango_layout(cl->l, width); + } + + return cl; +} + +static colored_layout *r_create_layout_for_xmore(cairo_t *c, notification *n, int qlen) +{ + colored_layout *cl = r_init_shared(c, n); + cl->text = g_strdup_printf("(%d more)", qlen); + cl->attr = NULL; + pango_layout_set_text(cl->l, cl->text, -1); + return cl; +} + +static colored_layout *r_create_layout_from_notification(cairo_t *c, notification *n) +{ + + colored_layout *cl = r_init_shared(c, n); + + /* markup */ + GError *err = NULL; + pango_parse_markup(n->text_to_render, -1, 0, &(cl->attr), &(cl->text), NULL, &err); + + if (!err) { + pango_layout_set_text(cl->l, cl->text, -1); + pango_layout_set_attributes(cl->l, cl->attr); + } else { + /* remove markup and display plain message instead */ + n->text_to_render = markup_strip(n->text_to_render); + cl->text = NULL; + cl->attr = NULL; + pango_layout_set_text(cl->l, n->text_to_render, -1); + if (n->first_render) { + printf("Error parsing markup: %s\n", err->message); + } + g_error_free(err); + } + + + pango_layout_get_pixel_size(cl->l, NULL, &(n->displayed_height)); + if (cl->icon) n->displayed_height = MAX(cairo_image_surface_get_height(cl->icon), n->displayed_height); + n->displayed_height = MAX(settings.notification_height, n->displayed_height + settings.padding * 2); + + n->first_render = false; + return cl; +} + +static GSList *r_create_layouts(cairo_t *c) +{ + GSList *layouts = NULL; + + int qlen = queues_length_waiting(); + bool xmore_is_needed = qlen > 0 && settings.indicate_hidden; + + notification *last = NULL; + for (const GList *iter = queues_get_displayed(); + iter; iter = iter->next) + { + notification *n = iter->data; + last = n; + + notification_update_text_to_render(n); + + if (!iter->next && xmore_is_needed && xctx.geometry.h == 1) { + char *new_ttr = g_strdup_printf("%s (%d more)", n->text_to_render, qlen); + g_free(n->text_to_render); + n->text_to_render = new_ttr; + } + layouts = g_slist_append(layouts, + r_create_layout_from_notification(c, n)); + } + + if (xmore_is_needed && xctx.geometry.h != 1) { + /* append xmore message as new message */ + layouts = g_slist_append(layouts, + r_create_layout_for_xmore(c, last, qlen)); + } + + return layouts; +} + +static void r_free_layouts(GSList *layouts) +{ + g_slist_free_full(layouts, free_colored_layout); +} + +static dimension_t x_render_layout(cairo_t *c, colored_layout *cl, colored_layout *cl_next, dimension_t dim, bool first, bool last) +{ + int h; + int h_text = 0; + pango_layout_get_pixel_size(cl->l, NULL, &h); + if (cl->icon) { + h_text = h; + h = MAX(cairo_image_surface_get_height(cl->icon), h); + } + + int bg_x = 0; + int bg_y = dim.y; + int bg_width = dim.w; + int bg_height = MAX(settings.notification_height, (2 * settings.padding) + h); + double bg_half_height = settings.notification_height/2.0; + int pango_offset = (int) floor(h/2.0); + + if (first) bg_height += settings.frame_width; + if (last) bg_height += settings.frame_width; + else bg_height += settings.separator_height; + + cairo_set_source_rgb(c, cl->frame.r, cl->frame.g, cl->frame.b); + cairo_rectangle(c, bg_x, bg_y, bg_width, bg_height); + cairo_fill(c); + + /* adding frame */ + bg_x += settings.frame_width; + if (first) { + dim.y += settings.frame_width; + bg_y += settings.frame_width; + bg_height -= settings.frame_width; + if (!last) bg_height -= settings.separator_height; + } + bg_width -= 2 * settings.frame_width; + if (last) + bg_height -= settings.frame_width; + + cairo_set_source_rgb(c, cl->bg.r, cl->bg.g, cl->bg.b); + cairo_rectangle(c, bg_x, bg_y, bg_width, bg_height); + cairo_fill(c); + + bool use_padding = settings.notification_height <= (2 * settings.padding) + h; + if (use_padding) + dim.y += settings.padding; + else + dim.y += (int) (ceil(bg_half_height) - pango_offset); + + if (cl->icon && settings.icon_position == icons_left) { + cairo_move_to(c, settings.frame_width + cairo_image_surface_get_width(cl->icon) + 2 * settings.h_padding, bg_y + settings.padding + h/2 - h_text/2); + } else if (cl->icon && settings.icon_position == icons_right) { + cairo_move_to(c, settings.frame_width + settings.h_padding, bg_y + settings.padding + h/2 - h_text/2); + } else { + cairo_move_to(c, settings.frame_width + settings.h_padding, bg_y + settings.padding); + } + + cairo_set_source_rgb(c, cl->fg.r, cl->fg.g, cl->fg.b); + pango_cairo_update_layout(c, cl->l); + pango_cairo_show_layout(c, cl->l); + if (use_padding) + dim.y += h + settings.padding; + else + dim.y += (int)(floor(bg_half_height) + pango_offset); + + if (settings.separator_height > 0 && !last) { + color_t sep_color = x_get_separator_color(cl, cl_next); + cairo_set_source_rgb(c, sep_color.r, sep_color.g, sep_color.b); + + if (settings.sep_color == FRAME) + // Draw over the borders on both sides to avoid + // the wrong color in the corners. + cairo_rectangle(c, 0, dim.y, dim.w, settings.separator_height); + else + cairo_rectangle(c, settings.frame_width, dim.y + settings.frame_width, dim.w - 2 * settings.frame_width, settings.separator_height); + + cairo_fill(c); + dim.y += settings.separator_height; + } + cairo_move_to(c, settings.h_padding, dim.y); + + if (cl->icon) { + unsigned int image_width = cairo_image_surface_get_width(cl->icon), + image_height = cairo_image_surface_get_height(cl->icon), + image_x, + image_y = bg_y + settings.padding + h/2 - image_height/2; + + if (settings.icon_position == icons_left) { + image_x = settings.frame_width + settings.h_padding; + } else { + image_x = bg_width - settings.h_padding - image_width + settings.frame_width; + } + + cairo_set_source_surface(c, cl->icon, image_x, image_y); + cairo_rectangle(c, image_x, image_y, image_width, image_height); + cairo_fill(c); + } + + return dim; +} + +void x_win_draw(void) +{ + + GSList *layouts = r_create_layouts(cairo_ctx.context); + + dimension_t dim = calculate_dimensions(layouts); + int width = dim.w; + int height = dim.h; + + cairo_t *c; + cairo_surface_t *image_surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height); + c = cairo_create(image_surface); + + x_win_move(width, height); + cairo_xlib_surface_set_size(cairo_ctx.surface, width, height); + + cairo_move_to(c, 0, 0); + + bool first = true; + for (GSList *iter = layouts; iter; iter = iter->next) { + if (iter->next) + dim = x_render_layout(c, iter->data, iter->next->data, dim, first, iter->next == NULL); + else + dim = x_render_layout(c, iter->data, NULL, dim, first, iter->next == NULL); + + first = false; + } + + cairo_set_source_surface(cairo_ctx.context, image_surface, 0, 0); + cairo_paint(cairo_ctx.context); + cairo_show_page(cairo_ctx.context); + + cairo_destroy(c); + cairo_surface_destroy(image_surface); + r_free_layouts(layouts); +} + +static void x_win_move(int width, int height) +{ + + int x, y; + screen_info *scr = get_active_screen(); + /* calculate window position */ + if (xctx.geometry.mask & XNegative) { + x = (scr->dim.x + (scr->dim.w - width)) + xctx.geometry.x; + } else { + x = scr->dim.x + xctx.geometry.x; + } + + if (xctx.geometry.mask & YNegative) { + y = scr->dim.y + (scr->dim.h + xctx.geometry.y) - height; + } else { + y = scr->dim.y + xctx.geometry.y; + } + + if (settings.centering == CENTERING_HORIZONTAL || settings.centering == CENTERING_BOTH) { + x = scr->dim.x + (scr->dim.w - width) / 2 + xctx.geometry.x; + } + + if (settings.centering == CENTERING_VERTICAL || settings.centering == CENTERING_BOTH) { + y = scr->dim.y + (scr->dim.h - height) / 2 + xctx.geometry.y; + } + + /* move and resize */ + if (x != xctx.window_dim.x || y != xctx.window_dim.y) { + XMoveWindow(xctx.dpy, xctx.win, x, y); + } + if (width != xctx.window_dim.w || height != xctx.window_dim.h) { + XResizeWindow(xctx.dpy, xctx.win, width, height); + } + + xctx.window_dim.x = x; + xctx.window_dim.y = y; + xctx.window_dim.h = height; + xctx.window_dim.w = width; +} + +static void setopacity(Window win, unsigned long opacity) +{ + Atom _NET_WM_WINDOW_OPACITY = + XInternAtom(xctx.dpy, "_NET_WM_WINDOW_OPACITY", false); + XChangeProperty(xctx.dpy, + win, + _NET_WM_WINDOW_OPACITY, + XA_CARDINAL, + 32, + PropModeReplace, + (unsigned char *)&opacity, + 1L); +} + +/* + * Returns the modifier which is NumLock. + */ +static KeySym x_numlock_mod() +{ + static KeyCode nl = 0; + KeySym sym = 0; + XModifierKeymap *map = XGetModifierMapping(xctx.dpy); + + if (!nl) + nl = XKeysymToKeycode(xctx.dpy, XStringToKeysym("Num_Lock")); + + for (int mod = 0; mod < 8; mod++) { + for (int j = 0; j < map->max_keypermod; j++) { + if (map->modifiermap[mod*map->max_keypermod+j] == nl) { + /* In theory, one could use `1 << mod`, but this + * could count as 'using implementation details', + * so use this large switch. */ + switch (mod) { + case ShiftMapIndex: + sym = ShiftMask; + goto end; + case LockMapIndex: + sym = LockMask; + goto end; + case ControlMapIndex: + sym = ControlMask; + goto end; + case Mod1MapIndex: + sym = Mod1Mask; + goto end; + case Mod2MapIndex: + sym = Mod2Mask; + goto end; + case Mod3MapIndex: + sym = Mod3Mask; + goto end; + case Mod4MapIndex: + sym = Mod4Mask; + goto end; + case Mod5MapIndex: + sym = Mod5Mask; + goto end; + } + } + } + } + +end: + XFreeModifiermap(map); + return sym; +} + +/* + * Helper function to use glib's mainloop mechanic + * with Xlib + */ +gboolean x_mainloop_fd_prepare(GSource *source, gint *timeout) +{ + if (timeout) + *timeout = -1; + else + g_print("BUG: x_mainloop_fd_prepare: timeout == NULL\n"); + return false; +} + +/* + * Helper function to use glib's mainloop mechanic + * with Xlib + */ +gboolean x_mainloop_fd_check(GSource *source) +{ + return XPending(xctx.dpy) > 0; +} + +/* + * Main Dispatcher for XEvents + */ +gboolean x_mainloop_fd_dispatch(GSource *source, GSourceFunc callback, gpointer user_data) +{ + XEvent ev; + unsigned int state; + while (XPending(xctx.dpy) > 0) { + XNextEvent(xctx.dpy, &ev); + switch (ev.type) { + case Expose: + if (ev.xexpose.count == 0 && xctx.visible) { + x_win_draw(); + } + break; + case SelectionNotify: + if (ev.xselection.property == xctx.utf8) + break; + case ButtonRelease: + if (ev.xbutton.window == xctx.win) { + x_handle_click(ev); + wake_up(); + } + break; + case KeyPress: + state = ev.xkey.state; + /* NumLock is also encoded in the state. Remove it. */ + state &= ~x_numlock_mod(); + if (settings.close_ks.str + && XLookupKeysym(&ev.xkey, + 0) == settings.close_ks.sym + && settings.close_ks.mask == state) { + const GList *displayed = queues_get_displayed(); + if (displayed && displayed->data) { + queues_notification_close(displayed->data, REASON_USER); + wake_up(); + } + } + if (settings.history_ks.str + && XLookupKeysym(&ev.xkey, + 0) == settings.history_ks.sym + && settings.history_ks.mask == state) { + queues_history_pop(); + wake_up(); + } + if (settings.close_all_ks.str + && XLookupKeysym(&ev.xkey, + 0) == settings.close_all_ks.sym + && settings.close_all_ks.mask == state) { + queues_history_push_all(); + wake_up(); + } + if (settings.context_ks.str + && XLookupKeysym(&ev.xkey, + 0) == settings.context_ks.sym + && settings.context_ks.mask == state) { + context_menu(); + wake_up(); + } + break; + case FocusIn: + case FocusOut: + case PropertyNotify: + wake_up(); + break; + default: + screen_check_event(ev); + break; + } + } + return true; +} + +/* + * Check whether the user is currently idle. + */ +bool x_is_idle(void) +{ + XScreenSaverQueryInfo(xctx.dpy, DefaultRootWindow(xctx.dpy), + xctx.screensaver_info); + if (settings.idle_threshold == 0) { + return false; + } + return xctx.screensaver_info->idle > settings.idle_threshold / 1000; +} + +/* TODO move to x_mainloop_* */ +/* + * Handle incoming mouse click events + */ +static void x_handle_click(XEvent ev) +{ + if (ev.xbutton.button == Button3) { + queues_history_push_all(); + + return; + } + + if (ev.xbutton.button == Button1 || ev.xbutton.button == Button2) { + int y = settings.separator_height; + notification *n = NULL; + int first = true; + for (const GList *iter = queues_get_displayed(); iter; + iter = iter->next) { + n = iter->data; + if (ev.xbutton.y > y && ev.xbutton.y < y + n->displayed_height) + break; + + y += n->displayed_height + settings.separator_height; + if (first) + y += settings.frame_width; + } + + if (n) { + if (ev.xbutton.button == Button1) + queues_notification_close(n, REASON_USER); + else + notification_do_action(n); + } + } +} + +void x_free(void) +{ + cairo_surface_destroy(cairo_ctx.surface); + cairo_destroy(cairo_ctx.context); + + if (xctx.dpy) + XCloseDisplay(xctx.dpy); +} + +/* + * Setup X11 stuff + */ +void x_setup(void) +{ + + /* initialize xctx.dc, font, keyboard, colors */ + if (!setlocale(LC_CTYPE, "") || !XSupportsLocale()) + fputs("no locale support\n", stderr); + if (!(xctx.dpy = XOpenDisplay(NULL))) { + die("cannot open display\n", EXIT_FAILURE); + } + + x_shortcut_init(&settings.close_ks); + x_shortcut_init(&settings.close_all_ks); + x_shortcut_init(&settings.history_ks); + x_shortcut_init(&settings.context_ks); + + x_shortcut_grab(&settings.close_ks); + x_shortcut_ungrab(&settings.close_ks); + x_shortcut_grab(&settings.close_all_ks); + x_shortcut_ungrab(&settings.close_all_ks); + x_shortcut_grab(&settings.history_ks); + x_shortcut_ungrab(&settings.history_ks); + x_shortcut_grab(&settings.context_ks); + x_shortcut_ungrab(&settings.context_ks); + + xctx.color_strings[ColFG][URG_LOW] = settings.lowfgcolor; + xctx.color_strings[ColFG][URG_NORM] = settings.normfgcolor; + xctx.color_strings[ColFG][URG_CRIT] = settings.critfgcolor; + + xctx.color_strings[ColBG][URG_LOW] = settings.lowbgcolor; + xctx.color_strings[ColBG][URG_NORM] = settings.normbgcolor; + xctx.color_strings[ColBG][URG_CRIT] = settings.critbgcolor; + + if (settings.lowframecolor) + xctx.color_strings[ColFrame][URG_LOW] = settings.lowframecolor; + else + xctx.color_strings[ColFrame][URG_LOW] = settings.frame_color; + if (settings.normframecolor) + xctx.color_strings[ColFrame][URG_NORM] = settings.normframecolor; + else + xctx.color_strings[ColFrame][URG_NORM] = settings.frame_color; + if (settings.critframecolor) + xctx.color_strings[ColFrame][URG_CRIT] = settings.critframecolor; + else + xctx.color_strings[ColFrame][URG_CRIT] = settings.frame_color; + + /* parse and set xctx.geometry and monitor position */ + if (settings.geom[0] == '-') { + xctx.geometry.negative_width = true; + settings.geom++; + } else { + xctx.geometry.negative_width = false; + } + + xctx.geometry.mask = XParseGeometry(settings.geom, + &xctx.geometry.x, &xctx.geometry.y, + &xctx.geometry.w, &xctx.geometry.h); + + /* calculate maximum notification count and push information to queue */ + if (xctx.geometry.h == 0) { + queues_displayed_limit(0); + } else if (xctx.geometry.h == 1) { + queues_displayed_limit(1); + } else if (settings.indicate_hidden) { + queues_displayed_limit(xctx.geometry.h - 1); + } else { + queues_displayed_limit(xctx.geometry.h); + } + + xctx.screensaver_info = XScreenSaverAllocInfo(); + + init_screens(); + x_win_setup(); + x_cairo_setup(); + x_shortcut_grab(&settings.history_ks); +} + +static void x_set_wm(Window win) +{ + + Atom data[2]; + + /* set window title */ + char *title = settings.title != NULL ? settings.title : "Dunst"; + Atom _net_wm_title = + XInternAtom(xctx.dpy, "_NET_WM_NAME", false); + + XStoreName(xctx.dpy, win, title); + XChangeProperty(xctx.dpy, + win, + _net_wm_title, + XInternAtom(xctx.dpy, "UTF8_STRING", false), + 8, + PropModeReplace, + (unsigned char *)title, + strlen(title)); + + /* set window class */ + char *class = settings.class != NULL ? settings.class : "Dunst"; + XClassHint classhint = { class, "Dunst" }; + + XSetClassHint(xctx.dpy, win, &classhint); + + /* set window type */ + Atom net_wm_window_type = + XInternAtom(xctx.dpy, "_NET_WM_WINDOW_TYPE", false); + + data[0] = XInternAtom(xctx.dpy, "_NET_WM_WINDOW_TYPE_NOTIFICATION", false); + data[1] = XInternAtom(xctx.dpy, "_NET_WM_WINDOW_TYPE_UTILITY", false); + + XChangeProperty(xctx.dpy, + win, + net_wm_window_type, + XA_ATOM, + 32, + PropModeReplace, + (unsigned char *)data, + 2L); + + /* set state above */ + Atom net_wm_state = + XInternAtom(xctx.dpy, "_NET_WM_STATE", false); + + data[0] = XInternAtom(xctx.dpy, "_NET_WM_STATE_ABOVE", false); + + XChangeProperty(xctx.dpy, win, net_wm_state, XA_ATOM, 32, + PropModeReplace, (unsigned char *) data, 1L); +} + +/* + * Setup the window + */ +static void x_win_setup(void) +{ + + Window root; + XSetWindowAttributes wa; + + xctx.window_dim.x = 0; + xctx.window_dim.y = 0; + xctx.window_dim.w = 0; + xctx.window_dim.h = 0; + + root = RootWindow(xctx.dpy, DefaultScreen(xctx.dpy)); + xctx.utf8 = XInternAtom(xctx.dpy, "UTF8_STRING", false); + + wa.override_redirect = true; + wa.background_pixmap = ParentRelative; + wa.event_mask = + ExposureMask | KeyPressMask | VisibilityChangeMask | + ButtonReleaseMask | FocusChangeMask| StructureNotifyMask; + + screen_info *scr = get_active_screen(); + xctx.win = XCreateWindow(xctx.dpy, + root, + scr->dim.x, + scr->dim.y, + scr->dim.w, + 1, + 0, + DefaultDepth(xctx.dpy, DefaultScreen(xctx.dpy)), + CopyFromParent, + DefaultVisual(xctx.dpy, DefaultScreen(xctx.dpy)), + CWOverrideRedirect | CWBackPixmap | CWEventMask, + &wa); + + x_set_wm(xctx.win); + settings.transparency = + settings.transparency > 100 ? 100 : settings.transparency; + setopacity(xctx.win, + (unsigned long)((100 - settings.transparency) * + (0xffffffff / 100))); + + if (settings.f_mode != FOLLOW_NONE) { + long root_event_mask = FocusChangeMask | PropertyChangeMask; + XSelectInput(xctx.dpy, root, root_event_mask); + } +} + +/* + * Show the window and grab shortcuts. + */ +void x_win_show(void) +{ + /* window is already mapped or there's nothing to show */ + if (xctx.visible || queues_length_displayed() == 0) { + return; + } + + x_shortcut_grab(&settings.close_ks); + x_shortcut_grab(&settings.close_all_ks); + x_shortcut_grab(&settings.context_ks); + + x_shortcut_setup_error_handler(); + XGrabButton(xctx.dpy, + AnyButton, + AnyModifier, + xctx.win, + false, + BUTTONMASK, + GrabModeAsync, + GrabModeSync, + None, + None); + if (x_shortcut_tear_down_error_handler()) { + fprintf(stderr, "Unable to grab mouse button(s)\n"); + } + + XMapRaised(xctx.dpy, xctx.win); + xctx.visible = true; +} + +/* + * Hide the window and ungrab unused keyboard_shortcuts + */ +void x_win_hide() +{ + x_shortcut_ungrab(&settings.close_ks); + x_shortcut_ungrab(&settings.close_all_ks); + x_shortcut_ungrab(&settings.context_ks); + + XUngrabButton(xctx.dpy, AnyButton, AnyModifier, xctx.win); + XUnmapWindow(xctx.dpy, xctx.win); + XFlush(xctx.dpy); + xctx.visible = false; +} + +/* + * Parse a string into a modifier mask. + */ +KeySym x_shortcut_string_to_mask(const char *str) +{ + if (!strcmp(str, "ctrl")) { + return ControlMask; + } else if (!strcmp(str, "mod4")) { + return Mod4Mask; + } else if (!strcmp(str, "mod3")) { + return Mod3Mask; + } else if (!strcmp(str, "mod2")) { + return Mod2Mask; + } else if (!strcmp(str, "mod1")) { + return Mod1Mask; + } else if (!strcmp(str, "shift")) { + return ShiftMask; + } else { + fprintf(stderr, "Warning: Unknown Modifier: %s\n", str); + return 0; + } +} + +/* + * Error handler for grabbing mouse and keyboard errors. + */ +static int GrabXErrorHandler(Display *display, XErrorEvent *e) +{ + dunst_grab_errored = true; + char err_buf[BUFSIZ]; + XGetErrorText(display, e->error_code, err_buf, BUFSIZ); + fputs(err_buf, stderr); + fputs("\n", stderr); + + if (e->error_code != BadAccess) { + exit(EXIT_FAILURE); + } + + return 0; +} + +/* + * Setup the Error handler. + */ +static void x_shortcut_setup_error_handler(void) +{ + dunst_grab_errored = false; + + XFlush(xctx.dpy); + XSetErrorHandler(GrabXErrorHandler); +} + +/* + * Tear down the Error handler. + */ +static int x_shortcut_tear_down_error_handler(void) +{ + XFlush(xctx.dpy); + XSync(xctx.dpy, false); + XSetErrorHandler(NULL); + return dunst_grab_errored; +} + +/* + * Grab the given keyboard shortcut. + */ +int x_shortcut_grab(keyboard_shortcut *ks) +{ + if (!ks->is_valid) + return 1; + Window root; + root = RootWindow(xctx.dpy, DefaultScreen(xctx.dpy)); + + x_shortcut_setup_error_handler(); + + if (ks->is_valid) { + XGrabKey(xctx.dpy, + ks->code, + ks->mask, + root, + true, + GrabModeAsync, + GrabModeAsync); + XGrabKey(xctx.dpy, + ks->code, + ks->mask | x_numlock_mod(), + root, + true, + GrabModeAsync, + GrabModeAsync); + } + + if (x_shortcut_tear_down_error_handler()) { + fprintf(stderr, "Unable to grab key \"%s\"\n", ks->str); + ks->is_valid = false; + return 1; + } + return 0; +} + +/* + * Ungrab the given keyboard shortcut. + */ +void x_shortcut_ungrab(keyboard_shortcut *ks) +{ + Window root; + root = RootWindow(xctx.dpy, DefaultScreen(xctx.dpy)); + if (ks->is_valid) { + XUngrabKey(xctx.dpy, ks->code, ks->mask, root); + XUngrabKey(xctx.dpy, ks->code, ks->mask | x_numlock_mod(), root); + } +} + +/* + * Initialize the keyboard shortcut. + */ +void x_shortcut_init(keyboard_shortcut *ks) +{ + if (ks == NULL || ks->str == NULL) + return; + + if (!strcmp(ks->str, "none") || (!strcmp(ks->str, ""))) { + ks->is_valid = false; + return; + } + + char *str = g_strdup(ks->str); + char *str_begin = str; + + while (strchr(str, '+')) { + char *mod = str; + while (*str != '+') + str++; + *str = '\0'; + str++; + g_strchomp(mod); + ks->mask = ks->mask | x_shortcut_string_to_mask(mod); + } + g_strstrip(str); + + ks->sym = XStringToKeysym(str); + /* find matching keycode for ks->sym */ + int min_keysym, max_keysym; + XDisplayKeycodes(xctx.dpy, &min_keysym, &max_keysym); + + ks->code = NoSymbol; + + for (int i = min_keysym; i <= max_keysym; i++) { + if (XkbKeycodeToKeysym(xctx.dpy, i, 0, 0) == ks->sym + || XkbKeycodeToKeysym(xctx.dpy, i, 0, 1) == ks->sym) { + ks->code = i; + break; + } + } + + if (ks->sym == NoSymbol || ks->code == NoSymbol) { + fprintf(stderr, "Warning: Unknown keyboard shortcut: %s\n", + ks->str); + ks->is_valid = false; + } else { + ks->is_valid = true; + } + + g_free(str_begin); +} + +/* vim: set tabstop=8 shiftwidth=8 expandtab textwidth=0: */ diff --git a/packages/dunst/src/x11/x.h b/packages/dunst/src/x11/x.h new file mode 100644 index 0000000..1ccd9a3 --- /dev/null +++ b/packages/dunst/src/x11/x.h @@ -0,0 +1,67 @@ +/* copyright 2013 Sascha Kruse and contributors (see LICENSE for licensing information) */ +#ifndef DUNST_X_H +#define DUNST_X_H + +#include <X11/X.h> +#include <X11/Xlib.h> +#include <X11/extensions/scrnsaver.h> +#include <glib.h> +#include <stdbool.h> + +#include "screen.h" + +#define BUTTONMASK (ButtonPressMask|ButtonReleaseMask) +#define FONT_HEIGHT_BORDER 2 +#define DEFFONT "Monospace-11" + +typedef struct _keyboard_shortcut { + const char *str; + KeyCode code; + KeySym sym; + KeySym mask; + bool is_valid; +} keyboard_shortcut; + +typedef struct _xctx { + Atom utf8; + Display *dpy; + Window win; + bool visible; + dimension_t geometry; + const char *color_strings[3][3]; + XScreenSaverInfo *screensaver_info; + dimension_t window_dim; + unsigned long sep_custom_col; +} xctx_t; + +typedef struct _color_t { + double r; + double g; + double b; +} color_t; + +extern xctx_t xctx; + +/* window */ +void x_win_draw(void); +void x_win_hide(void); +void x_win_show(void); + +/* shortcut */ +void x_shortcut_init(keyboard_shortcut *shortcut); +void x_shortcut_ungrab(keyboard_shortcut *ks); +int x_shortcut_grab(keyboard_shortcut *ks); +KeySym x_shortcut_string_to_mask(const char *str); + +/* X misc */ +bool x_is_idle(void); +void x_setup(void); +void x_free(void); + +gboolean x_mainloop_fd_dispatch(GSource *source, GSourceFunc callback, + gpointer user_data); +gboolean x_mainloop_fd_check(GSource *source); +gboolean x_mainloop_fd_prepare(GSource *source, gint *timeout); + +#endif +/* vim: set tabstop=8 shiftwidth=8 expandtab textwidth=0: */ diff --git a/packages/dunst/test/data/dunstrc.default b/packages/dunst/test/data/dunstrc.default new file mode 120000 index 0000000..921ed3c --- /dev/null +++ b/packages/dunst/test/data/dunstrc.default @@ -0,0 +1 @@ +../../dunstrc \ No newline at end of file diff --git a/packages/dunst/test/data/test-ini b/packages/dunst/test/data/test-ini new file mode 100644 index 0000000..9d2f7cd --- /dev/null +++ b/packages/dunst/test/data/test-ini @@ -0,0 +1,41 @@ +#General comment +[bool] + booltrue = true #This is a test inline comment + booltrue_capital = TRUE + + #This is a comment + boolfalse = false + boolfalse_capital = FALSE + + boolyes = yes + boolyes_capital = YES + + boolno = no + boolno_capital = NO + + boolbin0 = 0 + boolbin1 = 1 + + boolinvalid = invalidbool + +[string] + simple = A simple string + quoted = "A quoted string" + quoted_with_quotes = "A string "with quotes"" + +[path] + expand_tilde = ~/.path/to/tilde + +[int] + simple = 5 + negative = -10 + decimal = 2.71828 + leading_zeroes = 007 + multi_char = 1024 + +[double] + simple = 1 + decimal = 1.5 + negative = -1.2 + zeroes = 0.005 + long = 3.141592653589793 diff --git a/packages/dunst/test/functional-tests/dunstrc.default b/packages/dunst/test/functional-tests/dunstrc.default new file mode 100644 index 0000000..7abab08 --- /dev/null +++ b/packages/dunst/test/functional-tests/dunstrc.default @@ -0,0 +1,49 @@ +[global] + font = Monospace 8 + allow_markup = yes + format = "<b>%s</b>\n%b" + sort = yes + indicate_hidden = yes + alignment = left + show_age_threshold = 60 + word_wrap = yes + ignore_newline = no + geometry = "300x5-30+20" + transparency = 0 + idle_threshold = 120 + monitor = 0 + follow = mouse + sticky_history = yes + line_height = 0 + separator_height = 2 + padding = 8 + horizontal_padding = 8 + separator_color = frame + startup_notification = false + dmenu = /usr/bin/dmenu -p dunst + browser = /usr/bin/firefox -new-tab + +[frame] + width = 3 + color = "#aaaaaa" + +[shortcuts] + close = ctrl+space + close_all = ctrl+shift+space + history = ctrl+grave + context = ctrl+shift+period + +[urgency_low] + background = "#222222" + foreground = "#888888" + timeout = 10 + +[urgency_normal] + background = "#285577" + foreground = "#ffffff" + timeout = 10 + +[urgency_critical] + background = "#900000" + foreground = "#ffffff" + timeout = 0 diff --git a/packages/dunst/test/functional-tests/dunstrc.ignore_newline b/packages/dunst/test/functional-tests/dunstrc.ignore_newline new file mode 100644 index 0000000..609e709 --- /dev/null +++ b/packages/dunst/test/functional-tests/dunstrc.ignore_newline @@ -0,0 +1,49 @@ +[global] + font = Monospace 8 + allow_markup = yes + format = "<b>%s</b>\n%b" + sort = yes + indicate_hidden = yes + alignment = left + show_age_threshold = 60 + word_wrap = yes + ignore_newline = yes + geometry = "200x0-30+20" + transparency = 0 + idle_threshold = 120 + monitor = 0 + follow = mouse + sticky_history = yes + line_height = 0 + separator_height = 2 + padding = 8 + horizontal_padding = 8 + separator_color = frame + startup_notification = false + dmenu = /usr/bin/dmenu -p dunst + browser = /usr/bin/firefox -new-tab + +[frame] + width = 3 + color = "#aaaaaa" + +[shortcuts] + close = ctrl+space + close_all = ctrl+shift+space + history = ctrl+grave + context = ctrl+shift+period + +[urgency_low] + background = "#222222" + foreground = "#888888" + timeout = 10 + +[urgency_normal] + background = "#285577" + foreground = "#ffffff" + timeout = 10 + +[urgency_critical] + background = "#900000" + foreground = "#ffffff" + timeout = 0 diff --git a/packages/dunst/test/functional-tests/dunstrc.ignore_newline_no_wrap b/packages/dunst/test/functional-tests/dunstrc.ignore_newline_no_wrap new file mode 100644 index 0000000..2a650bf --- /dev/null +++ b/packages/dunst/test/functional-tests/dunstrc.ignore_newline_no_wrap @@ -0,0 +1,49 @@ +[global] + font = Monospace 8 + allow_markup = yes + format = "<b>%s</b>\n%b" + sort = yes + indicate_hidden = yes + alignment = left + show_age_threshold = 60 + word_wrap = no + ignore_newline = yes + geometry = "250x0-30+20" + transparency = 0 + idle_threshold = 120 + monitor = 0 + follow = mouse + sticky_history = yes + line_height = 0 + separator_height = 2 + padding = 8 + horizontal_padding = 8 + separator_color = frame + startup_notification = false + dmenu = /usr/bin/dmenu -p dunst + browser = /usr/bin/firefox -new-tab + +[frame] + width = 3 + color = "#aaaaaa" + +[shortcuts] + close = ctrl+space + close_all = ctrl+shift+space + history = ctrl+grave + context = ctrl+shift+period + +[urgency_low] + background = "#222222" + foreground = "#888888" + timeout = 10 + +[urgency_normal] + background = "#285577" + foreground = "#ffffff" + timeout = 10 + +[urgency_critical] + background = "#900000" + foreground = "#ffffff" + timeout = 0 diff --git a/packages/dunst/test/functional-tests/dunstrc.markup b/packages/dunst/test/functional-tests/dunstrc.markup new file mode 100644 index 0000000..87f09b1 --- /dev/null +++ b/packages/dunst/test/functional-tests/dunstrc.markup @@ -0,0 +1,49 @@ +[global] + font = Monospace 8 + allow_markup = yes + format = "%s\n%b" + sort = yes + indicate_hidden = yes + alignment = left + show_age_threshold = 60 + word_wrap = yes + ignore_newline = no + geometry = "300x5-30+20" + transparency = 0 + idle_threshold = 120 + monitor = 0 + follow = mouse + sticky_history = yes + line_height = 0 + separator_height = 2 + padding = 8 + horizontal_padding = 8 + separator_color = frame + startup_notification = false + dmenu = /usr/bin/dmenu -p dunst + browser = /usr/bin/firefox -new-tab + +[frame] + width = 3 + color = "#aaaaaa" + +[shortcuts] + close = ctrl+space + close_all = ctrl+shift+space + history = ctrl+grave + context = ctrl+shift+period + +[urgency_low] + background = "#222222" + foreground = "#888888" + timeout = 10 + +[urgency_normal] + background = "#285577" + foreground = "#ffffff" + timeout = 10 + +[urgency_critical] + background = "#900000" + foreground = "#ffffff" + timeout = 0 diff --git a/packages/dunst/test/functional-tests/dunstrc.nomarkup b/packages/dunst/test/functional-tests/dunstrc.nomarkup new file mode 100644 index 0000000..1ff4fb1 --- /dev/null +++ b/packages/dunst/test/functional-tests/dunstrc.nomarkup @@ -0,0 +1,49 @@ +[global] + font = Monospace 8 + allow_markup = no + format = "<b>%s</b>\n<i>%b</i>" + sort = yes + indicate_hidden = yes + alignment = left + show_age_threshold = 60 + word_wrap = yes + ignore_newline = no + geometry = "300x5-30+20" + transparency = 0 + idle_threshold = 120 + monitor = 0 + follow = mouse + sticky_history = yes + line_height = 0 + separator_height = 2 + padding = 8 + horizontal_padding = 8 + separator_color = frame + startup_notification = false + dmenu = /usr/bin/dmenu -p dunst + browser = /usr/bin/firefox -new-tab + +[frame] + width = 3 + color = "#aaaaaa" + +[shortcuts] + close = ctrl+space + close_all = ctrl+shift+space + history = ctrl+grave + context = ctrl+shift+period + +[urgency_low] + background = "#222222" + foreground = "#888888" + timeout = 10 + +[urgency_normal] + background = "#285577" + foreground = "#ffffff" + timeout = 10 + +[urgency_critical] + background = "#900000" + foreground = "#ffffff" + timeout = 0 diff --git a/packages/dunst/test/functional-tests/dunstrc.nowrap b/packages/dunst/test/functional-tests/dunstrc.nowrap new file mode 100644 index 0000000..497af8f --- /dev/null +++ b/packages/dunst/test/functional-tests/dunstrc.nowrap @@ -0,0 +1,49 @@ +[global] + font = Monospace-10 + allow_markup = yes + format = "%s\n%b" + sort = yes + indicate_hidden = yes + alignment = left + show_age_threshold = 60 + word_wrap = no + ignore_newline = no + geometry = "300x5-30+20" + transparency = 0 + idle_threshold = 120 + monitor = 0 + follow = mouse + sticky_history = yes + line_height = 0 + separator_height = 2; + padding = 8 + horizontal_padding = 8 + separator_color = frame + startup_notification = false + dmenu = /usr/bin/dmenu -p dunst: + browser = /usr/bin/firefox -new-tab + +[frame] + width = 3 + color = "#aaaaaa" + +[shortcuts] + close = ctrl+space + close_all = ctrl+shift+space + history = ctrl+grave + context = ctrl+shift+period + +[urgency_low] + background = "#222222" + foreground = "#888888" + timeout = 10 + +[urgency_normal] + background = "#285577" + foreground = "#ffffff" + timeout = 10 + +[urgency_critical] + background = "#900000" + foreground = "#ffffff" + timeout = 0 diff --git a/packages/dunst/test/functional-tests/dunstrc.run_script b/packages/dunst/test/functional-tests/dunstrc.run_script new file mode 100644 index 0000000..b76023b --- /dev/null +++ b/packages/dunst/test/functional-tests/dunstrc.run_script @@ -0,0 +1,53 @@ +[global] + font = Monospace 8 + allow_markup = yes + format = "<b>%s</b>\n%b" + sort = yes + indicate_hidden = yes + alignment = left + show_age_threshold = 60 + word_wrap = yes + ignore_newline = no + geometry = "300x5-30+20" + transparency = 0 + idle_threshold = 120 + monitor = 0 + follow = mouse + sticky_history = yes + line_height = 0 + separator_height = 2 + padding = 8 + horizontal_padding = 8 + separator_color = frame + startup_notification = false + dmenu = /usr/bin/dmenu -p dunst + browser = /usr/bin/firefox -new-tab + +[frame] + width = 3 + color = "#aaaaaa" + +[shortcuts] + close = ctrl+space + close_all = ctrl+shift+space + history = ctrl+grave + context = ctrl+shift+period + +[urgency_low] + background = "#222222" + foreground = "#888888" + timeout = 10 + +[urgency_normal] + background = "#285577" + foreground = "#ffffff" + timeout = 10 + +[urgency_critical] + background = "#900000" + foreground = "#ffffff" + timeout = 0 + +[script test] + summary = trigger + script = script_test.sh diff --git a/packages/dunst/test/functional-tests/dunstrc.show_age b/packages/dunst/test/functional-tests/dunstrc.show_age new file mode 100644 index 0000000..9f83c0f --- /dev/null +++ b/packages/dunst/test/functional-tests/dunstrc.show_age @@ -0,0 +1,49 @@ +[global] + font = Monospace 8 + allow_markup = yes + format = "<b>%s</b>\n%b" + sort = yes + indicate_hidden = yes + alignment = left + show_age_threshold = 2 + word_wrap = yes + ignore_newline = no + geometry = "300x5-30+20" + transparency = 0 + idle_threshold = 120 + monitor = 0 + follow = mouse + sticky_history = yes + line_height = 0 + separator_height = 2 + padding = 8 + horizontal_padding = 8 + separator_color = frame + startup_notification = false + dmenu = /usr/bin/dmenu -p dunst + browser = /usr/bin/firefox -new-tab + +[frame] + width = 3 + color = "#aaaaaa" + +[shortcuts] + close = ctrl+space + close_all = ctrl+shift+space + history = ctrl+grave + context = ctrl+shift+period + +[urgency_low] + background = "#222222" + foreground = "#888888" + timeout = 10 + +[urgency_normal] + background = "#285577" + foreground = "#ffffff" + timeout = 10 + +[urgency_critical] + background = "#900000" + foreground = "#ffffff" + timeout = 0 diff --git a/packages/dunst/test/functional-tests/script_test.sh b/packages/dunst/test/functional-tests/script_test.sh new file mode 100755 index 0000000..8d35f70 --- /dev/null +++ b/packages/dunst/test/functional-tests/script_test.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +notify-send "Success" "ooooh yeah" diff --git a/packages/dunst/test/functional-tests/test.sh b/packages/dunst/test/functional-tests/test.sh new file mode 100755 index 0000000..ae3ca17 --- /dev/null +++ b/packages/dunst/test/functional-tests/test.sh @@ -0,0 +1,195 @@ +#!/bin/bash + +function keypress { + echo "press enter to continue..." + read key +} + +function basic_notifications { + ../../dunstify -a "dunst tester" "normal" "<i>italic body</i>" + ../../dunstify -a "dunst tester" -u c "critical" "<b>bold body</b>" + ../../dunstify -a "dunst tester" "long body" "This is a notification with a very long body" + ../../dunstify -a "dunst tester" "duplucate" + ../../dunstify -a "dunst tester" "duplucate" + ../../dunstify -a "dunst tester" "duplucate" + ../../dunstify -a "dunst tester" "url" "www.google.de" + +} + +function show_age { + echo "###################################" + echo "show age" + echo "###################################" + killall dunst + ../../dunst -config dunstrc.show_age & + ../../dunstify -a "dunst tester" -u c "Show Age" "These should print their age after 2 seconds" + basic_notifications + keypress +} + +function run_script { + echo "###################################" + echo "run script" + echo "###################################" + killall dunst + PATH=".:$PATH" ../../dunst -config dunstrc.run_script & + ../../dunstify -a "dunst tester" -u c \ + "Run Script" "After Keypress, 2 other notification should pop up. THis needs notify-send installed" + keypress + ../../dunstify -a "dunst tester" -u c "trigger" "this should trigger a notification" + keypress +} + +function ignore_newline { + echo "###################################" + echo "ignore newline" + echo "###################################" + killall dunst + ../../dunst -config dunstrc.ignore_newline_no_wrap & + ../../dunstify -a "dunst tester" -u c "Ignore Newline No Wrap" "There should be no newline anywhere" + ../../dunstify -a "dunst tester" -u c "Th\nis\n\n\n is\n fu\nll of \n" "\nnew\nlines" + basic_notifications + keypress + + killall dunst + ../../dunst -config dunstrc.ignore_newline & + ../../dunstify -a "dunst tester" -u c "Ignore Newline" \ + "The only newlines you should encounter here are wordwraps. That's why I'm so long." + ../../dunstify -a "dunst tester" -u c "Th\nis\n\n\n is\n fu\nll of \n" "\nnew\nlines" + basic_notifications + keypress +} + +function replace { + echo "###################################" + echo "replace" + echo "###################################" + killall dunst + ../../dunst -config dunstrc.default & + id=$(../../dunstify -a "dunst tester" -p "Replace" "this should get replaces after keypress") + keypress + ../../dunstify -a "dunst tester" -r $id "Success?" "I hope this is not a new notification" + keypress + +} + +function markup { + echo "###################################" + echo "markup" + echo "###################################" + killall dunst + ../../dunst -config dunstrc.markup "200x0+10+10" & + ../../dunstify -a "dunst tester" "Markup Tests" -u "c" + ../../dunstify -a "dunst tester" "<b>bold</b> <i>italic</i>" + ../../dunstify -a "dunst tester" "<b>broken markup</i>" + keypress + + killall dunst + ../../dunst -config dunstrc.nomarkup "200x0+10+10" & + ../../dunstify -a "dunst tester" -u c "NO Markup Tests" + ../../dunstify -a "dunst tester" "<b>bold</b><i>italic</i>" + ../../dunstify -a "dunst tester" "<b>broken markup</i>" + keypress + +} + +function corners { + echo "###################################" + echo "corners" + echo "###################################" + killall dunst + ../../dunst -config dunstrc.default -geom "200x0+10+10" & + ../../dunstify -a "dunst tester" -u c "upper left" + basic_notifications + keypress + + killall dunst + ../../dunst -config dunstrc.default -geom "200x0-10+10" & + ../../dunstify -a "dunst tester" -u c "upper right" + basic_notifications + keypress + + killall dunst + ../../dunst -config dunstrc.default -geom "200x0-10-10" & + ../../dunstify -a "dunst tester" -u c "lower right" + basic_notifications + keypress + + killall dunst + ../../dunst -config dunstrc.default -geom "200x0+10-10" & + ../../dunstify -a "dunst tester" -u c "lower left" + basic_notifications + keypress + +} + +function geometry { + echo "###################################" + echo "geometry" + echo "###################################" + killall dunst + ../../dunst -config dunstrc.default -geom "0x0" & + ../../dunstify -a "dunst tester" -u c "0x0" + basic_notifications + keypress + + + killall dunst + ../../dunst -config dunstrc.default -geom "200x0" & + ../../dunstify -a "dunst tester" -u c "200x0" + basic_notifications + keypress + + killall dunst + ../../dunst -config dunstrc.default -geom "200x2" & + ../../dunstify -a "dunst tester" -u c "200x2" + basic_notifications + keypress + + killall dunst + ../../dunst -config dunstrc.default -geom "200x1" & + ../../dunstify -a "dunst tester" -u c "200x1" + basic_notifications + keypress + + killall dunst + ../../dunst -config dunstrc.default -geom "0x1" & + ../../dunstify -a "dunst tester" -u c "0x1" + basic_notifications + keypress + + killall dunst + ../../dunst -config dunstrc.default -geom "-300x1" & + ../../dunstify -a "dunst tester" -u c "-300x1" + basic_notifications + keypress + + killall dunst + ../../dunst -config dunstrc.default -geom "-300x1-20-20" & + ../../dunstify -a "dunst tester" -u c "-300x1-20-20" + basic_notifications + keypress + + killall dunst + ../../dunst -config dunstrc.default -geom "x1" & + ../../dunstify -a "dunst tester" -u c "x1-20-20" "across the screen" + basic_notifications + keypress +} + +if [ -n "$1" ]; then + while [ -n "$1" ]; do + $1 + shift + done +else + geometry + corners + show_age + run_script + ignore_newline + replace + markup +fi + +killall dunst diff --git a/packages/dunst/test/greatest.h b/packages/dunst/test/greatest.h new file mode 100644 index 0000000..bc48e15 --- /dev/null +++ b/packages/dunst/test/greatest.h @@ -0,0 +1,1035 @@ +/* + * Copyright (c) 2011-2016 Scott Vokes <vokes.s@gmail.com> + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, 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. + */ + +#ifndef GREATEST_H +#define GREATEST_H + +#ifdef __cplusplus +extern "C" { +#endif + +/* 1.2.1 */ +#define GREATEST_VERSION_MAJOR 1 +#define GREATEST_VERSION_MINOR 2 +#define GREATEST_VERSION_PATCH 1 + +/* A unit testing system for C, contained in 1 file. + * It doesn't use dynamic allocation or depend on anything + * beyond ANSI C89. + * + * An up-to-date version can be found at: + * https://github.com/silentbicycle/greatest/ + */ + + +/********************************************************************* + * Minimal test runner template + *********************************************************************/ +#if 0 + +#include "greatest.h" + +TEST foo_should_foo(void) { + PASS(); +} + +static void setup_cb(void *data) { + printf("setup callback for each test case\n"); +} + +static void teardown_cb(void *data) { + printf("teardown callback for each test case\n"); +} + +SUITE(suite) { + /* Optional setup/teardown callbacks which will be run before/after + * every test case. If using a test suite, they will be cleared when + * the suite finishes. */ + SET_SETUP(setup_cb, voidp_to_callback_data); + SET_TEARDOWN(teardown_cb, voidp_to_callback_data); + + RUN_TEST(foo_should_foo); +} + +/* Add definitions that need to be in the test runner's main file. */ +GREATEST_MAIN_DEFS(); + +/* Set up, run suite(s) of tests, report pass/fail/skip stats. */ +int run_tests(void) { + GREATEST_INIT(); /* init. greatest internals */ + /* List of suites to run (if any). */ + RUN_SUITE(suite); + + /* Tests can also be run directly, without using test suites. */ + RUN_TEST(foo_should_foo); + + GREATEST_PRINT_REPORT(); /* display results */ + return greatest_all_passed(); +} + +/* main(), for a standalone command-line test runner. + * This replaces run_tests above, and adds command line option + * handling and exiting with a pass/fail status. */ +int main(int argc, char **argv) { + GREATEST_MAIN_BEGIN(); /* init & parse command-line args */ + RUN_SUITE(suite); + GREATEST_MAIN_END(); /* display results */ +} + +#endif +/*********************************************************************/ + + +#include <stdlib.h> +#include <stdio.h> +#include <string.h> +#include <ctype.h> + +/*********** + * Options * + ***********/ + +/* Default column width for non-verbose output. */ +#ifndef GREATEST_DEFAULT_WIDTH +#define GREATEST_DEFAULT_WIDTH 72 +#endif + +/* FILE *, for test logging. */ +#ifndef GREATEST_STDOUT +#define GREATEST_STDOUT stdout +#endif + +/* Remove GREATEST_ prefix from most commonly used symbols? */ +#ifndef GREATEST_USE_ABBREVS +#define GREATEST_USE_ABBREVS 1 +#endif + +/* Set to 0 to disable all use of setjmp/longjmp. */ +#ifndef GREATEST_USE_LONGJMP +#define GREATEST_USE_LONGJMP 1 +#endif + +#if GREATEST_USE_LONGJMP +#include <setjmp.h> +#endif + +/* Set to 0 to disable all use of time.h / clock(). */ +#ifndef GREATEST_USE_TIME +#define GREATEST_USE_TIME 1 +#endif + +#if GREATEST_USE_TIME +#include <time.h> +#endif + +/* Floating point type, for ASSERT_IN_RANGE. */ +#ifndef GREATEST_FLOAT +#define GREATEST_FLOAT double +#define GREATEST_FLOAT_FMT "%g" +#endif + +/********* + * Types * + *********/ + +/* Info for the current running suite. */ +typedef struct greatest_suite_info { + unsigned int tests_run; + unsigned int passed; + unsigned int failed; + unsigned int skipped; + +#if GREATEST_USE_TIME + /* timers, pre/post running suite and individual tests */ + clock_t pre_suite; + clock_t post_suite; + clock_t pre_test; + clock_t post_test; +#endif +} greatest_suite_info; + +/* Type for a suite function. */ +typedef void (greatest_suite_cb)(void); + +/* Types for setup/teardown callbacks. If non-NULL, these will be run + * and passed the pointer to their additional data. */ +typedef void (greatest_setup_cb)(void *udata); +typedef void (greatest_teardown_cb)(void *udata); + +/* Type for an equality comparison between two pointers of the same type. + * Should return non-0 if equal, otherwise 0. + * UDATA is a closure value, passed through from ASSERT_EQUAL_T[m]. */ +typedef int greatest_equal_cb(const void *exp, const void *got, void *udata); + +/* Type for a callback that prints a value pointed to by T. + * Return value has the same meaning as printf's. + * UDATA is a closure value, passed through from ASSERT_EQUAL_T[m]. */ +typedef int greatest_printf_cb(const void *t, void *udata); + +/* Callbacks for an arbitrary type; needed for type-specific + * comparisons via GREATEST_ASSERT_EQUAL_T[m].*/ +typedef struct greatest_type_info { + greatest_equal_cb *equal; + greatest_printf_cb *print; +} greatest_type_info; + +typedef struct greatest_memory_cmp_env { + const unsigned char *exp; + const unsigned char *got; + size_t size; +} greatest_memory_cmp_env; + +/* Callbacks for string and raw memory types. */ +extern greatest_type_info greatest_type_info_string; +extern greatest_type_info greatest_type_info_memory; + +typedef enum { + GREATEST_FLAG_FIRST_FAIL = 0x01, + GREATEST_FLAG_LIST_ONLY = 0x02 +} greatest_flag_t; + +/* Struct containing all test runner state. */ +typedef struct greatest_run_info { + unsigned char flags; + unsigned char verbosity; + unsigned int tests_run; /* total test count */ + + /* overall pass/fail/skip counts */ + unsigned int passed; + unsigned int failed; + unsigned int skipped; + unsigned int assertions; + + /* currently running test suite */ + greatest_suite_info suite; + + /* info to print about the most recent failure */ + const char *fail_file; + unsigned int fail_line; + const char *msg; + + /* current setup/teardown hooks and userdata */ + greatest_setup_cb *setup; + void *setup_udata; + greatest_teardown_cb *teardown; + void *teardown_udata; + + /* formatting info for ".....s...F"-style output */ + unsigned int col; + unsigned int width; + + /* only run a specific suite or test */ + const char *suite_filter; + const char *test_filter; + +#if GREATEST_USE_TIME + /* overall timers */ + clock_t begin; + clock_t end; +#endif + +#if GREATEST_USE_LONGJMP + jmp_buf jump_dest; +#endif +} greatest_run_info; + +struct greatest_report_t { + /* overall pass/fail/skip counts */ + unsigned int passed; + unsigned int failed; + unsigned int skipped; + unsigned int assertions; +}; + +/* Global var for the current testing context. + * Initialized by GREATEST_MAIN_DEFS(). */ +extern greatest_run_info greatest_info; + +/* Type for ASSERT_ENUM_EQ's ENUM_STR argument. */ +typedef const char *greatest_enum_str_fun(int value); + +/********************** + * Exported functions * + **********************/ + +/* These are used internally by greatest. */ +void greatest_do_pass(const char *name); +void greatest_do_fail(const char *name); +void greatest_do_skip(const char *name); +int greatest_pre_test(const char *name); +void greatest_post_test(const char *name, int res); +void greatest_usage(const char *name); +int greatest_do_assert_equal_t(const void *exp, const void *got, + greatest_type_info *type_info, void *udata); + +/* These are part of the public greatest API. */ +void GREATEST_SET_SETUP_CB(greatest_setup_cb *cb, void *udata); +void GREATEST_SET_TEARDOWN_CB(greatest_teardown_cb *cb, void *udata); +int greatest_all_passed(void); +void greatest_set_test_filter(const char *name); +void greatest_set_suite_filter(const char *name); +void greatest_get_report(struct greatest_report_t *report); +unsigned int greatest_get_verbosity(void); +void greatest_set_verbosity(unsigned int verbosity); +void greatest_set_flag(greatest_flag_t flag); + + +/******************** +* Language Support * +********************/ + +/* If __VA_ARGS__ (C99) is supported, allow parametric testing +* without needing to manually manage the argument struct. */ +#if __STDC_VERSION__ >= 19901L || _MSC_VER >= 1800 +#define GREATEST_VA_ARGS +#endif + + +/********** + * Macros * + **********/ + +/* Define a suite. */ +#define GREATEST_SUITE(NAME) void NAME(void); void NAME(void) + +/* Declare a suite, provided by another compilation unit. */ +#define GREATEST_SUITE_EXTERN(NAME) void NAME(void) + +/* Start defining a test function. + * The arguments are not included, to allow parametric testing. */ +#define GREATEST_TEST static enum greatest_test_res + +/* PASS/FAIL/SKIP result from a test. Used internally. */ +typedef enum greatest_test_res { + GREATEST_TEST_RES_PASS = 0, + GREATEST_TEST_RES_FAIL = -1, + GREATEST_TEST_RES_SKIP = 1 +} greatest_test_res; + +/* Run a suite. */ +#define GREATEST_RUN_SUITE(S_NAME) greatest_run_suite(S_NAME, #S_NAME) + +/* Run a test in the current suite. */ +#define GREATEST_RUN_TEST(TEST) \ + do { \ + if (greatest_pre_test(#TEST) == 1) { \ + enum greatest_test_res res = GREATEST_SAVE_CONTEXT(); \ + if (res == GREATEST_TEST_RES_PASS) { \ + res = TEST(); \ + } \ + greatest_post_test(#TEST, res); \ + } else if (GREATEST_LIST_ONLY()) { \ + fprintf(GREATEST_STDOUT, " %s\n", #TEST); \ + } \ + } while (0) + +/* Ignore a test, don't warn about it being unused. */ +#define GREATEST_IGNORE_TEST(TEST) (void)TEST + +/* Run a test in the current suite with one void * argument, + * which can be a pointer to a struct with multiple arguments. */ +#define GREATEST_RUN_TEST1(TEST, ENV) \ + do { \ + if (greatest_pre_test(#TEST) == 1) { \ + int res = TEST(ENV); \ + greatest_post_test(#TEST, res); \ + } else if (GREATEST_LIST_ONLY()) { \ + fprintf(GREATEST_STDOUT, " %s\n", #TEST); \ + } \ + } while (0) + +#ifdef GREATEST_VA_ARGS +#define GREATEST_RUN_TESTp(TEST, ...) \ + do { \ + if (greatest_pre_test(#TEST) == 1) { \ + int res = TEST(__VA_ARGS__); \ + greatest_post_test(#TEST, res); \ + } else if (GREATEST_LIST_ONLY()) { \ + fprintf(GREATEST_STDOUT, " %s\n", #TEST); \ + } \ + } while (0) +#endif + + +/* Check if the test runner is in verbose mode. */ +#define GREATEST_IS_VERBOSE() ((greatest_info.verbosity) > 0) +#define GREATEST_LIST_ONLY() \ + (greatest_info.flags & GREATEST_FLAG_LIST_ONLY) +#define GREATEST_FIRST_FAIL() \ + (greatest_info.flags & GREATEST_FLAG_FIRST_FAIL) +#define GREATEST_FAILURE_ABORT() \ + (greatest_info.suite.failed > 0 && GREATEST_FIRST_FAIL()) + +/* Message-less forms of tests defined below. */ +#define GREATEST_PASS() GREATEST_PASSm(NULL) +#define GREATEST_FAIL() GREATEST_FAILm(NULL) +#define GREATEST_SKIP() GREATEST_SKIPm(NULL) +#define GREATEST_ASSERT(COND) \ + GREATEST_ASSERTm(#COND, COND) +#define GREATEST_ASSERT_OR_LONGJMP(COND) \ + GREATEST_ASSERT_OR_LONGJMPm(#COND, COND) +#define GREATEST_ASSERT_FALSE(COND) \ + GREATEST_ASSERT_FALSEm(#COND, COND) +#define GREATEST_ASSERT_EQ(EXP, GOT) \ + GREATEST_ASSERT_EQm(#EXP " != " #GOT, EXP, GOT) +#define GREATEST_ASSERT_EQ_FMT(EXP, GOT, FMT) \ + GREATEST_ASSERT_EQ_FMTm(#EXP " != " #GOT, EXP, GOT, FMT) +#define GREATEST_ASSERT_IN_RANGE(EXP, GOT, TOL) \ + GREATEST_ASSERT_IN_RANGEm(#EXP " != " #GOT " +/- " #TOL, EXP, GOT, TOL) +#define GREATEST_ASSERT_EQUAL_T(EXP, GOT, TYPE_INFO, UDATA) \ + GREATEST_ASSERT_EQUAL_Tm(#EXP " != " #GOT, EXP, GOT, TYPE_INFO, UDATA) +#define GREATEST_ASSERT_STR_EQ(EXP, GOT) \ + GREATEST_ASSERT_STR_EQm(#EXP " != " #GOT, EXP, GOT) +#define GREATEST_ASSERT_STRN_EQ(EXP, GOT, SIZE) \ + GREATEST_ASSERT_STRN_EQm(#EXP " != " #GOT, EXP, GOT, SIZE) +#define GREATEST_ASSERT_MEM_EQ(EXP, GOT, SIZE) \ + GREATEST_ASSERT_MEM_EQm(#EXP " != " #GOT, EXP, GOT, SIZE) +#define GREATEST_ASSERT_ENUM_EQ(EXP, GOT, ENUM_STR) \ + GREATEST_ASSERT_ENUM_EQm(#EXP " != " #GOT, EXP, GOT, ENUM_STR) + +/* The following forms take an additional message argument first, + * to be displayed by the test runner. */ + +/* Fail if a condition is not true, with message. */ +#define GREATEST_ASSERTm(MSG, COND) \ + do { \ + greatest_info.assertions++; \ + if (!(COND)) { GREATEST_FAILm(MSG); } \ + } while (0) + +/* Fail if a condition is not true, longjmping out of test. */ +#define GREATEST_ASSERT_OR_LONGJMPm(MSG, COND) \ + do { \ + greatest_info.assertions++; \ + if (!(COND)) { GREATEST_FAIL_WITH_LONGJMPm(MSG); } \ + } while (0) + +/* Fail if a condition is not false, with message. */ +#define GREATEST_ASSERT_FALSEm(MSG, COND) \ + do { \ + greatest_info.assertions++; \ + if ((COND)) { GREATEST_FAILm(MSG); } \ + } while (0) + +/* Fail if EXP != GOT (equality comparison by ==). */ +#define GREATEST_ASSERT_EQm(MSG, EXP, GOT) \ + do { \ + greatest_info.assertions++; \ + if ((EXP) != (GOT)) { GREATEST_FAILm(MSG); } \ + } while (0) + +/* Fail if EXP != GOT (equality comparison by ==). + * Warning: EXP and GOT will be evaluated more than once on failure. */ +#define GREATEST_ASSERT_EQ_FMTm(MSG, EXP, GOT, FMT) \ + do { \ + const char *greatest_FMT = ( FMT ); \ + greatest_info.assertions++; \ + if ((EXP) != (GOT)) { \ + fprintf(GREATEST_STDOUT, "\nExpected: "); \ + fprintf(GREATEST_STDOUT, greatest_FMT, EXP); \ + fprintf(GREATEST_STDOUT, "\n Got: "); \ + fprintf(GREATEST_STDOUT, greatest_FMT, GOT); \ + fprintf(GREATEST_STDOUT, "\n"); \ + GREATEST_FAILm(MSG); \ + } \ + } while (0) + +/* Fail if EXP is not equal to GOT, printing enum IDs. */ +#define GREATEST_ASSERT_ENUM_EQm(MSG, EXP, GOT, ENUM_STR) \ + do { \ + int greatest_EXP = (int)(EXP); \ + int greatest_GOT = (int)(GOT); \ + greatest_enum_str_fun *greatest_ENUM_STR = ENUM_STR; \ + if (greatest_EXP != greatest_GOT) { \ + fprintf(GREATEST_STDOUT, "\nExpected: %s", \ + greatest_ENUM_STR(greatest_EXP)); \ + fprintf(GREATEST_STDOUT, "\n Got: %s\n", \ + greatest_ENUM_STR(greatest_GOT)); \ + GREATEST_FAILm(MSG); \ + } \ + } while (0) \ + +/* Fail if GOT not in range of EXP +|- TOL. */ +#define GREATEST_ASSERT_IN_RANGEm(MSG, EXP, GOT, TOL) \ + do { \ + GREATEST_FLOAT greatest_EXP = (EXP); \ + GREATEST_FLOAT greatest_GOT = (GOT); \ + GREATEST_FLOAT greatest_TOL = (TOL); \ + greatest_info.assertions++; \ + if ((greatest_EXP > greatest_GOT && \ + greatest_EXP - greatest_GOT > greatest_TOL) || \ + (greatest_EXP < greatest_GOT && \ + greatest_GOT - greatest_EXP > greatest_TOL)) { \ + fprintf(GREATEST_STDOUT, \ + "\nExpected: " GREATEST_FLOAT_FMT \ + " +/- " GREATEST_FLOAT_FMT \ + "\n Got: " GREATEST_FLOAT_FMT \ + "\n", \ + greatest_EXP, greatest_TOL, greatest_GOT); \ + GREATEST_FAILm(MSG); \ + } \ + } while (0) + +/* Fail if EXP is not equal to GOT, according to strcmp. */ +#define GREATEST_ASSERT_STR_EQm(MSG, EXP, GOT) \ + do { \ + GREATEST_ASSERT_EQUAL_Tm(MSG, EXP, GOT, \ + &greatest_type_info_string, NULL); \ + } while (0) \ + +/* Fail if EXP is not equal to GOT, according to strcmp. */ +#define GREATEST_ASSERT_STRN_EQm(MSG, EXP, GOT, SIZE) \ + do { \ + size_t size = SIZE; \ + GREATEST_ASSERT_EQUAL_Tm(MSG, EXP, GOT, \ + &greatest_type_info_string, &size); \ + } while (0) \ + +/* Fail if EXP is not equal to GOT, according to memcmp. */ +#define GREATEST_ASSERT_MEM_EQm(MSG, EXP, GOT, SIZE) \ + do { \ + greatest_memory_cmp_env env; \ + env.exp = (const unsigned char *)EXP; \ + env.got = (const unsigned char *)GOT; \ + env.size = SIZE; \ + GREATEST_ASSERT_EQUAL_Tm(MSG, env.exp, env.got, \ + &greatest_type_info_memory, &env); \ + } while (0) \ + +/* Fail if EXP is not equal to GOT, according to a comparison + * callback in TYPE_INFO. If they are not equal, optionally use a + * print callback in TYPE_INFO to print them. */ +#define GREATEST_ASSERT_EQUAL_Tm(MSG, EXP, GOT, TYPE_INFO, UDATA) \ + do { \ + greatest_type_info *type_info = (TYPE_INFO); \ + greatest_info.assertions++; \ + if (!greatest_do_assert_equal_t(EXP, GOT, \ + type_info, UDATA)) { \ + if (type_info == NULL || type_info->equal == NULL) { \ + GREATEST_FAILm("type_info->equal callback missing!"); \ + } else { \ + GREATEST_FAILm(MSG); \ + } \ + } \ + } while (0) \ + +/* Pass. */ +#define GREATEST_PASSm(MSG) \ + do { \ + greatest_info.msg = MSG; \ + return GREATEST_TEST_RES_PASS; \ + } while (0) + +/* Fail. */ +#define GREATEST_FAILm(MSG) \ + do { \ + greatest_info.fail_file = __FILE__; \ + greatest_info.fail_line = __LINE__; \ + greatest_info.msg = MSG; \ + return GREATEST_TEST_RES_FAIL; \ + } while (0) + +/* Optional GREATEST_FAILm variant that longjmps. */ +#if GREATEST_USE_LONGJMP +#define GREATEST_FAIL_WITH_LONGJMP() GREATEST_FAIL_WITH_LONGJMPm(NULL) +#define GREATEST_FAIL_WITH_LONGJMPm(MSG) \ + do { \ + greatest_info.fail_file = __FILE__; \ + greatest_info.fail_line = __LINE__; \ + greatest_info.msg = MSG; \ + longjmp(greatest_info.jump_dest, GREATEST_TEST_RES_FAIL); \ + } while (0) +#endif + +/* Skip the current test. */ +#define GREATEST_SKIPm(MSG) \ + do { \ + greatest_info.msg = MSG; \ + return GREATEST_TEST_RES_SKIP; \ + } while (0) + +/* Check the result of a subfunction using ASSERT, etc. */ +#define GREATEST_CHECK_CALL(RES) \ + do { \ + enum greatest_test_res greatest_RES = RES; \ + if (greatest_RES != GREATEST_TEST_RES_PASS) { \ + return greatest_RES; \ + } \ + } while (0) \ + +#if GREATEST_USE_TIME +#define GREATEST_SET_TIME(NAME) \ + NAME = clock(); \ + if (NAME == (clock_t) -1) { \ + fprintf(GREATEST_STDOUT, \ + "clock error: %s\n", #NAME); \ + exit(EXIT_FAILURE); \ + } + +#define GREATEST_CLOCK_DIFF(C1, C2) \ + fprintf(GREATEST_STDOUT, " (%lu ticks, %.3f sec)", \ + (long unsigned int) (C2) - (long unsigned int)(C1), \ + (double)((C2) - (C1)) / (1.0 * (double)CLOCKS_PER_SEC)) +#else +#define GREATEST_SET_TIME(UNUSED) +#define GREATEST_CLOCK_DIFF(UNUSED1, UNUSED2) +#endif + +#if GREATEST_USE_LONGJMP +#define GREATEST_SAVE_CONTEXT() \ + /* setjmp returns 0 (GREATEST_TEST_RES_PASS) on first call */ \ + /* so the test runs, then RES_FAIL from FAIL_WITH_LONGJMP. */ \ + ((enum greatest_test_res)(setjmp(greatest_info.jump_dest))) +#else +#define GREATEST_SAVE_CONTEXT() \ + /*a no-op, since setjmp/longjmp aren't being used */ \ + GREATEST_TEST_RES_PASS +#endif + +/* Include several function definitions in the main test file. */ +#define GREATEST_MAIN_DEFS() \ + \ +/* Is FILTER a subset of NAME? */ \ +static int greatest_name_match(const char *name, \ + const char *filter) { \ + size_t offset = 0; \ + size_t filter_len = strlen(filter); \ + while (name[offset] != '\0') { \ + if (name[offset] == filter[0]) { \ + if (0 == strncmp(&name[offset], filter, filter_len)) { \ + return 1; \ + } \ + } \ + offset++; \ + } \ + \ + return 0; \ +} \ + \ +int greatest_pre_test(const char *name) { \ + if (!GREATEST_LIST_ONLY() \ + && (!GREATEST_FIRST_FAIL() || greatest_info.suite.failed == 0) \ + && (greatest_info.test_filter == NULL || \ + greatest_name_match(name, greatest_info.test_filter))) { \ + GREATEST_SET_TIME(greatest_info.suite.pre_test); \ + if (greatest_info.setup) { \ + greatest_info.setup(greatest_info.setup_udata); \ + } \ + return 1; /* test should be run */ \ + } else { \ + return 0; /* skipped */ \ + } \ +} \ + \ +void greatest_post_test(const char *name, int res) { \ + GREATEST_SET_TIME(greatest_info.suite.post_test); \ + if (greatest_info.teardown) { \ + void *udata = greatest_info.teardown_udata; \ + greatest_info.teardown(udata); \ + } \ + \ + if (res <= GREATEST_TEST_RES_FAIL) { \ + greatest_do_fail(name); \ + } else if (res >= GREATEST_TEST_RES_SKIP) { \ + greatest_do_skip(name); \ + } else if (res == GREATEST_TEST_RES_PASS) { \ + greatest_do_pass(name); \ + } \ + greatest_info.suite.tests_run++; \ + greatest_info.col++; \ + if (GREATEST_IS_VERBOSE()) { \ + GREATEST_CLOCK_DIFF(greatest_info.suite.pre_test, \ + greatest_info.suite.post_test); \ + fprintf(GREATEST_STDOUT, "\n"); \ + } else if (greatest_info.col % greatest_info.width == 0) { \ + fprintf(GREATEST_STDOUT, "\n"); \ + greatest_info.col = 0; \ + } \ + if (GREATEST_STDOUT == stdout) fflush(stdout); \ +} \ + \ +static void report_suite(void) { \ + if (greatest_info.suite.tests_run > 0) { \ + fprintf(GREATEST_STDOUT, \ + "\n%u test%s - %u passed, %u failed, %u skipped", \ + greatest_info.suite.tests_run, \ + greatest_info.suite.tests_run == 1 ? "" : "s", \ + greatest_info.suite.passed, \ + greatest_info.suite.failed, \ + greatest_info.suite.skipped); \ + GREATEST_CLOCK_DIFF(greatest_info.suite.pre_suite, \ + greatest_info.suite.post_suite); \ + fprintf(GREATEST_STDOUT, "\n"); \ + } \ +} \ + \ +static void update_counts_and_reset_suite(void) { \ + greatest_info.setup = NULL; \ + greatest_info.setup_udata = NULL; \ + greatest_info.teardown = NULL; \ + greatest_info.teardown_udata = NULL; \ + greatest_info.passed += greatest_info.suite.passed; \ + greatest_info.failed += greatest_info.suite.failed; \ + greatest_info.skipped += greatest_info.suite.skipped; \ + greatest_info.tests_run += greatest_info.suite.tests_run; \ + memset(&greatest_info.suite, 0, sizeof(greatest_info.suite)); \ + greatest_info.col = 0; \ +} \ + \ +static void greatest_run_suite(greatest_suite_cb *suite_cb, \ + const char *suite_name) { \ + if (greatest_info.suite_filter && \ + !greatest_name_match(suite_name, greatest_info.suite_filter)) { \ + return; \ + } \ + update_counts_and_reset_suite(); \ + if (GREATEST_FIRST_FAIL() && greatest_info.failed > 0) { return; } \ + fprintf(GREATEST_STDOUT, "\n* Suite %s:\n", suite_name); \ + GREATEST_SET_TIME(greatest_info.suite.pre_suite); \ + suite_cb(); \ + GREATEST_SET_TIME(greatest_info.suite.post_suite); \ + report_suite(); \ +} \ + \ +void greatest_do_pass(const char *name) { \ + if (GREATEST_IS_VERBOSE()) { \ + fprintf(GREATEST_STDOUT, "PASS %s: %s", \ + name, greatest_info.msg ? greatest_info.msg : ""); \ + } else { \ + fprintf(GREATEST_STDOUT, "."); \ + } \ + greatest_info.suite.passed++; \ +} \ + \ +void greatest_do_fail(const char *name) { \ + if (GREATEST_IS_VERBOSE()) { \ + fprintf(GREATEST_STDOUT, \ + "FAIL %s: %s (%s:%u)", \ + name, greatest_info.msg ? greatest_info.msg : "", \ + greatest_info.fail_file, greatest_info.fail_line); \ + } else { \ + fprintf(GREATEST_STDOUT, "F"); \ + greatest_info.col++; \ + /* add linebreak if in line of '.'s */ \ + if (greatest_info.col != 0) { \ + fprintf(GREATEST_STDOUT, "\n"); \ + greatest_info.col = 0; \ + } \ + fprintf(GREATEST_STDOUT, "FAIL %s: %s (%s:%u)\n", \ + name, \ + greatest_info.msg ? greatest_info.msg : "", \ + greatest_info.fail_file, greatest_info.fail_line); \ + } \ + greatest_info.suite.failed++; \ +} \ + \ +void greatest_do_skip(const char *name) { \ + if (GREATEST_IS_VERBOSE()) { \ + fprintf(GREATEST_STDOUT, "SKIP %s: %s", \ + name, \ + greatest_info.msg ? \ + greatest_info.msg : "" ); \ + } else { \ + fprintf(GREATEST_STDOUT, "s"); \ + } \ + greatest_info.suite.skipped++; \ +} \ + \ +int greatest_do_assert_equal_t(const void *exp, const void *got, \ + greatest_type_info *type_info, void *udata) { \ + int eq = 0; \ + if (type_info == NULL || type_info->equal == NULL) { \ + return 0; \ + } \ + eq = type_info->equal(exp, got, udata); \ + if (!eq) { \ + if (type_info->print != NULL) { \ + fprintf(GREATEST_STDOUT, "\nExpected: "); \ + (void)type_info->print(exp, udata); \ + fprintf(GREATEST_STDOUT, "\n Got: "); \ + (void)type_info->print(got, udata); \ + fprintf(GREATEST_STDOUT, "\n"); \ + } else { \ + fprintf(GREATEST_STDOUT, \ + "GREATEST_ASSERT_EQUAL_T failure at %s:%u\n", \ + greatest_info.fail_file, \ + greatest_info.fail_line); \ + } \ + } \ + return eq; \ +} \ + \ +void greatest_usage(const char *name) { \ + fprintf(GREATEST_STDOUT, \ + "Usage: %s [-hlfv] [-s SUITE] [-t TEST]\n" \ + " -h, --help print this Help\n" \ + " -l List suites and their tests, then exit\n" \ + " -f Stop runner after first failure\n" \ + " -v Verbose output\n" \ + " -s SUITE only run suites containing string SUITE\n" \ + " -t TEST only run tests containing string TEST\n", \ + name); \ +} \ + \ +static void greatest_parse_args(int argc, char **argv) { \ + int i = 0; \ + for (i = 1; i < argc; i++) { \ + if (0 == strncmp("-t", argv[i], 2)) { \ + if (argc <= i + 1) { \ + greatest_usage(argv[0]); \ + exit(EXIT_FAILURE); \ + } \ + greatest_info.test_filter = argv[i+1]; \ + i++; \ + } else if (0 == strncmp("-s", argv[i], 2)) { \ + if (argc <= i + 1) { \ + greatest_usage(argv[0]); \ + exit(EXIT_FAILURE); \ + } \ + greatest_info.suite_filter = argv[i+1]; \ + i++; \ + } else if (0 == strncmp("-f", argv[i], 2)) { \ + greatest_info.flags |= GREATEST_FLAG_FIRST_FAIL; \ + } else if (0 == strncmp("-v", argv[i], 2)) { \ + greatest_info.verbosity++; \ + } else if (0 == strncmp("-l", argv[i], 2)) { \ + greatest_info.flags |= GREATEST_FLAG_LIST_ONLY; \ + } else if (0 == strncmp("-h", argv[i], 2) || \ + 0 == strncmp("--help", argv[i], 6)) { \ + greatest_usage(argv[0]); \ + exit(EXIT_SUCCESS); \ + } else if (0 == strncmp("--", argv[i], 2)) { \ + break; \ + } else { \ + fprintf(GREATEST_STDOUT, \ + "Unknown argument '%s'\n", argv[i]); \ + greatest_usage(argv[0]); \ + exit(EXIT_FAILURE); \ + } \ + } \ +} \ + \ +int greatest_all_passed(void) { return (greatest_info.failed == 0); } \ + \ +void greatest_set_test_filter(const char *name) { \ + greatest_info.test_filter = name; \ +} \ + \ +void greatest_set_suite_filter(const char *name) { \ + greatest_info.suite_filter = name; \ +} \ + \ +void greatest_get_report(struct greatest_report_t *report) { \ + if (report) { \ + report->passed = greatest_info.passed; \ + report->failed = greatest_info.failed; \ + report->skipped = greatest_info.skipped; \ + report->assertions = greatest_info.assertions; \ + } \ +} \ + \ +unsigned int greatest_get_verbosity(void) { \ + return greatest_info.verbosity; \ +} \ + \ +void greatest_set_verbosity(unsigned int verbosity) { \ + greatest_info.verbosity = (unsigned char)verbosity; \ +} \ + \ +void greatest_set_flag(greatest_flag_t flag) { \ + greatest_info.flags |= flag; \ +} \ + \ +void GREATEST_SET_SETUP_CB(greatest_setup_cb *cb, void *udata) { \ + greatest_info.setup = cb; \ + greatest_info.setup_udata = udata; \ +} \ + \ +void GREATEST_SET_TEARDOWN_CB(greatest_teardown_cb *cb, \ + void *udata) { \ + greatest_info.teardown = cb; \ + greatest_info.teardown_udata = udata; \ +} \ + \ +static int greatest_string_equal_cb(const void *exp, const void *got, \ + void *udata) { \ + size_t *size = (size_t *)udata; \ + return (size != NULL \ + ? (0 == strncmp((const char *)exp, (const char *)got, *size)) \ + : (0 == strcmp((const char *)exp, (const char *)got))); \ +} \ + \ +static int greatest_string_printf_cb(const void *t, void *udata) { \ + (void)udata; /* note: does not check \0 termination. */ \ + return fprintf(GREATEST_STDOUT, "%s", (const char *)t); \ +} \ + \ +greatest_type_info greatest_type_info_string = { \ + greatest_string_equal_cb, \ + greatest_string_printf_cb, \ +}; \ + \ +static int greatest_memory_equal_cb(const void *exp, const void *got, \ + void *udata) { \ + greatest_memory_cmp_env *env = (greatest_memory_cmp_env *)udata; \ + return (0 == memcmp(exp, got, env->size)); \ +} \ + \ +static int greatest_memory_printf_cb(const void *t, void *udata) { \ + greatest_memory_cmp_env *env = (greatest_memory_cmp_env *)udata; \ + unsigned char *buf = (unsigned char *)t, diff_mark = ' '; \ + FILE *out = GREATEST_STDOUT; \ + size_t i, line_i, line_len = 0; \ + int len = 0; /* format hexdump with differences highlighted */ \ + for (i = 0; i < env->size; i+= line_len) { \ + diff_mark = ' '; \ + line_len = env->size - i; \ + if (line_len > 16) { line_len = 16; } \ + for (line_i = i; line_i < i + line_len; line_i++) { \ + if (env->exp[line_i] != env->got[line_i]) diff_mark = 'X'; \ + } \ + len += fprintf(out, "\n%04x %c ", (unsigned int)i, diff_mark); \ + for (line_i = i; line_i < i + line_len; line_i++) { \ + int m = env->exp[line_i] == env->got[line_i]; /* match? */ \ + len += fprintf(out, "%02x%c", buf[line_i], m ? ' ' : '<'); \ + } \ + for (line_i = 0; line_i < 16 - line_len; line_i++) { \ + len += fprintf(out, " "); \ + } \ + fprintf(out, " "); \ + for (line_i = i; line_i < i + line_len; line_i++) { \ + unsigned char c = buf[line_i]; \ + len += fprintf(out, "%c", isprint(c) ? c : '.'); \ + } \ + } \ + len += fprintf(out, "\n"); \ + return len; \ +} \ + \ +greatest_type_info greatest_type_info_memory = { \ + greatest_memory_equal_cb, \ + greatest_memory_printf_cb, \ +}; \ + \ +greatest_run_info greatest_info + +/* Init internals. */ +#define GREATEST_INIT() \ + do { \ + /* Suppress unused function warning if features aren't used */ \ + (void)greatest_run_suite; \ + (void)greatest_parse_args; \ + \ + memset(&greatest_info, 0, sizeof(greatest_info)); \ + greatest_info.width = GREATEST_DEFAULT_WIDTH; \ + GREATEST_SET_TIME(greatest_info.begin); \ + } while (0) \ + +/* Handle command-line arguments, etc. */ +#define GREATEST_MAIN_BEGIN() \ + do { \ + GREATEST_INIT(); \ + greatest_parse_args(argc, argv); \ + } while (0) + +/* Report passes, failures, skipped tests, the number of + * assertions, and the overall run time. */ +#define GREATEST_PRINT_REPORT() \ + do { \ + if (!GREATEST_LIST_ONLY()) { \ + update_counts_and_reset_suite(); \ + GREATEST_SET_TIME(greatest_info.end); \ + fprintf(GREATEST_STDOUT, \ + "\nTotal: %u test%s", \ + greatest_info.tests_run, \ + greatest_info.tests_run == 1 ? "" : "s"); \ + GREATEST_CLOCK_DIFF(greatest_info.begin, \ + greatest_info.end); \ + fprintf(GREATEST_STDOUT, ", %u assertion%s\n", \ + greatest_info.assertions, \ + greatest_info.assertions == 1 ? "" : "s"); \ + fprintf(GREATEST_STDOUT, \ + "Pass: %u, fail: %u, skip: %u.\n", \ + greatest_info.passed, \ + greatest_info.failed, greatest_info.skipped); \ + } \ + } while (0) + +/* Report results, exit with exit status based on results. */ +#define GREATEST_MAIN_END() \ + do { \ + GREATEST_PRINT_REPORT(); \ + return (greatest_all_passed() ? EXIT_SUCCESS : EXIT_FAILURE); \ + } while (0) + +/* Make abbreviations without the GREATEST_ prefix for the + * most commonly used symbols. */ +#if GREATEST_USE_ABBREVS +#define TEST GREATEST_TEST +#define SUITE GREATEST_SUITE +#define SUITE_EXTERN GREATEST_SUITE_EXTERN +#define RUN_TEST GREATEST_RUN_TEST +#define RUN_TEST1 GREATEST_RUN_TEST1 +#define RUN_SUITE GREATEST_RUN_SUITE +#define IGNORE_TEST GREATEST_IGNORE_TEST +#define ASSERT GREATEST_ASSERT +#define ASSERTm GREATEST_ASSERTm +#define ASSERT_FALSE GREATEST_ASSERT_FALSE +#define ASSERT_EQ GREATEST_ASSERT_EQ +#define ASSERT_EQ_FMT GREATEST_ASSERT_EQ_FMT +#define ASSERT_IN_RANGE GREATEST_ASSERT_IN_RANGE +#define ASSERT_EQUAL_T GREATEST_ASSERT_EQUAL_T +#define ASSERT_STR_EQ GREATEST_ASSERT_STR_EQ +#define ASSERT_STRN_EQ GREATEST_ASSERT_STRN_EQ +#define ASSERT_MEM_EQ GREATEST_ASSERT_MEM_EQ +#define ASSERT_ENUM_EQ GREATEST_ASSERT_ENUM_EQ +#define ASSERT_FALSEm GREATEST_ASSERT_FALSEm +#define ASSERT_EQm GREATEST_ASSERT_EQm +#define ASSERT_EQ_FMTm GREATEST_ASSERT_EQ_FMTm +#define ASSERT_IN_RANGEm GREATEST_ASSERT_IN_RANGEm +#define ASSERT_EQUAL_Tm GREATEST_ASSERT_EQUAL_Tm +#define ASSERT_STR_EQm GREATEST_ASSERT_STR_EQm +#define ASSERT_STRN_EQm GREATEST_ASSERT_STRN_EQm +#define ASSERT_MEM_EQm GREATEST_ASSERT_MEM_EQm +#define ASSERT_ENUM_EQm GREATEST_ASSERT_ENUM_EQm +#define PASS GREATEST_PASS +#define FAIL GREATEST_FAIL +#define SKIP GREATEST_SKIP +#define PASSm GREATEST_PASSm +#define FAILm GREATEST_FAILm +#define SKIPm GREATEST_SKIPm +#define SET_SETUP GREATEST_SET_SETUP_CB +#define SET_TEARDOWN GREATEST_SET_TEARDOWN_CB +#define CHECK_CALL GREATEST_CHECK_CALL + +#ifdef GREATEST_VA_ARGS +#define RUN_TESTp GREATEST_RUN_TESTp +#endif + +#if GREATEST_USE_LONGJMP +#define ASSERT_OR_LONGJMP GREATEST_ASSERT_OR_LONGJMP +#define ASSERT_OR_LONGJMPm GREATEST_ASSERT_OR_LONGJMPm +#define FAIL_WITH_LONGJMP GREATEST_FAIL_WITH_LONGJMP +#define FAIL_WITH_LONGJMPm GREATEST_FAIL_WITH_LONGJMPm +#endif + +#endif /* USE_ABBREVS */ + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/packages/dunst/test/markup.c b/packages/dunst/test/markup.c new file mode 100644 index 0000000..cbd8bad --- /dev/null +++ b/packages/dunst/test/markup.c @@ -0,0 +1,57 @@ +#include "greatest.h" + +#include <stdbool.h> +#include <glib.h> + +#include "src/markup.h" + +TEST test_markup_strip(void) +{ + char *ptr; + + ASSERT_STR_EQ(""", (ptr=markup_strip(g_strdup("&quot;")))); + g_free(ptr); + ASSERT_STR_EQ("'", (ptr=markup_strip(g_strdup("&apos;")))); + g_free(ptr); + ASSERT_STR_EQ("<", (ptr=markup_strip(g_strdup("&lt;")))); + g_free(ptr); + ASSERT_STR_EQ(">", (ptr=markup_strip(g_strdup("&gt;")))); + g_free(ptr); + ASSERT_STR_EQ("&", (ptr=markup_strip(g_strdup("&amp;")))); + g_free(ptr); + ASSERT_STR_EQ(">A ", (ptr=markup_strip(g_strdup(">A <img> <string")))); + g_free(ptr); + + PASS(); +} + +TEST test_markup_transform(void) +{ + char *ptr; + + settings.ignore_newline = false; + ASSERT_STR_EQ("<i>foo</i><br>bar\nbaz", (ptr=markup_transform(g_strdup("<i>foo</i><br>bar\nbaz"), MARKUP_NO))); + g_free(ptr); + ASSERT_STR_EQ("foo\nbar\nbaz", (ptr=markup_transform(g_strdup("<i>foo</i><br>bar\nbaz"), MARKUP_STRIP))); + g_free(ptr); + ASSERT_STR_EQ("<i>foo</i>\nbar\nbaz", (ptr=markup_transform(g_strdup("<i>foo</i><br>bar\nbaz"), MARKUP_FULL))); + g_free(ptr); + + settings.ignore_newline = true; + ASSERT_STR_EQ("<i>foo</i><br>bar baz", (ptr=markup_transform(g_strdup("<i>foo</i><br>bar\nbaz"), MARKUP_NO))); + g_free(ptr); + ASSERT_STR_EQ("foo bar baz", (ptr=markup_transform(g_strdup("<i>foo</i><br>bar\nbaz"), MARKUP_STRIP))); + g_free(ptr); + ASSERT_STR_EQ("<i>foo</i> bar baz", (ptr=markup_transform(g_strdup("<i>foo</i><br>bar\nbaz"), MARKUP_FULL))); + g_free(ptr); + + PASS(); +} + +SUITE(suite_markup) +{ + RUN_TEST(test_markup_strip); + RUN_TEST(test_markup_transform); +} + +/* vim: set tabstop=8 shiftwidth=8 expandtab textwidth=0: */ diff --git a/packages/dunst/test/notification.c b/packages/dunst/test/notification.c new file mode 100644 index 0000000..c4e3494 --- /dev/null +++ b/packages/dunst/test/notification.c @@ -0,0 +1,125 @@ +#include "greatest.h" +#include "src/notification.h" +#include "src/option_parser.h" +#include "src/settings.h" + +#include <glib.h> + +TEST test_notification_is_duplicate_field(char **field, notification *a, + notification *b) +{ + ASSERT(notification_is_duplicate(a, b)); + char *tmp = *field; + (*field) = "Something different"; + ASSERT_FALSE(notification_is_duplicate(a, b)); + (*field) = tmp; + + PASS(); +} + +TEST test_notification_is_duplicate(void *notifications) +{ + notification **n = (notification**)notifications; + notification *a = n[0]; + notification *b = n[1]; + + ASSERT(notification_is_duplicate(a, b)); + + CHECK_CALL(test_notification_is_duplicate_field(&(b->appname), a, b)); + CHECK_CALL(test_notification_is_duplicate_field(&(b->summary), a, b)); + CHECK_CALL(test_notification_is_duplicate_field(&(b->body), a, b)); + + ASSERT(notification_is_duplicate(a, b)); + + char *tmp = b->icon; + enum icon_position_t icon_setting_tmp = settings.icon_position; + + b->icon = "Test1"; + + settings.icon_position = icons_off; + ASSERT(notification_is_duplicate(a, b)); + //Setting pointer to a random value since we are checking for null + b->raw_icon = (RawImage*)0xff; + ASSERT(notification_is_duplicate(a, b)); + b->raw_icon = NULL; + + settings.icon_position = icons_left; + ASSERT_FALSE(notification_is_duplicate(a, b)); + b->raw_icon = (RawImage*)0xff; + ASSERT_FALSE(notification_is_duplicate(a, b)); + b->raw_icon = NULL; + + settings.icon_position = icons_right; + ASSERT_FALSE(notification_is_duplicate(a, b)); + b->raw_icon = (RawImage*)0xff; + ASSERT_FALSE(notification_is_duplicate(a, b)); + b->raw_icon = NULL; + + b->icon = tmp; + settings.icon_position = icon_setting_tmp; + + ASSERT(notification_is_duplicate(a, b)); + + b->urgency = URG_LOW; + ASSERT_FALSE(notification_is_duplicate(a, b)); + b->urgency = URG_NORM; + ASSERT(notification_is_duplicate(a, b)); + b->urgency = URG_CRIT; + ASSERT_FALSE(notification_is_duplicate(a, b)); + + PASS(); +} + +TEST test_notification_replace_single_field(void) +{ + char *str = g_malloc(128 * sizeof(char)); + char *substr = NULL; + + strcpy(str, "Markup %a preserved"); + substr = strchr(str, '%'); + notification_replace_single_field(&str, &substr, "and & <i>is</i>", MARKUP_FULL); + ASSERT_STR_EQ("Markup and & <i>is</i> preserved", str); + ASSERT_EQ(26, substr - str); + + strcpy(str, "Markup %a escaped"); + substr = strchr(str, '%'); + notification_replace_single_field(&str, &substr, "and & <i>is</i>", MARKUP_NO); + ASSERT_STR_EQ("Markup and & <i>is</i> escaped", str); + ASSERT_EQ(38, substr - str); + + strcpy(str, "Markup %a"); + substr = strchr(str, '%'); + notification_replace_single_field(&str, &substr, "<i>is removed</i> and & escaped", MARKUP_STRIP); + ASSERT_STR_EQ("Markup is removed and & escaped", str); + ASSERT_EQ(35, substr - str); + + g_free(str); + PASS(); +} + +SUITE(suite_notification) +{ + cmdline_load(0, NULL); + load_settings("data/dunstrc.default"); + + notification *a = notification_create(); + a->appname = "Test"; + a->summary = "Summary"; + a->body = "Body"; + a->icon = "Icon"; + a->urgency = URG_NORM; + + notification *b = notification_create(); + memcpy(b, a, sizeof(*b)); + + //2 equal notifications to be passed for duplicate checking, + notification *n[2] = {a, b}; + + RUN_TEST1(test_notification_is_duplicate, (void*) n); + g_free(a); + g_free(b); + + RUN_TEST(test_notification_replace_single_field); +} + +/* vim: set tabstop=8 shiftwidth=8 expandtab textwidth=0: */ diff --git a/packages/dunst/test/option_parser.c b/packages/dunst/test/option_parser.c new file mode 100644 index 0000000..d5e283a --- /dev/null +++ b/packages/dunst/test/option_parser.c @@ -0,0 +1,319 @@ +#include "greatest.h" + +#include <stdbool.h> +#include <glib.h> + +#include "src/option_parser.h" + +TEST test_next_section(void) +{ + const char *section = NULL; + ASSERT_STR_EQ("bool", (section = next_section(section))); + ASSERT_STR_EQ("string", (section = next_section(section))); + ASSERT_STR_EQ("path", (section = next_section(section))); + ASSERT_STR_EQ("int", (section = next_section(section))); + ASSERT_STR_EQ("double", (section = next_section(section))); + PASS(); +} + +TEST test_ini_get_bool(void) +{ + char *bool_section = "bool"; + ASSERT(ini_get_bool(bool_section, "booltrue", false)); + ASSERT(ini_get_bool(bool_section, "booltrue_capital", false)); + + ASSERT_FALSE(ini_get_bool(bool_section, "boolfalse", true)); + ASSERT_FALSE(ini_get_bool(bool_section, "boolfalse_capital", true)); + + ASSERT(ini_get_bool(bool_section, "boolyes", false)); + ASSERT(ini_get_bool(bool_section, "boolyes_capital", false)); + + ASSERT_FALSE(ini_get_bool(bool_section, "boolno", true)); + ASSERT_FALSE(ini_get_bool(bool_section, "boolno_capital", true)); + + ASSERT(ini_get_bool(bool_section, "boolbin1", false)); + ASSERT_FALSE(ini_get_bool(bool_section, "boolbin0", true)); + + ASSERT(ini_get_bool(bool_section, "boolinvalid", true)); + ASSERT_FALSE(ini_get_bool(bool_section, "boolinvalid", false)); + + ASSERT(ini_get_bool(bool_section, "nonexistent", true)); + ASSERT_FALSE(ini_get_bool(bool_section, "nonexistent", false)); + PASS(); +} + +TEST test_ini_get_string(void) +{ + char *string_section = "string"; + char *ptr; + ASSERT_STR_EQ("A simple string", (ptr = ini_get_string(string_section, "simple", ""))); + free(ptr); + + ASSERT_STR_EQ("A quoted string", (ptr = ini_get_string(string_section, "quoted", ""))); + free(ptr); + ASSERT_STR_EQ("A string \"with quotes\"", (ptr = ini_get_string(string_section, "quoted_with_quotes", ""))); + free(ptr); + + ASSERT_STR_EQ("default value", (ptr = ini_get_string(string_section, "nonexistent", "default value"))); + free(ptr); + + PASS(); +} + +TEST test_ini_get_path(void) +{ + char *section = "path"; + char *ptr, *exp; + char *home = getenv("HOME"); + + // return default, if nonexistent key + ASSERT_EQ(NULL, (ptr = ini_get_path(section, "nonexistent", NULL))); + ASSERT_STR_EQ("default", (ptr = ini_get_path(section, "nonexistent", "default"))); + g_free(ptr); + + // return path with replaced home + ASSERT_STR_EQ((exp = g_strconcat(home, "/.path/to/tilde", NULL)), + (ptr = ini_get_path(section, "expand_tilde", NULL))); + g_free(ptr); + g_free(exp); + + PASS(); +} + + +TEST test_ini_get_int(void) +{ + char *int_section = "int"; + + ASSERT_EQ(5, ini_get_int(int_section, "simple", 0)); + ASSERT_EQ(-10, ini_get_int(int_section, "negative", 0)); + ASSERT_EQ(2, ini_get_int(int_section, "decimal", 0)); + ASSERT_EQ(7, ini_get_int(int_section, "leading_zeroes", 0)); + ASSERT_EQ(1024, ini_get_int(int_section, "multi_char", 0)); + + ASSERT_EQ(10, ini_get_int(int_section, "nonexistent", 10)); + PASS(); +} + +TEST test_ini_get_double(void) +{ + char *double_section = "double"; + ASSERT_EQ(1, ini_get_double(double_section, "simple", 0)); + ASSERT_EQ(1.5, ini_get_double(double_section, "decimal", 0)); + ASSERT_EQ(-1.2, ini_get_double(double_section, "negative", 0)); + ASSERT_EQ(0.005, ini_get_double(double_section, "zeroes", 0)); + ASSERT_EQ(3.141592653589793, ini_get_double(double_section, "long", 0)); + + ASSERT_EQ(10.5, ini_get_double(double_section, "nonexistent", 10.5)); + PASS(); +} + +TEST test_cmdline_get_path(void) +{ + char *ptr, *exp; + char *home = getenv("HOME"); + + // return default, if nonexistent key + ASSERT_EQ(NULL, (ptr = cmdline_get_path("-nonexistent", NULL, "desc"))); + ASSERT_STR_EQ("default", (ptr = cmdline_get_path("-nonexistent", "default", "desc"))); + g_free(ptr); + + // return path with replaced home + ASSERT_STR_EQ((exp = g_strconcat(home, "/path/from/cmdline", NULL)), + (ptr = cmdline_get_path("-path", NULL, "desc"))); + g_free(ptr); + g_free(exp); + + PASS(); +} + +TEST test_cmdline_get_string(void) +{ + char *ptr; + ASSERT_STR_EQ("A simple string from the cmdline", (ptr =cmdline_get_string("-string", "", ""))); + free(ptr); + ASSERT_STR_EQ("Single_word_string", (ptr = cmdline_get_string("-str/-s", "", ""))); + free(ptr); + ASSERT_STR_EQ("Default", (ptr = cmdline_get_string("-nonexistent", "Default", ""))); + free(ptr); + PASS(); +} + +TEST test_cmdline_get_int(void) +{ + ASSERT_EQ(3, cmdline_get_int("-int", 0, "")); + ASSERT_EQ(2, cmdline_get_int("-int2/-i", 0, "")); + ASSERT_EQ(-7, cmdline_get_int("-negative", 0, "")); + ASSERT_EQ(4, cmdline_get_int("-zeroes", 0, "")); + ASSERT_EQ(2, cmdline_get_int("-intdecim", 0, "")); + ASSERT_EQ(10, cmdline_get_int("-nonexistent", 10, "")); + PASS(); +} + +TEST test_cmdline_get_double(void) +{ + ASSERT_EQ(2, cmdline_get_double("-simple_double", 0, "")); + ASSERT_EQ(5.2, cmdline_get_double("-double", 0, "")); + ASSERT_EQ(3.14, cmdline_get_double("-nonexistent", 3.14, "")); + PASS(); +} + +TEST test_cmdline_get_bool(void) +{ + ASSERT(cmdline_get_bool("-bool", false, "")); + ASSERT(cmdline_get_bool("-shortbool/-b", false, "")); + ASSERT(cmdline_get_bool("-boolnd/-n", true, "")); + ASSERT_FALSE(cmdline_get_bool("-boolnd/-n", false, "")); + PASS(); +} + +TEST test_cmdline_create_usage(void) +{ + g_free(cmdline_get_string("-msgstring/-ms", "", "A string to test usage creation")); + cmdline_get_int("-msgint/-mi", 0, "An int to test usage creation"); + cmdline_get_double("-msgdouble/-md", 0, "A double to test usage creation"); + cmdline_get_bool("-msgbool/-mb", false, "A bool to test usage creation"); + const char *usage = cmdline_create_usage(); + ASSERT(strstr(usage, "-msgstring/-ms")); + ASSERT(strstr(usage, "A string to test usage creation")); + ASSERT(strstr(usage, "-msgint/-mi")); + ASSERT(strstr(usage, "An int to test usage creation")); + ASSERT(strstr(usage, "-msgdouble/-md")); + ASSERT(strstr(usage, "A double to test usage creation")); + ASSERT(strstr(usage, "-msgbool/-mb")); + ASSERT(strstr(usage, "A bool to test usage creation")); + PASS(); +} + +TEST test_option_get_string(void) +{ + char *string_section = "string"; + char *ptr; + + ASSERT_STR_EQ("A simple string", (ptr =option_get_string(string_section, "simple", "-nonexistent", "", ""))); + free(ptr); + ASSERT_STR_EQ("Single_word_string", (ptr = option_get_string(string_section, "simple", "-str/-s", "", ""))); + free(ptr); + ASSERT_STR_EQ("A simple string from the cmdline", (ptr = option_get_string(string_section, "simple", "-string", "", ""))); + free(ptr); + ASSERT_STR_EQ("A simple string from the cmdline", (ptr = option_get_string(string_section, "simple", "-string/-s", "", ""))); + free(ptr); + ASSERT_STR_EQ("Single_word_string", (ptr = option_get_string(string_section, "simple", "-s", "", ""))); + free(ptr); + ASSERT_STR_EQ("Default", (ptr = option_get_string(string_section, "nonexistent", "-nonexistent", "Default", ""))); + free(ptr); + PASS(); +} + +TEST test_option_get_path(void) +{ + char *section = "path"; + char *ptr, *exp; + char *home = getenv("HOME"); + + // invalid ini, invalid cmdline + ASSERT_EQ(NULL, (ptr = option_get_path(section, "nonexistent", "-nonexistent", NULL, "desc"))); + ASSERT_STR_EQ("default", (ptr = option_get_path(section, "nonexistent", "-nonexistent", "default", "desc"))); + free(ptr); + + // valid ini, invalid cmdline + ASSERT_STR_EQ((exp = g_strconcat(home, "/.path/to/tilde", NULL)), + (ptr = option_get_path(section, "expand_tilde", "-nonexistent", NULL, "desc"))); + g_free(exp); + g_free(ptr); + + // valid ini, valid cmdline + ASSERT_STR_EQ((exp = g_strconcat(home, "/path/from/cmdline", NULL)), + (ptr = option_get_path(section, "expand_tilde", "-path", NULL, "desc"))); + g_free(exp); + g_free(ptr); + + // invalid ini, valid cmdline + ASSERT_STR_EQ((exp = g_strconcat(home, "/path/from/cmdline", NULL)), + (ptr = option_get_path(section, "nonexistent", "-path", NULL, "desc"))); + g_free(exp); + g_free(ptr); + + PASS(); +} + +TEST test_option_get_int(void) +{ + char *int_section = "int"; + ASSERT_EQ(3, option_get_int(int_section, "negative", "-int", 0, "")); + ASSERT_EQ(2, option_get_int(int_section, "simple", "-int2/-i", 0, "")); + ASSERT_EQ(-7, option_get_int(int_section, "decimal", "-negative", 0, "")); + ASSERT_EQ(4, option_get_int(int_section, "simple", "-zeroes", 0, "")); + ASSERT_EQ(2, option_get_int(int_section, "simple", "-intdecim", 0, "")); + + ASSERT_EQ(5, option_get_int(int_section, "simple", "-nonexistent", 0, "")); + ASSERT_EQ(-10, option_get_int(int_section, "negative", "-nonexistent", 0, "")); + ASSERT_EQ(2, option_get_int(int_section, "decimal", "-nonexistent", 0, "")); + ASSERT_EQ(7, option_get_int(int_section, "leading_zeroes", "-nonexistent", 0, "")); + ASSERT_EQ(1024, option_get_int(int_section, "multi_char", "-nonexistent", 0, "")); + + ASSERT_EQ(3, option_get_int(int_section, "nonexistent", "-nonexistent", 3, "")); + PASS(); +} + +TEST test_option_get_double(void) +{ + char *double_section = "double"; + ASSERT_EQ(2, option_get_double(double_section, "simple", "-simple_double", 0, "")); + ASSERT_EQ(5.2, option_get_double(double_section, "simple", "-double", 0, "")); + ASSERT_EQ(0.005, option_get_double(double_section, "zeroes", "-nonexistent", 0, "")); + ASSERT_EQ(10.5, option_get_double(double_section, "nonexistent", "-nonexistent", 10.5, "")); + PASS(); +} + +TEST test_option_get_bool(void) +{ + char *bool_section = "bool"; + ASSERT(option_get_bool(bool_section, "boolfalse", "-bool/-b", false, "")); + ASSERT(option_get_bool(bool_section, "boolbin1", "-nonexistent", false, "")); + ASSERT_FALSE(option_get_bool(bool_section, "boolbin0", "-nonexistent", false, "")); + ASSERT_FALSE(option_get_bool(bool_section, "nonexistent", "-nonexistent", false, "")); + PASS(); +} + +SUITE(suite_option_parser) +{ + FILE *config_file = fopen("data/test-ini", "r"); + if (config_file == NULL) { + fputs("\nTest config file 'data/test-ini' couldn't be opened, failing.\n", stderr); + exit(1); + } + load_ini_file(config_file); + RUN_TEST(test_next_section); + RUN_TEST(test_ini_get_bool); + RUN_TEST(test_ini_get_string); + RUN_TEST(test_ini_get_path); + RUN_TEST(test_ini_get_int); + RUN_TEST(test_ini_get_double); + char cmdline[] = "dunst -bool -b " + "-string \"A simple string from the cmdline\" -s Single_word_string " + "-int 3 -i 2 -negative -7 -zeroes 04 -intdecim 2.5 " + "-path ~/path/from/cmdline " + "-simple_double 2 -double 5.2" + ; + int argc; + char **argv; + g_shell_parse_argv(&cmdline[0], &argc, &argv, NULL); + cmdline_load(argc, argv); + RUN_TEST(test_cmdline_get_string); + RUN_TEST(test_cmdline_get_path); + RUN_TEST(test_cmdline_get_int); + RUN_TEST(test_cmdline_get_double); + RUN_TEST(test_cmdline_get_bool); + RUN_TEST(test_cmdline_create_usage); + + RUN_TEST(test_option_get_string); + RUN_TEST(test_option_get_path); + RUN_TEST(test_option_get_int); + RUN_TEST(test_option_get_double); + RUN_TEST(test_option_get_bool); + free_ini(); + g_strfreev(argv); + fclose(config_file); +} +/* vim: set tabstop=8 shiftwidth=8 expandtab textwidth=0: */ diff --git a/packages/dunst/test/test.c b/packages/dunst/test/test.c new file mode 100644 index 0000000..90959a6 --- /dev/null +++ b/packages/dunst/test/test.c @@ -0,0 +1,18 @@ +#include "greatest.h" + +SUITE_EXTERN(suite_utils); +SUITE_EXTERN(suite_option_parser); +SUITE_EXTERN(suite_notification); +SUITE_EXTERN(suite_markup); + +GREATEST_MAIN_DEFS(); + +int main(int argc, char *argv[]) { + GREATEST_MAIN_BEGIN(); + RUN_SUITE(suite_utils); + RUN_SUITE(suite_option_parser); + RUN_SUITE(suite_notification); + RUN_SUITE(suite_markup); + GREATEST_MAIN_END(); +} +/* vim: set tabstop=8 shiftwidth=8 expandtab textwidth=0: */ diff --git a/packages/dunst/test/utils.c b/packages/dunst/test/utils.c new file mode 100644 index 0000000..d0b5f4b --- /dev/null +++ b/packages/dunst/test/utils.c @@ -0,0 +1,185 @@ +#include "greatest.h" +#include "src/utils.h" + +#include <glib.h> + +TEST test_string_replace_char(void) +{ + char *text = malloc(128 * sizeof(char)); + strcpy(text, "a aa aaa"); + ASSERT_STR_EQ("b bb bbb", string_replace_char('a', 'b', text)); + + strcpy(text, "Nothing to replace"); + ASSERT_STR_EQ("Nothing to replace", string_replace_char('s', 'a', text)); + + strcpy(text, ""); + ASSERT_STR_EQ("", string_replace_char('a', 'b', text)); + free(text); + + PASS(); +} + +/* + * We trust that string_replace_all and string_replace properly reallocate + * memory if the result is longer than the given string, no real way to test for + * that far as I know. + */ + +TEST test_string_replace_all(void) +{ + + char *text = malloc(128 * sizeof(char)); + strcpy(text, "aaaaa"); + ASSERT_STR_EQ("bbbbb", (text = string_replace_all("a", "b", text))); + + strcpy(text, ""); + ASSERT_STR_EQ("", (text = string_replace_all("a", "b", text))); + + strcpy(text, "Nothing to replace"); + ASSERT_STR_EQ((text = string_replace_all("z", "a", text)), "Nothing to replace"); + + strcpy(text, "Reverse this"); + ASSERT_STR_EQ("Reverse sith", (text = string_replace_all("this", "sith", text))); + + strcpy(text, "abcdabc"); + ASSERT_STR_EQ("xyzabcdxyzabc", (text = string_replace_all("a", "xyza", text))); + + free(text); + PASS(); +} + +TEST test_string_replace(void) +{ + char *text = malloc(128 * sizeof(char)); + strcpy(text, "aaaaa"); + ASSERT_STR_EQ("baaaa", (text = string_replace("a", "b", text)) ); + + strcpy(text, ""); + ASSERT_STR_EQ((text = string_replace("a", "b", text)), ""); + + strcpy(text, "Nothing to replace"); + ASSERT_STR_EQ((text = string_replace("z", "a", text)), "Nothing to replace"); + + strcpy(text, "Reverse this"); + ASSERT_STR_EQ("Reverse sith", (text = string_replace("this", "sith", text))); + + strcpy(text, "abcdabc"); + ASSERT_STR_EQ("xyzabcdabc", (text = string_replace("a", "xyza", text))); + + free(text); + PASS(); +} + +TEST test_string_append(void) +{ + char *exp; + + ASSERT_STR_EQ("text_sep_bit", (exp = string_append(g_strdup("text"), "bit", "_sep_"))); + g_free(exp); + ASSERT_STR_EQ("textbit", (exp = string_append(g_strdup("text"), "bit", NULL))); + g_free(exp); + ASSERT_STR_EQ("textbit", (exp = string_append(g_strdup("text"), "bit", ""))); + g_free(exp); + + ASSERT_STR_EQ("text", (exp = string_append(g_strdup("text"), "", NULL))); + g_free(exp); + ASSERT_STR_EQ("text", (exp = string_append(g_strdup("text"), "", "_sep_"))); + g_free(exp); + + ASSERT_STR_EQ("b", (exp = string_append(g_strdup(""), "b", NULL))); + g_free(exp); + ASSERT_STR_EQ("b", (exp = string_append(NULL, "b", "_sep_"))); + g_free(exp); + + ASSERT_STR_EQ("a", (exp = string_append(g_strdup("a"), "", NULL))); + g_free(exp); + ASSERT_STR_EQ("a", (exp = string_append(g_strdup("a"), NULL, "_sep_"))); + g_free(exp); + + ASSERT_STR_EQ("", (exp = string_append(g_strdup(""), "", "_sep_"))); + g_free(exp); + ASSERT_EQ(NULL, (exp = string_append(NULL, NULL, "_sep_"))); + g_free(exp); + + PASS(); +} + +TEST test_string_strip_delimited(void) +{ + char *text = malloc(128 * sizeof(char)); + + strcpy(text, "A <simple> string_strip_delimited test"); + string_strip_delimited(text, '<', '>'); + ASSERT_STR_EQ("A string_strip_delimited test", text); + + strcpy(text, "Remove <blink>html <b><i>tags</i></b></blink>"); + string_strip_delimited(text, '<', '>'); + ASSERT_STR_EQ("Remove html tags", text); + + strcpy(text, "Calls|with|identical|delimiters|are|handled|properly"); + string_strip_delimited(text, '|', '|'); + ASSERT_STR_EQ("Calls", text); + + strcpy(text, "<Return empty string if there is nothing left>"); + string_strip_delimited(text, '<', '>'); + ASSERT_STR_EQ("", text); + + strcpy(text, "Nothing is done if there are no delimiters in the string"); + string_strip_delimited(text, '<', '>'); + ASSERT_STR_EQ("Nothing is done if there are no delimiters in the string", text); + + free(text); + PASS(); +} + +TEST test_string_to_path(void) +{ + char *ptr, *exp; + char *home = getenv("HOME"); + + exp = "/usr/local/bin/script"; + ASSERT_STR_EQ(exp, (ptr = string_to_path(g_strdup(exp)))); + free(ptr); + + exp = "~path/with/wrong/tilde"; + ASSERT_STR_EQ(exp, (ptr = string_to_path(g_strdup(exp)))); + free(ptr); + + ASSERT_STR_EQ((exp = g_strconcat(home, "/.path/with/tilde", NULL)), + (ptr = string_to_path(g_strdup("~/.path/with/tilde")))); + free(exp); + free(ptr); + + ASSERT_STR_EQ((exp = g_strconcat(home, "/.path/with/tilde and some space", NULL)), + (ptr = string_to_path(g_strdup("~/.path/with/tilde and some space")))); + free(exp); + free(ptr); + + PASS(); +} + +TEST test_string_to_time(void) +{ + char *input[] = { "5000 ms", "5000ms", "100", "10s", "2m", "11h", "9d", " 5 ms ", NULL }; + gint64 exp[] = { 5000, 5000, 100000, 10000, 120000, 39600000, 777600000, 5, 0}; + + int i = 0; + while (input[i]){ + ASSERT_EQ_FMT(string_to_time(input[i]), exp[i]*1000, "%ld"); + i++; + } + + PASS(); +} + +SUITE(suite_utils) +{ + RUN_TEST(test_string_replace_char); + RUN_TEST(test_string_replace_all); + RUN_TEST(test_string_replace); + RUN_TEST(test_string_append); + RUN_TEST(test_string_strip_delimited); + RUN_TEST(test_string_to_path); + RUN_TEST(test_string_to_time); +} +/* vim: set tabstop=8 shiftwidth=8 expandtab textwidth=0: */