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