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 (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 (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 (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
+
+B: -key
+
+Specifies the keyboard shortcut for closing a notification.
+
+=item B
+
+B: -all_key
+
+Specifies the keyboard shortcut for closing all currently displayed notifications.
+
+=item B
+
+B: -history_key
+
+Specifies the keyboard shortcut for recalling a single notification from history.
+
+=item B
+
+B: -context_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 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) in memory.
+These notifications can be recalled (i.e. redesiplayed) by pressing the
+B (see the shortcuts section), whether these notifications will
+time out like if they have been just send depends on the value of the
+B 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
+
+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
+
+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 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: '#' 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
+
+=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
+#include
+#include
+#include
+#include
+#include
+#include
+
+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:
+ # bold
+ # italic
+ # strikethrough
+ # underline
+ #
+ # For a complete reference see
+ # .
+ #
+ # 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 = "%s\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
+#include
+#include
+#include
+
+#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 =
+ ""
+ ""
+ " "
+
+ " "
+ " "
+ " "
+
+ " "
+ " "
+ " "
+ " "
+ " "
+ " "
+ " "
+ " "
+ " "
+ " "
+ " "
+
+ " "
+ " "
+ " "
+
+ " "
+ " "
+ " "
+ " "
+ " "
+ " "
+
+ " "
+ " "
+ " "
+ " "
+
+ " "
+ " "
+ " "
+ " "
+ " "
+ "";
+
+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
+#include
+#include
+#include
+#include
+#include
+#include
+
+#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
+#include
+#include
+
+#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
+#include
+
+#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("
", "\n", str);
+ str = string_replace_all("
", "\n", str);
+ str = string_replace_all("
", "\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
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#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
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#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, "");
+ 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("", index_buf, str);
+ g_free(index_buf);
+ g_free(url);
+ } else {
+ str = string_replace(replace_buf, "", str);
+ str = string_replace("", "", 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
+#include
+
+#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
+#include
+#include
+#include
+#include
+
+#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
+#include
+#include
+
+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
+#include
+#include
+#include
+
+#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
+#include
+
+#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
+#include
+
+#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
+#include
+#include
+#ifndef STATIC_CONFIG
+#include
+#include
+#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
+
+#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
+#include
+#include
+#include
+#include
+#include
+
+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
+
+/* 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 characters with at position of the string */
+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
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#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
+
+#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
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#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
+#include
+#include
+#include
+#include
+
+#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 = "%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.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 = "%s\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 = "%s\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 = "%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.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 = "%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
+
+[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 = "%s\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" "italic body"
+ ../../dunstify -a "dunst tester" -u c "critical" "bold body"
+ ../../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" "bold italic"
+ ../../dunstify -a "dunst tester" "broken markup"
+ keypress
+
+ killall dunst
+ ../../dunst -config dunstrc.nomarkup "200x0+10+10" &
+ ../../dunstify -a "dunst tester" -u c "NO Markup Tests"
+ ../../dunstify -a "dunst tester" "bolditalic"
+ ../../dunstify -a "dunst tester" "broken markup"
+ 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
+ *
+ * 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
+#include
+#include
+#include
+
+/***********
+ * 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
+#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
+#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
+#include
+
+#include "src/markup.h"
+
+TEST test_markup_strip(void)
+{
+ char *ptr;
+
+ ASSERT_STR_EQ(""", (ptr=markup_strip(g_strdup("""))));
+ g_free(ptr);
+ ASSERT_STR_EQ("'", (ptr=markup_strip(g_strdup("'"))));
+ g_free(ptr);
+ ASSERT_STR_EQ("<", (ptr=markup_strip(g_strdup("<"))));
+ g_free(ptr);
+ ASSERT_STR_EQ(">", (ptr=markup_strip(g_strdup(">"))));
+ g_free(ptr);
+ ASSERT_STR_EQ("&", (ptr=markup_strip(g_strdup("&"))));
+ g_free(ptr);
+ ASSERT_STR_EQ(">A ", (ptr=markup_strip(g_strdup(">A foo
bar\nbaz"), MARKUP_NO)));
+ g_free(ptr);
+ ASSERT_STR_EQ("foo\nbar\nbaz", (ptr=markup_transform(g_strdup("foo
bar\nbaz"), MARKUP_STRIP)));
+ g_free(ptr);
+ ASSERT_STR_EQ("foo\nbar\nbaz", (ptr=markup_transform(g_strdup("foo
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("foo
bar\nbaz"), MARKUP_NO)));
+ g_free(ptr);
+ ASSERT_STR_EQ("foo bar baz", (ptr=markup_transform(g_strdup("foo
bar\nbaz"), MARKUP_STRIP)));
+ g_free(ptr);
+ ASSERT_STR_EQ("foo bar baz", (ptr=markup_transform(g_strdup("foo
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
+
+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 & is", MARKUP_FULL);
+ ASSERT_STR_EQ("Markup and & is preserved", str);
+ ASSERT_EQ(26, substr - str);
+
+ strcpy(str, "Markup %a escaped");
+ substr = strchr(str, '%');
+ notification_replace_single_field(&str, &substr, "and & is", 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, "is removed 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
+#include
+
+#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
+
+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 string_strip_delimited test");
+ string_strip_delimited(text, '<', '>');
+ ASSERT_STR_EQ("A string_strip_delimited test", text);
+
+ strcpy(text, "Remove ");
+ 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, "");
+ 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: */